diff --git a/constv b/constv deleted file mode 100755 index bc0641662..000000000 Binary files a/constv and /dev/null differ diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index c4a3d9c22..70b9f1b9a 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -1348,47 +1348,6 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { namespace_key_globals.push((gname, byte_len)); } } - let mut namespace_extern_prefixes = std::collections::BTreeSet::new(); - for entry in &cross_module.namespace_entries { - if let super::opts::NamespaceEntryKind::NestedNamespace { source_prefix } = &entry.kind { - if source_prefix != module_prefix { - namespace_extern_prefixes.insert(source_prefix.clone()); - } - } - } - for source_prefix in cross_module.namespace_import_prefixes.values() { - if source_prefix != module_prefix { - namespace_extern_prefixes.insert(source_prefix.clone()); - } - } - for source_prefix in cross_module.namespace_member_namespace_prefixes.values() { - if source_prefix != module_prefix { - namespace_extern_prefixes.insert(source_prefix.clone()); - } - } - let mut emitted_namespace_extern_prefixes = std::collections::BTreeSet::new(); - for source_prefix in namespace_extern_prefixes { - if emitted_namespace_extern_prefixes.insert(source_prefix.clone()) { - llmod.add_external_global(&format!("__perry_ns_{}", source_prefix), DOUBLE); - } - } - for export in &hir.exports { - let perry_hir::Export::Named { local, exported } = export else { - continue; - }; - let Some(source_prefix) = cross_module.namespace_import_prefixes.get(local) else { - continue; - }; - let getter_name = format!("perry_fn_{}__{}", module_prefix, sanitize(exported)); - if llmod.has_function(&getter_name) { - continue; - } - let getter = llmod.define_function(&getter_name, DOUBLE, vec![]); - let _ = getter.create_block("entry"); - let blk = getter.block_mut(0).unwrap(); - let value = blk.load(DOUBLE, &format!("@__perry_ns_{}", source_prefix)); - blk.ret(DOUBLE, &value); - } // For each `Expr::DynamicImport` target this module dispatches to, // declare the foreign module's `@__perry_ns_` as an // extern global so the dispatch site can load it. Deduped via @@ -1415,10 +1374,8 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { if prefix.starts_with("__native_mod__") || prefix.starts_with("__node_submod__") { continue; } - if emitted_namespace_extern_prefixes.insert(prefix.clone()) { - let ns_name = format!("__perry_ns_{}", prefix); - llmod.add_external_global(&ns_name, DOUBLE); - } + let ns_name = format!("__perry_ns_{}", prefix); + llmod.add_external_global(&ns_name, DOUBLE); // Issue #753: declare each dynamic-import target's `__init` // so the dispatch site in `Expr::DynamicImport` can call it // before loading the namespace. The wrapper-side init is diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 443434fd2..e85bc21a6 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -780,9 +780,6 @@ pub(super) fn compile_closure( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index bd7f865f0..ce6aebb35 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -613,9 +613,6 @@ pub(super) fn compile_module_entry( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, @@ -1148,9 +1145,6 @@ pub(super) fn compile_module_entry( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 7315ab3f1..9ab0c840c 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -516,9 +516,6 @@ pub(super) fn compile_function( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index ee70cd564..64c3fa82b 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1145,7 +1145,7 @@ pub(super) fn emit_namespace_populator( let wrapper_name = format!( "__perry_wrap_perry_fn_{}__{}", source_prefix, - sanitize_member(source_local) + sanitize(source_local) ); let arity = (*param_count).min(16); let mut wrapper_params: Vec = vec![I64]; diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index b33b8fc91..d1406c286 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -235,31 +235,6 @@ fn node_stream_parent_kind( None } -fn map_set_default_super_kind<'a>( - classes: &HashMap, - mut parent: Option<&'a str>, -) -> Option { - let mut depth = 0usize; - while let Some(name) = parent { - match name { - "Map" => return Some(0), - "Set" => return Some(1), - _ => {} - } - let class = classes.get(name).copied()?; - if class.constructor.is_some() { - return None; - } - parent = class.extends_name.as_deref(); - depth += 1; - // Keep this bound in sync with the twin in lower_call/new_helpers.rs. - if depth > 64 { - break; - } - } - None -} - /// Compile a class instance method as a top-level LLVM function with the /// signature `perry_method__(this_box: double, args: double…) /// -> double`. The first parameter (`this`) is stored in a slot whose @@ -425,9 +400,6 @@ pub(super) fn compile_method( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, @@ -745,31 +717,6 @@ pub(super) fn compile_method( ctx.block() .call(DOUBLE, runtime_fn, &[(DOUBLE, &this_box), (DOUBLE, &opts)]); } - if let Some(kind) = map_set_default_super_kind(classes, class.extends_name.as_deref()) { - let undef_lit = - crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let iterable = method - .params - .first() - .and_then(|param| ctx.locals.get(¶m.id).cloned()) - .map(|slot| ctx.block().load(DOUBLE, &slot)) - .unwrap_or_else(|| undef_lit.clone()); - let this_box = ctx - .this_stack - .last() - .cloned() - .map(|slot| ctx.block().load(DOUBLE, &slot)) - .unwrap_or_else(|| undef_lit.clone()); - ctx.block().call( - DOUBLE, - "js_map_set_subclass_init", - &[ - (DOUBLE, &this_box), - (I32, &kind.to_string()), - (DOUBLE, &iterable), - ], - ); - } // Wall 51: a no-own-ctor class with a DYNAMIC / cross-module parent // (`class X extends _mod.Parent {}`, captured as `extends_expr`) that @@ -1326,9 +1273,6 @@ pub(super) fn compile_static_method( namespace_reexport_named_imports: &cross_module.namespace_reexport_named_imports, namespace_member_prefixes: &cross_module.namespace_member_prefixes, namespace_member_origin_names: &cross_module.namespace_member_origin_names, - namespace_member_vars: &cross_module.namespace_member_vars, - namespace_member_namespace_prefixes: &cross_module.namespace_member_namespace_prefixes, - namespace_import_prefixes: &cross_module.namespace_import_prefixes, imported_async_funcs: &cross_module.imported_async_funcs, local_async_funcs: &cross_module.local_async_funcs, local_generator_funcs: &cross_module.local_generator_funcs, diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 4ea7280d2..77202c4f3 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1457,9 +1457,6 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> namespace_reexport_named_imports: opts.namespace_reexport_named_imports.clone(), namespace_member_prefixes: opts.namespace_member_prefixes, namespace_member_origin_names: opts.namespace_member_origin_names, - namespace_member_vars: opts.namespace_member_vars, - namespace_member_namespace_prefixes: opts.namespace_member_namespace_prefixes, - namespace_import_prefixes: opts.namespace_import_prefixes, imported_async_funcs: opts.imported_async_funcs, local_async_funcs, local_generator_funcs, @@ -1493,7 +1490,6 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> symbol: ctor_name, param_count: ic.constructor_param_count, has_own_constructor: ic.has_own_constructor, - uses_new_target: ic.constructor_uses_new_target, has_instance_fields: ic.has_instance_fields, has_rest: ic.constructor_has_rest, }, diff --git a/crates/perry-codegen/src/codegen/module_globals_emit.rs b/crates/perry-codegen/src/codegen/module_globals_emit.rs index 97c48796b..580ea88b1 100644 --- a/crates/perry-codegen/src/codegen/module_globals_emit.rs +++ b/crates/perry-codegen/src/codegen/module_globals_emit.rs @@ -270,13 +270,7 @@ pub(crate) fn emit_module_globals( // semantically wrong (it'd return the closure value instead // of invoking it). let is_function_alias = hir.exported_functions.iter().any(|(exp, _)| exp == name); - let is_exported_alias_source = hir.exports.iter().any(|export| { - matches!(export, perry_hir::Export::Named { local, .. } if local == name) - }); - if (is_exported || is_exported_alias_source) - && !is_also_function - && !is_function_alias - { + if is_exported && !is_also_function && !is_function_alias { let fn_name = format!("perry_fn_{}__{}", module_prefix, sanitize(name),); let getter = llmod.define_function(&fn_name, DOUBLE, vec![]); let _ = getter.create_block("entry"); diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index da3ecf4d9..bec9a8333 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -175,35 +175,16 @@ pub struct CompileOptions { /// it dispatched `tracer.make(Math.random())` instead of /// `random.make(Math.random())`. pub namespace_member_prefixes: std::collections::HashMap<(String, String), String>, - /// Issue #680 / #5924 follow-up: per-namespace member origin-name overrides. - /// `namespace_member_prefixes` picks the producer module, but re-exported - /// members can be stored under a different origin export name; without this - /// `ns.alias` calls the right module with the wrong symbol name. Keyed by - /// `(namespace_local_name, exported_member_name)` → `origin_export_name`. - /// Being per-namespace also fixes #5924: `import_function_origin_names` is a - /// flat map, so two namespaces imported into the same file that both expose - /// a member with the same name (only one of them a re-export rename) would - /// clobber each other — `import { Effect, Context } from "effect"` broke - /// `Context.Service` because `Effect`'s own re-exported `Service` clobbered - /// the flat map first. + /// Issue #5924 (companion to #680/#678): per-namespace origin-name + /// resolution. Keyed by `(namespace_local_name, member_name)` → + /// `origin_name`. `import_function_origin_names` is a flat map, so when + /// two namespaces imported into the same file both have a member with + /// the same name and only one of them is a re-export rename, the + /// rename's origin-name override clobbers the other namespace's + /// (correct, unrenamed) suffix — `import { Effect, Context } from + /// "effect"` broke `Context.Service` because `Effect`'s own re-exported + /// `Service` clobbered the flat map first. pub namespace_member_origin_names: std::collections::HashMap<(String, String), String>, - /// Issue #680 follow-up: per-namespace exported-variable members. Namespace - /// member calls need to know when `ns.member` is a getter returning a - /// closure rather than a direct function symbol; otherwise codegen emits a - /// direct call to the zero-arg getter. Keyed by `(namespace_local_name, - /// exported_member_name)`. - pub namespace_member_vars: std::collections::HashSet<(String, String)>, - /// Issue #680 follow-up: per-namespace nested namespace re-exports. - /// `export * as child` members are namespace objects, not callable/value - /// exports from the parent prefix; this map preserves the nested target - /// prefix. Keyed by `(namespace_local_name, exported_member_name)` → - /// nested module prefix. - pub namespace_member_namespace_prefixes: std::collections::HashMap<(String, String), String>, - /// Issue #680 follow-up: namespace import local → target module prefix. - /// Member-specific maps are insufficient when the namespace binding itself - /// is read as a value; codegen must return the producer's real module - /// namespace object, including nested `export * as` members. - pub namespace_import_prefixes: std::collections::HashMap, /// When true, `compile_module` returns the textual LLVM IR (`.ll`) /// as bytes instead of invoking `clang -c` to produce an object file. /// Used by the bitcode-link path (`PERRY_LLVM_BITCODE_LINK=1`). @@ -494,8 +475,6 @@ pub struct ImportedClass { pub constructor_param_count: usize, /// Whether the source class declared its own constructor body. pub has_own_constructor: bool, - /// Whether the source constructor symbol may read `new.target`. - pub constructor_uses_new_target: bool, /// Whether the source class's constructor's last declared parameter is /// `...rest`. Symmetric to `method_has_rest` but for the constructor: the /// source module compiled `_constructor(this, arg0, …)` expecting @@ -570,7 +549,6 @@ pub(crate) struct ImportedCtor { pub symbol: String, pub param_count: usize, pub has_own_constructor: bool, - pub uses_new_target: bool, pub has_instance_fields: bool, /// True when the constructor's last declared param is `...rest`. Tells /// the cross-module `new` dispatch to pack the trailing args into an @@ -596,14 +574,9 @@ pub(crate) struct CrossModuleCtx { /// Issue #680: per-namespace member resolution. See doc on /// `CompileOptions::namespace_member_prefixes`. pub namespace_member_prefixes: std::collections::HashMap<(String, String), String>, - /// See `CompileOptions::namespace_member_origin_names` (#680 / #5924). + /// Issue #5924: per-namespace origin-name resolution. See doc on + /// `CompileOptions::namespace_member_origin_names`. pub namespace_member_origin_names: std::collections::HashMap<(String, String), String>, - /// See `CompileOptions::namespace_member_vars`. - pub namespace_member_vars: std::collections::HashSet<(String, String)>, - /// See `CompileOptions::namespace_member_namespace_prefixes`. - pub namespace_member_namespace_prefixes: std::collections::HashMap<(String, String), String>, - /// See `CompileOptions::namespace_import_prefixes`. - pub namespace_import_prefixes: std::collections::HashMap, pub imported_async_funcs: std::collections::HashSet, /// FuncIds of locally-defined async functions in this module. Populated /// from `hir.functions.is_async`. Used by `is_promise_expr` to refine diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index 8cb1a7890..a9b28d688 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -568,11 +568,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `Object.prototype.hasOwnProperty.call(ImportedClass, sym)` // always returned false because the receiver was a closure-pointer // NaN-box (POINTER_TAG) rather than a class-ref (INT32_TAG). - if !ctx.namespace_imports.contains(name) { - if let Some(&cid) = ctx.class_ids.get(name) { - let bits = crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF); - return Ok(double_literal(f64::from_bits(bits))); - } + if let Some(&cid) = ctx.class_ids.get(name) { + let bits = crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF); + return Ok(double_literal(f64::from_bits(bits))); } // Issue #841: named imports from Node submodules Perry recognizes // as runtime-backed values must win over the generic native-module @@ -602,69 +600,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ], )); } - if let Some(submod_key) = ctx.namespace_node_submodules.get(name) { - let submod_label = emit_string_literal_global(ctx, submod_key); - let submod_len = submod_key.len(); - let install_sym = crate::nm_install::nm_submod_install_symbol(submod_key); - let blk = ctx.block(); - if let Some(s) = install_sym { - blk.call_void(s, &[]); - } - return Ok(blk.call( - DOUBLE, - "js_node_submodule_namespace", - &[(PTR, &submod_label), (I32, &submod_len.to_string())], - )); - } - if ctx.namespace_imports.contains(name) { - if let Some(prefix) = ctx.namespace_import_prefixes.get(name) { - return Ok(ctx.block().load(DOUBLE, &format!("@__perry_ns_{}", prefix))); - } - let mut members: Vec = ctx - .namespace_member_prefixes - .keys() - .filter(|(ns, _)| ns == name) - .map(|(_, m)| m.clone()) - .collect(); - if !members.is_empty() { - members.sort(); - members.dedup(); - let n_str = (members.len() as u32).to_string(); - let zero_str = "0".to_string(); - let handle = ctx.block().call( - I64, - "js_object_alloc", - &[(I32, &zero_str), (I32, &n_str)], - ); - for member in &members { - let member_get = Expr::PropertyGet { - object: Box::new(Expr::ExternFuncRef { - name: name.clone(), - param_types: Vec::new(), - return_type: HirType::Any, - }), - property: member.clone(), - }; - let v_box = lower_expr(ctx, &member_get)?; - let key_idx = ctx.strings.intern(member); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - blk.call_void( - "js_object_set_field_by_name", - &[(I64, &handle), (I64, &key_raw), (DOUBLE, &v_box)], - ); - } - let blk = ctx.block(); - return Ok(nanbox_pointer_inline(blk, &handle)); - } - return Ok(ctx - .block() - .call(DOUBLE, "js_unresolved_namespace_stub", &[])); - } if let Some(source_prefix) = ctx.import_function_prefixes.get(name).cloned() { // Next.js lazy-require: a `_lazyreq_N` binding is the CJS require // shim's handle to a FUNCTION-LOCAL `require('S')`. S is @@ -759,6 +694,96 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { blk.call(I64, "js_closure_alloc_singleton", &[(PTR, &wrap_ptr)]); return Ok(nanbox_pointer_inline(blk, &closure_handle)); } + // Issue #841 companion: namespace imports for the same five + // submodules. The `collect_modules.rs` rejection skips + // these so the namespace binding flows through HIR and + // lands here. Emit a call to `js_node_submodule_namespace` + // which returns a per-submodule stub object whose properties + // are the function singletons named imports produce. + if let Some(submod_key) = ctx.namespace_node_submodules.get(name) { + let submod_label = emit_string_literal_global(ctx, submod_key); + let submod_len = submod_key.len(); + let install_sym = crate::nm_install::nm_submod_install_symbol(submod_key); + let blk = ctx.block(); + if let Some(s) = install_sym { + blk.call_void(s, &[]); + } + return Ok(blk.call( + DOUBLE, + "js_node_submodule_namespace", + &[(PTR, &submod_label), (I32, &submod_len.to_string())], + )); + } + // Issue #629: namespace imports for unresolved modules + // (`import * as fsp from "node:fs/promises"`) — when the + // module isn't backed by perry-stdlib bindings or compiled + // sources, the binding lands here. Pre-fix the catch-all + // returned TAG_TRUE so `typeof fsp === "boolean"` and every + // property access produced the confusing "(boolean).X is + // not a function" error. Route to the runtime stub which + // returns an empty-object pointer (typeof "object", every + // property reads undefined). Namespace bindings registered + // in `ctx.namespace_imports` already short-circuit via + // dedicated arms above; this catch-all only fires for + // names with no resolution at all. + if ctx.namespace_imports.contains(name) { + // A namespace import used as a whole VALUE (passed to a + // function, iterated by `Object.keys`/`for…in`/`Object.entries`, + // spread, …) must be a real object whose OWN ENUMERABLE + // properties are the source module's exports — not the empty + // `js_unresolved_namespace_stub`. Drizzle's + // `drizzle(pool, { schema })` (with `import * as schema`) and + // Stripe's `_prepResources` (`for (const name in resources)` + // over `import * as resources`) both enumerate the namespace and + // silently saw zero members otherwise. Materialize the object by + // resolving each exported member through the SAME per-member + // `ns.member` PropertyGet lowering (functions → closure + // singletons, consts → getters, classes → class refs). + let mut members: Vec = ctx + .namespace_member_prefixes + .keys() + .filter(|(ns, _)| ns == name) + .map(|(_, m)| m.clone()) + .collect(); + if !members.is_empty() { + members.sort(); + members.dedup(); + let n_str = (members.len() as u32).to_string(); + let zero_str = "0".to_string(); + let handle = ctx.block().call( + I64, + "js_object_alloc", + &[(I32, &zero_str), (I32, &n_str)], + ); + for member in &members { + let member_get = Expr::PropertyGet { + object: Box::new(Expr::ExternFuncRef { + name: name.clone(), + param_types: Vec::new(), + return_type: HirType::Any, + }), + property: member.clone(), + }; + let v_box = lower_expr(ctx, &member_get)?; + let key_idx = ctx.strings.intern(member); + let key_handle_global = + format!("@{}", ctx.strings.entry(key_idx).handle_global); + let blk = ctx.block(); + let key_box = blk.load(DOUBLE, &key_handle_global); + let key_bits = blk.bitcast_double_to_i64(&key_box); + let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); + blk.call_void( + "js_object_set_field_by_name", + &[(I64, &handle), (I64, &key_raw), (DOUBLE, &v_box)], + ); + } + let blk = ctx.block(); + return Ok(nanbox_pointer_inline(blk, &handle)); + } + return Ok(ctx + .block() + .call(DOUBLE, "js_unresolved_namespace_stub", &[])); + } // #4950: built-in globals that HIR lowers to `ExternFuncRef` even // in VALUE position (the `is_builtin_function` timer set — // `setTimeout`, `setImmediate`, …) used to fall through to the diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 43dbe3e59..13969ae36 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -1407,7 +1407,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let s_box = lower_expr(ctx, object)?; let idx_d = lower_expr(ctx, index)?; let blk = ctx.block(); - let s_handle = unbox_str_handle(blk, &s_box); + let s_handle = unbox_to_i64(blk, &s_box); // #3987: route through the canonical-index runtime helper (it // takes the raw NaN-boxed key, not an `fptosi`'d i32) so a valid // array index returns its char and every non-canonical key diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 7522f422c..46c99e76f 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -72,7 +72,6 @@ pub(crate) fn builtin_parent_reserved_class_id(name: &str) -> Option { "DataView" => 0xFFFF002B, "WeakMap" => 0xFFFF002C, "WeakSet" => 0xFFFF002D, - "URL" => 0xFFFF002F, "Promise" => 0xFFFF0027, "Number" => 0xFFFF00D0, "String" => 0xFFFF00D1, @@ -420,10 +419,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // `Blob` — stream consumers allocate a scoped Blob-shaped // ObjectHeader tagged with this reserved class id. "Blob" => 0xFFFF0026u32, - // `File` — Blob-registry handles with File metadata. The - // runtime also treats File handles as Blob instances. - "File" => 0xFFFF002Eu32, - "URL" => 0xFFFF002Fu32, // `Promise` — runtime detects via GC_TYPE_PROMISE because // Promise values are raw promise allocations, not ObjectHeader // instances with a class_id field. diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 841927181..d4b0ddaba 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -454,20 +454,13 @@ pub(crate) struct FnCtx<'a> { /// by namespace member access lowering to disambiguate when the same /// export name appears in multiple `import * as X / Y` sources. pub namespace_member_prefixes: &'a std::collections::HashMap<(String, String), String>, - /// Per-namespace member origin-name overrides (#680 / #5924). Consulted - /// before the flat `import_function_origin_names` when computing the symbol + /// Issue #5924: per-namespace origin-name resolution. Keyed by + /// `(namespace_local_name, member_name)` → `origin_name`. Consulted + /// before `import_function_origin_names` when computing the symbol /// suffix for a namespace-member access, so a re-export rename in one /// namespace can't clobber another namespace's unrenamed member of the /// same name. pub namespace_member_origin_names: &'a std::collections::HashMap<(String, String), String>, - /// Per-namespace exported-variable members. - pub namespace_member_vars: &'a std::collections::HashSet<(String, String)>, - /// Per-namespace nested namespace re-exports. - pub namespace_member_namespace_prefixes: - &'a std::collections::HashMap<(String, String), String>, - /// Namespace import local → target module prefix for whole-namespace value - /// reads. - pub namespace_import_prefixes: &'a std::collections::HashMap, /// Names of imported functions that are async. Used to wrap /// cross-module calls in promise machinery. // #854: cross-module async-import wrapping context; currently routed via diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index bd4ba23f5..fcdf8bae1 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -111,17 +111,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let blk = ctx.block(); let recv_handle = unbox_to_i64(blk, &recv_box); let arr_handle = blk.call(I64, "js_error_get_errors", &[(I64, &recv_handle)]); - let is_missing = blk.icmp_eq(I64, &arr_handle, "0"); - let tagged = nanbox_pointer_inline(blk, &arr_handle); - let tagged_bits = blk.bitcast_double_to_i64(&tagged); - let selected = blk.select( - I1, - &is_missing, - I64, - crate::nanbox::TAG_UNDEFINED_I64, - &tagged_bits, - ); - Ok(blk.bitcast_i64_to_double(&selected)) + Ok(nanbox_pointer_inline(blk, &arr_handle)) } Expr::PropertyGet { object, property } @@ -576,8 +566,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // discards the INT32 tag during the unbox and ends up returning // undefined. let is_class_ref_object = matches!(object.as_ref(), Expr::ClassRef(_)) - || matches!(object.as_ref(), Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name) - && !ctx.namespace_imports.contains(name)); + || matches!(object.as_ref(), Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name)); if is_class_ref_object { let obj_box = lower_expr(ctx, object)?; let key_idx = ctx.strings.intern(property); @@ -847,14 +836,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // misses the class ref and falls back to the global // `Number`, dropping all inherited statics (effect's // `S.Number.ast`). - if let Some(nested_prefix) = ctx - .namespace_member_namespace_prefixes - .get(&(name.clone(), property.clone())) - { - return Ok(ctx - .block() - .load(DOUBLE, &format!("@__perry_ns_{}", nested_prefix))); - } let class_cid = ctx.class_ids.get(property).copied().or_else(|| { ctx.import_function_origin_names .get(property) @@ -867,19 +848,22 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Issue #680: prefer the per-namespace map so // `random.make` and `tracer.make` resolve to their // own sources even when both modules export `make`. - // Do not fall back to flat import maps here: another - // module can export a homonymous class/function with the - // same local name as this namespace. - let ns_lookup_name = if let Expr::ExternFuncRef { name, .. } = object.as_ref() { + // Falls back to the flat `import_function_prefixes` + // for namespaces with no overlapping conflicts. + let _ns_lookup_name = if let Expr::ExternFuncRef { name, .. } = object.as_ref() + { Some(name.clone()) } else { None }; - let source_prefix_opt = ns_lookup_name.as_ref().and_then(|ns| { - ctx.namespace_member_prefixes - .get(&(ns.clone(), property.clone())) - .cloned() - }); + let source_prefix_opt = _ns_lookup_name + .as_ref() + .and_then(|ns| { + ctx.namespace_member_prefixes + .get(&(ns.clone(), property.clone())) + .cloned() + }) + .or_else(|| ctx.import_function_prefixes.get(property).cloned()); if let Some(source_prefix) = source_prefix_opt { // Issue #678 followup: V8-fallback namespace member // read as a value (e.g. `let r = ns.render`) — there @@ -928,50 +912,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // body. The body only runs later when the consumer // actually calls `HashMap.keySet(self)`, by which time // both modules have finished `__init`. - // Issue #678 / #5924: re-export renames mean the suffix - // in the origin module differs from the consumer-visible - // name. Consult the per-namespace map first (so a rename - // in a different namespace imported into this file can't - // clobber this namespace's unrenamed member of the same - // name), then the flat `import_function_origin_names`. + // Issue #678/#5924: re-export renames mean the suffix + // in the origin module differs from the + // consumer-visible name. Namespace-scoped lookup + // first so a rename in a different namespace + // imported into this file can't clobber this + // namespace's unrenamed member of the same name. let origin_suffix = import_origin_suffix_ns( ctx.import_function_origin_names, ctx.namespace_member_origin_names, - ns_lookup_name.as_deref().unwrap_or(""), + _ns_lookup_name.as_deref().unwrap_or(""), property, ); - // Issue #680: a per-namespace exported VAR member is a - // getter returning a closure — read it off the actual - // namespace object rather than emitting a direct call to - // the zero-arg getter symbol. - let is_namespace_var = ns_lookup_name.as_ref().is_some_and(|ns| { - ctx.namespace_member_vars - .contains(&(ns.clone(), property.clone())) - }); - if is_namespace_var { - if let Some(namespace_prefix) = ns_lookup_name - .as_ref() - .and_then(|ns| ctx.namespace_import_prefixes.get(ns)) - { - let ns_value = ctx - .block() - .load(DOUBLE, &format!("@__perry_ns_{}", namespace_prefix)); - let key_idx = ctx.strings.intern(property); - let key_handle_global = - format!("@{}", ctx.strings.entry(key_idx).handle_global); - let blk = ctx.block(); - let ns_bits = blk.bitcast_double_to_i64(&ns_value); - let ns_handle = blk.and(I64, &ns_bits, POINTER_MASK_I64); - let key_box = blk.load(DOUBLE, &key_handle_global); - let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_handle = blk.and(I64, &key_bits, POINTER_MASK_I64); - return Ok(blk.call( - DOUBLE, - "js_object_get_field_by_name_f64", - &[(I64, &ns_handle), (I64, &key_handle)], - )); - } - } if ctx.imported_vars.contains(property) { let getter = format!("perry_fn_{}__{}", source_prefix, origin_suffix); ctx.pending_declares.push((getter.clone(), DOUBLE, vec![])); diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index d944c682a..52e9efc3d 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -810,13 +810,6 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); } } - let current_class_name = - ctx.class_stack.last().cloned().unwrap_or_default(); - crate::lower_call::apply_field_initializers_recursive( - ctx, - ¤t_class_name, - crate::lower_call::FieldInitMode::SelfOnly, - )?; } return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))); } diff --git a/crates/perry-codegen/src/lower_call/console_promise.rs b/crates/perry-codegen/src/lower_call/console_promise.rs index 80b062ede..0a7e08799 100644 --- a/crates/perry-codegen/src/lower_call/console_promise.rs +++ b/crates/perry-codegen/src/lower_call/console_promise.rs @@ -511,14 +511,6 @@ pub fn try_lower_console_call( Ok(None) } -fn property_get_chain_starts_at_namespace_import(ctx: &FnCtx<'_>, expr: &Expr) -> bool { - let mut current = expr; - while let Expr::PropertyGet { object, .. } = current { - current = object.as_ref(); - } - matches!(current, Expr::ExternFuncRef { name, .. } if ctx.namespace_imports.contains(name)) -} - pub fn try_lower_promise_static_call( ctx: &mut FnCtx<'_>, callee: &Expr, @@ -766,7 +758,6 @@ pub fn try_lower_native_method_str_dispatch( .is_some_and(|kind| is_collection_method_for_kind(kind, property.as_str())); let skip_native = matches!(object.as_ref(), Expr::GlobalGet(_)) || matches!(object.as_ref(), Expr::NativeModuleRef(_)) - || property_get_chain_starts_at_namespace_import(ctx, object) || (class_name_opt.is_some() && !is_buffer_class && !class_unknown_to_codegen diff --git a/crates/perry-codegen/src/lower_call/namespace_call.rs b/crates/perry-codegen/src/lower_call/namespace_call.rs index d12ae7c9d..2b895ce5d 100644 --- a/crates/perry-codegen/src/lower_call/namespace_call.rs +++ b/crates/perry-codegen/src/lower_call/namespace_call.rs @@ -238,6 +238,7 @@ pub fn try_lower_namespace_member_call( .namespace_member_prefixes .get(&(ns_name.clone(), property.clone())) .cloned() + .or_else(|| ctx.import_function_prefixes.get(property).cloned()) else { return Ok(None); }; @@ -269,10 +270,7 @@ pub fn try_lower_namespace_member_call( property, ); let symbol = format!("perry_fn_{}__{}", source_prefix, origin_suffix); - if ctx - .namespace_member_vars - .contains(&(ns_name.clone(), property.clone())) - { + if ctx.imported_vars.contains(property) { // Var-shaped export: fetch closure via zero-arg // getter, then closure-call with the user args. ctx.pending_declares.push((symbol.clone(), DOUBLE, vec![])); @@ -299,22 +297,10 @@ pub fn try_lower_namespace_member_call( // Function-decl-shaped export: direct call with rest bundling. let declared_count = ctx .imported_func_param_counts - .get(origin_suffix) - .or_else(|| ctx.imported_func_param_counts.get(property)) + .get(property) .copied() - .unwrap_or_else(|| { - // No arity metadata for this re-exported function. When it's a - // renamed re-export (origin != property) called with no args, pad a - // single `undefined` so a 1-param callee still receives a slot; - // otherwise fall back to the actual arg count. - if args.is_empty() && origin_suffix != property { - 1 - } else { - args.len() - } - }); - let has_rest = ctx.imported_func_has_rest.contains(origin_suffix) - || ctx.imported_func_has_rest.contains(property); + .unwrap_or(args.len()); + let has_rest = ctx.imported_func_has_rest.contains(property); let mut lowered: Vec = Vec::with_capacity(declared_count); if has_rest { let fixed_count = declared_count.saturating_sub(1); diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index 1ec3f2877..81577062a 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -13,10 +13,9 @@ use super::field_init::{apply_field_initializers_recursive, FieldInitMode}; use super::lower_builtin_new; use super::new_helpers::{ collect_decl_local_ids, ctor_body_calls_super, ctor_body_closure_calls_super, - ctor_body_has_value_return, ctor_body_uses_this, ctor_chain_uses_new_target, - effective_constructor_param_count, emit_promise_subclass_init, local_constructor_symbol_exists, - map_set_default_super_kind, node_stream_parent_kind, restore_imported_ctor_new_target, - set_imported_ctor_new_target, + ctor_body_has_value_return, ctor_body_uses_new_target, ctor_body_uses_this, + ctor_chain_uses_new_target, effective_constructor_param_count, emit_promise_subclass_init, + local_constructor_symbol_exists, node_stream_parent_kind, }; use crate::expr::{lower_expr, lower_js_args_array, nanbox_pointer_inline, FnCtx}; use crate::nanbox::{double_literal, POINTER_MASK_I64}; @@ -309,6 +308,12 @@ fn marshal_imported_ctor_args( } } +/// The effective constructor arity for `new (...)`: the class's own +/// ctor params, else — for a subclass with no own ctor — the closest +/// ancestor-with-a-ctor's param count (the synthesized default ctor forwards +/// `super(...args)`). Matches the standalone-ctor signature emitted in +/// `codegen/artifacts.rs`, so callers pass the right number of args. + /// Emit a call to the shared standalone `_constructor` symbol and /// return the raw value it produced. The standalone ctor function returns /// `undefined` for an ordinary constructor (implicit `return this`) or the @@ -1315,14 +1320,9 @@ fn lower_new_impl( None }; // `class X extends Promise {}` with no own ctor — `new X(executor)` runs the - // Promise constructor against a hidden backing cell (see new_helpers). + // Promise constructor against a hidden backing cell (see new_helpers). (#5991) let promise_parent_runtime = !has_own_ctor && !has_imported_ctor && class.extends_name.as_deref() == Some("Promise"); - let map_set_parent_kind = if !has_own_ctor && !has_imported_ctor { - map_set_default_super_kind(ctx.classes, class.extends_name.as_deref()) - } else { - None - }; let inherited_ctor_class: Option = if !has_own_ctor && has_extends { // Walk the inheritance chain to find the closest ancestor with // an explicit ctor — same logic as the body-inlining loop below. @@ -1738,9 +1738,7 @@ fn lower_new_impl( // to match the symbol's real signature (see codegen/mod.rs). ctx.pending_declares .push((ctor.symbol.clone(), DOUBLE, ctor_param_types)); - let saved_new_target = set_imported_ctor_new_target(ctx, class_name, &ctor); let _ = ctx.block().call(DOUBLE, &ctor.symbol, &ctor_args); - restore_imported_ctor_new_target(ctx, saved_new_target); } else if let Some(ctor) = ctx.imported_class_ctors.get(class_name).cloned() { // Pad missing optional args with TAG_UNDEFINED so the constructor // doesn't read garbage from stale registers, and pack the rest @@ -1766,9 +1764,7 @@ fn lower_new_impl( // ("value is not a function" on `new Chalk(...).red(...)`). ctx.pending_declares .push((ctor.symbol.clone(), DOUBLE, ctor_param_types)); - let saved_new_target = set_imported_ctor_new_target(ctx, class_name, &ctor); let ctor_ret = ctx.block().call(DOUBLE, &ctor.symbol, &ctor_args); - restore_imported_ctor_new_target(ctx, saved_new_target); ctx.block().store(DOUBLE, &ctor_ret, &ctor_result_slot); found_inherited_ctor = true; } @@ -1819,6 +1815,7 @@ fn lower_new_impl( .unwrap_or(false); if !found_inherited_ctor && class.extends_expr.is_some() && !parent_is_uncallable_builtin { if let Some(cid) = ctx.class_ids.get(class_name).copied().filter(|c| *c != 0) { + let undef_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); let parent_val = ctx.block().call( DOUBLE, "js_get_dynamic_parent_value", @@ -1860,30 +1857,6 @@ fn lower_new_impl( ); } } - if !found_inherited_ctor { - if let Some(kind) = map_set_parent_kind { - let undef_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); - let iterable = lowered_args - .first() - .cloned() - .unwrap_or_else(|| undef_lit.clone()); - let this_box = ctx - .this_stack - .last() - .cloned() - .map(|slot| ctx.block().load(DOUBLE, &slot)) - .unwrap_or_else(|| undef_lit.clone()); - ctx.block().call( - DOUBLE, - "js_map_set_subclass_init", - &[ - (DOUBLE, &this_box), - (I32, &kind.to_string()), - (DOUBLE, &iterable), - ], - ); - } - } } // Now that the parent body chain has run (setting `this.config`, etc.), @@ -1928,20 +1901,8 @@ fn lower_new_impl( class_name, FieldInitMode::BetweenExclusiveTo(stop_at), )?; - } else if class - .extends_name - .as_deref() - .map(|p| ctx.classes.contains_key(p)) - .unwrap_or(false) - { - apply_field_initializers_recursive(ctx, class_name, FieldInitMode::AfterRoot)?; } else { - // Extends a builtin (Error/Array/Map/Set/…) with no user-class - // ancestor: the leaf IS the root of the user chain, so AfterRoot - // (chain[1..]) drops its own fields. Apply the leaf's initializers - // after the builtin super-init, mirroring the explicit-super() - // SelfOnly arm in this_super_call.rs. - apply_field_initializers_recursive(ctx, class_name, FieldInitMode::SelfOnly)?; + apply_field_initializers_recursive(ctx, class_name, FieldInitMode::AfterRoot)?; } } emit_typed_shape_layout_init(ctx, class_name, &obj_handle); diff --git a/crates/perry-codegen/src/lower_call/new_helpers.rs b/crates/perry-codegen/src/lower_call/new_helpers.rs index ed9cb0ccb..514b73db2 100644 --- a/crates/perry-codegen/src/lower_call/new_helpers.rs +++ b/crates/perry-codegen/src/lower_call/new_helpers.rs @@ -6,7 +6,7 @@ //! to drive `lower_new`'s static no-super-throw / inline-vs-call decisions, //! plus `node_stream_parent_kind` and `collect_decl_local_ids`. -use perry_hir::Expr; +use perry_hir::{Class, Expr}; use crate::expr::FnCtx; use crate::types::DOUBLE; @@ -241,146 +241,6 @@ pub(crate) fn ctor_body_has_value_return(body: &[perry_hir::Stmt]) -> bool { ) } -/// The effective constructor arity for `new (...)`: the class's own -/// ctor params, else — for a subclass with no own ctor — the closest -/// ancestor-with-a-ctor's param count (the synthesized default ctor forwards -/// `super(...args)`). Matches the standalone-ctor signature emitted in -/// `codegen/artifacts.rs`, so callers pass the right number of args. -pub(super) fn effective_constructor_param_count( - ctx: &FnCtx<'_>, - class: &perry_hir::Class, -) -> usize { - if let Some(ctor) = class.constructor.as_ref() { - return ctor.params.len(); - } - let mut parent = class.extends_name.as_deref(); - while let Some(pname) = parent { - if let Some(ctor) = ctx.imported_class_ctors.get(pname) { - if ctor.stops_constructor_walk() { - return ctor.param_count; - } - } - match ctx.classes.get(pname).copied() { - Some(pc) => { - if let Some(pctor) = pc.constructor.as_ref() { - return pctor.params.len(); - } - parent = pc.extends_name.as_deref(); - } - None => break, - } - } - 0 -} - -/// True when the standalone `_constructor` symbol exists (so the -/// recursion-guard / capture-collision redirect can call it instead of -/// inlining). Mirrors the lookup in `call_local_constructor_symbol`. -pub(super) fn local_constructor_symbol_exists(ctx: &FnCtx<'_>, class: &perry_hir::Class) -> bool { - let ctor_method_name = format!("{}_constructor", class.name); - ctx.methods - .contains_key(&(class.name.clone(), ctor_method_name)) -} - -/// #2768: true when the standalone `_constructor` symbol's body reads -/// `new.target` — either the class's OWN ctor body, or an ancestor ctor body -/// it reaches through `super(...)`. The symbol is a separately compiled -/// function whose only `new.target` source is the runtime cell, and a -/// `super(...)` call inlines the parent ctor body into that same symbol, so an -/// ancestor that reads `new.target` (e.g. an abstract-class guard in a base) -/// still observes the cell. Gating the cell write on the WHOLE chain keeps -/// `new Child()` correct when only the inherited body reads `new.target`, while -/// a chain with no reader anywhere stays on the zero-overhead fast path. The -/// walk follows `extends_name` through the codegen class map; an unresolved -/// parent name just stops the walk, and a depth cap guards a cyclic graph. -pub(super) fn ctor_chain_uses_new_target(ctx: &FnCtx<'_>, class: &perry_hir::Class) -> bool { - let reads = |c: &perry_hir::Class| { - c.constructor - .as_ref() - .is_some_and(|f| ctor_body_uses_new_target(&f.body)) - }; - if reads(class) { - return true; - } - let mut parent = class.extends_name.as_deref(); - let mut depth = 0; - while let Some(parent_name) = parent { - depth += 1; - if depth > 64 { - break; - } - let Some(pc) = ctx.classes.get(parent_name).copied() else { - break; - }; - if reads(pc) { - return true; - } - parent = pc.extends_name.as_deref(); - } - false -} - -pub(super) fn map_set_default_super_kind<'a>( - classes: &std::collections::HashMap, - mut parent: Option<&'a str>, -) -> Option { - // Bound the walk like `ctor_chain_uses_new_target` above: a cyclic - // `extends` graph of constructorless classes would otherwise loop forever. - // Keep this bound in sync with the twin in codegen/method.rs. - let mut depth = 0usize; - while let Some(name) = parent { - depth += 1; - if depth > 64 { - return None; - } - match name { - "Map" => return Some(0), - "Set" => return Some(1), - _ => {} - } - let class = classes.get(name)?; - if class.constructor.is_some() { - return None; - } - parent = class.extends_name.as_deref(); - } - None -} - -pub(super) fn set_imported_ctor_new_target( - ctx: &mut FnCtx<'_>, - constructed_class_name: &str, - ctor: &crate::codegen::ImportedCtor, -) -> Option { - if !ctor.uses_new_target { - return None; - } - ctx.class_ids.get(constructed_class_name).map(|&cid| { - let prev = ctx - .block() - .call(crate::types::DOUBLE, "js_new_target_get", &[]); - let class_ref = crate::nanbox::double_literal(f64::from_bits( - crate::nanbox::INT32_TAG | (cid as u64 & 0xFFFF_FFFF), - )); - ctx.block().call( - crate::types::DOUBLE, - "js_new_target_set", - &[(crate::types::DOUBLE, &class_ref)], - ); - prev - }) -} - -pub(super) fn restore_imported_ctor_new_target(ctx: &mut FnCtx<'_>, saved: Option) { - if let Some(prev) = saved { - ctx.block().call( - crate::types::DOUBLE, - "js_new_target_set", - &[(crate::types::DOUBLE, &prev)], - ); - } -} - pub(super) fn node_stream_parent_kind( ctx: &FnCtx<'_>, class: &perry_hir::Class, @@ -472,3 +332,74 @@ pub(super) fn collect_decl_local_ids( } } } + +pub(crate) fn effective_constructor_param_count(ctx: &FnCtx<'_>, class: &Class) -> usize { + if let Some(ctor) = class.constructor.as_ref() { + return ctor.params.len(); + } + let mut parent = class.extends_name.as_deref(); + while let Some(pname) = parent { + if let Some(ctor) = ctx.imported_class_ctors.get(pname) { + if ctor.stops_constructor_walk() { + return ctor.param_count; + } + } + match ctx.classes.get(pname).copied() { + Some(pc) => { + if let Some(pctor) = pc.constructor.as_ref() { + return pctor.params.len(); + } + parent = pc.extends_name.as_deref(); + } + None => break, + } + } + 0 +} + +/// True when the standalone `_constructor` symbol exists (so the +/// recursion-guard / capture-collision redirect can call it instead of +/// inlining). Mirrors the lookup in `call_local_constructor_symbol`. +pub(crate) fn local_constructor_symbol_exists(ctx: &FnCtx<'_>, class: &Class) -> bool { + let ctor_method_name = format!("{}_constructor", class.name); + ctx.methods + .contains_key(&(class.name.clone(), ctor_method_name)) +} + +/// #2768: true when the standalone `_constructor` symbol's body reads +/// `new.target` — either the class's OWN ctor body, or an ancestor ctor body +/// it reaches through `super(...)`. The symbol is a separately compiled +/// function whose only `new.target` source is the runtime cell, and a +/// `super(...)` call inlines the parent ctor body into that same symbol, so an +/// ancestor that reads `new.target` (e.g. an abstract-class guard in a base) +/// still observes the cell. Gating the cell write on the WHOLE chain keeps +/// `new Child()` correct when only the inherited body reads `new.target`, while +/// a chain with no reader anywhere stays on the zero-overhead fast path. The +/// walk follows `extends_name` through the codegen class map; an unresolved +/// parent name just stops the walk, and a depth cap guards a cyclic graph. +pub(crate) fn ctor_chain_uses_new_target(ctx: &FnCtx<'_>, class: &Class) -> bool { + let reads = |c: &Class| { + c.constructor + .as_ref() + .is_some_and(|f| ctor_body_uses_new_target(&f.body)) + }; + if reads(class) { + return true; + } + let mut parent = class.extends_name.as_deref(); + let mut depth = 0; + while let Some(parent_name) = parent { + depth += 1; + if depth > 64 { + break; + } + let Some(pc) = ctx.classes.get(parent_name).copied() else { + break; + }; + if reads(pc) { + return true; + } + parent = pc.extends_name.as_deref(); + } + false +} diff --git a/crates/perry-codegen/src/lower_call/property_get.rs b/crates/perry-codegen/src/lower_call/property_get.rs index f1840d094..f6723af32 100644 --- a/crates/perry-codegen/src/lower_call/property_get.rs +++ b/crates/perry-codegen/src/lower_call/property_get.rs @@ -95,8 +95,10 @@ pub fn try_lower_property_get_method_call( // array. startsWith/endsWith are string-only in JS so the // 2-arg form (searchString, position) is also unambiguous. let is_string_only_method = match property.as_str() { - "split" | "charCodeAt" | "charAt" | "substring" | "substr" | "replaceAll" - | "padStart" | "padEnd" | "repeat" | "codePointAt" | "localeCompare" => true, + "split" | "charCodeAt" | "charAt" | "trim" | "trimStart" | "trimEnd" | "substring" + | "substr" | "toLowerCase" | "toUpperCase" | "toLocaleLowerCase" + | "toLocaleUpperCase" | "replaceAll" | "padStart" | "padEnd" | "repeat" + | "codePointAt" | "localeCompare" => true, // Annex B §B.2.2 HTML wrappers (`bold`, `link`, `anchor`, …) are // string-only in the spec but collide with common user method // names — chalk's `chalk.bold(s)` is a styled-string builder @@ -117,13 +119,6 @@ pub fn try_lower_property_get_method_call( // js_native_call_method fallback handles it correctly via // js_string_replace_string. "replace" if args.len() == 2 && matches!(&args[1], Expr::Closure { .. }) => true, - // trim / case-conversion names are also commonly defined as their - // own methods on Any-typed library/builder objects. Let runtime - // dispatch choose by receiver shape so those objects keep their own - // methods while real strings still use String.prototype methods - // (same treatment as `slice`/`indexOf`/`normalize` above). - "trim" | "trimStart" | "trimEnd" | "toLowerCase" | "toUpperCase" - | "toLocaleLowerCase" | "toLocaleUpperCase" => false, // `slice` exists on strings, arrays, buffers, and Blob-like // objects. Let the runtime dispatcher choose by receiver shape; // forcing the string path here turns Blob slices into string diff --git a/crates/perry-codegen/src/lower_call/property_get/number_string.rs b/crates/perry-codegen/src/lower_call/property_get/number_string.rs index 82920219d..26966e8bf 100644 --- a/crates/perry-codegen/src/lower_call/property_get/number_string.rs +++ b/crates/perry-codegen/src/lower_call/property_get/number_string.rs @@ -14,11 +14,10 @@ use crate::type_analysis::{ is_array_expr, is_global_constructor_expr, is_map_expr, is_native_module_dynamic_index, is_promise_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, }; -use crate::type_analysis::{is_bigint_expr, is_numeric_expr}; use crate::types::{DOUBLE, I32, I64}; /// Number `.toFixed` / `.toPrecision` / `.toExponential`, Buffer/Number -/// `.toString(encoding|radix)`, and numeric `.toString()` arms. Returns +/// `.toString(encoding|radix)`, and the universal `.toString()` arms. Returns /// `Ok(Some(_))` when one of these claims the call, otherwise `Ok(None)` so the /// caller continues down the dispatch tower. pub(crate) fn try_lower_number_string_methods( @@ -153,7 +152,6 @@ pub(crate) fn try_lower_number_string_methods( // "ff" instead of "255". if property == "toString" && args.len() == 1 - && (is_numeric_expr(ctx, object) || is_bigint_expr(ctx, object)) && !is_string_expr(ctx, object) && !is_array_expr(ctx, object) && !is_date_receiver(ctx, object) @@ -190,12 +188,15 @@ pub(crate) fn try_lower_number_string_methods( return Ok(Some(nanbox_string_inline(blk, &handle))); } } - // Numeric `.toString()` without a radix. Do not claim arbitrary Any - // receivers here: plain objects may define an own `toString` method that - // must be called with the source arguments. + // Universal `.toString()` — works for any JS value via the + // runtime's js_jsvalue_to_string dispatch (numbers print as + // their decimal form, strings as themselves, objects as + // [object Object], etc.). Only intercepts if NO class + // method dispatch can win (i.e. the receiver isn't a known + // class with its own toString) — otherwise the user's + // override wouldn't run. if property == "toString" && args.len() <= 1 - && (is_numeric_expr(ctx, object) || is_bigint_expr(ctx, object)) && !is_string_expr(ctx, object) && !is_array_expr(ctx, object) && !is_date_receiver(ctx, object) diff --git a/crates/perry-codegen/tests/argless_builtin_extra_args.rs b/crates/perry-codegen/tests/argless_builtin_extra_args.rs index 27214a66a..278c077eb 100644 --- a/crates/perry-codegen/tests/argless_builtin_extra_args.rs +++ b/crates/perry-codegen/tests/argless_builtin_extra_args.rs @@ -22,9 +22,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/class_keys_gc_root.rs b/crates/perry-codegen/tests/class_keys_gc_root.rs index 6ea648e60..d300b854d 100644 --- a/crates/perry-codegen/tests/class_keys_gc_root.rs +++ b/crates/perry-codegen/tests/class_keys_gc_root.rs @@ -41,9 +41,6 @@ fn entry_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/constructor_recursion.rs b/crates/perry-codegen/tests/constructor_recursion.rs index 12c304836..2ec991984 100644 --- a/crates/perry-codegen/tests/constructor_recursion.rs +++ b/crates/perry-codegen/tests/constructor_recursion.rs @@ -16,9 +16,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/destructure_call_location.rs b/crates/perry-codegen/tests/destructure_call_location.rs index a86cf511f..8dd2ed1ec 100644 --- a/crates/perry-codegen/tests/destructure_call_location.rs +++ b/crates/perry-codegen/tests/destructure_call_location.rs @@ -33,9 +33,6 @@ fn base_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/large_object_barriers.rs b/crates/perry-codegen/tests/large_object_barriers.rs index ce8238d1c..76a61cec1 100644 --- a/crates/perry-codegen/tests/large_object_barriers.rs +++ b/crates/perry-codegen/tests/large_object_barriers.rs @@ -16,9 +16,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs index 2bf26ce1b..874805536 100644 --- a/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs +++ b/crates/perry-codegen/tests/macos_bundle_chdir_gate.rs @@ -22,9 +22,6 @@ fn entry_opts(target: Option<&str>) -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index 89bbcb64f..ae3bdd2bc 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -21,9 +21,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 34595502c..37d763c4a 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -22,9 +22,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, @@ -1350,8 +1347,7 @@ fn pod_field_read_after_dynamic_materialization_uses_number_coerce() { let ir = compile_ir("pod_dynamic_materialized_read_coerce.ts", body); assert!( - ir.contains("call double @js_number_coerce") - || ir.contains("call double @js_object_get_field_by_name_f64"), + ir.contains("call double @js_number_coerce"), "POD field reads after dynamic materialization must not feed boxed JSValue fallbacks into raw numeric arithmetic:\n{ir}" ); } @@ -7429,7 +7425,6 @@ fn typed_f64_clone_test_module(use_any_param: bool) -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, @@ -7601,7 +7596,6 @@ fn typed_i1_clone_test_module_named(name: &str) -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, @@ -7693,7 +7687,6 @@ fn typed_string_clone_test_module(case: &str) -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, @@ -7807,7 +7800,6 @@ fn typed_i1_numeric_predicate_module() -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, @@ -7884,7 +7876,6 @@ fn typed_i1_i32_predicate_module() -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, @@ -8010,7 +8001,6 @@ fn typed_i32_return_module(case: &str) -> Module { exported_functions: Vec::new(), script_global_functions: Vec::new(), references_global_this: false, - annexb_global_undefined_names: Vec::new(), widgets: Vec::new(), uses_fetch: false, uses_webassembly: false, diff --git a/crates/perry-codegen/tests/shadow_slot_hygiene.rs b/crates/perry-codegen/tests/shadow_slot_hygiene.rs index 94bad0424..059f9e49b 100644 --- a/crates/perry-codegen/tests/shadow_slot_hygiene.rs +++ b/crates/perry-codegen/tests/shadow_slot_hygiene.rs @@ -16,9 +16,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/static_symbol_hygiene.rs b/crates/perry-codegen/tests/static_symbol_hygiene.rs index a68400bb0..495306dd4 100644 --- a/crates/perry-codegen/tests/static_symbol_hygiene.rs +++ b/crates/perry-codegen/tests/static_symbol_hygiene.rs @@ -16,9 +16,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index 3c953b6a0..e6f8474e4 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -47,9 +47,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/typed_shape_descriptor.rs b/crates/perry-codegen/tests/typed_shape_descriptor.rs index 77b7c42d5..3aa85b3d1 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptor.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptor.rs @@ -16,9 +16,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index 0e0ad4718..f9290a0ce 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -46,9 +46,6 @@ fn empty_opts() -> CompileOptions { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: true, verify_native_regions: false, disable_buffer_fast_path: false, diff --git a/crates/perry-hir/src/dynamic_import.rs b/crates/perry-hir/src/dynamic_import.rs index f45856b17..b66b7c9d3 100644 --- a/crates/perry-hir/src/dynamic_import.rs +++ b/crates/perry-hir/src/dynamic_import.rs @@ -156,28 +156,18 @@ fn flatten_into<'a, F>( imported, exported, } => { - let mut source_exports = Vec::new(); - let mut nested_visited = visited.clone(); - flatten_into(source, lookup, &mut source_exports, &mut nested_visited); - let resolved = source_exports - .into_iter() - .rev() - .find(|entry| entry.name == *imported); - if let Some(entry) = resolved { - out.push(FlatExport { - name: exported.clone(), - source_module: entry.source_module, - source_local: entry.source_local, - nested_namespace_of: entry.nested_namespace_of, - }); - } else { - out.push(FlatExport { - name: exported.clone(), - source_module: source.clone(), - source_local: imported.clone(), - nested_namespace_of: None, - }); - } + // The value lives in `source`; if `source` re-exports it + // again, we want to follow that chain so codegen reaches + // the ULTIMATE owner. But for the MVP we surface one hop + // — the cross-module access pattern works regardless of + // how many hops away the value's defining module is, as + // long as we name the directly-importing source. + out.push(FlatExport { + name: exported.clone(), + source_module: source.clone(), + source_local: imported.clone(), + nested_namespace_of: None, + }); } Export::ExportAll { source } => { // Recursively flatten the source's exports into ours. @@ -1363,9 +1353,12 @@ pub fn resolve_import_path_with_context>( // chunk set is what we want to ingest, and the runtime dispatch still // picks the right one by path string). // - // Guard rail: the driver validates these strings with the real module - // resolver before registering dynamic edges. Unresolvable values keep - // the site deferred instead of compiling arbitrary data-object strings. + // Guard rail: this only fires when *every* value resolves to a relative + // module specifier (`./…` / `../…`). That keeps it a strict + // deferrals-into-compiles change — a plain data object indexed for a + // non-module reason (`const cfg = { name: "app", port: "3000" }; + // import(cfg[k])`) has non-relative values, so it stays deferred exactly + // as before instead of trying to compile `"app"`/`"3000"` as modules. Expr::PropertyGet { object, .. } | Expr::IndexGet { object, .. } => { // Record every const-local id on the registry's indirection chain in // the *outer* `visiting` set so a value that member-accesses back @@ -1394,6 +1387,13 @@ pub fn resolve_import_path_with_context>( } } +/// True for a relative module specifier (`./x`, `../x`). Registry-object +/// dynamic-import resolution (#5207) only over-approximates to values that look +/// like relative chunk paths, so a non-module data object stays deferred. +fn is_relative_specifier(s: &str) -> bool { + s.starts_with("./") || s.starts_with("../") || s == "." || s == ".." +} + /// #5207: collect the candidate value expressions of a const object-literal /// "registry", following `const`-local indirection (`const R = { … }; /// import(R[k])`). Handles both object-literal HIR shapes: open-shape literals @@ -1431,9 +1431,9 @@ fn object_registry_values<'a, V: Borrow>( } /// #5207: resolve a registry's value expressions to the union of their module -/// specifier strings. Filesystem/package resolution is intentionally left to -/// the compile driver; if any string cannot be resolved there, the site stays -/// deferred so data-object strings are not compiled as modules. +/// specifiers, but only when *every* value resolves to a relative specifier. +/// Any non-relative or unresolvable value collapses the whole site to +/// `Unresolved` so it keeps deferring (no false compile of a non-module string). fn resolve_registry_value_union>( values: &[&Expr], consts: &std::collections::HashMap, @@ -1452,7 +1452,7 @@ fn resolve_registry_value_union>( ) { Resolution::Set(set) => { for s in set { - if !is_relative_module_specifier(&s) { + if !is_relative_specifier(&s) { return Resolution::Unresolved(NOT_STATICALLY_RESOLVABLE.to_string()); } if !out.contains(&s) { @@ -1470,10 +1470,6 @@ fn resolve_registry_value_union>( } } -fn is_relative_module_specifier(value: &str) -> bool { - value.starts_with("./") || value.starts_with("../") -} - /// Shared "couldn't statically resolve this dynamic import() specifier" /// message used by the resolver's fall-through arms. const NOT_STATICALLY_RESOLVABLE: &str = diff --git a/crates/perry-hir/src/dynamic_import/tests.rs b/crates/perry-hir/src/dynamic_import/tests.rs index d434422df..47a7cc2c6 100644 --- a/crates/perry-hir/src/dynamic_import/tests.rs +++ b/crates/perry-hir/src/dynamic_import/tests.rs @@ -941,36 +941,6 @@ fn flatten_export_all_cycle_safe() { assert!(names.contains(&"fromB".to_string())); } -#[test] -fn flatten_named_reexport_cycle_safe() { - let mut a = Module::new("a"); - a.exports.push(Export::ReExport { - source: "b".into(), - imported: "value".into(), - exported: "value".into(), - }); - let mut b = Module::new("b"); - b.exports.push(Export::ReExport { - source: "a".into(), - imported: "value".into(), - exported: "value".into(), - }); - b.exports.push(Export::Named { - local: "fallback".into(), - exported: "other".into(), - }); - let map = std::collections::HashMap::from([ - ("a".to_string(), a.clone()), - ("b".to_string(), b.clone()), - ]); - let lookup = |s: &str| map.get(s); - let flat = flatten_exports("a", &lookup); - assert_eq!(flat.len(), 1); - assert_eq!(flat[0].name, "value"); - assert_eq!(flat[0].source_module, "a"); - assert_eq!(flat[0].source_local, "value"); -} - #[test] fn flatten_namespace_re_export() { let mut m = Module::new("m"); diff --git a/crates/perry-hir/src/dynamic_import/visitors.rs b/crates/perry-hir/src/dynamic_import/visitors.rs index 1ca542aca..d7b1fc5a2 100644 --- a/crates/perry-hir/src/dynamic_import/visitors.rs +++ b/crates/perry-hir/src/dynamic_import/visitors.rs @@ -437,11 +437,6 @@ fn visit_expr_for_dyn_imports_ref(expr: &Expr, f: &mut F) { if matches!(expr, Expr::DynamicImport { .. }) { f(expr); } - if let Expr::Closure { body, .. } = expr { - for s in body { - visit_stmt_for_dyn_imports_ref(s, f); - } - } walk_expr_children(expr, &mut |child| visit_expr_for_dyn_imports_ref(child, f)); } diff --git a/crates/perry-hir/src/eval_classifier.rs b/crates/perry-hir/src/eval_classifier.rs index 0464abfd8..29279c1dc 100644 --- a/crates/perry-hir/src/eval_classifier.rs +++ b/crates/perry-hir/src/eval_classifier.rs @@ -319,21 +319,20 @@ pub fn eval_override_enabled() -> bool { env_flag("PERRY_ALLOW_EVAL") } -/// Whether trivial dynamic-codegen capability probes should report unavailable. -/// A no-op `new Function("")` / `Function("")` is commonly used as an -/// `unsafe-eval` feature-test. Perry is ahead-of-time compiled, so the default -/// is to throw at construction for that probe shape and let callers select their -/// non-codegen fallback. Set `PERRY_EVAL_CSP=0` to opt back into const-folding -/// the empty function. Real literal bodies (`new Function("return 42")`, the -/// `return this` globalThis polyfill) still fold. +/// Whether `PERRY_EVAL_CSP` is set — present runtime dynamic-code generation as +/// *unavailable* to capability probes. With it set, a trivial no-op +/// `new Function("")` / `Function("")` (the canonical +/// `try { new Function(""), true } catch { false }` feature-test — the same +/// shape a CSP `unsafe-eval` policy blocks) throws at construction instead of +/// const-folding to a no-op function. Libraries that probe this way (e.g. zod 4's +/// object-validator JIT) then take their non-codegen interpreter fallback, which +/// perry compiles normally. Default off (spec-compliant: Node returns an empty +/// function); opt-in for ahead-of-time binaries that hit such a JIT. Only the +/// trivial no-op shape is affected — real literal bodies (`new Function("return +/// 42")`, the `return this` globalThis polyfill) still fold, so those idioms are +/// unaffected. pub fn eval_csp_probe_unavailable() -> bool { - match std::env::var("PERRY_EVAL_CSP") { - Ok(v) => { - let v = v.trim().to_ascii_lowercase(); - !matches!(v.as_str(), "" | "0" | "off" | "false" | "no") - } - Err(_) => true, - } + env_flag("PERRY_EVAL_CSP") } /// Whether `PERRY_ALLOW_UNIMPLEMENTED` is set — forces non-strict (defer) mode diff --git a/crates/perry-hir/src/lower/closure_analysis.rs b/crates/perry-hir/src/lower/closure_analysis.rs index f3afaa75c..e436d00d6 100644 --- a/crates/perry-hir/src/lower/closure_analysis.rs +++ b/crates/perry-hir/src/lower/closure_analysis.rs @@ -56,105 +56,6 @@ pub(crate) fn widen_mutable_captures_stmts(stmts: &mut [Stmt]) { } } -/// Insert preallocated boxes when a closure is created before a later lexical -/// binding that it captures. Later lowering may split/in-line expressions into -/// `let getter = () => value; let value = ...`; the closure must capture the -/// future binding's box, not its current undefined snapshot. -pub(crate) fn preallocate_forward_captured_lets_stmts(stmts: &mut Vec) { - let mut forward = collect_forward_captured_let_ids(stmts); - if !forward.is_empty() { - forward.sort(); - forward.dedup(); - match stmts.first_mut() { - Some(Stmt::PreallocateBoxes(existing)) => { - for id in forward { - if !existing.contains(&id) { - existing.push(id); - } - } - existing.sort(); - } - _ => stmts.insert(0, Stmt::PreallocateBoxes(forward)), - } - } - - for stmt in stmts.iter_mut() { - preallocate_forward_captured_lets_stmt(stmt); - } -} - -fn collect_forward_captured_let_ids(stmts: &[Stmt]) -> Vec { - let mut scope_lets = std::collections::HashSet::new(); - for stmt in stmts { - if let Stmt::Let { id, .. } = stmt { - scope_lets.insert(*id); - } - } - - let mut declared = std::collections::HashSet::new(); - let mut out = std::collections::HashSet::new(); - for stmt in stmts { - let mut captures = std::collections::HashSet::new(); - collect_closure_captures_stmt(stmt, &mut captures); - for id in captures { - if scope_lets.contains(&id) && !declared.contains(&id) { - out.insert(id); - } - } - if let Stmt::Let { id, .. } = stmt { - declared.insert(*id); - } - } - - out.into_iter().collect() -} - -fn preallocate_forward_captured_lets_stmt(stmt: &mut Stmt) { - match stmt { - Stmt::If { - then_branch, - else_branch, - .. - } => { - preallocate_forward_captured_lets_stmts(then_branch); - if let Some(else_branch) = else_branch { - preallocate_forward_captured_lets_stmts(else_branch); - } - } - Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { - preallocate_forward_captured_lets_stmts(body); - } - Stmt::For { init, body, .. } => { - if let Some(init) = init { - preallocate_forward_captured_lets_stmt(init); - } - preallocate_forward_captured_lets_stmts(body); - } - Stmt::Try { - body, - catch, - finally, - } => { - preallocate_forward_captured_lets_stmts(body); - if let Some(catch) = catch { - preallocate_forward_captured_lets_stmts(&mut catch.body); - } - if let Some(finally) = finally { - preallocate_forward_captured_lets_stmts(finally); - } - } - Stmt::Switch { cases, .. } => { - for case in cases { - preallocate_forward_captured_lets_stmts(&mut case.body); - } - } - Stmt::Labeled { body, .. } => { - preallocate_forward_captured_lets_stmt(body); - } - _ => {} - } -} - fn widen_mutable_captures_stmt( stmt: &mut Stmt, scope_mutable: &std::collections::HashSet, @@ -1058,44 +959,6 @@ fn collect_closure_captures_expr(expr: &Expr, out: &mut std::collections::HashSe collect_closure_captures_expr(e, out); } } - // A fresh class expression can hold closures in its static - // initializers and in the captured-args snapshot. This path is - // reachable via `class X extends Y {}` bindings, so a closure - // capturing a forward-declared `let` here must be seen or its box - // won't be preallocated. - Expr::ClassExprFresh { - named_statics, - symbol_statics, - captured_args, - .. - } => { - for (_, e) in named_statics { - collect_closure_captures_expr(e, out); - } - for (k, v) in symbol_statics { - collect_closure_captures_expr(k, out); - collect_closure_captures_expr(v, out); - } - for e in captured_args { - collect_closure_captures_expr(e, out); - } - } - Expr::InstanceOf { expr, ty_expr, .. } => { - collect_closure_captures_expr(expr, out); - if let Some(te) = ty_expr { - collect_closure_captures_expr(te, out); - } - } - Expr::In { property, object } => { - collect_closure_captures_expr(property, out); - collect_closure_captures_expr(object, out); - } - Expr::TaggedTemplateStrings { cooked, .. } => { - for e in cooked { - collect_closure_captures_expr(e, out); - } - } - Expr::TemplateRaw(inner) => collect_closure_captures_expr(inner, out), _ => {} } } diff --git a/crates/perry-hir/src/lower/const_fold_fn.rs b/crates/perry-hir/src/lower/const_fold_fn.rs index ab0139e86..45d5e8414 100644 --- a/crates/perry-hir/src/lower/const_fold_fn.rs +++ b/crates/perry-hir/src/lower/const_fold_fn.rs @@ -310,23 +310,17 @@ pub(crate) fn try_const_fold_function_construct_kind( None => (String::new(), String::new()), }; - // Capability-probe handling. A trivial no-op `new Function("")` / - // `Function("")` — an EXPLICIT empty-string body — is commonly used to decide - // whether runtime code generation is available. Perry is ahead-of-time - // compiled, so report "unavailable" by throwing at construction; callers can - // then take their non-codegen fallback. Only the trivial empty-body no-op is - // refused; real literal bodies (`return 42`, the `return this` globalThis - // polyfill) still fold. - // - // `new Function()` with NO arguments is NOT this probe — it is the plain - // "make an empty function" spelling (e.g. `Reflect.construct(Intl.X, args, - // new Function())` to pick a custom prototype). Node returns - // `function anonymous(){}` for it, so fold it rather than throw; the throw is - // gated on `!consts.is_empty()` so only an explicit empty-string body probes. - if !consts.is_empty() - && body_src.trim().is_empty() - && crate::eval_classifier::eval_csp_probe_unavailable() - { + // CSP capability-probe handling (`PERRY_EVAL_CSP`). A trivial no-op + // `new Function("")` / `Function("")` is the canonical runtime-codegen + // feature-test (`try { new Function(""), true } catch { false }`). perry is + // ahead-of-time compiled and cannot generate code from a runtime string, so + // under CSP mode this probe must report "unavailable" — throw at + // *construction* (not when called), exactly as a CSP `unsafe-eval`-blocked + // environment does — so probing callers (e.g. zod 4's validator JIT) take + // their non-codegen interpreter fallback. Only the trivial empty-body no-op + // is refused; real literal bodies (`return 42`, the `return this` globalThis + // polyfill) still fold, preserving spec behavior by default. + if body_src.trim().is_empty() && crate::eval_classifier::eval_csp_probe_unavailable() { return synth_throwing_iife( ctx, "throw new TypeError(\"Function: runtime dynamic code generation is \ diff --git a/crates/perry-hir/src/lower/expr_call/array_only_methods.rs b/crates/perry-hir/src/lower/expr_call/array_only_methods.rs index 0c0cab3e8..6eba59ed6 100644 --- a/crates/perry-hir/src/lower/expr_call/array_only_methods.rs +++ b/crates/perry-hir/src/lower/expr_call/array_only_methods.rs @@ -162,24 +162,6 @@ fn is_fs_dir_receiver(ctx: &LoweringContext, expr: &ast::Expr) -> bool { }) } -fn chain_roots_at_namespace_or_import(ctx: &LoweringContext, expr: &ast::Expr) -> bool { - let mut current = unwrap_transparent_expr(expr); - loop { - match current { - ast::Expr::Ident(ident) => { - let name = ident.sym.as_ref(); - return ctx.namespace_import_locals.contains(name) - || (ctx.lookup_local(name).is_none() - && ctx.lookup_imported_func(name).is_some()); - } - ast::Expr::Member(member) => { - current = unwrap_transparent_expr(member.obj.as_ref()); - } - _ => return false, - } - } -} - /// Does this expression's method chain originate from a node:stream /// source — `Readable.from(...)` / `Readable.of(...)`, a local already tagged /// as a readable stream, `new Transform()`, or a chain of lazy iterator helpers @@ -616,7 +598,6 @@ pub(super) fn try_array_only_methods( let recv_is_class = recv_is_class || chain_roots_at_stream(ctx, member_obj) || chain_roots_at_iterator_from(member_obj) - || chain_roots_at_namespace_or_import(ctx, member_obj) || is_util_mime_params_receiver(ctx, member_obj); // thisArg routing: the dense `Expr::Array` fast paths // carry only the callback and silently drop a 2nd positional diff --git a/crates/perry-hir/src/lower/expr_call/imported_array_methods.rs b/crates/perry-hir/src/lower/expr_call/imported_array_methods.rs index d9d4d99ba..38d07960a 100644 --- a/crates/perry-hir/src/lower/expr_call/imported_array_methods.rs +++ b/crates/perry-hir/src/lower/expr_call/imported_array_methods.rs @@ -49,11 +49,6 @@ pub(super) fn try_imported_array_methods( param_types, return_type, }; - let extern_is_array = matches!( - extern_ref, - Expr::ExternFuncRef { ref return_type, .. } - if matches!(return_type, Type::Array(_)) - ); match method_name { "join" => { // Issue #420 (drizzle): `sql.join(arr)` — `sql` @@ -80,7 +75,7 @@ pub(super) fn try_imported_array_methods( // Fall through. } "map" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let cb = args.into_iter().next().unwrap(); let cb = ctx.maybe_wrap_builtin_callback(cb, &call.args[0]); return Ok(Ok(Expr::ArrayMap { @@ -89,7 +84,7 @@ pub(super) fn try_imported_array_methods( })); } "filter" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let cb = args.into_iter().next().unwrap(); let cb = ctx.maybe_wrap_builtin_callback(cb, &call.args[0]); return Ok(Ok(Expr::ArrayFilter { @@ -98,7 +93,7 @@ pub(super) fn try_imported_array_methods( })); } "forEach" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let cb = args.into_iter().next().unwrap(); let cb = ctx.maybe_wrap_builtin_callback(cb, &call.args[0]); return Ok(Ok(Expr::ArrayForEach { @@ -107,7 +102,7 @@ pub(super) fn try_imported_array_methods( })); } "find" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let cb = args.into_iter().next().unwrap(); let cb = ctx.maybe_wrap_builtin_callback(cb, &call.args[0]); return Ok(Ok(Expr::ArrayFind { @@ -143,7 +138,7 @@ pub(super) fn try_imported_array_methods( } "indexOf" // #2804: carry the optional fromIndex (2nd arg). - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let mut it = args.into_iter(); let value = it.next().unwrap(); let from_index = it.next().map(Box::new); @@ -154,7 +149,7 @@ pub(super) fn try_imported_array_methods( })); } "includes" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let mut it = args.into_iter(); let value = it.next().unwrap(); let from_index = it.next().map(Box::new); @@ -165,7 +160,7 @@ pub(super) fn try_imported_array_methods( })); } "slice" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let mut args_iter = args.into_iter(); let start = args_iter.next().unwrap(); let end = args_iter.next(); @@ -176,7 +171,7 @@ pub(super) fn try_imported_array_methods( })); } "reduce" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let mut args_iter = args.into_iter(); let callback = args_iter.next().unwrap(); let initial = args_iter.next().map(Box::new); @@ -188,13 +183,13 @@ pub(super) fn try_imported_array_methods( } "flat" // depth-aware calls fall through. - if args.is_empty() && extern_is_array => { + if args.is_empty() => { return Ok(Ok(Expr::ArrayFlat { array: Box::new(extern_ref), })); } "reduceRight" - if !args.is_empty() && extern_is_array => { + if !args.is_empty() => { let mut args_iter = args.into_iter(); let callback = args_iter.next().unwrap(); let initial = args_iter.next().map(Box::new); @@ -204,19 +199,19 @@ pub(super) fn try_imported_array_methods( initial, })); } - "toReversed" if extern_is_array => { + "toReversed" => { return Ok(Ok(Expr::ArrayToReversed { array: Box::new(extern_ref), })); } - "toSorted" if extern_is_array => { + "toSorted" => { let comparator = args.into_iter().next().map(Box::new); return Ok(Ok(Expr::ArrayToSorted { array: Box::new(extern_ref), comparator, })); } - "toSpliced" if extern_is_array => { + "toSpliced" => { // #2794: handle omitted args. let arg_count = args.len(); let mut args_iter = args.into_iter(); @@ -235,7 +230,7 @@ pub(super) fn try_imported_array_methods( })); } "with" - if args.len() >= 2 && extern_is_array => { + if args.len() >= 2 => { let mut args_iter = args.into_iter(); let index = args_iter.next().unwrap(); let value = args_iter.next().unwrap(); @@ -245,13 +240,13 @@ pub(super) fn try_imported_array_methods( value: Box::new(value), })); } - "entries" if extern_is_array => { + "entries" => { return Ok(Ok(Expr::ArrayEntries(Box::new(extern_ref)))); } - "keys" if extern_is_array => { + "keys" => { return Ok(Ok(Expr::ArrayKeys(Box::new(extern_ref)))); } - "values" if extern_is_array => { + "values" => { return Ok(Ok(Expr::ArrayValues(Box::new(extern_ref)))); } _ => {} // Fall through for other methods diff --git a/crates/perry-hir/src/lower/expr_function.rs b/crates/perry-hir/src/lower/expr_function.rs index bde50dce1..e69f4993c 100644 --- a/crates/perry-hir/src/lower/expr_function.rs +++ b/crates/perry-hir/src/lower/expr_function.rs @@ -1063,12 +1063,7 @@ fn lower_fn_expr_anon(ctx: &mut LoweringContext, fn_expr: &ast::FnExpr) -> Resul .entry(cname.clone()) .and_modify(|d| *d = (*d).min(fn_body_scope_depth)) .or_insert(fn_body_scope_depth); - ctx.forward_class_names.insert(cname.clone()); - if class_decl.class.super_class.is_some() - && ctx.lookup_local_in_current_scope(&cname).is_none() - { - ctx.define_local(cname, Type::Any); - } + ctx.forward_class_names.insert(cname); } } } diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 45e773a53..0a98ea01c 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -178,19 +178,14 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // param shadows it — and the `resolve_class_alias().is_none()` // guard on the reroute block below would then skip it. // - // Capturing the binding here keeps the reroute stable against both, - // and also lets a lexical binding shadow a same-named class. - let callee_local_at_entry = ctx.lookup_local(ident.sym.as_ref()); - if ctx.lookup_class(&class_name).is_some() { - if let Some(local_id) = callee_local_at_entry { - let args = lower_optional_args(ctx, new_expr.args.as_deref())?; - return Ok(Expr::NewDynamic { - callee: Box::new(Expr::LocalGet(local_id)), - args, - byte_offset: new_byte_offset, - }); - } - } + // Capturing the binding here (when no real class of this name is in + // scope) keeps the reroute stable against both. + let callee_local_at_entry: Option = if ctx.lookup_class(&class_name).is_none() + { + ctx.lookup_local(&class_name) + } else { + None + }; if matches!( ctx.lookup_native_module(&class_name), Some(("url", Some("Url"))) diff --git a/crates/perry-hir/src/lower/lower_expr/arm_class.rs b/crates/perry-hir/src/lower/lower_expr/arm_class.rs index ad1460249..a3c7194ad 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_class.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_class.rs @@ -145,12 +145,16 @@ pub(crate) fn lower_class_expr( .lookup_class_captures(&synthetic_name) .map(|ids| ids.iter().map(|id| Expr::LocalGet(*id)).collect()) .unwrap_or_default(); - let static_init_exprs = crate::lower_decl::build_interleaved_static_init_exprs( - &class_expr.class.body, - &synthetic_name, - &class.static_fields, - &class.static_methods, - ); + // Static block synthetic-method names (`__perry_static_init_N`), in + // source order — emitted as inline `StaticMethodCall`s on the + // shared-template path so blocks run at class-evaluation time (the + // same treatment the class-declaration path gives them). + let static_block_names: Vec = class + .static_methods + .iter() + .filter(|m| m.name.starts_with("__perry_static_init_")) + .map(|m| m.name.clone()) + .collect(); ctx.pending_classes.push(class); // #1772: a class EXPRESSION that carries per-evaluation static // fields and is NOT a mixin (`class extends `) lowers to a @@ -239,7 +243,37 @@ pub(crate) fn lower_class_expr( }); } seq.extend(computed_member_registrations); - seq.extend(static_init_exprs); + for (k, v) in static_symbol_registrations { + seq.push(Expr::RegisterClassStaticSymbol { + class_name: synthetic_name.clone(), + key_expr: Box::new(k), + value_expr: Box::new(v), + }); + } + // Inline the named static field/element initializers at the point + // the class expression evaluates (source order), mirroring the + // class-declaration path. Without this the shared-template path + // relied solely on the late `init_static_fields_late` pass, which + // runs AFTER the surrounding top-level statements — so a read like + // `C.x` immediately after `var C = class { static x = 1 }` saw the + // uninitialized (0.0) slot. (Private statics carry a `#`-prefixed + // name and flow through the same StaticFieldSet path.) + for (name, v) in named_statics { + seq.push(Expr::StaticFieldSet { + class_name: synthetic_name.clone(), + field_name: name, + value: Box::new(v), + }); + } + // Static blocks run right after the static-field initializers, in + // source order, with the class as `this`. + for block_name in static_block_names { + seq.push(Expr::StaticMethodCall { + class_name: synthetic_name.clone(), + method_name: block_name, + args: Vec::new(), + }); + } if seq.is_empty() { Ok(Expr::ClassRef(synthetic_name)) } else { diff --git a/crates/perry-hir/src/lower/lower_module_fn.rs b/crates/perry-hir/src/lower/lower_module_fn.rs index fadcf1610..ba0a5c4f2 100644 --- a/crates/perry-hir/src/lower/lower_module_fn.rs +++ b/crates/perry-hir/src/lower/lower_module_fn.rs @@ -1046,31 +1046,24 @@ pub fn lower_module_full( // also go through the box so they observe each other's writes. Without // this pass, a `get: () => value` sibling of `inc: () => value++` captures // the raw initial value instead of the shared boxed binding. - preallocate_forward_captured_lets_stmts(&mut module.init); widen_mutable_captures_stmts(&mut module.init); for func in &mut module.functions { - preallocate_forward_captured_lets_stmts(&mut func.body); widen_mutable_captures_stmts(&mut func.body); } for class in &mut module.classes { for method in &mut class.methods { - preallocate_forward_captured_lets_stmts(&mut method.body); widen_mutable_captures_stmts(&mut method.body); } for (_, getter) in &mut class.getters { - preallocate_forward_captured_lets_stmts(&mut getter.body); widen_mutable_captures_stmts(&mut getter.body); } for (_, setter) in &mut class.setters { - preallocate_forward_captured_lets_stmts(&mut setter.body); widen_mutable_captures_stmts(&mut setter.body); } for static_method in &mut class.static_methods { - preallocate_forward_captured_lets_stmts(&mut static_method.body); widen_mutable_captures_stmts(&mut static_method.body); } if let Some(ref mut ctor) = class.constructor { - preallocate_forward_captured_lets_stmts(&mut ctor.body); widen_mutable_captures_stmts(&mut ctor.body); } } diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index a62b596d8..1c67a6a1c 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -17,7 +17,6 @@ use crate::ir::*; // Topical sub-modules extracted from this file (issue #1435 — pure code move). mod namespace; mod native_default_import; -mod reexports; // Re-export moved items so existing `crate::...` / `super::*` call paths keep // resolving. `lower_namespace_as_class` is also called from `lower/stmt.rs`. @@ -25,7 +24,6 @@ pub(crate) use namespace::lower_namespace_as_class; use native_default_import::{ is_cjs_style_native_default_import, node_submodule_default_export_key, }; -use reexports::imported_binding_reexport; pub(crate) fn lower_module_decl( ctx: &mut LoweringContext, @@ -1322,7 +1320,6 @@ pub(crate) fn lower_module_decl( ast::Decl::TsEnum(enum_decl) => { let en = lower_enum_decl(ctx, enum_decl, true)?; let enum_name = en.name.clone(); - module.init.push(crate::lower_decl::enum_runtime_let(ctx, &en)); module.enums.push(en); module.exported_objects.push(enum_name.clone()); module.exports.push(Export::Named { @@ -1475,17 +1472,6 @@ pub(crate) fn lower_module_decl( continue; } - if let Some((source, imported)) = - imported_binding_reexport(&module.imports, &local) - { - module.exports.push(Export::ReExport { - source, - imported, - exported: exported.clone(), - }); - continue; - } - module.exports.push(Export::Named { local: local.clone(), exported: exported.clone(), diff --git a/crates/perry-hir/src/lower/module_decl/reexports.rs b/crates/perry-hir/src/lower/module_decl/reexports.rs deleted file mode 100644 index 49c7e3fd1..000000000 --- a/crates/perry-hir/src/lower/module_decl/reexports.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::ir::{Import, ImportSpecifier}; - -pub(crate) fn imported_binding_reexport( - imports: &[Import], - local: &str, -) -> Option<(String, String)> { - imports.iter().find_map(|import| { - if import.is_native { - return None; - } - import - .specifiers - .iter() - .find_map(|specifier| match specifier { - ImportSpecifier::Named { - imported, - local: imported_local, - } if imported_local == local => Some((import.source.clone(), imported.clone())), - ImportSpecifier::Default { - local: imported_local, - } if imported_local == local => { - Some((import.source.clone(), "default".to_string())) - } - ImportSpecifier::Namespace { .. } => None, - _ => None, - }) - }) -} diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index f0ba3ddd6..b7e655dea 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -1147,7 +1147,6 @@ pub(crate) fn lower_stmt( } ast::Decl::TsEnum(enum_decl) => { let en = lower_enum_decl(ctx, enum_decl, false)?; - module.init.push(crate::lower_decl::enum_runtime_let(ctx, &en)); module.enums.push(en); } ast::Decl::TsInterface(iface_decl) => { diff --git a/crates/perry-hir/src/lower_decl/block.rs b/crates/perry-hir/src/lower_decl/block.rs index 21c74998d..05178b391 100644 --- a/crates/perry-hir/src/lower_decl/block.rs +++ b/crates/perry-hir/src/lower_decl/block.rs @@ -976,12 +976,7 @@ pub fn lower_fn_body_block_stmt( .entry(cname.clone()) .and_modify(|d| *d = (*d).min(cur_scope_depth)) .or_insert(cur_scope_depth); - ctx.forward_class_names.insert(cname.clone()); - if class_decl.class.super_class.is_some() - && ctx.lookup_local_in_current_scope(&cname).is_none() - { - ctx.define_local(cname, Type::Any); - } + ctx.forward_class_names.insert(cname); } } diff --git a/crates/perry-hir/src/lower_decl/body_stmt.rs b/crates/perry-hir/src/lower_decl/body_stmt.rs index 55692514e..1583a25f2 100644 --- a/crates/perry-hir/src/lower_decl/body_stmt.rs +++ b/crates/perry-hir/src/lower_decl/body_stmt.rs @@ -282,26 +282,10 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result = ctx - .lookup_class_captures(&class.name) - .map(|ids| ids.iter().map(|id| Expr::LocalGet(*id)).collect()) - .unwrap_or_default(); - let class_local = ctx - .lookup_local_in_current_scope(&class_name) - .unwrap_or_else(|| ctx.define_local(class_name.clone(), Type::Any)); - let (mut named_statics, symbol_statics, fresh_static_init_stmts) = - crate::lower_decl::build_fresh_class_static_init( - &class_decl.class.body, - &class.name, - &class.static_fields, - &class.static_methods, - class_local, - ); - if let Some((parent_id, _)) = parent_value_local { - named_statics.push(( - "__perry_parent_value".to_string(), - Expr::LocalGet(parent_id), - )); - } - result.push(Stmt::Let { - id: class_local, - name: class_name.clone(), - ty: Type::Any, - init: Some(Expr::ClassExprFresh { - template: class.name.clone(), - named_statics, - symbol_statics, - captured_args, - }), - mutable: false, - }); - result.extend(fresh_static_init_stmts); - } // Static field initializers + static blocks for a // function-nested class. The module-level path // (`lower/stmt.rs`) emits these into `module.init`; here they @@ -374,14 +321,12 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result Result Stmt { - let id = ctx - .lookup_local(&en.name) - .unwrap_or_else(|| ctx.define_local(en.name.clone(), perry_types::Type::Any)); - let mut properties = Vec::new(); - for member in &en.members { - match &member.value { - EnumValue::Number(value) => { - properties.push((member.name.clone(), Expr::Number(*value as f64))); - properties.push((value.to_string(), Expr::String(member.name.clone()))); - } - EnumValue::String(value) => { - properties.push((member.name.clone(), Expr::String(value.clone()))); - } - } - } - Stmt::Let { - id, - name: en.name.clone(), - ty: perry_types::Type::Any, - mutable: true, - init: Some(Expr::Object(properties)), - } -} - /// Compute an enum's members (names + values) without touching the lowering /// context. Pure so it can run in the forward-reference pre-scan /// (`pre_register_module_enums`) and again at the real declaration site and diff --git a/crates/perry-hir/src/lower_decl/mod.rs b/crates/perry-hir/src/lower_decl/mod.rs index 578085db1..7809b04f7 100644 --- a/crates/perry-hir/src/lower_decl/mod.rs +++ b/crates/perry-hir/src/lower_decl/mod.rs @@ -48,7 +48,7 @@ pub(crate) use class_members::{ pub(crate) use class_validation::{ validate_class_element_early_errors, validate_legacy_decorator_surface, }; -pub(crate) use enum_decl::{compute_enum_members, enum_runtime_let, lower_enum_decl}; +pub(crate) use enum_decl::{compute_enum_members, lower_enum_decl}; pub(crate) use fn_decl::lower_fn_decl; pub(crate) use helpers::{ append_synthetic_arguments_param, body_has_use_strict, body_uses_arguments, @@ -61,8 +61,5 @@ pub(crate) use private_members::{ build_private_scope, lower_private_getter, lower_private_method, lower_private_prop, lower_private_setter, }; -pub(crate) use static_init::{ - build_fresh_class_static_init, build_interleaved_static_init_exprs, - build_interleaved_static_init_stmts, -}; +pub(crate) use static_init::build_interleaved_static_init_stmts; pub(crate) use type_alias::lower_type_alias_decl; diff --git a/crates/perry-hir/src/lower_decl/static_init.rs b/crates/perry-hir/src/lower_decl/static_init.rs index 4a154db13..1a2e9ede5 100644 --- a/crates/perry-hir/src/lower_decl/static_init.rs +++ b/crates/perry-hir/src/lower_decl/static_init.rs @@ -5,7 +5,6 @@ use swc_ecma_ast as ast; use crate::ir::*; -use perry_types::LocalId; /// Per ClassDefinitionEvaluation step 34, a class's static fields and /// static blocks evaluate in a single pass over source order — a static @@ -90,154 +89,3 @@ pub(crate) fn build_interleaved_static_init_stmts( } out } - -pub(crate) fn build_interleaved_static_init_exprs( - class_body: &[ast::ClassMember], - class_name: &str, - static_fields: &[ClassField], - static_methods: &[Function], -) -> Vec { - let emit_field = |out: &mut Vec, sf: &ClassField| { - let Some(init) = &sf.init else { return }; - let mut init_value = init.clone(); - crate::analysis::substitute_lexical_this_in_expr( - &mut init_value, - &Expr::ClassRef(class_name.to_string()), - ); - out.push(if let Some(key) = sf.key_expr.as_ref() { - Expr::RegisterClassStaticSymbol { - class_name: class_name.to_string(), - key_expr: Box::new(key.clone()), - value_expr: Box::new(init_value), - } - } else { - Expr::StaticFieldSet { - class_name: class_name.to_string(), - field_name: sf.name.clone(), - value: Box::new(init_value), - } - }); - }; - - let mut out = Vec::new(); - let mut field_idx = 0usize; - let mut block_idx = 0usize; - for member in class_body { - match member { - ast::ClassMember::ClassProp(prop) if !prop.declare && prop.is_static => { - if let Some(sf) = static_fields.get(field_idx) { - emit_field(&mut out, sf); - } - field_idx += 1; - } - ast::ClassMember::PrivateProp(prop) if prop.is_static => { - if let Some(sf) = static_fields.get(field_idx) { - emit_field(&mut out, sf); - } - field_idx += 1; - } - ast::ClassMember::StaticBlock(_) => { - let method_name = format!("__perry_static_init_{}", block_idx); - block_idx += 1; - if static_methods.iter().any(|m| m.name == method_name) { - out.push(Expr::StaticMethodCall { - class_name: class_name.to_string(), - method_name, - args: Vec::new(), - }); - } - } - _ => {} - } - } - out -} - -pub(crate) fn build_fresh_class_static_init( - class_body: &[ast::ClassMember], - class_name: &str, - static_fields: &[ClassField], - static_methods: &[Function], - class_local: LocalId, -) -> (Vec<(String, Expr)>, Vec<(Expr, Expr)>, Vec) { - let class_ref = Expr::LocalGet(class_local); - let mut initial_named = Vec::new(); - let mut initial_symbol = Vec::new(); - let mut post_init = Vec::new(); - let mut field_idx = 0usize; - let mut block_idx = 0usize; - let mut after_block = false; - - for member in class_body { - match member { - ast::ClassMember::ClassProp(prop) if !prop.declare && prop.is_static => { - if let Some(sf) = static_fields.get(field_idx) { - push_fresh_class_static_field( - sf, - after_block, - &class_ref, - &mut initial_named, - &mut initial_symbol, - &mut post_init, - ); - } - field_idx += 1; - } - ast::ClassMember::PrivateProp(prop) if prop.is_static => { - if let Some(sf) = static_fields.get(field_idx) { - push_fresh_class_static_field( - sf, - after_block, - &class_ref, - &mut initial_named, - &mut initial_symbol, - &mut post_init, - ); - } - field_idx += 1; - } - ast::ClassMember::StaticBlock(_) => { - after_block = true; - let method_name = format!("__perry_static_init_{}", block_idx); - block_idx += 1; - if static_methods.iter().any(|m| m.name == method_name) { - post_init.push(Stmt::Expr(Expr::StaticMethodCall { - class_name: class_name.to_string(), - method_name, - args: Vec::new(), - })); - } - } - _ => {} - } - } - - (initial_named, initial_symbol, post_init) -} - -fn push_fresh_class_static_field( - sf: &ClassField, - after_block: bool, - class_ref: &Expr, - initial_named: &mut Vec<(String, Expr)>, - initial_symbol: &mut Vec<(Expr, Expr)>, - post_init: &mut Vec, -) { - let Some(init) = &sf.init else { return }; - let mut init_value = init.clone(); - crate::analysis::substitute_lexical_this_in_expr(&mut init_value, class_ref); - match (after_block, sf.key_expr.as_ref()) { - (false, Some(key)) => initial_symbol.push((key.clone(), init_value)), - (false, None) => initial_named.push((sf.name.clone(), init_value)), - (true, Some(key)) => post_init.push(Stmt::Expr(Expr::IndexSet { - object: Box::new(class_ref.clone()), - index: Box::new(key.clone()), - value: Box::new(init_value), - })), - (true, None) => post_init.push(Stmt::Expr(Expr::PropertySet { - object: Box::new(class_ref.clone()), - property: sf.name.clone(), - value: Box::new(init_value), - })), - } -} diff --git a/crates/perry-hir/src/lower_patterns.rs b/crates/perry-hir/src/lower_patterns.rs index d1e77fe0b..2eb34a4a0 100644 --- a/crates/perry-hir/src/lower_patterns.rs +++ b/crates/perry-hir/src/lower_patterns.rs @@ -159,18 +159,10 @@ pub(crate) fn lower_lit(lit: &ast::Lit) -> Result { ast::Lit::Bool(b) => Ok(Expr::Bool(b.value)), ast::Lit::Null(_) => Ok(Expr::Null), ast::Lit::BigInt(bi) => Ok(Expr::BigInt(bi.value.to_string())), - ast::Lit::Regex(re) => { - let pattern = re.exp.to_string(); - let pattern = if pattern.is_ascii() { - pattern - } else { - repair_latin1_decoded_utf8(&pattern) - }; - Ok(Expr::RegExp { - pattern, - flags: re.flags.to_string(), - }) - } + ast::Lit::Regex(re) => Ok(Expr::RegExp { + pattern: re.exp.to_string(), + flags: re.flags.to_string(), + }), _ => Err(anyhow!("Unsupported literal type")), } } @@ -182,10 +174,6 @@ fn normalize_swc_string_literal(lit: &ast::Str, decoded: &str) -> String { if raw.is_ascii() || decoded.is_ascii() { return decoded.to_string(); } - repair_latin1_decoded_utf8(decoded) -} - -fn repair_latin1_decoded_utf8(decoded: &str) -> String { let mut bytes = Vec::with_capacity(decoded.len()); for ch in decoded.chars() { let code = ch as u32; @@ -194,7 +182,10 @@ fn repair_latin1_decoded_utf8(decoded: &str) -> String { } bytes.push(code as u8); } - String::from_utf8(bytes).unwrap_or_else(|_| decoded.to_string()) + match String::from_utf8(bytes) { + Ok(repaired) => repaired, + Err(_) => decoded.to_string(), + } } /// Convert an assignment target to an expression for reading its current value diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index fa4305262..d31cc8843 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -107,13 +107,7 @@ pub(crate) fn is_iterable(value: f64) -> bool { if crate::array::js_array_is_array(value).to_bits() == crate::value::TAG_TRUE { return true; } - let raw = if jsv.is_pointer() { - js_nanbox_get_pointer(value) - } else if value.to_bits() >> 48 == 0 && value.to_bits() > 0x10000 { - value.to_bits() as i64 - } else { - 0 - }; + let raw = js_nanbox_get_pointer(value); if raw == 0 { return false; } @@ -121,9 +115,6 @@ pub(crate) fn is_iterable(value: f64) -> bool { if crate::map::is_registered_map(addr) || crate::set::is_registered_set(addr) { return true; } - if crate::object::map_set_subclass::subclass_backing_for_default_iteration(value).is_some() { - return true; - } // A built-in iterator object — an array/map/set/string/buffer/iterator- // helper iterator — IS already an iterator and is therefore iterable // (`[Symbol.iterator]()` returns itself). Mirrors the equivalent diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 5dbb70136..54a1bbac3 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -307,16 +307,8 @@ pub(crate) unsafe fn stringify_object_with_replacer_pretty( let fields_ptr = (ptr as *const u8).add(std::mem::size_of::()) as *const f64; - let alloc_limit = std::cmp::max(num_fields, 8); - let read_field_bits = |f: u32| -> u64 { - if f < alloc_limit { - (*fields_ptr.add(f as usize)).to_bits() - } else { - crate::object::js_object_get_field(obj, f).bits() - } - }; - let actual_fields = keys_len; - let key_order = crate::object::ecma_own_key_order(keys_arr); + // Use keys_len as the iteration count since field_count may include pre-allocated slots. + let actual_fields = std::cmp::min(num_fields, keys_len); let use_pretty = !indent.is_empty(); let inner_depth = depth + 1; // A function replacer only sees own ENUMERABLE keys (EnumerableOwnProperty @@ -324,14 +316,7 @@ pub(crate) unsafe fn stringify_object_with_replacer_pretty( let filter_non_enum = crate::object::descriptors_in_use(); buf.push('{'); let mut first = true; - let pos = |j: u32| -> u32 { - match &key_order { - Some(ord) => ord[j as usize], - None => j, - } - }; - for j in 0..actual_fields { - let f = pos(j); + for f in 0..actual_fields { // Skip non-enumerable own keys before invoking the replacer. if filter_non_enum && f < keys_len @@ -365,7 +350,7 @@ pub(crate) unsafe fn stringify_object_with_replacer_pretty( // Get the field value (invoking an own getter, as spec [[Get]] does), // resolve toJSON, then apply the replacer. - let mut field_val = f64::from_bits(read_field_bits(f)); + let mut field_val = *fields_ptr.add(f as usize); if filter_non_enum && f < keys_len { if let Some(gv) = crate::object::json_object_getter_value(obj, *keys_elements.add(f as usize)) diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 50dd860a1..22a7b86a4 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -580,15 +580,6 @@ fn jsvalue_eq(a: f64, b: f64) -> bool { return false; } - // BigInt keys compare by numeric value, not allocation identity: `1n` - // stored and `1n` looked up are distinct heap allocations. Mirrors the - // Set path (set.rs::jsvalue_eq); BigInt keys take the linear scan below. - let a_val = crate::JSValue::from_bits(a_bits); - let b_val = crate::JSValue::from_bits(b_bits); - if a_val.is_bigint() && b_val.is_bigint() { - return crate::bigint::js_bigint_eq(a_val.as_bigint_ptr(), b_val.as_bigint_ptr()) != 0; - } - if is_string_like(a_bits) && is_string_like(b_bits) { let mut a_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let mut b_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; diff --git a/crates/perry-runtime/src/node_submodules/blob.rs b/crates/perry-runtime/src/node_submodules/blob.rs index 610410c21..feb8ca5e0 100644 --- a/crates/perry-runtime/src/node_submodules/blob.rs +++ b/crates/perry-runtime/src/node_submodules/blob.rs @@ -25,9 +25,7 @@ use std::os::unix::fs::MetadataExt; #[cfg(not(unix))] use std::time::UNIX_EPOCH; -pub(crate) const CLASS_ID_BLOB: u32 = 0xFFFF0026; -/// `File extends Blob`; shares this module as its canonical class-id home. -pub(crate) const CLASS_ID_FILE: u32 = 0xFFFF002E; +const CLASS_ID_BLOB: u32 = 0xFFFF0026; #[derive(Clone)] struct FileBlobState { diff --git a/crates/perry-runtime/src/object/array_object_ops.rs b/crates/perry-runtime/src/object/array_object_ops.rs index 20401dbaa..c6304d23e 100644 --- a/crates/perry-runtime/src/object/array_object_ops.rs +++ b/crates/perry-runtime/src/object/array_object_ops.rs @@ -13,14 +13,6 @@ unsafe fn is_array_object(obj: *const ObjectHeader) -> bool { (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY } -pub(crate) unsafe fn array_has_descriptors(obj: *const ObjectHeader) -> bool { - if !is_array_object(obj) { - return false; - } - let gc_header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc_header)._reserved & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 -} - /// Apply `Object.freeze` / `Object.seal` to an array's OWN index + named data /// properties. The generic `mark_all_keys` walks `(*obj).keys_array`, but an /// array's indices live in the dense element store and its named props in the diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index ee338234d..bb7d87a7b 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -58,8 +58,7 @@ pub(crate) use state::{ class_prototype_method_set_enumerable, class_prototype_method_value_cache_root_store, class_prototype_object_root_store, global_object_prototype_bits, is_bound_native_method_closure_value, is_non_constructable_builtin_function_value, - parent_closure_in_chain, refresh_class_decl_prototype_parent, - throw_non_constructable_builtin_function, + parent_closure_in_chain, throw_non_constructable_builtin_function, }; pub use state::{ ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, CLASS_DYNAMIC_PARENT_VALUE, diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index a5063640e..0afff5407 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -846,7 +846,6 @@ pub unsafe extern "C" fn js_new_function_construct( let class_cid = js_object_get_class_id(obj); if class_cid != 0 { let inst = js_object_alloc(class_cid, 0); - let dynamic_parent = class_object_parent_value(obj, class_cid); // Replay the class's registered constructor (instance-field // initializers + body) on the fresh instance, filling the // capture params from the snapshotted `__perry_ctor_caps`. The @@ -855,35 +854,6 @@ pub unsafe extern "C" fn js_new_function_construct( super::super::class_constructors::replay_class_object_constructor( func_value, class_cid, inst, args_ptr, args_len, ); - let proto = - ensure_class_object_prototype(obj as *mut ObjectHeader, dynamic_parent, func_value); - if !proto.is_null() { - super::super::prototype_chain::object_set_static_prototype( - inst as usize, - crate::value::js_nanbox_pointer(proto as i64).to_bits(), - ); - } - let dynamic_parent_is_error = matches!( - identify_global_builtin_constructor(dynamic_parent), - Some( - "Error" - | "TypeError" - | "RangeError" - | "ReferenceError" - | "SyntaxError" - | "EvalError" - | "URIError" - ) - ); - if crate::object::class_meta_registry::extends_builtin_error(class_cid) - || dynamic_parent_is_error - { - let key = crate::string::js_string_from_bytes( - b"constructor".as_ptr(), - b"constructor".len() as u32, - ); - super::super::js_object_set_field_by_name_nonenum(inst, key, func_value); - } // `class X extends Request/Response {}` constructed via the dynamic // (class-expression value) path: the replayed ctor's `super()` // can't statically route an aliased parent, so attach the native @@ -934,38 +904,35 @@ pub unsafe extern "C" fn js_new_function_construct( if fp != 0 && crate::closure::is_closure_ptr(fp) { let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); let dp = JSValue::from_bits(dyn_proto.to_bits()); - let has_user_proto = super::super::class_ref_id(dyn_proto).is_some() - || if dp.is_pointer() { - let raw = dp.as_pointer::() as usize; - // Function objects (closures) are identified by CLOSURE_MAGIC, - // not a GC type tag — check them first. - let is_fn = crate::closure::is_closure_ptr(raw); - is_fn - || (raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && { - let hdr = unsafe { - &*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) - }; - // Arrays: test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*. - // Objects: Intl/Temporal constructors install .prototype via - // closure_set_dynamic_prop (bypassing js_set_function_prototype), - // so CLASS_PROTOTYPE_OBJECTS has no entry; ensure_function_prototype_object - // would otherwise create a fresh empty proto and overwrite .prototype. - matches!( - hdr.obj_type, - crate::gc::GC_TYPE_ARRAY - | crate::gc::GC_TYPE_LAZY_ARRAY - | crate::gc::GC_TYPE_OBJECT - ) - }) - } else { - false - }; - if has_user_proto { - super::super::prototype_chain::object_set_static_prototype( - obj_ptr as usize, - dyn_proto.to_bits(), - ); - linked_user_proto = true; + if dp.is_pointer() { + let raw = dp.as_pointer::() as usize; + // Function objects (closures) are identified by CLOSURE_MAGIC, + // not a GC type tag — check them first. + let is_fn = crate::closure::is_closure_ptr(raw); + let has_user_proto = is_fn + || (raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && { + let hdr = unsafe { + &*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) + }; + // Arrays: test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*. + // Objects: Intl/Temporal constructors install .prototype via + // closure_set_dynamic_prop (bypassing js_set_function_prototype), + // so CLASS_PROTOTYPE_OBJECTS has no entry; ensure_function_prototype_object + // would otherwise create a fresh empty proto and overwrite .prototype. + matches!( + hdr.obj_type, + crate::gc::GC_TYPE_ARRAY + | crate::gc::GC_TYPE_LAZY_ARRAY + | crate::gc::GC_TYPE_OBJECT + ) + }); + if has_user_proto { + super::super::prototype_chain::object_set_static_prototype( + obj_ptr as usize, + dyn_proto.to_bits(), + ); + linked_user_proto = true; + } } } } @@ -1026,99 +993,6 @@ pub unsafe extern "C" fn js_new_function_construct( nan_boxed } -unsafe fn ensure_class_object_prototype( - class_obj: *mut ObjectHeader, - parent_value: f64, - constructor_value: f64, -) -> *mut ObjectHeader { - let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); - let existing = js_object_get_field_by_name_f64(class_obj as *const ObjectHeader, key); - if super::super::value_is_object_like(existing) { - return crate::value::JSValue::from_bits(existing.to_bits()).as_pointer::() - as *mut ObjectHeader; - } - - let proto = js_object_alloc(0, 0); - if proto.is_null() { - return proto; - } - - let constructor_key = - crate::string::js_string_from_bytes(b"constructor".as_ptr(), b"constructor".len() as u32); - js_object_set_field_by_name(proto, constructor_key, constructor_value); - set_builtin_property_attrs( - proto as usize, - "constructor".to_string(), - PropertyAttrs::new(true, false, true), - ); - - if let Some(parent_proto_bits) = class_constructor_prototype_bits(parent_value) { - super::super::prototype_chain::object_set_static_prototype( - proto as usize, - parent_proto_bits, - ); - } else if let Some(object_proto_bits) = global_object_prototype_bits() { - super::super::prototype_chain::object_set_static_prototype( - proto as usize, - object_proto_bits, - ); - } - - js_object_set_field_by_name( - class_obj, - key, - crate::value::js_nanbox_pointer(proto as i64), - ); - set_builtin_property_attrs( - class_obj as usize, - "prototype".to_string(), - PropertyAttrs::new(false, false, false), - ); - proto -} - -unsafe fn class_object_parent_value(class_obj: *const ObjectHeader, class_id: u32) -> f64 { - let key = crate::string::js_string_from_bytes( - b"__perry_parent_value".as_ptr(), - b"__perry_parent_value".len() as u32, - ); - let parent = js_object_get_field_by_name_f64(class_obj, key); - if parent.to_bits() != crate::value::TAG_UNDEFINED { - parent - } else { - js_get_dynamic_parent_value(class_id) - } -} - -unsafe fn class_constructor_prototype_bits(constructor_value: f64) -> Option { - if let Some(parent_cid) = super::super::class_ref_id(constructor_value) { - if let Some(proto) = class_decl_prototype_value_for_instance_class(parent_cid) { - return Some(proto.to_bits()); - } - let proto = class_prototype_object(parent_cid); - if !proto.is_null() { - return Some(crate::value::js_nanbox_pointer(proto as i64).to_bits()); - } - return None; - } - - let value = crate::value::JSValue::from_bits(constructor_value.to_bits()); - if !value.is_pointer() { - return None; - } - let raw = value.as_pointer::(); - if raw.is_null() { - return None; - } - let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); - let proto = js_object_get_field_by_name_f64(raw, key); - if super::super::value_is_object_like(proto) { - Some(proto.to_bits()) - } else { - None - } -} - /// `new (...spread)` — spread-bearing construction. Codegen builds a /// single JS array containing every argument in evaluation order (regular args /// pushed, spread sources expanded via `js_array_like_to_array` + concat), then diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 846ce415d..42a5ba036 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -8,22 +8,11 @@ use std::sync::RwLock; /// Register a class with its parent class ID in the global registry pub(crate) fn register_class(class_id: u32, parent_class_id: u32) { - const CLASS_ID_OBJECT: u32 = 0xFFFF0050; let mut registry = CLASS_REGISTRY.write().unwrap(); if registry.is_none() { *registry = Some(HashMap::new()); } - let map = registry.as_mut().unwrap(); - if parent_class_id == CLASS_ID_OBJECT - && map - .get(&class_id) - .is_some_and(|existing| *existing != 0 && *existing != CLASS_ID_OBJECT) - { - return; - } - map.insert(class_id, parent_class_id); - drop(registry); - refresh_class_decl_prototype_parent(class_id); + registry.as_mut().unwrap().insert(class_id, parent_class_id); } /// Public registration entry point used by codegen module init. diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index ea3c2e995..ab66c41da 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -100,10 +100,7 @@ pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec { .map(|props| { props .keys() - .filter(|k| { - !k.starts_with('#') - && !matches!(k.as_str(), "length" | "name" | "prototype") - }) + .filter(|k| !k.starts_with('#')) .cloned() .collect() }) @@ -427,35 +424,6 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id } } -fn builtin_prototype_value_for_class_id(class_id: u32) -> Option { - let name = match class_id { - crate::error::CLASS_ID_ERROR => "Error", - crate::error::CLASS_ID_TYPE_ERROR => "TypeError", - crate::error::CLASS_ID_RANGE_ERROR => "RangeError", - crate::error::CLASS_ID_REFERENCE_ERROR => "ReferenceError", - crate::error::CLASS_ID_SYNTAX_ERROR => "SyntaxError", - crate::error::CLASS_ID_EVAL_ERROR => "EvalError", - crate::error::CLASS_ID_URI_ERROR => "URIError", - crate::error::CLASS_ID_AGGREGATE_ERROR => "AggregateError", - 0xFFFF0020 => "Date", - 0xFFFF0021 => "RegExp", - 0xFFFF0022 => "Map", - 0xFFFF0023 => "Set", - 0xFFFF0024 => "Array", - 0xFFFF0025 => "ArrayBuffer", - 0xFFFF0027 => "Promise", - 0xFFFF0050 => "Object", - 0xFFFF00D0 => "Number", - 0xFFFF00D1 => "String", - 0xFFFF00D2 => "Boolean", - 0xFFFF00D3 => "BigInt", - 0xFFFF00D4 => "Symbol", - _ => return None, - }; - let proto = crate::object::builtin_prototype_value(name); - ((proto.to_bits() >> 48) == 0x7FFD).then_some(proto) -} - pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { if class_id == 0 || class_name_for_id(class_id).is_none() { return f64::from_bits(crate::value::TAG_UNDEFINED); @@ -507,7 +475,14 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { unsafe { mirror_prototype_method_on_object(proto, &name, value_bits, enumerable) }; } - let parent_proto_bits = resolve_class_parent_prototype_bits(class_id); + let parent_proto_bits = get_parent_class_id(class_id) + .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) + .and_then(|parent_id| { + let parent_proto = class_decl_prototype_value(parent_id); + let parent_bits = parent_proto.to_bits(); + ((parent_bits >> 48) == 0x7FFD).then_some(parent_bits) + }) + .or_else(global_object_prototype_bits); if let Some(bits) = parent_proto_bits { super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); } @@ -515,33 +490,6 @@ pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { crate::value::js_nanbox_pointer(proto as i64) } -pub(crate) fn refresh_class_decl_prototype_parent(class_id: u32) { - let proto = class_decl_prototype_object(class_id); - if proto.is_null() { - return; - } - let parent_proto_bits = resolve_class_parent_prototype_bits(class_id); - if let Some(bits) = parent_proto_bits { - super::super::prototype_chain::object_set_static_prototype(proto as usize, bits); - } -} - -fn resolve_class_parent_prototype_bits(class_id: u32) -> Option { - dynamic_parent_prototype_bits(class_id).or_else(|| { - get_parent_class_id(class_id) - .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) - .and_then(|parent_id| { - builtin_prototype_value_for_class_id(parent_id) - .or_else(|| { - let parent_proto = class_decl_prototype_value(parent_id); - ((parent_proto.to_bits() >> 48) == 0x7FFD).then_some(parent_proto) - }) - .map(f64::to_bits) - }) - .or_else(global_object_prototype_bits) - }) -} - pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Option { if class_id == 0 || class_name_for_id(class_id).is_none() { return None; @@ -550,33 +498,6 @@ pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Op ((proto.to_bits() >> 48) == 0x7FFD).then_some(proto) } -fn dynamic_parent_prototype_bits(class_id: u32) -> Option { - let parent = js_get_dynamic_parent_value(class_id); - if let Some(parent_cid) = class_ref_id(parent) { - if parent_cid == 0 || parent_cid == class_id { - return None; - } - let parent_proto = class_decl_prototype_value(parent_cid); - return ((parent_proto.to_bits() >> 48) == 0x7FFD).then_some(parent_proto.to_bits()); - } - - let value = JSValue::from_bits(parent.to_bits()); - if !value.is_pointer() { - return None; - } - let ptr = value.as_pointer::(); - if ptr.is_null() { - return None; - } - let key = crate::string::js_string_from_bytes(b"prototype".as_ptr(), b"prototype".len() as u32); - let proto = js_object_get_field_by_name_f64(ptr, key); - if unsafe { value_is_object_like(proto) } { - Some(proto.to_bits()) - } else { - None - } -} - pub(crate) fn global_object_prototype_bits() -> Option { let object_ctor = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); let ctor_bits = object_ctor.to_bits(); diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 2834f143b..f30a88fa8 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -342,18 +342,6 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu && super::class_registry::lookup_static_method_in_chain(class_id, "name") .is_none() { - if let Some(v) = - super::class_registry::class_own_static_field_value(class_id, &method_name) - { - let attrs = super::get_property_attrs(class_id as usize, &method_name) - .unwrap_or_else(|| super::PropertyAttrs::new(false, false, true)); - return build_data_descriptor( - v, - attrs.writable(), - attrs.enumerable(), - attrs.configurable(), - ); - } if let Some(class_name) = super::class_registry::class_name_for_id(class_id) { let s = crate::string::js_string_from_bytes( class_name.as_ptr(), @@ -443,14 +431,7 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if let Some(v) = super::class_registry::class_own_static_field_value(class_id, &method_name) { - let attrs = super::get_property_attrs(class_id as usize, &method_name) - .unwrap_or_else(|| super::PropertyAttrs::new(true, true, true)); - return build_data_descriptor( - v, - attrs.writable(), - attrs.enumerable(), - attrs.configurable(), - ); + return build_data_descriptor(v, true, true, true); } } } @@ -627,11 +608,6 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if let Some(desc) = super::arguments_object_descriptor(obj, key_str) { return desc; } - if let Some(ref name) = key_rust { - if super::is_inherited_object_proto_shim_on_builtin_prototype(obj, name) { - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - } if (obj as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc_header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; @@ -1164,12 +1140,6 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { crate::array::js_array_push(result, JSValue::string_ptr(lk)); let named = crate::array::array_named_property_names(ap, false); for name in &named { - if super::is_inherited_object_proto_shim_on_builtin_prototype( - ap as *const ObjectHeader, - name, - ) { - continue; - } let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); crate::array::js_array_push(result, JSValue::string_ptr(k)); @@ -1180,10 +1150,6 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { for name in super::accessor_descriptor_keys_for_obj(ap as usize) { if super::canonical_array_index(&name).is_some() || named.contains(&name) - || super::is_inherited_object_proto_shim_on_builtin_prototype( - ap as *const ObjectHeader, - &name, - ) { continue; } @@ -1300,13 +1266,6 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; for i in 0..len { let key_val = crate::array::js_array_get(keys, pos(i)); - if let Some(b) = crate::string::js_string_key_bytes(key_val, &mut sso_buf) { - if let Ok(name) = std::str::from_utf8(b) { - if super::is_inherited_object_proto_shim_on_builtin_prototype(obj, name) { - continue; - } - } - } if hide_private { if let Some(b) = crate::string::js_string_key_bytes(key_val, &mut sso_buf) { if b.first() == Some(&b'#') diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index e535b5619..bd4ae8a94 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -27,34 +27,6 @@ pub extern "C" fn js_object_get_field_by_name( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { - if !key.is_null() { - let obj_bits = obj as u64; - let tagged = f64::from_bits(obj_bits); - let tagged_value = crate::value::JSValue::from_bits(obj_bits); - let string_ptr = if tagged_value.is_any_string() { - crate::value::js_get_string_pointer_unified(tagged) as *const crate::StringHeader - } else if obj_bits >= crate::gc::GC_HEADER_SIZE as u64 - && crate::value::addr_class::is_valid_obj_ptr(obj as *const u8) - { - let gc_hdr = unsafe { - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader - }; - if unsafe { (*gc_hdr).obj_type } == crate::gc::GC_TYPE_STRING { - obj as *const crate::StringHeader - } else { - std::ptr::null() - } - } else { - std::ptr::null() - }; - if crate::string::is_valid_string_ptr(string_ptr) { - let key_value = crate::value::js_nanbox_string(key as i64); - let indexed = crate::string::js_string_index_get(string_ptr, key_value); - if indexed.to_bits() != crate::value::TAG_UNDEFINED { - return JSValue::from_bits(indexed.to_bits()); - } - } - } // #2846: the receiver may be a Proxy value that arrived through a generic // property read (e.g. `rec.proxy.a` where `rec = Proxy.revocable(...)`). // Proxies are encoded as small fake pointers; deref-ing one as an @@ -941,14 +913,31 @@ pub extern "C" fn js_object_get_field_by_name( } } } - // SSO property access for keys not handled by the unified string fast path - // above. `.length` and canonical indices are centralized there; remaining - // unknown properties on untyped string locals resolve to undefined here - // rather than falling through to the object-deref path with an inline-string - // payload. + // SSO property access (v0.5.213 Step 1 gate). The codegen inline + // `.length` path routes SHORT_STRING_TAG receivers here because + // it doesn't yet know about the SSO tag. Handle `.length` by + // reading the length byte directly from the NaN-box payload. + // Other property accesses on an SSO string (e.g. `.charAt` via + // `[0]`, `.slice`) aren't yet routed here — handled by the + // string method dispatch in a future migration step; today they + // fall through to "undefined" which matches the behavior for + // string-valued property access on untyped locals in general. { let obj_bits = obj as u64; if (obj_bits & crate::value::TAG_MASK) == crate::value::SHORT_STRING_TAG { + if !key.is_null() { + unsafe { + let key_ptr = + (key as *const u8).add(std::mem::size_of::()); + let key_len = (*key).byte_len as usize; + let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); + if key_bytes == b"length" { + let len = (obj_bits & crate::value::SHORT_STRING_LEN_MASK) + >> crate::value::SHORT_STRING_LEN_SHIFT; + return JSValue::number(len as f64); + } + } + } return JSValue::undefined(); } } diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index bc7e9c10e..644b0cc69 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -1351,17 +1351,6 @@ pub(crate) fn get_field_by_name_object_tail( let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6); return JSValue::from_bits(v.to_bits()); } - if class_id != 0 { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - if let Some(v) = - super::super::class_registry::resolve_proto_chain_field_with_receiver( - class_id, key, receiver, - ) - { - return v; - } - } if let Some(func_value) = super::super::class_registry::function_value_for_class_id(class_id) { @@ -1371,10 +1360,6 @@ pub(crate) fn get_field_by_name_object_tail( let bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64); return JSValue::from_bits(bits); } - if crate::url::url_class::is_url_object_shape(obj as *mut crate::ObjectHeader) { - let v = js_get_global_this_builtin_value(b"URL".as_ptr(), 3); - return JSValue::from_bits(v.to_bits()); - } // class_id == 0 fallback: plain ObjectHeader allocated // without an HIR shape (Object.create(null) hybrids, raw // empty `{}` produced by JSON.parse, etc.). Report diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index 7222b1d30..49eea6feb 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -11,27 +11,6 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { let nanbox_false = f64::from_bits(0x7FFC_0000_0000_0003u64); // TAG_FALSE let nanbox_true = f64::from_bits(0x7FFC_0000_0000_0004u64); // TAG_TRUE - // The [[Prototype]] walk at the tail recurses through this entry point (a - // loop bounded at 1024 in earlier PRs became a recursion). `setPrototypeOf` - // rejects cycles, but a pathologically deep chain — or a cycle that slips - // past cycle-detection (see #b201538f3) — would otherwise overflow the - // stack. Bound total recursion depth to the same 1024 the manual walk below - // already caps at, so this introduces no new false-negative under that limit. - thread_local! { - static HP_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; - } - struct DepthGuard; - impl Drop for DepthGuard { - fn drop(&mut self) { - HP_DEPTH.with(|d| d.set(d.get() - 1)); - } - } - if HP_DEPTH.with(|d| d.get()) > 1024 { - return nanbox_false; - } - HP_DEPTH.with(|d| d.set(d.get() + 1)); - let _depth_guard = DepthGuard; - let obj_val = JSValue::from_bits(obj.to_bits()); let key_val = JSValue::from_bits(key.to_bits()); @@ -62,22 +41,6 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { if addr >= crate::value::addr_class::COMMON_HANDLE_BAND_END && crate::value::addr_class::is_handle_band(addr) { - if key_val.is_any_string() { - unsafe { - if let Some(dispatch) = super::super::class_registry::handle_property_dispatch() - { - let key_ptr = crate::value::js_get_string_pointer_unified(key) - as *const crate::StringHeader; - let name_ptr = - (key_ptr as *const u8).add(std::mem::size_of::()); - let name_len = (*key_ptr).byte_len as usize; - let result = dispatch(addr as i64, name_ptr, name_len); - if result.to_bits() != crate::value::TAG_UNDEFINED { - return nanbox_true; - } - } - } - } return nanbox_false; } } @@ -622,19 +585,6 @@ unsafe fn ordinary_has_property( } } } - let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj_ptr as i64).to_bits()); - let proto = super::super::js_object_get_prototype_of(receiver); - let proto_bits = proto.to_bits(); - if proto_bits != crate::value::TAG_NULL - && proto_bits != crate::value::TAG_UNDEFINED - && proto_bits != receiver.to_bits() - { - let key_value = crate::value::js_nanbox_string(key as i64); - if crate::value::js_is_truthy(super::super::js_object_has_property(proto, key_value)) != 0 { - return true; - } - } - // Inherited `Object.prototype` properties (`toString`, `hasOwnProperty`, …, // plus any user-assigned `Object.prototype` members). ordinary_object_prototype_property_value(last_valid, key).is_some() diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 0a9092505..86a71741a 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -121,7 +121,6 @@ pub(crate) use math_temporal::{install_math_namespace, temporal_ctor_kind}; pub(crate) use populate::{ default_prepare_stack_trace_func_ptr, populate_global_this_builtins, ERROR_CONSTRUCTOR_PTR, }; -pub(crate) use proto_methods::is_inherited_object_proto_shim_on_builtin_prototype; pub(crate) use proto_methods::{ install_error_prototype_data_properties, populate_builtin_prototype_methods, }; diff --git a/crates/perry-runtime/src/object/global_this/builtin_thunks.rs b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs index 9b68dc836..3749eac34 100644 --- a/crates/perry-runtime/src/object/global_this/builtin_thunks.rs +++ b/crates/perry-runtime/src/object/global_this/builtin_thunks.rs @@ -388,11 +388,11 @@ pub(crate) extern "C" fn global_this_error_is_error_thunk( /// AOT-compiled in HIR; only dynamic ones reach here. Perry has no JS /// interpreter, but it CAN recognize the fixed templates a few popular codegen /// libraries emit and return a real native function. Currently: `depd`'s -/// deprecation wrapper. depd's wrapper just logs a deprecation then forwards to -/// the wrapped fn, so the "wrapper" can simply BE that fn — -/// `new Function(...)(fn,log,deprecate,msg,site)` returns `fn`. Unrecognized -/// templates throw so feature-detecting libraries can fall back; there is no -/// general eval. +/// deprecation wrapper (used eagerly by `send` → Next.js). depd's wrapper just +/// logs a deprecation then forwards to the wrapped fn, so the "wrapper" can +/// simply BE that fn — `new Function(...)(fn,log,deprecate,msg,site)` returns +/// `fn`. Unrecognized templates fall back to a non-callable placeholder object +/// (prior behavior); there is no general eval. #[no_mangle] pub extern "C" fn js_function_ctor_from_strings(args_ptr: *const f64, args_len: usize) -> f64 { let arg_str = |i: usize| -> String { @@ -437,9 +437,24 @@ pub extern "C" fn js_function_ctor_from_strings(args_ptr: *const f64, args_len: } } // Unrecognized dynamic-Function shape. Perry is ahead-of-time compiled and - // cannot compile a code string built from runtime data. Throwing lets - // feature-detecting libraries take their non-`Function` fallback without - // polluting stderr for handled probes. + // cannot compile a code string built from runtime data. THROW (rather than + // return a placeholder object) — this is the honest signal, and it lets + // feature-detecting libraries take their non-`Function` fallback. zod 4's + // JIT does `try { new Function(""), true } catch { false }` and switches to + // its interpreter when this throws, clearing the entire zod-object-validation + // wall with no native validator needed. (A placeholder made the feature-test + // *succeed*, forcing the JIT path that then crashes.) The eprintln names the + // offending library for diagnostics; it fires once per distinct feature-test. + let body = if args_len > 0 { + arg_str(args_len - 1) + } else { + String::new() + }; + let preview: String = body.chars().take(160).collect(); + eprintln!( + "[perry] dynamic Function refused (AOT) — {} arg(s); body[..160]={:?}", + args_len, preview + ); super::super::object_ops::throw_object_type_error( b"Function: dynamic code generation from a runtime string is not supported \ in an ahead-of-time compiled binary", diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs index a80030445..d4bd87b18 100644 --- a/crates/perry-runtime/src/object/global_this/proto_methods.rs +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -25,35 +25,6 @@ const OBJECT_PROTO_METHODS: &[(&str, u32)] = &[ // dedicated thunks; do not include it here to avoid clobbering those. ]; -const INHERITED_OBJECT_PROTO_SHIMS: &[&str] = &[ - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "__defineGetter__", - "__defineSetter__", - "__lookupGetter__", - "__lookupSetter__", -]; - -pub(crate) fn is_inherited_object_proto_shim_on_builtin_prototype( - proto_obj: *const ObjectHeader, - key: &str, -) -> bool { - if proto_obj.is_null() || !INHERITED_OBJECT_PROTO_SHIMS.contains(&key) { - return false; - } - let proto_bits = crate::value::js_nanbox_pointer(proto_obj as i64).to_bits(); - for builtin in GLOBAL_THIS_BUILTIN_CONSTRUCTORS.iter().copied() { - if builtin == "Object" { - continue; - } - if builtin_prototype_value(builtin).to_bits() == proto_bits { - return true; - } - } - false -} - /// Populate well-known method properties on a built-in constructor's /// prototype object. Each registered method is a closure carrying a /// proper `name` property so feature-detection idioms like @@ -705,7 +676,6 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: ("text", 0), ], ); - set_intrinsic_to_string_tag(proto_obj, builtin_name); install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); } "FormData" => { diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 20b85bb32..9d08c8ad6 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -33,10 +33,6 @@ pub(crate) fn value_is_callable(value: f64) -> bool { } let jv = crate::JSValue::from_bits(value.to_bits()); if !jv.is_pointer() { - let bits = value.to_bits(); - if bits > 0x10000 && bits <= crate::value::POINTER_MASK { - return crate::closure::is_closure_ptr(bits as usize); - } return false; } crate::closure::is_closure_ptr((jv.bits() & crate::value::POINTER_MASK) as usize) @@ -75,87 +71,6 @@ fn value_addr(value: f64) -> usize { } } -fn prototype_identity_key(value: f64) -> Option { - let bits = value.to_bits(); - if let Some(class_id) = super::class_prototype_ref_id(value) { - return Some(0xFFFF_0000_0000_0000 | class_id as u64); - } - match bits >> 48 { - 0x7FFD => { - let ptr = (bits & crate::value::POINTER_MASK) as usize; - super::class_registry::class_id_for_decl_prototype_object(ptr) - .map(|class_id| 0xFFFF_0000_0000_0000 | class_id as u64) - .or(Some(ptr as u64)) - } - 0x7FFE if super::class_ref_id(value).is_some() => Some(bits), - 0 if bits > 0x10000 => { - super::class_registry::class_id_for_decl_prototype_object(bits as usize) - .map(|class_id| 0xFFFF_0000_0000_0000 | class_id as u64) - .or(Some(bits)) - } - _ => None, - } -} - -fn prototype_identity_chain_contains(value: f64, target_proto: f64) -> bool { - if !unsafe { crate::object::value_is_object_like(value) } { - return false; - } - if !unsafe { crate::object::value_is_object_like(target_proto) } - && super::class_ref_id(target_proto).is_none() - && super::class_prototype_ref_id(target_proto).is_none() - { - return false; - } - - let Some(target_key) = prototype_identity_key(target_proto) else { - return false; - }; - let mut current = value; - for _ in 0..128 { - current = super::object_ops::js_object_get_prototype_of(current); - let bits = current.to_bits(); - if bits == crate::value::TAG_NULL || bits == crate::value::TAG_UNDEFINED { - return false; - } - if prototype_identity_key(current) == Some(target_key) { - return true; - } - } - false -} - -fn ordinary_function_has_instance(value: f64, type_ref: f64) -> Option { - if !value_is_callable(type_ref) { - return None; - } - let bits = type_ref.to_bits(); - let function_value = if (bits >> 48) == 0x7FFD { - type_ref - } else if bits > 0x10000 && bits <= crate::value::POINTER_MASK { - if !crate::closure::is_closure_ptr(bits as usize) { - return None; - } - crate::value::js_nanbox_pointer(bits as i64) - } else { - return None; - }; - let proto = super::class_registry::js_function_prototype_value_for_read(function_value); - if unsafe { crate::object::value_is_object_like(proto) } - || super::class_ref_id(proto).is_some() - || super::class_prototype_ref_id(proto).is_some() - { - return Some(f64::from_bits( - if prototype_identity_chain_contains(value, proto) { - crate::value::TAG_TRUE - } else { - crate::value::TAG_FALSE - }, - )); - } - None -} - fn is_native_module_namespace_value(value: f64, expected: &str) -> bool { let jv = crate::JSValue::from_bits(value.to_bits()); if !jv.is_pointer() { @@ -438,9 +353,6 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { f64::from_bits(TAG_FALSE) }; } - if let Some(result) = ordinary_function_has_instance(value, type_ref) { - return result; - } // `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. @@ -498,9 +410,6 @@ pub(crate) fn global_builtin_constructor_class_id(name: &str) -> u32 { "URIError" => crate::error::CLASS_ID_URI_ERROR, "AggregateError" => crate::error::CLASS_ID_AGGREGATE_ERROR, "Promise" => CLASS_ID_PROMISE, - "Blob" => crate::node_submodules::blob::CLASS_ID_BLOB, - "File" => crate::node_submodules::blob::CLASS_ID_FILE, - "URL" => crate::url::CLASS_ID_URL, "Navigator" => crate::navigator::NAVIGATOR_CLASS_ID, "TextEncoderStream" => crate::object::CLASS_ID_TEXT_ENCODER_STREAM, "TextDecoderStream" => crate::object::CLASS_ID_TEXT_DECODER_STREAM, @@ -1061,7 +970,7 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { return false_val; } - // WHATWG fetch: `instanceof Response` / `Request` / `Headers` / `Blob` / `File`. + // WHATWG fetch: `instanceof Response` / `Request` / `Headers` / `Blob`. // These are pointer-tagged small-integer handles (stdlib fetch registries), // not heap objects, so consult the stdlib fetch kind-probe rather than the // class chain. Without this, Hono's `res instanceof Response` route-fallback @@ -1069,25 +978,21 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { const CLASS_ID_RESPONSE: u32 = 0xFFFF0028; const CLASS_ID_REQUEST: u32 = 0xFFFF0029; const CLASS_ID_HEADERS: u32 = 0xFFFF002A; - const CLASS_ID_BLOB: u32 = crate::node_submodules::blob::CLASS_ID_BLOB; - const CLASS_ID_FILE: u32 = crate::node_submodules::blob::CLASS_ID_FILE; + const CLASS_ID_BLOB: u32 = 0xFFFF0026; if class_id == CLASS_ID_RESPONSE || class_id == CLASS_ID_REQUEST || class_id == CLASS_ID_HEADERS || class_id == CLASS_ID_BLOB - || class_id == CLASS_ID_FILE { let want = match class_id { CLASS_ID_RESPONSE => 1u8, CLASS_ID_REQUEST => 2, CLASS_ID_HEADERS => 3, - CLASS_ID_BLOB => 4, - _ => 5, // CLASS_ID_FILE + _ => 4, // CLASS_ID_BLOB }; if let Some(handle) = small_native_handle_id(value) { if let Some(probe) = crate::object::fetch_handle_kind_probe() { - let kind = unsafe { probe(handle as usize) }; - if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { + if unsafe { probe(handle as usize) } == want { return true_val; } } @@ -1100,8 +1005,7 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { let raw = jsval.as_pointer::() as usize; if let Some(id) = unsafe { crate::object::fetch_subclass_handle_id(raw) } { if let Some(probe) = crate::object::fetch_handle_kind_probe() { - let kind = unsafe { probe(id as usize) }; - if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { + if unsafe { probe(id as usize) } == want { return true_val; } } @@ -1201,16 +1105,6 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { false_val }; } - const CLASS_ID_URL: u32 = crate::url::CLASS_ID_URL; - if class_id == CLASS_ID_URL { - return if crate::url::object_from_f64(value) - .is_some_and(crate::url::url_class::is_url_object_shape) - { - true_val - } else { - false_val - }; - } // `Object` — ECMAScript spec: `x instanceof Object` is true for any // non-primitive (every object/array/function/Map/Set/Buffer/RegExp/ @@ -1398,10 +1292,6 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { } } - if gc_type != crate::gc::GC_TYPE_OBJECT { - return false_val; - } - // For user-defined classes that extend Error: `myErr instanceof Error` should be true. if class_id == crate::error::CLASS_ID_ERROR { let obj_class_id = (*obj_ptr).class_id; @@ -1436,27 +1326,3 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { false_val } } - -#[cfg(test)] -mod tests { - use super::*; - - extern "C" fn test_ctor(_closure: *const crate::closure::ClosureHeader) -> f64 { - f64::from_bits(crate::value::TAG_UNDEFINED) - } - - #[test] - fn ordinary_function_has_instance_reads_raw_closure_prototype_safely() { - let closure = crate::closure::js_closure_alloc(test_ctor as *const u8, 0); - let function_value = crate::value::js_nanbox_pointer(closure as i64); - let prototype = super::class_registry::js_function_prototype_value_for_read(function_value); - - let instance = js_object_alloc(0, 0); - let instance_value = crate::value::js_nanbox_pointer(instance as i64); - js_object_set_prototype_of(instance_value, prototype); - - let raw_function_value = f64::from_bits(closure as u64); - let result = ordinary_function_has_instance(instance_value, raw_function_value); - assert_eq!(result.map(f64::to_bits), Some(crate::value::TAG_TRUE)); - } -} diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 09933f299..037fee789 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -171,9 +171,7 @@ pub(crate) use this_binding::{ IMPLICIT_THIS, NEW_TARGET, }; pub use to_string_tag::js_object_to_string; -pub(crate) use to_string_tag::{ - fetch_handle_to_string_tag, typed_array_to_string_tag_name, web_stream_to_string_tag, -}; +pub(crate) use to_string_tag::{typed_array_to_string_tag_name, web_stream_to_string_tag}; static HTTP_METHODS_CACHE: AtomicU64 = AtomicU64::new(0); static FS_CONSTANTS_CACHE: AtomicU64 = AtomicU64::new(0); diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 8e7fd6704..5ae2170ea 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -30,27 +30,6 @@ pub(crate) use proto_dispatch::{ }; pub(super) use typed_array::dispatch_typed_array_method; -fn interned_default_method_key(method_name: &str) -> *mut crate::string::StringHeader { - use std::sync::OnceLock; - - static TO_STRING: OnceLock = OnceLock::new(); - static TO_LOCALE_STRING: OnceLock = OnceLock::new(); - static VALUE_OF: OnceLock = OnceLock::new(); - - let slot = match method_name { - "toString" => &TO_STRING, - "toLocaleString" => &TO_LOCALE_STRING, - "valueOf" => &VALUE_OF, - _ => return std::ptr::null_mut(), - }; - *slot.get_or_init(|| { - crate::string::js_string_from_bytes_longlived( - method_name.as_ptr(), - method_name.len() as u32, - ) as usize - }) as *mut crate::string::StringHeader -} - unsafe fn call_primitive_closure_value( receiver: f64, value: JSValue, @@ -1307,27 +1286,6 @@ pub unsafe extern "C" fn js_native_call_method( } } } - - if matches!(method_name, "toString" | "toLocaleString" | "valueOf") { - let method_key = interned_default_method_key(method_name); - if !method_key.is_null() { - let method = super::js_object_get_field_by_name(obj, method_key); - if !method.is_undefined() && !method.is_null() { - let bound = crate::closure::clone_closure_rebind_this( - method.bits(), - f64::from_bits(jsval.bits()), - ); - let prev_this = IMPLICIT_THIS.with(|c| c.replace(jsval.bits())); - let result = crate::closure::js_native_call_value( - f64::from_bits(bound), - args_ptr, - args_len, - ); - IMPLICIT_THIS.with(|c| c.set(prev_this)); - return result; - } - } - } } // Issue #510: throw `TypeError: is not a function` when diff --git a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs index 1fdce6766..4fc76c8eb 100644 --- a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs @@ -49,15 +49,6 @@ fn is_backed_collection_method( } } -unsafe fn dispatch_inherited_object_method(object: f64, method_name: &str) -> Option { - match method_name { - "toString" => Some(crate::object::js_object_to_string(object)), - "toLocaleString" => Some(js_object_default_to_locale_string(object)), - "valueOf" => Some(js_object_default_value_of(object)), - _ => None, - } -} - pub(super) unsafe fn dispatch_map_set( root_scope: &crate::gc::RuntimeHandleScope, object_handle: &crate::gc::RuntimeHandle, @@ -226,14 +217,6 @@ pub(super) unsafe fn dispatch_map_set( crate::map::js_map_foreach(map, args[0], this_arg); f64::from_bits(crate::value::TAG_UNDEFINED) } - _ if args.is_empty() => { - if let Some(result) = dispatch_inherited_object_method(object, method_name) - { - result - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - } - } _ => f64::from_bits(crate::value::TAG_UNDEFINED), }); } @@ -288,14 +271,6 @@ pub(super) unsafe fn dispatch_map_set( crate::set::js_set_foreach(set, args[0], this_arg); f64::from_bits(crate::value::TAG_UNDEFINED) } - _ if args.is_empty() => { - if let Some(result) = dispatch_inherited_object_method(object, method_name) - { - result - } else { - f64::from_bits(crate::value::TAG_UNDEFINED) - } - } // #2872: ES2024 Set composition methods. union/intersection/ // difference/symmetricDifference return a new Set; the // is* predicates return a boolean. diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index 9ee80aea9..494544e9a 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -696,7 +696,13 @@ pub(super) unsafe fn dispatch_common( } else { payload }; - let str_ptr = crate::string::js_number_to_string(n); + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } Some("Boolean") => { @@ -747,7 +753,12 @@ pub(super) unsafe fn dispatch_common( crate::value::js_jsvalue_to_string_radix(object, radix_arg.unwrap()); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } - let str_ptr = crate::string::js_number_to_string(n); + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } else if jsval.is_bool() { let s = if jsval.as_bool() { "true" } else { "false" }; diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 2279f1c99..db3ed199b 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -189,7 +189,13 @@ pub(super) unsafe fn dispatch_primitive( } else { payload }; - let str_ptr = crate::string::js_number_to_string(n); + let s = if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) { + (n as i64).to_string() + } else { + n.to_string() + }; + let str_ptr = + crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } Some("Boolean") => { diff --git a/crates/perry-runtime/src/object/object_ops/define_properties.rs b/crates/perry-runtime/src/object/object_ops/define_properties.rs index 809a4099b..0cb55ca9e 100644 --- a/crates/perry-runtime/src/object/object_ops/define_properties.rs +++ b/crates/perry-runtime/src/object/object_ops/define_properties.rs @@ -216,10 +216,6 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 const TAG_NULL_U64: u64 = 0x7FFC_0000_0000_0002; const TAG_UNDEFINED_U64: u64 = 0x7FFC_0000_0000_0001; let advance = |bits: u64| -> u64 { - let current_js = crate::value::JSValue::from_bits(bits); - if current_js.is_null() || current_js.is_undefined() { - return TAG_NULL_U64; - } let val = f64::from_bits(bits); // OrdinarySetPrototypeOf step 7.b.ii.1: if `p`'s [[GetPrototypeOf]] // is not the ordinary internal method (a Proxy's is exotic — it may diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index fa6f0a666..f2a91dfe6 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -311,58 +311,6 @@ pub extern "C" fn js_object_define_property( if let Some(name) = super::super::metadata_key_to_string(key_value) { let desc_ptr = extract_obj_ptr(descriptor_value); if !desc_ptr.is_null() { - let desc_has_value = desc_has_field(descriptor_value, b"value"); - let desc_has_writable = desc_has_field(descriptor_value, b"writable"); - if desc_has_value || desc_has_writable { - let existing_attrs = - super::super::get_property_attrs(target_cid as usize, &name).or_else( - || { - if matches!(name.as_str(), "name" | "length" | "prototype") { - Some(PropertyAttrs::new(false, false, true)) - } else if super::super::class_registry::class_own_static_field_value( - target_cid, &name, - ) - .is_some() - { - Some(PropertyAttrs::new(true, true, true)) - } else { - None - } - }, - ); - let value = if desc_has_value { - f64::from_bits(desc_read_field(descriptor_value, b"value").bits()) - } else { - super::super::class_registry::class_own_static_field_value( - target_cid, &name, - ) - .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) - }; - super::super::class_registry::class_dynamic_prop_root_store( - target_cid, - name.clone(), - value, - ); - let read_bool = |field: &[u8]| -> Option { - if !desc_has_field(descriptor_value, field) { - return None; - } - let v = desc_read_field(descriptor_value, field); - Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) - }; - let attrs = PropertyAttrs::new( - read_bool(b"writable").unwrap_or_else(|| { - existing_attrs.map(|a| a.writable()).unwrap_or(false) - }), - read_bool(b"enumerable").unwrap_or_else(|| { - existing_attrs.map(|a| a.enumerable()).unwrap_or(false) - }), - read_bool(b"configurable").unwrap_or_else(|| { - existing_attrs.map(|a| a.configurable()).unwrap_or(false) - }), - ); - super::super::set_property_attrs(target_cid as usize, name.clone(), attrs); - } let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); let value_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs index 72a349035..ca8747175 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -54,13 +54,6 @@ pub extern "C" fn js_object_is(a: f64, b: f64) -> f64 { return f64::from_bits(TAG_FALSE); } - if a_jsval.is_bigint() && b_jsval.is_bigint() { - if crate::bigint::js_bigint_eq(a_jsval.as_bigint_ptr(), b_jsval.as_bigint_ptr()) != 0 { - return f64::from_bits(TAG_TRUE); - } - return f64::from_bits(TAG_FALSE); - } - // For everything else, bit-pattern equality if a_bits == b_bits { f64::from_bits(TAG_TRUE) @@ -328,11 +321,6 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { let gc_header = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { - if super::super::is_inherited_object_proto_shim_on_builtin_prototype(obj, key) { - return f64::from_bits(TAG_FALSE); - } - } let present = super::super::has_own_helpers::array_own_key_present( obj as *const crate::array::ArrayHeader, key_str, @@ -365,17 +353,6 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { { return f64::from_bits(TAG_FALSE); } - if super::super::is_inherited_object_proto_shim_on_builtin_prototype(obj, key) { - return f64::from_bits(TAG_FALSE); - } - } - } - - if (*obj).class_id == 0 { - if let Some(key) = super::super::has_own_helpers::str_from_string_header(key_str) { - if super::super::is_inherited_object_proto_shim_on_builtin_prototype(obj, key) { - return f64::from_bits(TAG_FALSE); - } } } @@ -588,9 +565,6 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 if !is_valid_obj_ptr(obj as *const u8) { return f64::from_bits(TAG_FALSE); } - if super::super::is_inherited_object_proto_shim_on_builtin_prototype(obj, key_name) { - return f64::from_bits(TAG_FALSE); - } if (*obj).class_id == NATIVE_MODULE_CLASS_ID { if let Some(module_name) = read_native_module_name(obj) { return f64::from_bits( diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 1202865e2..d5782fd8c 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -426,11 +426,6 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { } return function_prototype_or_null(); } - if let Some(proto_bits) = - super::super::prototype_chain::object_static_prototype(raw_addr as usize) - { - return f64::from_bits(proto_bits); - } // Fast [[Prototype]] for a DECLARED-class instance: resolve // directly from the class id instead of the generic // `constructor_dynamic_prototype` probe, which reads the @@ -459,20 +454,6 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { return proto; } } - // #3986: `Object.create(proto)` / `new F()` (plain function - // ctor) instances carry a synthetic class id whose prototype - // object is stored keyed by that id. Return the exact stored - // object so prototype identity is preserved. Gated on - // GC_TYPE_OBJECT so non-object heap values don't misresolve. - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { - let synth_proto = - super::super::class_registry::class_prototype_object((*obj).class_id); - if !synth_proto.is_null() { - return f64::from_bits( - crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), - ); - } - } if let Some(proto) = constructor_dynamic_prototype(obj) { return proto; } @@ -506,6 +487,27 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { { return proto; } + // #3986: `Object.create(proto)` and `new F()` (a plain + // function ctor, whose instances carry a synthetic + // function-prototype class id) record the actual + // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS + // keyed by that synthetic class id. Return the exact stored + // pointer so `Object.getPrototypeOf(o) === proto` holds by + // identity (test262 built-ins/Object/create/15.2.3.5-*, + // S9.9 ToObject identity). Declared ES classes use the + // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just + // above, so this does not perturb the + // `getPrototypeOf(instance) === instance` model their + // `.constructor` resolution relies on. Without this the + // synthetic-class instance fell through to the + // `return obj_value` self-prototype fallback below. + let synth_proto = + super::super::class_registry::class_prototype_object((*obj).class_id); + if !synth_proto.is_null() { + return f64::from_bits( + crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), + ); + } } // A native-module namespace object (`require("path")` etc., // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged @@ -585,17 +587,6 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { } return function_prototype_or_null(); } - // #3986: synthetic-class instance (see the sibling site above) — - // return its stored prototype object to preserve identity. - if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { - let synth_proto = - super::super::class_registry::class_prototype_object((*obj).class_id); - if !synth_proto.is_null() { - return f64::from_bits( - crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), - ); - } - } if let Some(proto) = constructor_dynamic_prototype(obj) { return proto; } @@ -621,6 +612,27 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { { return proto; } + // #3986: `Object.create(proto)` and `new F()` (a plain + // function ctor, whose instances carry a synthetic + // function-prototype class id) record the actual + // [[Prototype]] object pointer in CLASS_PROTOTYPE_OBJECTS + // keyed by that synthetic class id. Return the exact stored + // pointer so `Object.getPrototypeOf(o) === proto` holds by + // identity (test262 built-ins/Object/create/15.2.3.5-*, + // S9.9 ToObject identity). Declared ES classes use the + // separate CLASS_DECL_PROTOTYPE_OBJECTS table handled just + // above, so this does not perturb the + // `getPrototypeOf(instance) === instance` model their + // `.constructor` resolution relies on. Without this the + // synthetic-class instance fell through to the + // `return obj_value` self-prototype fallback below. + let synth_proto = + super::super::class_registry::class_prototype_object((*obj).class_id); + if !synth_proto.is_null() { + return f64::from_bits( + crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), + ); + } // A native-module namespace object (`require("path")` etc., // class_id NATIVE_MODULE_CLASS_ID, the `__module__`-tagged // object) is an ordinary object whose [[Prototype]] is diff --git a/crates/perry-runtime/src/object/to_string_tag.rs b/crates/perry-runtime/src/object/to_string_tag.rs index ead63d26c..b2e8d4861 100644 --- a/crates/perry-runtime/src/object/to_string_tag.rs +++ b/crates/perry-runtime/src/object/to_string_tag.rs @@ -25,26 +25,6 @@ pub(crate) fn web_stream_to_string_tag(value: f64) -> Option<&'static str> { } } -pub(crate) fn fetch_handle_to_string_tag(value: f64) -> Option<&'static str> { - let bits = value.to_bits(); - if (bits >> 48) != 0x7FFD { - return None; - } - let id = (bits & crate::value::POINTER_MASK) as usize; - if !crate::value::addr_class::is_handle_band(id) { - return None; - } - let kind_probe = crate::object::fetch_handle_kind_probe()?; - match unsafe { kind_probe(id) } { - 1 => Some("Response"), - 2 => Some("Request"), - 3 => Some("Headers"), - 4 => Some("Blob"), - 5 => Some("File"), - _ => None, - } -} - unsafe fn string_value_to_owned(value: f64) -> Option { let jv = crate::value::JSValue::from_bits(value.to_bits()); if !jv.is_any_string() { @@ -281,18 +261,6 @@ pub unsafe extern "C" fn js_object_to_string(value: f64) -> f64 { let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); } - if let Some(tag) = fetch_handle_to_string_tag(value) { - let formatted = format!("[object {}]", tag); - let bytes = formatted.as_bytes(); - let str_ptr = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - if let Some(obj) = crate::url::object_from_f64(value) { - if crate::url::url_class::is_url_object_shape(obj) { - let str_ptr = crate::string::js_string_from_bytes(b"[object URL]".as_ptr(), 12); - return f64::from_bits(STRING_TAG | (str_ptr as u64 & POINTER_MASK)); - } - } if let Some(tag) = crate::builtins::boxed_primitive_to_string_tag(value) { let formatted = format!("[object {}]", tag); let bytes = formatted.as_bytes(); diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 875bb9cf5..421c3e632 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -983,12 +983,7 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { // allocation. Closures don't carry the flag, so keep consulting the side // tables for them (their `name`/`length` + user `defineProperty` descriptors // live there). - let array_has_descriptors = - unsafe { crate::object::array_has_descriptors(obj_ptr as *const crate::ObjectHeader) }; - if crate::object::object_has_descriptors(obj_ptr) - || array_has_descriptors - || crate::closure::is_closure_ptr(obj_ptr) - { + if crate::object::object_has_descriptors(obj_ptr) || crate::closure::is_closure_ptr(obj_ptr) { if let Some(acc) = crate::object::get_accessor_descriptor(obj_ptr, &key_name) { return Some(OwnSetDescriptor::Accessor { setter_bits: acc.set, diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index e9b5b092a..25780b55a 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -62,19 +62,6 @@ impl Hash for JSValueKey { fn hash(&self, state: &mut H) { let bits = self.0.to_bits(); let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let value = crate::JSValue::from_bits(bits); - if value.is_bigint() { - 0xFFFF_FFFEu32.hash(state); - let ptr = crate::bigint::clean_bigint_ptr(value.as_bigint_ptr()); - if ptr.is_null() { - 0u8.hash(state); - } else { - unsafe { - (*ptr).limbs.hash(state); - } - } - return; - } if is_string_like(bits) { if let Some((data, len)) = string_view_from_bits(bits, &mut scratch) { // String value: hash by content so identical strings with @@ -519,12 +506,6 @@ fn jsvalue_eq(a: f64, b: f64) -> bool { return false; } - let a_val = crate::JSValue::from_bits(a_bits); - let b_val = crate::JSValue::from_bits(b_bits); - if a_val.is_bigint() && b_val.is_bigint() { - return crate::bigint::js_bigint_eq(a_val.as_bigint_ptr(), b_val.as_bigint_ptr()) != 0; - } - if is_string_like(a_bits) && is_string_like(b_bits) { let mut a_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let mut b_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; diff --git a/crates/perry-runtime/src/string/char_ops.rs b/crates/perry-runtime/src/string/char_ops.rs index b40dad746..226076b19 100644 --- a/crates/perry-runtime/src/string/char_ops.rs +++ b/crates/perry-runtime/src/string/char_ops.rs @@ -98,14 +98,13 @@ pub extern "C" fn js_string_char_code_at(s: *const StringHeader, index: i32) -> /// `s[key]` indexed read with ECMAScript CanonicalNumericIndexString semantics /// (#3987): returns the single-UTF-16-code-unit string at `key` only when `key` -/// is `"length"` or a canonical array index — a non-negative integer (or a -/// numeric string that round-trips, e.g. `"1"`) within `[0, length)`. Other -/// keys return `undefined`: `NaN`, `Infinity`, negatives, fractions like -/// `1.5`, out-of-range indices, non-canonical strings like `"01"` / `" 1"` / -/// `"1.0"`, and non-index property names. Previously codegen `fptosi`'d the -/// key and called `js_string_char_at`, which truncated `1.5`→`1`, mapped -/// `NaN`→`0`, returned `""` (not `undefined`) for OOB/negatives, and -/// mis-resolved string keys. +/// is a canonical array index — a non-negative integer (or a numeric string +/// that round-trips, e.g. `"1"`) within `[0, length)`. Every other key returns +/// `undefined`: `NaN`, `Infinity`, negatives, fractions like `1.5`, +/// out-of-range indices, non-canonical strings like `"01"` / `" 1"` / `"1.0"`, +/// and non-numeric keys. Previously codegen `fptosi`'d the key and called +/// `js_string_char_at`, which truncated `1.5`→`1`, mapped `NaN`→`0`, returned +/// `""` (not `undefined`) for OOB/negatives, and mis-resolved string keys. #[no_mangle] pub extern "C" fn js_string_index_get(s: *const StringHeader, key: f64) -> f64 { const UNDEFINED: f64 = f64::from_bits(crate::value::TAG_UNDEFINED); @@ -114,8 +113,6 @@ pub extern "C" fn js_string_index_get(s: *const StringHeader, key: f64) -> f64 { } let len = unsafe { (*s).utf16_len } as u64; let jsval = crate::value::JSValue::from_bits(key.to_bits()); - let raw_key_bits = key.to_bits(); - let raw_key_ptr = raw_key_bits as *const StringHeader; let idx: u64 = if jsval.is_int32() { let i = jsval.as_int32(); @@ -129,44 +126,14 @@ pub extern "C" fn js_string_index_get(s: *const StringHeader, key: f64) -> f64 { return UNDEFINED; } key as u64 // saturating; an out-of-range magnitude fails the bound below - } else if (raw_key_bits >> 48) == 0 && is_valid_string_ptr(raw_key_ptr) { - let key_content = string_as_str(raw_key_ptr).to_string(); - if key_content == "length" { - return len as f64; - } - match canonical_string_index(&key_content) { + } else if jsval.is_any_string() { + match crate::builtins::jsvalue_string_content(key).and_then(|k| canonical_string_index(&k)) + { Some(i) => i, None => return UNDEFINED, } } else { - let key_content = if jsval.is_any_string() { - let Some(content) = crate::builtins::jsvalue_string_content(key) else { - return UNDEFINED; - }; - content - } else { - let direct_key_ptr = crate::value::js_jsvalue_to_string(key); - if is_valid_string_ptr(direct_key_ptr) { - string_as_str(direct_key_ptr).to_string() - } else { - let property_key = unsafe { crate::object::js_to_property_key(key) }; - if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { - return UNDEFINED; - } - let key_ptr = crate::value::js_jsvalue_to_string(property_key); - if !is_valid_string_ptr(key_ptr) { - return UNDEFINED; - } - string_as_str(key_ptr).to_string() - } - }; - if key_content == "length" { - return len as f64; - } - match canonical_string_index(&key_content) { - Some(i) => i, - None => return UNDEFINED, - } + return UNDEFINED; }; if idx >= len { diff --git a/crates/perry-runtime/src/string/format.rs b/crates/perry-runtime/src/string/format.rs index 4a316c12e..c8e00d06a 100644 --- a/crates/perry-runtime/src/string/format.rs +++ b/crates/perry-runtime/src/string/format.rs @@ -135,18 +135,13 @@ pub(crate) fn js_format_f64(value: f64) -> String { // matches Node's output). let abs = value.abs(); if !(1e-6..1e21).contains(&abs) { - format_short_exponential(value) + fix_exponent_format(&format!("{:e}", value)) } else { format!("{}", value) } } } -fn format_short_exponential(value: f64) -> String { - let mut buf = ryu::Buffer::new(); - fix_exponent_format(buf.format(value)) -} - /// GC root scanner for the small-integer string cache. /// /// The cache stores raw `StringHeader*` values, not NaN-boxed JSValues. The @@ -594,24 +589,9 @@ fn format_number_for_js(value: f64) -> String { // ECMAScript NumberToString — see js_number_to_string for rationale. let abs = value.abs(); if !(1e-6..1e21).contains(&abs) { - format_short_exponential(value) + fix_exponent_format(&format!("{:e}", value)) } else { format!("{}", value) } } } - -#[cfg(test)] -mod tests { - use super::js_format_f64; - - #[test] - fn formats_large_numbers_with_short_js_exponent() { - assert_eq!( - js_format_f64(3.4028234663852886e38), - "3.4028234663852886e+38" - ); - assert_eq!(js_format_f64(1e21), "1e+21"); - assert_eq!(js_format_f64(1e-7), "1e-7"); - } -} diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 4db60e7b5..36bfb0c5e 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -303,30 +303,6 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 if let Some(v) = web_stream_symbol_property(obj_f64, sym_f64) { return v; } - if let Some(tag) = crate::object::fetch_handle_to_string_tag(obj_f64) { - let tag_wk = well_known_symbol("toStringTag"); - if !tag_wk.is_null() { - let tag_f64 = - f64::from_bits(crate::value::JSValue::pointer(tag_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(tag_f64) { - let s = crate::string::js_string_from_bytes(tag.as_ptr(), tag.len() as u32); - return f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - } - } - if let Some(obj) = crate::url::object_from_f64(obj_f64) { - if crate::url::url_class::is_url_object_shape(obj) { - let tag_wk = well_known_symbol("toStringTag"); - if !tag_wk.is_null() { - let tag_f64 = - f64::from_bits(crate::value::JSValue::pointer(tag_wk as *const u8).bits()); - if sym_key_from_f64(sym_f64) == sym_key_from_f64(tag_f64) { - let s = crate::string::js_string_from_bytes(b"URL".as_ptr(), 3); - return f64::from_bits(crate::js_nanbox_string(s as i64).to_bits()); - } - } - } - } // #1213: Timeout/Immediate handles expose `Symbol.dispose` so // `using t = setTimeout(...)` and `t[Symbol.dispose]()` clear the timer. // The handle is a small id NaN-boxed as POINTER; the symbol-keyed read diff --git a/crates/perry-runtime/src/url/mod.rs b/crates/perry-runtime/src/url/mod.rs index e611b4c7b..dc266dd77 100644 --- a/crates/perry-runtime/src/url/mod.rs +++ b/crates/perry-runtime/src/url/mod.rs @@ -44,8 +44,6 @@ pub use self::search_params::{ // a URLSearchParams (a plain class_id-0 ObjectHeader) and pull its entries. pub(crate) use self::search_params::try_read_as_search_params; pub(crate) use self::url_class::is_url_object_shape; -/// Canonical class-id for `URL` instances (see instanceof / branding paths). -pub(crate) const CLASS_ID_URL: u32 = 0xFFFF002F; pub use self::url_class::{ js_url_can_parse, js_url_can_parse_with_base, js_url_get_hash, js_url_get_host, js_url_get_hostname, js_url_get_href, js_url_get_origin, js_url_get_pathname, js_url_get_port, @@ -129,9 +127,9 @@ pub extern "C" fn js_url_coerce_string(value: f64) -> *mut StringHeader { pub(crate) fn object_from_f64(value: f64) -> Option<*mut ObjectHeader> { let bits = value.to_bits(); if (bits & 0xFFFF_0000_0000_0000) == 0x7FFD_0000_0000_0000 { - let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::value::addr_class::is_plausible_heap_addr(addr) { - return Some(addr as *mut ObjectHeader); + let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; + if !ptr.is_null() { + return Some(ptr); } } None diff --git a/crates/perry-runtime/src/url/parse.rs b/crates/perry-runtime/src/url/parse.rs index a0299fda1..ab520599d 100644 --- a/crates/perry-runtime/src/url/parse.rs +++ b/crates/perry-runtime/src/url/parse.rs @@ -14,7 +14,7 @@ pub(crate) fn parse_url(url_str: &str) -> (String, String, String, String, Strin // future code path that fails to assign before use. let mut host: String; let hostname: String; - let mut pathname: String; + let pathname: String; let mut port = String::new(); let mut search = String::new(); let mut hash = String::new(); @@ -174,24 +174,6 @@ pub(crate) fn parse_url(url_str: &str) -> (String, String, String, String, Strin } } - #[cfg(feature = "url-engine")] - if matches!( - protocol.as_str(), - "http:" | "https:" | "ws:" | "wss:" | "ftp:" - ) { - if let Ok(parsed) = url::Url::parse(url_str) { - pathname = parsed.path().to_string(); - search = parsed - .query() - .map(|query| format!("?{query}")) - .unwrap_or_default(); - hash = parsed - .fragment() - .map(|fragment| format!("#{fragment}")) - .unwrap_or_default(); - } - } - (protocol, host, hostname, port, pathname, search, hash) } @@ -409,10 +391,6 @@ pub(crate) fn create_url_object(url_string: &str) -> *mut ObjectHeader { // Allocate object with URL_FIELD_COUNT fields // Using class_id 0 for now (generic object) let obj = js_object_alloc(0, URL_FIELD_COUNT); - let proto = crate::object::builtin_prototype_value("URL"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - crate::object::prototype_chain::object_set_static_prototype(obj as usize, proto.to_bits()); - } // Create the keys array with property names (order must match field indices) let mut keys = js_array_alloc(URL_FIELD_COUNT); diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index 7f235d79f..5471895a6 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -29,10 +29,10 @@ fn finite_nonnegative_u32_index(index: f64) -> Option { } /// Tag-aware dynamic index dispatch for `obj[key]` where `obj` has unknown -/// static type. Issue #514. Strings use canonical string-index/property -/// handling; objects stringify numeric keys (`obj[0]` is `obj["0"]`), while -/// arrays/buffers keep numeric element reads. LAZY_ARRAY / FORWARDED arrays -/// route through `js_array_get_f64` to chase the materialized chain. +/// static type. Issue #514. Strings → js_string_char_at; objects stringify +/// numeric keys (`obj[0]` is `obj["0"]`), while arrays/buffers keep numeric +/// element reads. LAZY_ARRAY / FORWARDED arrays route through +/// `js_array_get_f64` to chase the materialized chain. #[no_mangle] pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { let bits = value.to_bits(); @@ -43,24 +43,6 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { if bits == TAG_UNDEFINED || bits == TAG_NULL { crate::object::has_own_helpers::throw_to_object_nullish_type_error(); } - // A raw string pointer (a module-level string stored as raw I64, top16 == 0) - // routes through string indexing. Validate the GcHeader (obj_type == STRING) - // rather than a bare `>= 0x1000` range check: a plain `number` whose f64 bits - // look like a low pointer — e.g. the denormal ~1.7e-314 (bits 0x8_0000_0000) - // effect's fiber loop produces — also has top16 == 0, and dereferencing it as - // a StringHeader SIGBUSes. `try_read_gc_header` rejects non-heap addresses - // WITHOUT touching memory, so a denormal-bits number falls through to the - // number path below and correctly yields `undefined`. #63 / #321. - if (bits >> 48) == 0 { - if let Some(hdr) = unsafe { crate::value::addr_class::try_read_gc_header(bits as usize) } { - if hdr.obj_type == crate::gc::GC_TYPE_STRING { - return crate::string::js_string_index_get( - bits as *const crate::StringHeader, - index, - ); - } - } - } let jsval = JSValue::from_bits(bits); // #5525: a Symbol *index* (`obj[Symbol.iterator]`) must resolve through the // symbol side-table, never the integer-index / stringify paths below (which @@ -90,7 +72,16 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { if s_ptr.is_null() { return f64::from_bits(TAG_UNDEFINED); } - return crate::string::js_string_index_get(s_ptr, index); + let idx_i32 = if index.is_nan() || index.is_infinite() { + 0 + } else { + index as i32 + }; + let result = crate::string::js_string_char_at(s_ptr, idx_i32); + if result.is_null() { + return f64::from_bits(TAG_UNDEFINED); + } + return f64::from_bits(JSValue::string_ptr(result).bits()); } // Class-ref value (INT32-tagged, top16 == 0x7FFE): `C[key]` where `C` is a // runtime class-ref value (e.g. a function parameter). Member-expression diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index ac418b91a..e9673679a 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -93,16 +93,6 @@ unsafe fn both_bigint_or_throw(a: f64, b: f64) -> bool { } } -#[inline] -unsafe fn dynamic_number_operand(value: f64) -> f64 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_number() || jsval.is_int32() { - value - } else { - crate::builtins::js_number_coerce(value) - } -} - #[cold] unsafe fn throw_add_type_error(message: &[u8]) -> ! { let s = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); @@ -287,8 +277,7 @@ pub unsafe extern "C" fn js_dynamic_mul(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_mul); } - numify_arith_operand(dynamic_number_operand(a)) - * numify_arith_operand(dynamic_number_operand(b)) + numify_arith_operand(a) * numify_arith_operand(b) } /// Dynamic add: BigInt + BigInt if either operand is BigInt, else f64 + f64. @@ -297,10 +286,6 @@ pub unsafe extern "C" fn js_dynamic_add(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_add); } - // Unlike `- * / %`, `+` intentionally does NOT `dynamic_number_operand` - // (ToNumber) its operands: string concatenation is handled by the `+` - // lowering before reaching here, so any non-number that survives to this - // point is already meant to add numerically. numify_arith_operand(a) + numify_arith_operand(b) } @@ -472,8 +457,7 @@ pub unsafe extern "C" fn js_dynamic_sub(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_sub); } - numify_arith_operand(dynamic_number_operand(a)) - - numify_arith_operand(dynamic_number_operand(b)) + numify_arith_operand(a) - numify_arith_operand(b) } /// Dynamic divide: BigInt / BigInt if either operand is BigInt, else f64 / f64. @@ -484,8 +468,7 @@ pub unsafe extern "C" fn js_dynamic_div(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_div); } - numify_arith_operand(dynamic_number_operand(a)) - / numify_arith_operand(dynamic_number_operand(b)) + numify_arith_operand(a) / numify_arith_operand(b) } /// Dynamic modulo: BigInt % BigInt if either operand is BigInt, else f64 % f64. @@ -496,8 +479,6 @@ pub unsafe extern "C" fn js_dynamic_mod(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_mod); } - let a = dynamic_number_operand(a); - let b = dynamic_number_operand(b); let a = numify_arith_operand(a); let b = numify_arith_operand(b); // JS `%` is C `fmod`: the result takes the *sign of the dividend*, so @@ -629,7 +610,7 @@ pub unsafe extern "C" fn js_dynamic_pow(a: f64, b: f64) -> f64 { if both_bigint_or_throw(a, b) { return dynamic_bigint_binary_op(a, b, crate::bigint::js_bigint_pow); } - crate::math::js_math_pow(dynamic_number_operand(a), dynamic_number_operand(b)) + crate::math::js_math_pow(a, b) } /// Dynamic unsigned right shift. BigInts have no `>>>` operator in diff --git a/crates/perry-runtime/src/value/nanbox.rs b/crates/perry-runtime/src/value/nanbox.rs index e4454ef59..0568ad579 100644 --- a/crates/perry-runtime/src/value/nanbox.rs +++ b/crates/perry-runtime/src/value/nanbox.rs @@ -308,7 +308,6 @@ pub extern "C" fn js_get_string_pointer_unified(value: f64) -> i64 { /// - string vs non-string → false /// - number vs number → IEEE `==` after int32 unboxing (NaN ≠ NaN, -0 == +0, /// int32-boxed 1 == raw 1.0) -/// - bigint vs bigint → value compare (`js_bigint_eq`); bigint vs other → false /// - everything else (undefined/null/bool/pointers) → bit identity #[no_mangle] pub extern "C" fn js_switch_strict_equals(a: f64, b: f64) -> i32 { @@ -347,17 +346,6 @@ pub extern "C" fn js_switch_strict_equals(a: f64, b: f64) -> i32 { }; return (an == bn) as i32; } - // BigInt cases compare by value, not allocation identity, so - // `switch (1n + 0n) { case 1n: }` matches — mirrors the Set/Map key and - // general `===` paths. A BigInt is never `===` a non-BigInt. - let a_big = av.is_bigint(); - let b_big = bv.is_bigint(); - if a_big != b_big { - return 0; - } - if a_big { - return (crate::bigint::js_bigint_eq(av.as_bigint_ptr(), bv.as_bigint_ptr()) != 0) as i32; - } (a.to_bits() == b.to_bits()) as i32 } diff --git a/crates/perry-stdlib/src/fetch/dispatch.rs b/crates/perry-stdlib/src/fetch/dispatch.rs index 8df07fa10..abab8b97f 100644 --- a/crates/perry-stdlib/src/fetch/dispatch.rs +++ b/crates/perry-stdlib/src/fetch/dispatch.rs @@ -235,10 +235,10 @@ fn form_data_bound_method_value(form_id: usize, method_name: &'static str) -> f6 /// `instanceof` kind-probe for fetch handles (registered with the runtime at /// init via `js_register_fetch_handle_kind_probe`). Returns 0 = none, -/// 1 = Response, 2 = Request, 3 = Headers, 4 = Blob, 5 = File. Lets -/// `x instanceof Response` (etc.) resolve for the pointer-tagged small-integer -/// handles these types use instead of heap objects. Lives here (not `mod.rs`) -/// to keep that file under the 2,000-line lint gate. +/// 1 = Response, 2 = Request, 3 = Headers, 4 = Blob. Lets `x instanceof +/// Response` (etc.) resolve for the pointer-tagged small-integer handles these +/// types use instead of heap objects. Lives here (not `mod.rs`) to keep that +/// file under the 2,000-line lint gate. #[no_mangle] pub extern "C" fn js_fetch_handle_kind(id: usize) -> u8 { if FETCH_RESPONSES.lock().unwrap().contains_key(&id) { @@ -250,8 +250,8 @@ pub extern "C" fn js_fetch_handle_kind(id: usize) -> u8 { if HEADERS_REGISTRY.lock().unwrap().contains_key(&id) { return 3; } - if let Some(blob) = BLOB_REGISTRY.lock().unwrap().get(&id) { - return if blob.file_name.is_some() { 5 } else { 4 }; + if BLOB_REGISTRY.lock().unwrap().contains_key(&id) { + return 4; } 0 } @@ -809,17 +809,6 @@ pub fn dispatch_blob_property(blob_id: usize, prop: &str) -> Option { }); } let bits = match prop { - "constructor" => { - let ctor_name = if blob.file_name.is_some() { - "File" - } else { - "Blob" - }; - return Some(perry_runtime::object::js_get_global_this_builtin_value( - ctor_name.as_ptr(), - ctor_name.len(), - )); - } "size" => return Some(blob.body.len() as f64), "type" => { let p = diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index 95b704d16..aea2a88fb 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -836,25 +836,6 @@ fn collect_module_one( )); return; } - if let Some(unresolved) = set.iter().find(|p| { - cached_resolve_import_with_lexical_base(p, entry_path, &canonical, ctx) - .is_none() - }) { - let line = - perry_hir::current_module_line_at(*byte_offset).filter(|&l| l != 0); - let loc = match line { - Some(l) => format!("{}:{}", source_file_path, l), - None => source_file_path.clone(), - }; - let msg = format!( - "dynamic import() of registry value {:?} cannot be resolved in an \ - ahead-of-time compiled binary ({loc})", - unresolved - ); - perry_hir::record_deferred_aot_site("import(...)", loc); - dynamic_path_sets.push(DynImportOutcome::Deferred(msg)); - return; - } for p in &set { if !new_dyn_imports.contains(p) { new_dyn_imports.push(p.clone()); diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index 6654aeb94..a573734c7 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -537,8 +537,8 @@ fn compute_object_cache_key_with_env( .join(","); h.field("namespace_member_prefixes", &s); } - // Issue #680 / #5924: per-namespace origin-name resolution. Same rationale - // as `namespace_member_prefixes` above — not reflected in the consumer + // Issue #5924: per-namespace origin-name resolution. Same rationale as + // `namespace_member_prefixes` above — not reflected in the consumer // module's HIR, but changes the symbol suffix a namespace member // call/property access targets. { @@ -552,37 +552,6 @@ fn compute_object_cache_key_with_env( .join(","); h.field("namespace_member_origin_names", &s); } - { - let mut v: Vec<&(String, String)> = opts.namespace_member_vars.iter().collect(); - v.sort(); - let s: String = v - .iter() - .map(|(ns, member)| format!("{}:{}", ns, member)) - .collect::>() - .join(","); - h.field("namespace_member_vars", &s); - } - { - let mut v: Vec<(&(String, String), &String)> = - opts.namespace_member_namespace_prefixes.iter().collect(); - v.sort_by(|a, b| a.0.cmp(b.0)); - let s: String = v - .iter() - .map(|((ns, member), prefix)| format!("{}:{}={}", ns, member, prefix)) - .collect::>() - .join(","); - h.field("namespace_member_namespace_prefixes", &s); - } - { - let mut v: Vec<(&String, &String)> = opts.namespace_import_prefixes.iter().collect(); - v.sort_by(|a, b| a.0.cmp(b.0)); - let s: String = v - .iter() - .map(|(local, prefix)| format!("{}={}", local, prefix)) - .collect::>() - .join(","); - h.field("namespace_import_prefixes", &s); - } // Imported classes — sort by name. Serialize every field that codegen // reads so a changed constructor arity or new method on a re-exported @@ -597,12 +566,11 @@ fn compute_object_cache_key_with_env( let mut buf = String::new(); for c in v { buf.push_str(&format!( - "{}@{}:ctor={}:own_ctor={}:ctor_new_target={}:instance_fields={}:parent={}:alias={}:id={}:fields={}:methods={}:method_arities={}|", + "{}@{}:ctor={}:own_ctor={}:instance_fields={}:parent={}:alias={}:id={}:fields={}:methods={}:method_arities={}|", c.name, c.source_prefix, c.constructor_param_count, if c.has_own_constructor { "1" } else { "0" }, - if c.constructor_uses_new_target { "1" } else { "0" }, if c.has_instance_fields { "1" } else { "0" }, c.parent_name.as_deref().unwrap_or(""), c.local_alias.as_deref().unwrap_or(""), @@ -1079,9 +1047,6 @@ mod object_cache_tests { namespace_v8_specifiers: std::collections::HashMap::new(), namespace_member_prefixes: std::collections::HashMap::new(), namespace_member_origin_names: std::collections::HashMap::new(), - namespace_member_vars: std::collections::HashSet::new(), - namespace_member_namespace_prefixes: std::collections::HashMap::new(), - namespace_import_prefixes: std::collections::HashMap::new(), emit_ir_only: false, verify_native_regions: false, disable_buffer_fast_path: false, @@ -1406,7 +1371,6 @@ mod object_cache_tests { source_prefix: "feature_ts".into(), constructor_param_count: 0, has_own_constructor: false, - constructor_uses_new_target: false, constructor_has_rest: false, has_instance_fields: true, method_names: vec![], @@ -1442,7 +1406,6 @@ mod object_cache_tests { source_prefix: "src".into(), constructor_param_count: 1, has_own_constructor: true, - constructor_uses_new_target: false, constructor_has_rest: false, has_instance_fields: true, method_names: vec!["bar".into()], @@ -1463,7 +1426,6 @@ mod object_cache_tests { source_prefix: "src".into(), constructor_param_count: 2, // different arity has_own_constructor: true, - constructor_uses_new_target: false, constructor_has_rest: false, has_instance_fields: true, method_names: vec!["bar".into()], @@ -1492,7 +1454,6 @@ mod object_cache_tests { source_prefix: "src".into(), constructor_param_count: 1, has_own_constructor: true, - constructor_uses_new_target: false, constructor_has_rest: false, has_instance_fields: true, method_names: vec!["bar".into()], @@ -1559,33 +1520,6 @@ mod object_cache_tests { compute_object_cache_key(&a, 1, "0.5.156"), compute_object_cache_key(&b, 1, "0.5.156") ); - - let mut a = empty_opts(); - let mut b = empty_opts(); - b.namespace_member_origin_names - .insert(("ns".into(), "string".into()), "stringType".into()); - assert_ne!( - compute_object_cache_key(&a, 1, "0.5.156"), - compute_object_cache_key(&b, 1, "0.5.156") - ); - - let mut a = empty_opts(); - let mut b = empty_opts(); - b.namespace_member_vars - .insert(("ns".into(), "string".into())); - assert_ne!( - compute_object_cache_key(&a, 1, "0.5.156"), - compute_object_cache_key(&b, 1, "0.5.156") - ); - - let mut a = empty_opts(); - let mut b = empty_opts(); - b.namespace_import_prefixes - .insert("ns".into(), "source_prefix".into()); - assert_ne!( - compute_object_cache_key(&a, 1, "0.5.156"), - compute_object_cache_key(&b, 1, "0.5.156") - ); } #[test] diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 6338e7d7b..5de169627 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -17,162 +17,6 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use crate::OutputFormat; -fn expr_uses_new_target(expr: &perry_hir::Expr) -> bool { - match expr { - perry_hir::Expr::NewTarget => true, - perry_hir::Expr::Closure { - captures_new_target, - .. - } => *captures_new_target, - _ => { - let mut found = false; - perry_hir::walker::walk_expr_children(expr, &mut |child| { - if !found && expr_uses_new_target(child) { - found = true; - } - }); - found - } - } -} - -fn stmt_uses_new_target(stmt: &perry_hir::Stmt) -> bool { - use perry_hir::Stmt; - match stmt { - Stmt::Let { init, .. } => init.as_ref().is_some_and(expr_uses_new_target), - Stmt::Expr(expr) | Stmt::Return(Some(expr)) | Stmt::Throw(expr) => { - expr_uses_new_target(expr) - } - Stmt::Return(None) => false, - Stmt::If { - condition, - then_branch, - else_branch, - } => { - expr_uses_new_target(condition) - || then_branch.iter().any(stmt_uses_new_target) - || else_branch - .as_ref() - .is_some_and(|branch| branch.iter().any(stmt_uses_new_target)) - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - expr_uses_new_target(condition) || body.iter().any(stmt_uses_new_target) - } - Stmt::For { - init, - condition, - update, - body, - } => { - init.as_deref().is_some_and(stmt_uses_new_target) - || condition.as_ref().is_some_and(expr_uses_new_target) - || update.as_ref().is_some_and(expr_uses_new_target) - || body.iter().any(stmt_uses_new_target) - } - Stmt::Labeled { body, .. } => stmt_uses_new_target(body), - Stmt::Try { - body, - catch, - finally, - } => { - body.iter().any(stmt_uses_new_target) - || catch - .as_ref() - .is_some_and(|catch| catch.body.iter().any(stmt_uses_new_target)) - || finally - .as_ref() - .is_some_and(|body| body.iter().any(stmt_uses_new_target)) - } - Stmt::Switch { - discriminant, - cases, - } => { - expr_uses_new_target(discriminant) - || cases.iter().any(|case| { - case.test.as_ref().is_some_and(expr_uses_new_target) - || case.body.iter().any(stmt_uses_new_target) - }) - } - Stmt::Break - | Stmt::Continue - | Stmt::LabeledBreak(_) - | Stmt::LabeledContinue(_) - | Stmt::PreallocateBoxes(_) => false, - } -} - -fn ctor_uses_new_target(class: &perry_hir::Class) -> bool { - class - .constructor - .as_ref() - .is_some_and(|ctor| ctor.body.iter().any(stmt_uses_new_target)) -} - -fn exported_class_by_name<'a>( - exported_classes: &'a BTreeMap<(String, String), &'a perry_hir::Class>, - class_canonical_path: &std::collections::HashMap, - preferred_path: &str, - name: &str, -) -> Option<(String, &'a perry_hir::Class)> { - let same_path_key = (preferred_path.to_string(), name.to_string()); - if let Some(class) = exported_classes.get(&same_path_key) { - return Some((preferred_path.to_string(), *class)); - } - let mut candidates = exported_classes - .iter() - .filter(|((_, class_name), _)| class_name == name); - let first = candidates.next()?; - if candidates.next().is_none() { - return Some((first.0 .0.clone(), *first.1)); - } - exported_classes - .iter() - .find(|((_, class_name), class)| { - class_name == name - && class_canonical_path - .get(&class.id) - .is_some_and(|path| path == preferred_path) - }) - .map(|((path, _), class)| (path.clone(), *class)) -} - -fn class_chain_uses_new_target( - exported_classes: &BTreeMap<(String, String), &perry_hir::Class>, - class_canonical_path: &std::collections::HashMap, - class_path: &str, - class: &perry_hir::Class, -) -> bool { - if ctor_uses_new_target(class) { - return true; - } - let mut current_path = class_canonical_path - .get(&class.id) - .cloned() - .unwrap_or_else(|| class_path.to_string()); - let mut parent = class.extends_name.as_deref(); - let mut depth = 0usize; - while let Some(parent_name) = parent { - depth += 1; - if depth > 64 { - break; - } - let Some((path, parent_class)) = exported_class_by_name( - exported_classes, - class_canonical_path, - ¤t_path, - parent_name, - ) else { - break; - }; - if ctor_uses_new_target(parent_class) { - return true; - } - current_path = path; - parent = parent_class.extends_name.as_deref(); - } - false -} - /// Same as [`run`] but accepts an optional in-memory [`ParseCache`] that /// `perry dev` uses to reuse parsed ASTs across rebuilds in a single session. /// Pass `None` for the batch-compile path. @@ -1761,46 +1605,12 @@ pub fn run_with_parse_cache( } out }; - let sanitize_member_name = |s: &str| -> String { - if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { - return sanitize_module_name(s); - } - let mut out = String::from("u_"); - for c in s.chars() { - if c.is_ascii_alphanumeric() { - out.push(c); - } else { - out.push('_'); - out.push_str(&format!("{:x}", c as u32)); - out.push('_'); - } - } - out - }; let mut path_to_module_name: HashMap = HashMap::new(); let mut module_name_to_path: HashMap = HashMap::new(); for (path, hir_module) in &ctx.native_modules { path_to_module_name.insert(path.clone(), hir_module.name.clone()); - if let Ok(canonical) = std::fs::canonicalize(path) { - path_to_module_name.insert(canonical.clone(), hir_module.name.clone()); - module_name_to_path.insert(hir_module.name.clone(), canonical); - } else { - module_name_to_path.insert(hir_module.name.clone(), path.clone()); - } + module_name_to_path.insert(hir_module.name.clone(), path.clone()); } - let namespace_path_cache: std::sync::Mutex> = - std::sync::Mutex::new(HashMap::new()); - let normalize_namespace_path = |path: PathBuf| { - if let Some(cached) = namespace_path_cache.lock().unwrap().get(&path).cloned() { - return cached; - } - let normalized = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone()); - namespace_path_cache - .lock() - .unwrap() - .insert(path, normalized.clone()); - normalized - }; // Build a normalized HIR-by-name map for `flatten_exports`. Each // module's `Export::ReExport::source`, `Export::ExportAll::source`, // and `Export::NamespaceReExport::source` strings hold the raw @@ -1824,7 +1634,6 @@ pub fn run_with_parse_cache( &ctx.compile_packages, &ctx.compile_package_dirs, ) { - let resolved_path = normalize_namespace_path(resolved_path); if let Some(name) = path_to_module_name.get(&resolved_path) { *source = name.clone(); } @@ -1835,8 +1644,8 @@ pub fn run_with_parse_cache( } module_name_to_module.insert(hir_module.name.clone(), rewritten); } - // Set of native-module paths that are dynamic-import targets. Those - // drive dynamic-import dispatch maps below. + // Set of native-module paths that are dynamic-import targets. We + // also build a parallel set keyed by Module::name for flatten_exports. let mut dyn_target_paths: std::collections::HashSet = std::collections::HashSet::new(); for hir_module in ctx.native_modules.values() { for import in &hir_module.imports { @@ -1848,73 +1657,14 @@ pub fn run_with_parse_cache( continue; } if let Some(rp) = &import.resolved_path { - dyn_target_paths.insert(normalize_namespace_path(PathBuf::from(rp))); - } - } - } - - // Namespace objects are needed for more than dynamic import: - // `import * as ns` values and `export * as ns from "./mod"` both need the - // producer module to emit/populate `@__perry_ns_`. Dynamic imports - // are still a subset because `await import()` resolves to that same object. - let mut namespace_target_paths = dyn_target_paths.clone(); - for (path, hir_module) in &ctx.native_modules { - for import in &hir_module.imports { - if import.type_only { - continue; - } - let has_namespace_import = import - .specifiers - .iter() - .any(|spec| matches!(spec, perry_hir::ImportSpecifier::Namespace { .. })); - if has_namespace_import { - if let Some(rp) = &import.resolved_path { - namespace_target_paths.insert(normalize_namespace_path(PathBuf::from(rp))); - } - } - } - for export in &hir_module.exports { - if let perry_hir::Export::NamespaceReExport { source, .. } = export { - if let Some((resolved_path, _)) = resolve_import( - source, - path, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) { - namespace_target_paths.insert(normalize_namespace_path(resolved_path)); - } - } - } - } - - // A namespace target can itself expose nested namespace re-exports. Include - // those nested sources too so `export * as coerce from "./coerce.js"` can - // load a real `@__perry_ns_` when building its parent namespace. - let mut changed = true; - while changed { - changed = false; - let targets: Vec = namespace_target_paths.iter().cloned().collect(); - for target_path in targets { - let Some(target_hir) = ctx.native_modules.get(&target_path) else { - continue; - }; - let lookup = |s: &str| module_name_to_module.get(s); - for fe in perry_hir::flatten_exports(&target_hir.name, &lookup) { - if let Some(nested) = &fe.nested_namespace_of { - if let Some(nested_path) = module_name_to_path.get(nested) { - if namespace_target_paths.insert(nested_path.clone()) { - changed = true; - } - } - } + dyn_target_paths.insert(PathBuf::from(rp)); } } } // Per-module precomputed namespace_entries (keyed by path). let mut per_module_namespace_entries: HashMap> = HashMap::new(); - for target_path in &namespace_target_paths { + for target_path in &dyn_target_paths { let target_hir = match ctx.native_modules.get(target_path) { Some(m) => m, None => continue, // native/JS module — handled elsewhere @@ -1947,7 +1697,7 @@ pub fn run_with_parse_cache( let scoped = format!( "perry_fn_{}__{}", sanitize_module_name(&target_hir.name), - sanitize_member_name(&func.name) + sanitize_module_name(&func.name) ); perry_codegen::NamespaceEntryKind::LocalFunction { wrap_symbol: format!("__perry_wrap_{}", scoped), @@ -1970,13 +1720,13 @@ pub fn run_with_parse_cache( ); perry_codegen::NamespaceEntryKind::LocalVar { global_name: gname } } else { - // Best-effort: treat unknown locals as getter-backed vars. - // For renamed exports (`export { _null as null }`) the - // producer emits the public getter name, so prefer the - // namespace key over the private local spelling here. + // Best-effort: treat unknown locals as Var sourced + // by getter. This covers re-export shapes that the + // local-detection misses; the cross-module getter + // for the same module returns the value too. perry_codegen::NamespaceEntryKind::ForeignVar { source_prefix: sanitize_module_name(&target_hir.name), - source_local: fe.name.clone(), + source_local: fe.source_local.clone(), } } } else { @@ -2234,7 +1984,7 @@ pub fn run_with_parse_cache( continue; } if let Some(resolved) = &import.resolved_path { - let resolved_path = normalize_namespace_path(PathBuf::from(resolved)); + let resolved_path = PathBuf::from(resolved); if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); } @@ -2257,7 +2007,6 @@ pub fn run_with_parse_cache( &ctx.compile_packages, &ctx.compile_package_dirs, ) { - let resolved_path = normalize_namespace_path(resolved_path); if let Some(src_mod) = ctx.native_modules.get(&resolved_path) { push_dep(&mut deps, &mut seen, sanitize_name(&src_mod.name)); } @@ -2337,19 +2086,16 @@ pub fn run_with_parse_cache( // member_name)` → `source_prefix`. let mut namespace_member_prefixes: std::collections::HashMap<(String, String), String> = std::collections::HashMap::new(); - // Issue #680 / #5924: per-namespace origin-name resolution - // (mirroring `namespace_member_prefixes`). Being per-namespace - // instead of the flat `import_function_origin_names` avoids the - // #5924 clobber where two namespaces sharing a member name (only one - // a re-export rename) overwrite each other (`Effect`'s re-exported - // `Service` broke `Context.Service`). - let mut namespace_member_origin_names: std::collections::HashMap<(String, String), String> = - std::collections::HashMap::new(); - let mut namespace_member_vars: std::collections::HashSet<(String, String)> = - std::collections::HashSet::new(); - let mut namespace_member_namespace_prefixes: std::collections::HashMap<(String, String), String> = - std::collections::HashMap::new(); - let mut namespace_import_prefixes: std::collections::HashMap = + // Issue #5924 (companion to #680/#678): per-namespace origin-name + // resolution. `import_function_origin_names` is flat (keyed by + // bare member name), so when two namespaces imported into the + // same file both have a member with the same name and only ONE + // of them is a re-export rename, the rename's origin-name + // override clobbers the other namespace's (correct, unrenamed) + // suffix. Keyed by `(namespace_local, member_name)` → + // `origin_name`, mirroring `namespace_member_prefixes`. + let mut namespace_member_origin_names: + std::collections::HashMap<(String, String), String> = std::collections::HashMap::new(); let mut namespace_imports: Vec = Vec::new(); // Issue #321: subset of `namespace_imports` populated only by the @@ -2514,25 +2260,6 @@ pub fn run_with_parse_cache( }; if let Some(local) = namespace_like_local { namespace_imports.push(local.clone()); - let resolved_key = - normalize_namespace_path(PathBuf::from(&resolved_path_str)); - if let Some(target_mod) = ctx.native_modules.get(&resolved_key) { - let prefix = sanitize_name(&target_mod.name); - namespace_import_prefixes.insert(local.clone(), prefix); - let lookup = |s: &str| module_name_to_module.get(s); - for entry in perry_hir::flatten_exports(&target_mod.name, &lookup) { - if let Some(nested_module_name) = entry.nested_namespace_of { - let nested_prefix = module_name_to_module - .get(&nested_module_name) - .map(|m| sanitize_module_name(&m.name)) - .unwrap_or_else(|| sanitize_module_name(&nested_module_name)); - namespace_member_namespace_prefixes.insert( - (local.clone(), entry.name), - nested_prefix, - ); - } - } - } // Register all exports from the source module if let Some(exports) = all_module_exports.get(&resolved_path_str) { for (export_name, origin_path) in exports { @@ -2615,14 +2342,6 @@ pub fn run_with_parse_cache( (local.clone(), export_name.clone()), origin_prefix.clone(), ); - if let Some(ref origin_name) = resolved_origin_name { - if origin_name != export_name { - namespace_member_origin_names.insert( - (local.clone(), export_name.clone()), - origin_name.clone(), - ); - } - } let key = (origin_path.clone(), export_name.clone()); if let Some(¶m_count) = exported_func_param_counts.get(&key) { @@ -2671,7 +2390,6 @@ pub fn run_with_parse_cache( .unwrap_or(false) { imported_vars.insert(export_name.clone()); - namespace_member_vars.insert((local.clone(), export_name.clone())); } if let Some(class) = exported_classes.get(&key) { let class_prefix = canonical_class_source_prefix( @@ -2690,12 +2408,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &key.0, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -2838,26 +2550,39 @@ pub fn run_with_parse_cache( // name, then route the local through `namespace_imports` // + register the namespace target's full export surface. let mut handled_as_namespace_reexport = false; - macro_rules! register_namespace_export_surface { - ($namespace_local:expr, $target_path:expr) => {{ - let target_path_str = $target_path; - let target_key = normalize_namespace_path(PathBuf::from(target_path_str)); - if let Some(target_hir_for_ns) = ctx.native_modules.get(&target_key) { - let lookup = |s: &str| module_name_to_module.get(s); - for entry in perry_hir::flatten_exports(&target_hir_for_ns.name, &lookup) { - if let Some(nested_module_name) = entry.nested_namespace_of { - let nested_prefix = module_name_to_module - .get(&nested_module_name) - .map(|m| sanitize_module_name(&m.name)) - .unwrap_or_else(|| sanitize_module_name(&nested_module_name)); - namespace_member_namespace_prefixes.insert( - ($namespace_local.clone(), entry.name), - nested_prefix, - ); - } + if let Some(src_hir) = source_module { + for export in &src_hir.exports { + if let perry_hir::Export::NamespaceReExport { + source: ns_src, + name, + } = export + { + if name != &exported_name { + continue; } - } - if let Some(target_exports) = all_module_exports.get(target_path_str) { + let importer = std::path::Path::new(&resolved_path_str); + let Some((ns_target, _)) = resolve_import( + ns_src, + importer, + &ctx.project_root, + &ctx.compile_packages, + &ctx.compile_package_dirs, + ) else { + break; + }; + let ns_target_str = ns_target.to_string_lossy().to_string(); + let Some(target_exports) = all_module_exports.get(&ns_target_str) + else { + break; + }; + namespace_imports.push(local_name.clone()); + // Issue #321: tag this local as a "named-import- + // of-namespace-reexport" so codegen's + // StaticMethodCall arm knows to route var-shape + // members through `js_closure_callN`. See the + // expr.rs StaticMethodCall comment for why this + // is scoped narrowly. + namespace_reexport_named_imports.insert(local_name.clone()); for (export_name, origin_path) in target_exports { let origin_prefix = compute_module_prefix(origin_path, &ctx.project_root); @@ -2887,13 +2612,13 @@ pub fn run_with_parse_cache( // `import_function_prefixes`, which the // last-registered namespace silently wins. namespace_member_prefixes.insert( - ($namespace_local.clone(), export_name.clone()), + (local_name.clone(), export_name.clone()), origin_prefix.clone(), ); // Issue #678: surface origin-name overrides // for the NamespaceReExport branch too. let resolved_origin_name = all_module_export_origin_names - .get(target_path_str) + .get(&ns_target_str) .and_then(|m| m.get(export_name)) .cloned(); if let Some(ref origin_name) = resolved_origin_name { @@ -2936,10 +2661,8 @@ pub fn run_with_parse_cache( // resolved origin name when renamed, // else the export name itself. namespace_member_origin_names.insert( - ($namespace_local.clone(), export_name.clone()), - resolved_origin_name - .clone() - .unwrap_or_else(|| export_name.clone()), + (local_name.clone(), export_name.clone()), + resolved_origin_name.unwrap_or_else(|| export_name.clone()), ); let key = (origin_path.clone(), export_name.clone()); @@ -2954,18 +2677,27 @@ pub fn run_with_parse_cache( if exported_func_synthetic_arguments.contains(&key) { imported_synthetic_arguments.insert(export_name.clone()); } - let origin_key_under_origin_name = resolved_origin_name - .as_ref() - .map(|n| (origin_path.clone(), n.clone())); - if exported_var_names.contains(&key) - || origin_key_under_origin_name - .as_ref() - .map(|k| exported_var_names.contains(k)) - .unwrap_or(false) - { + // Issue #321: NamespaceReExport members + // that are var-shaped exports (the + // canonical `export const succeed = (v) => + // ...` shape in effect/Effect.ts and + // co-equivalent re-export hubs) must land + // in `imported_vars` so the codegen's + // StaticMethodCall and namespace-member + // call sites route through the zero-arg + // getter + `js_closure_callN`. Without + // this, `import { Effect } from "effect"; + // Effect.succeed(42)` emitted a 1-arg + // direct call against the 0-arg getter + // — the source returned the closure + // pointer unchanged and `typeof + // Effect.succeed(42)` was `"function"`, + // and `runSync(program)` then threw + // `Cannot read properties of undefined` + // on `program._tag`. Mirrors the + // `Namespace { local }` branch above. + if exported_var_names.contains(&key) { imported_vars.insert(export_name.clone()); - namespace_member_vars - .insert(($namespace_local.clone(), export_name.clone())); } if let Some(class) = exported_classes.get(&key) { let class_prefix = canonical_class_source_prefix( @@ -2984,13 +2716,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: - class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &key.0, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -3052,124 +2777,6 @@ pub fn run_with_parse_cache( imported_enums.push((export_name.clone(), members.clone())); } } - } - }}; - } - if let Some(src_hir) = source_module { - let direct_namespace_source = src_hir.imports.iter().find_map(|import| { - import.specifiers.iter().find_map(|spec| match spec { - perry_hir::ImportSpecifier::Namespace { local: ns_local } - if ns_local == &exported_name => - { - import.resolved_path.as_ref() - } - _ => None, - }) - }); - if let Some(namespace_source) = direct_namespace_source { - let resolved_key = - normalize_namespace_path(PathBuf::from(namespace_source)); - if let Some(target_mod) = ctx.native_modules.get(&resolved_key) { - namespace_imports.push(local_name.clone()); - namespace_import_prefixes - .insert(local_name.clone(), sanitize_name(&target_mod.name)); - register_namespace_export_surface!(local_name, namespace_source); - continue; - } - } - - let lookup = |name: &str| module_name_to_module.get(name); - let transitive_namespace_source = - perry_hir::flatten_exports(&src_hir.name, &lookup) - .into_iter() - .find(|entry| { - entry.name == exported_name - && entry.nested_namespace_of.is_some() - }) - .and_then(|entry| entry.nested_namespace_of); - if let Some(namespace_module_name) = transitive_namespace_source { - if let Some(namespace_path) = module_name_to_path.get(&namespace_module_name) - { - if let Some(target_mod) = ctx.native_modules.get(namespace_path) { - namespace_imports.push(local_name.clone()); - namespace_import_prefixes.insert( - local_name.clone(), - sanitize_name(&target_mod.name), - ); - let namespace_path_str = namespace_path.to_string_lossy(); - register_namespace_export_surface!( - local_name, - namespace_path_str.as_ref() - ); - continue; - } - } - } - - for export in &src_hir.exports { - let perry_hir::Export::Named { local, exported } = export else { - continue; - }; - if exported != &exported_name { - continue; - } - let namespace_source = src_hir.imports.iter().find_map(|import| { - import.specifiers.iter().find_map(|spec| match spec { - perry_hir::ImportSpecifier::Namespace { local: ns_local } - if ns_local == local => - { - import.resolved_path.as_ref() - } - _ => None, - }) - }); - let Some(namespace_source) = namespace_source else { - continue; - }; - let resolved_key = - normalize_namespace_path(PathBuf::from(namespace_source)); - let Some(target_mod) = ctx.native_modules.get(&resolved_key) else { - continue; - }; - namespace_imports.push(local_name.clone()); - namespace_import_prefixes - .insert(local_name.clone(), sanitize_name(&target_mod.name)); - register_namespace_export_surface!(local_name, namespace_source); - handled_as_namespace_reexport = true; - break; - } - if handled_as_namespace_reexport { - continue; - } - for export in &src_hir.exports { - if let perry_hir::Export::NamespaceReExport { - source: ns_src, - name, - } = export - { - if name != &exported_name { - continue; - } - let importer = std::path::Path::new(&resolved_path_str); - let Some((ns_target, _)) = resolve_import( - ns_src, - importer, - &ctx.project_root, - &ctx.compile_packages, - &ctx.compile_package_dirs, - ) else { - break; - }; - let ns_target_str = ns_target.to_string_lossy().to_string(); - namespace_imports.push(local_name.clone()); - // Issue #321: tag this local as a "named-import- - // of-namespace-reexport" so codegen's - // StaticMethodCall arm knows to route var-shape - // members through `js_closure_callN`. See the - // expr.rs StaticMethodCall comment for why this - // is scoped narrowly. - namespace_reexport_named_imports.insert(local_name.clone()); - register_namespace_export_surface!(local_name, ns_target_str.as_str()); handled_as_namespace_reexport = true; break; } @@ -3383,12 +2990,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &key.0, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -3460,12 +3061,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &key.0, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -3624,12 +3219,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &src_path, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -4103,12 +3692,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &src_path, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -4308,12 +3891,6 @@ pub fn run_with_parse_cache( .map(|c| c.params.len()) .unwrap_or(0), has_own_constructor: class.constructor.is_some(), - constructor_uses_new_target: class_chain_uses_new_target( - &exported_classes, - &class_canonical_path, - &src_path, - class, - ), constructor_has_rest: class .constructor .as_ref() @@ -4417,9 +3994,6 @@ pub fn run_with_parse_cache( namespace_v8_specifiers, namespace_member_prefixes, namespace_member_origin_names, - namespace_member_vars, - namespace_member_namespace_prefixes, - namespace_import_prefixes, emit_ir_only: bitcode_link, verify_native_regions, disable_buffer_fast_path, @@ -4450,10 +4024,10 @@ pub fn run_with_parse_cache( fast_math: ctx.fast_math, fp_contract_mode: ctx.fp_contract_mode, app_metadata: ctx.app_metadata.clone(), - // Namespace entries are emitted for modules whose namespace is - // observable: dynamic import targets, `import * as` targets, - // and `export * as` nested targets. The dynamic dispatch map is - // still limited to modules that actually perform dynamic imports. + // Issue #100: namespace_entries empty unless this + // module is a dynamic-import target; the consumer-side + // dispatch map is empty unless this module performs + // dynamic imports. namespace_entries: per_module_namespace_entries .get(path) .cloned() @@ -4465,11 +4039,14 @@ pub fn run_with_parse_cache( nextjs_path_init_modules, deferred_module_prefixes, module_init_deps, - // Issue #842 plus static namespace imports/re-exports: signal - // namespace-producing modules even when the entry list is empty. - // The field name is historical from dynamic import support, but - // the codegen behavior is "emit namespace global + populator". - is_dynamic_import_target: namespace_target_paths.contains(path), + // Issue #842: signal side-effect-only dynamic-import + // targets to codegen so it still emits + // `@__perry_ns_` + populator. `dyn_target_paths` + // is the authoritative set built from every consumer's + // `import.is_dynamic` resolved paths; `namespace_entries` + // alone is insufficient because it's empty when the + // target has no `export` statements. + is_dynamic_import_target: dyn_target_paths.contains(path), // #5247: source-location tracking for the dynamic call-dispatch // throw path. Gated by `--debug-symbols` so the default build is // unchanged (no source read, no per-call emission). When on, read diff --git a/direct b/direct deleted file mode 100755 index 94591b244..000000000 Binary files a/direct and /dev/null differ diff --git a/node_trace.1.log b/node_trace.1.log deleted file mode 100644 index 7c6981516..000000000 --- a/node_trace.1.log +++ /dev/null @@ -1 +0,0 @@ -{"traceEvents":[{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}},{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}},{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}},{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}},{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}},{"pid":78832,"tid":259,"ts":1049338985970,"tts":54122,"ph":"M","cat":"__metadata","name":"process_name","dur":0,"tdur":0,"args":{"name":"node"}},{"pid":78832,"tid":259,"ts":1049338985976,"tts":54128,"ph":"M","cat":"__metadata","name":"version","dur":0,"tdur":0,"args":{"node":"24.16.0"}},{"pid":78832,"tid":259,"ts":1049338985977,"tts":54129,"ph":"M","cat":"__metadata","name":"thread_name","dur":0,"tdur":0,"args":{"name":"JavaScriptMainThread"}},{"pid":78832,"tid":259,"ts":1049338985987,"tts":54139,"ph":"M","cat":"__metadata","name":"node","dur":0,"tdur":0,"args":{"process":{"versions":{"acorn":"8.16.0","ada":"3.4.4","amaro":"1.1.9","ares":"1.34.6","brotli":"1.2.0","cldr":"48.0","icu":"78.3","llhttp":"9.4.1","merve":"1.2.2","modules":"137","napi":"10","nbytes":"0.1.4","ncrypto":"0.0.1","nghttp2":"1.68.0","nghttp3":"","ngtcp2":"","node":"24.16.0","openssl":"3.5.6","simdjson":"4.6.1","simdutf":"6.4.0","sqlite":"3.53.0","tz":"2026b","undici":"7.25.0","unicode":"17.0","uv":"1.52.1","uvwasi":"0.0.23","v8":"13.6.233.17-node.49","zlib":"1.3.1-e00f703","zstd":"1.5.7"},"arch":"arm64","platform":"darwin","release":{"name":"node","lts":"Krypton"}}}}]} \ No newline at end of file diff --git a/test-files/test_parity_bigint_switch.ts b/test-files/test_parity_bigint_switch.ts deleted file mode 100644 index daa92ccd9..000000000 --- a/test-files/test_parity_bigint_switch.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Parity: `switch` on a BigInt must match `case Nn` by value, not identity. -function classify(x: bigint): string { - switch (x) { - case 0n: return "zero"; - case 1n: return "one"; - case 9007199254740993n: return "big"; - default: return "other"; - } -} -console.log(classify(1n + 0n)); -console.log(classify(9007199254740992n + 1n)); -console.log(classify(5n)); -console.log(new Map([[2n, "two"]]).get(1n + 1n) ?? "miss"); -console.log([3n].includes(1n + 2n)); diff --git a/test-files/test_parity_builtin_subclass_fields.ts b/test-files/test_parity_builtin_subclass_fields.ts deleted file mode 100644 index 510c4634f..000000000 --- a/test-files/test_parity_builtin_subclass_fields.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Parity: instance field initializers run on builtin subclasses whose -// constructor is implicit (Error/Map/Set/Array), same as an explicit one. -class E1 extends Error { tag = "e1"; n = 1 + 2; } -class M1 extends Map { tag = "m1"; } -class S1 extends Set { tag = "s1"; } -class A1 extends Array { tag = "a1"; } -class M2 extends Map { tag = "m2"; constructor() { super(); } } -const e = new E1(); -console.log(e.tag, (e as any).n, e instanceof Error); -console.log(new M1().tag, new S1().tag, (new A1() as any).tag, new M2().tag); -try { throw new E1("boom"); } catch (x: any) { console.log(x.message, x.tag); } diff --git a/test-parity/known_failures.json b/test-parity/known_failures.json index a2449791a..288583d3b 100644 --- a/test-parity/known_failures.json +++ b/test-parity/known_failures.json @@ -199,23 +199,5 @@ "added": "2026-07-04", "category": "module-inventory", "reason": "node:readline surface (#3698+) \u2014 compile_fail in the 2026-07-03 baseline (tls/zlib/readline compile cluster, possibly runner env); standing per #5917." - }, - "test_gap_class_advanced": { - "issue": "5952", - "added": "2026-07-04", - "category": "bug-open", - "reason": "Regression tracked in #5952: a mixin factory that DIRECTLY returns `class extends {}` (dynamic parent) loses its binding - `const M = make(Base); typeof M` is undefined. Workaround: assign to a local first (`const C = ...; return C`). Passes at merge-base bfa0f258d; introduced by the class-expression/factory work in #5874. Fix belongs in the HIR factory-class-inlining pass (skip dynamic-parent mixins)." - }, - "test_gap_dynamic_import_node_builtin": { - "issue": null, - "added": "2026-07-04", - "category": "gap-categorical", - "reason": "Categorical AOT gap: `import(\"node:crypto\")` and other dynamic import() of a node builtin cannot be resolved in an ahead-of-time compiled binary (compiles to a deferred runtime rejection). Standing (compile-failed at merge-base bfa0f258d, now a graceful deferred error); NOT a regression of #5874. Main's #5947 triage missed it because it was compile-failing at the time." - }, - "test_gap_disposablestack_2875": { - "issue": "2875", - "added": "2026-07-04", - "category": "gap-categorical", - "reason": "Categorical feature gap: DisposableStack / using-declaration (TC39) not implemented \u2014 methods return undefined. Standing, unrelated to #5874's Zod/namespace/class work; fails on main too (missed by the #5947 standing triage)." } } diff --git a/tests/_perry_test_lib.sh b/tests/_perry_test_lib.sh deleted file mode 100644 index f77253011..000000000 --- a/tests/_perry_test_lib.sh +++ /dev/null @@ -1,87 +0,0 @@ -# Shared harness for the tests/test_*.sh regression suite. -# -# SOURCED, never executed — the leading underscore keeps it out of -# run_tests.sh's `tests/test_*.sh` glob (same convention as the fixture -# harness's _fixture_lib.sh). It centralizes the perry-binary + runtime-lib -# detection that every regression test would otherwise copy-paste, so a change -# to the compile invocation lives in one place instead of ~130 files. -# -# Usage — single source file, literal-oracle: -# source "$(dirname "$0")/_perry_test_lib.sh" -# perry_run main.ts <<'TS' -# console.log("hi"); -# TS -# perry_expect 'hi' -# perry_pass "greeting" -# -# Usage — compare against Node instead of a literal: -# perry_run main.ts <<'TS' ... TS -# perry_expect_node # SKIPs cleanly if node is absent -# -# Usage — multi-module: write extra files first, then compile the entry: -# perry_write dep.ts <<'TS' ... TS -# perry_run main.ts <<'TS' ... TS # main.ts imports ./dep - -set -euo pipefail - -_PT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)" -REPO_ROOT="$(cd "$_PT_SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" -[[ -x "$PERRY" ]] || PERRY="$REPO_ROOT/target/debug/perry" -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -_PT_TMPDIR="$(mktemp -d)" -trap 'rm -rf "$_PT_TMPDIR"' EXIT -_PT_SRC="" - -# perry_write : write a source file into the temp dir from stdin. -perry_write() { - cat > "$_PT_TMPDIR/$1" -} - -# perry_run : write from stdin, then compile and run it, -# capturing stdout to run.log. Write any extra modules with perry_write first. -perry_run() { - _PT_SRC="$_PT_TMPDIR/$1" - cat > "$_PT_SRC" - local bin="$_PT_TMPDIR/out" - local args=(compile --no-cache) - # Link the prebuilt static libs only from the SAME target dir as the - # selected perry binary, so a release binary never pairs with debug libs - # (or vice versa). If that dir has no libs, leave PERRY_RUNTIME_DIR unset - # and let perry build/link its own runtime. - local perry_dir; perry_dir="$(cd "$(dirname "$PERRY")" && pwd)" - if [[ -f "$perry_dir/libperry_runtime.a" && -f "$perry_dir/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$perry_dir"; args+=(--no-auto-optimize) - fi - "$PERRY" "${args[@]}" "$_PT_SRC" -o "$bin" > "$_PT_TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed"; sed 's/^/ /' "$_PT_TMPDIR/compile.log" | tail -80; exit 1 - } - "$bin" > "$_PT_TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed"; sed 's/^/ /' "$_PT_TMPDIR/run.log" | tail -80; exit 1 - } -} - -# perry_expect [literal] : diff run.log against the expected output. With an -# argument, expects that single line; with no argument, reads the expected -# (possibly multi-line) output from stdin — e.g. perry_expect <<'EOF' ... EOF. -perry_expect() { - if [[ $# -gt 0 ]]; then - printf '%s\n' "$1" > "$_PT_TMPDIR/expected.log" - else - cat > "$_PT_TMPDIR/expected.log" - fi - diff -u "$_PT_TMPDIR/expected.log" "$_PT_TMPDIR/run.log" || { echo "FAIL: output mismatch"; exit 1; } -} - -# perry_expect_node : diff run.log against `node `; SKIP if node absent. -perry_expect_node() { - command -v node >/dev/null 2>&1 || { echo "SKIP: node binary not found"; exit 0; } - node "$_PT_SRC" > "$_PT_TMPDIR/expected.log" - diff -u "$_PT_TMPDIR/expected.log" "$_PT_TMPDIR/run.log" || { echo "FAIL: output mismatch vs node"; exit 1; } -} - -perry_pass() { echo "PASS: $1"; } diff --git a/tests/release/packages/zod3-basic/entry.ts b/tests/release/packages/zod3-basic/entry.ts deleted file mode 100644 index e4500891c..000000000 --- a/tests/release/packages/zod3-basic/entry.ts +++ /dev/null @@ -1,1836 +0,0 @@ -import zDefault, { z } from "zod"; -import * as zNamespace from "zod"; -import z3Default, { z as z3 } from "zod/v3"; -import * as z3Namespace from "zod/v3"; - -function print(label: string, value: unknown): void { - console.log(`${label}=${JSON.stringify(value)}`); -} - -function summarizeIssues(error: z.ZodError): { path: string; code: string; message: string }[] { - return error.issues.map((issue) => ({ - path: issue.path.join("."), - code: issue.code, - message: issue.message, - })); -} - -function issueMetadata(error: z.ZodError) { - return error.issues.map((issue) => { - const metadata: Record = { - code: issue.code, - path: issue.path.join("."), - }; - for (const key of [ - "expected", - "received", - "keys", - "options", - "validation", - "minimum", - "maximum", - "inclusive", - "exact", - "multipleOf", - "type", - "params", - ]) { - if (key in issue) { - metadata[key] = issue[key as keyof typeof issue]; - } - } - return metadata; - }); -} - -function issueMetadataFromResult(result: z.SafeParseReturnType) { - return result.success ? [] : issueMetadata(result.error); -} - -async function issueMetadataFromThrown(call: () => Promise) { - try { - await call(); - return []; - } catch (error) { - return issueMetadata(error as z.ZodError); - } -} - -function thrownSummary(call: () => unknown) { - try { - call(); - return "ok"; - } catch (error) { - return error instanceof Error ? `${error.constructor.name}:${error.message.split("\n")[0]}` : String(error); - } -} - -const userSchema = z.object({ - id: z.number().int().positive(), - name: z.string().min(2).transform((value) => value.trim().toUpperCase()), - tags: z.array(z.string()).default([]), - role: z.enum(["admin", "user"]).default("user"), - meta: z.object({ active: z.boolean(), score: z.number().optional() }).passthrough(), -}); - -const parsed = userSchema.parse({ - id: 7, - name: " ada ", - meta: { active: true, source: "seed" }, -}); -print("parse", parsed); - -const badUser = userSchema.safeParse({ - id: -1, - name: "x", - tags: ["ok", 2], - meta: { active: "yes" }, -}); -print("safeParse.success", badUser.success); -if (!badUser.success) { - print("safeParse.issues", summarizeIssues(badUser.error)); -} - -print("primitives", { - string: z.string().parse("x"), - number: z.number().parse(1.5), - int: z.number().int().safeParse(2.2).success, - boolean: z.boolean().parse(false), - bigint: z.bigint().parse(10n).toString(), - symbol: typeof z.symbol().parse(Symbol.for("fixture")), - nan: Number.isNaN(z.nan().parse(NaN)), - void: z.void().parse(undefined) === undefined, - null: z.null().parse(null), - undefined: z.undefined().parse(undefined) === undefined, - any: z.any().parse({ a: 1 }).a, - unknown: z.unknown().parse([1, 2]).length, - never: z.never().safeParse("nope").success, -}); - -print("packageExports", { - parsed: { - string: z.getParsedType("x"), - nan: z.getParsedType(NaN), - map: z.getParsedType(new Map()), - promise: z.getParsedType(Promise.resolve(1)), - }, - status: { - ok: z.OK(1), - dirty: z.DIRTY(2), - invalid: z.INVALID, - isValid: z.isValid(z.OK(1)), - isDirty: z.isDirty(z.DIRTY(2)), - isAborted: z.isAborted(z.INVALID), - }, - parsedType: { - string: z.ZodParsedType.string, - nan: z.ZodParsedType.nan, - map: z.ZodParsedType.map, - promise: z.ZodParsedType.promise, - }, - typeKind: { - object: z.ZodFirstPartyTypeKind.ZodObject, - pipeline: z.ZodFirstPartyTypeKind.ZodPipeline, - readonly: z.ZodFirstPartyTypeKind.ZodReadonly, - }, - aliases: { - schema: z.Schema === z.ZodType, - schemaInstance: z.string() instanceof z.Schema, - brand: typeof z.BRAND, - brandDescription: z.BRAND.description, - defaultEqualsNamed: zDefault === z, - namespaceHasZ: zNamespace.z === z, - namespaceDefaultEqualsNamed: zNamespace.default === z, - namespaceString: zNamespace.string().parse("namespace"), - }, - names: [z.ZodString.name, z.ZodNumber.name, z.ZodObject.name, z.ZodError.name], -}); - -const parseStatus = new z.ParseStatus(); -parseStatus.dirty(); -const parseStatusMerged = z.ParseStatus.mergeArray(parseStatus, [z.OK("a"), z.DIRTY("b")]); -parseStatus.abort(); -const datetimeWithOffset = z.datetimeRegex({ offset: true, precision: 3 }); -const validStatus = z.OK("valid"); -const dirtyStatus = z.DIRTY("dirty"); -const abortedStatus = z.INVALID; -print("packageUtilities", { - isAsync: [z.isAsync(Promise.resolve(1)), z.isAsync(1)], - parseStatus: { - dirty: parseStatusMerged.status, - merged: parseStatusMerged.value, - aborted: parseStatus.value, - }, - parsedTypes: [ - z.getParsedType(null), - z.getParsedType([]), - z.getParsedType(new Map()), - z.getParsedType(1n), - z.getParsedType(Promise.resolve(1)), - z.getParsedType(NaN), - ], - statusHelpers: { - valid: [validStatus.status, z.isValid(validStatus), z.isDirty(validStatus), z.isAborted(validStatus)], - dirty: [dirtyStatus.status, z.isValid(dirtyStatus), z.isDirty(dirtyStatus), z.isAborted(dirtyStatus)], - aborted: [abortedStatus.status, z.isValid(abortedStatus), z.isDirty(abortedStatus), z.isAborted(abortedStatus)], - never: z.NEVER.status, - emptyPathLength: z.EMPTY_PATH.length, - }, - util: { - arrayToEnum: z.util.arrayToEnum(["a", "b"]).b, - validEnumValues: z.util.getValidEnumValues({ A: "a", B: 1, 1: "B" }), - joinValues: z.util.joinValues(["a", 1, true]), - objectKeys: z.util.objectKeys({ b: 1, a: 2 }).join("|"), - objectValues: z.util.objectValues({ b: 1, a: 2 }), - find: z.util.find([1, 2, 3], (value) => value > 1), - isInteger: [z.util.isInteger(1), z.util.isInteger(1.5)], - jsonStringifyReplacer: JSON.stringify({ n: 1n }, z.util.jsonStringifyReplacer), - }, - quoteless: z.quotelessJson({ a: "x", nested: { b: 1 }, arr: [true, null] }).split("\n").length, - datetimeRegex: [ - datetimeWithOffset.test("2020-01-02T03:04:05.123Z"), - datetimeWithOffset.test("2020-01-02T03:04:05Z"), - ], - defaultError: z.defaultErrorMap( - { code: z.ZodIssueCode.invalid_type, expected: "string", received: "number", path: [] }, - { data: 1, defaultError: "fallback" }, - ).message, -}); - -const directIssue = z.makeIssue({ - data: 1, - path: ["root"], - issueData: { code: z.ZodIssueCode.custom, path: ["leaf"], message: "direct" }, - errorMaps: [z.defaultErrorMap], -}); -const mappedIssue = z.makeIssue({ - data: "bad", - path: ["root"], - issueData: { code: z.ZodIssueCode.invalid_type, expected: "number", received: "string" }, - errorMaps: [z.defaultErrorMap], -}); -const parseContext = { - common: { issues: [], async: false }, - path: ["ctx"], - schemaErrorMap: undefined, - parent: null, - data: "bad", - parsedType: z.ZodParsedType.string, -} as any; -z.addIssueToContext(parseContext, { - code: z.ZodIssueCode.invalid_type, - expected: "number", - received: "string", -}); -const transformerSchema = z.ZodTransformer.create(z.string(), { - type: "transform", - transform: (value: string) => value.length, -}); -print("parseHelpers", { - direct: { code: directIssue.code, path: directIssue.path.join("."), message: directIssue.message }, - mapped: { path: mappedIssue.path.join("."), message: mappedIssue.message }, - context: parseContext.common.issues.map((issue: z.ZodIssue) => ({ - path: issue.path.join("."), - message: issue.message, - })), - aliases: { - schema: z.string() instanceof z.ZodSchema, - type: z.string() instanceof z.ZodType, - transformer: transformerSchema instanceof z.ZodTransformer, - transformerParse: transformerSchema.parse("tuna"), - }, -}); - -print("coerce", { - string: z.coerce.string().parse(42), - number: z.coerce.number().parse("12.5"), - boolean: z.coerce.boolean().parse(1), - bigint: z.coerce.bigint().parse("42").toString(), - date: z.coerce.date().parse("2020-01-02T00:00:00.000Z").toISOString(), -}); - -print("primitiveEdges", { - boolean: z.boolean().parse(true), - booleanRejectsString: z.boolean().safeParse("true").success, - coerceBooleanString: z.coerce.boolean().parse("false"), - coerceNumberRejectsNaN: z.coerce.number().safeParse("not-a-number").success, - coerceBigintRejectsText: z.coerce.bigint().safeParse("not-a-bigint").success, - coerceDateEpoch: z.coerce.date().parse(0).toISOString(), - coerceDateRejectsInvalid: z.coerce.date().safeParse("not-a-date").success, -}); - -const boundedDateSchema = z.date() - .min(new Date("2020-01-01T00:00:00.000Z"), "too old") - .max(new Date("2020-12-31T00:00:00.000Z"), "too new"); -const primitiveInstanceSchema = z.instanceof(Date); -print("primitiveMetadata", { - types: [ - z.string()._def.typeName, - z.number()._def.typeName, - z.bigint()._def.typeName, - z.boolean()._def.typeName, - z.nan()._def.typeName, - z.symbol()._def.typeName, - z.any()._def.typeName, - z.unknown()._def.typeName, - z.never()._def.typeName, - z.undefined()._def.typeName, - z.null()._def.typeName, - z.void()._def.typeName, - ], - date: { - typeName: boundedDateSchema._def.typeName, - coerce: boundedDateSchema._def.coerce, - minDate: boundedDateSchema.minDate?.toISOString(), - maxDate: boundedDateSchema.maxDate?.toISOString(), - checks: boundedDateSchema._def.checks.map((check) => ({ - kind: check.kind, - value: check.value, - message: check.message, - })), - }, - coerceDate: z.coerce.date()._def.coerce, - instanceof: { - typeName: primitiveInstanceSchema._def.typeName, - inner: primitiveInstanceSchema._def.schema.constructor.name, - effect: primitiveInstanceSchema._def.effect.type, - }, -}); - -const colorEnum = z.enum(["red", "blue", "green"]); -const literalReady = z.literal("ready"); -const nativeTextEnum = z.nativeEnum({ A: "a", B: "b" } as const); -const nativeMixedEnumObject = { Text: "text", Zero: 0, One: 1, 0: "Zero", 1: "One" } as const; -const nativeMixedEnum = z.nativeEnum(nativeMixedEnumObject); -const nativeMixedReverseResult = nativeMixedEnum.safeParse("Zero"); -const enumRequiredResult = z - .enum(["red", "blue"], { required_error: "need color", invalid_type_error: "bad color" }) - .safeParse(undefined); -const enumInvalidTypeResult = z - .enum(["red", "blue"], { required_error: "need color", invalid_type_error: "bad color" }) - .safeParse(1); -print("literals.enums", { - literal: literalReady.safeParse("ready").success, - literalBigint: z.literal(2n).safeParse(2n).success, - literalBoolean: z.literal(true).safeParse(false).success, - literalNull: z.literal(null).parse(null), - literalFail: z.literal(3).safeParse(4).success, - literalValue: literalReady.value, - literalTypeName: literalReady._def.typeName, - enum: colorEnum.parse("blue"), - enumBlue: colorEnum.enum.blue, - enumValues: colorEnum.Values.green, - enumEnum: colorEnum.Enum.red, - enumOptions: colorEnum.options.join("|"), - enumTypeName: colorEnum._def.typeName, - enumDefValues: colorEnum._def.values.join("|"), - enumKeys: Object.keys(colorEnum.enum).sort(), - enumExtract: colorEnum.extract(["red", "green"]).safeParse("blue").success, - enumExtractOk: colorEnum.extract(["red", "green"]).parse("red"), - enumExtractOptions: colorEnum.extract(["red", "green"]).options.join("|"), - enumExclude: colorEnum.exclude(["blue"]).parse("green"), - enumExcludeOptions: colorEnum.exclude(["blue"]).options.join("|"), - enumRequiredMessage: enumRequiredResult.success ? "ok" : enumRequiredResult.error.issues[0].message, - enumInvalidTypeMessage: enumInvalidTypeResult.success ? "ok" : enumInvalidTypeResult.error.issues[0].message, - nativeEnum: nativeTextEnum.parse("a"), - nativeEnumNumber: z.nativeEnum({ A: 1, B: 2 } as const).parse(2), - nativeEnumMixed: [nativeMixedEnum.parse("text"), nativeMixedEnum.parse(0), nativeMixedEnum.parse(1)], - nativeEnumReverseRejected: issueMetadataFromResult(nativeMixedReverseResult), - nativeEnumTypeName: nativeTextEnum._def.typeName, - nativeEnumKeys: Object.keys(nativeTextEnum.enum).join("|"), - nativeEnumMixedKeys: Object.keys(nativeMixedEnum.enum).join("|"), -}); - -const stringSchema = z - .string() - .min(3) - .max(8) - .regex(/^[a-z-]+$/) - .startsWith("ab") - .endsWith("yz") - .includes("-") - .trim() - .toUpperCase(); -print("strings", { - value: stringSchema.parse("ab-yz"), - email: z.string().email().safeParse("a@example.com").success, - uuid: z.string().uuid().safeParse("550e8400-e29b-41d4-a716-446655440000").success, - url: z.string().url().safeParse("https://example.com/a?b=1").success, - datetime: z.string().datetime().safeParse("2020-01-02T03:04:05.000Z").success, - datetimeOffset: z.string().datetime({ offset: true }).safeParse("2020-01-02T03:04:05+02:00").success, - datetimePrecision: z.string().datetime({ precision: 3 }).safeParse("2020-01-02T03:04:05.123Z").success, - datetimeLocal: z.string().datetime({ local: true }).safeParse("2020-01-02T03:04:05").success, - ip: z.string().ip().safeParse("127.0.0.1").success, - ipv4: z.string().ip({ version: "v4" }).safeParse("127.0.0.1").success, - ipv6: z.string().ip({ version: "v6" }).safeParse("::1").success, - length: z.string().length(3).safeParse("abc").success, - nonempty: z.string().nonempty().safeParse("").success, - timePrecision: z.string().time({ precision: 0 }).safeParse("03:04:05.123").success, - includesPosition: z.string().includes("b", { position: 1 }).safeParse("abc").success, - includesPositionFail: z.string().includes("b", { position: 2 }).safeParse("abc").success, - startsWith: z.string().startsWith("tu").safeParse("tuna").success, - startsWithFail: z.string().startsWith("tu").safeParse("fish").success, - endsWith: z.string().endsWith("na").safeParse("tuna").success, - endsWithFail: z.string().endsWith("na").safeParse("tune").success, - trim: z.string().trim().parse(" tuna "), - uppercase: z.string().toUpperCase().parse("tuna"), - lowercase: z.string().toLowerCase().parse("ABC"), - chainedTransform: z.string().trim().toUpperCase().startsWith("TU").endsWith("NA").parse(" tuna "), - cuid: z.string().cuid().safeParse("ckj8lp2e90000v4j5x6j8s9abc").success, - cuid2: z.string().cuid2().safeParse("tz4a98xxat96iws9zmbrgj3a").success, - ulid: z.string().ulid().safeParse("01ARZ3NDEKTSV4RRFFQ69G5FAV").success, - emoji: z.string().emoji().safeParse("😀").success, - nanoid: z.string().nanoid().safeParse("V1StGXR8_Z5jdHi6B-myT").success, - base64: z.string().base64().safeParse("aGVsbG8=").success, - base64url: z.string().base64url().safeParse("aGVsbG8").success, - jwt: z.string().jwt().safeParse("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature").success, - date: z.string().date().safeParse("2020-01-02").success, - time: z.string().time().safeParse("03:04:05").success, - duration: z.string().duration().safeParse("P1Y2M3DT4H5M6S").success, - cidr: z.string().cidr().safeParse("192.168.0.0/24").success, - cidrv4: z.string().cidr({ version: "v4" }).safeParse("192.168.0.0/24").success, - cidrv6: z.string().cidr({ version: "v6" }).safeParse("2001:db8::/32").success, - regexFail: z.string().regex(/^[a-z]+$/).safeParse("abc1").success, -}); - -const boundedString = z.string().min(2).max(5); -const formattedStrings = { - email: z.string().email(), - url: z.string().url(), - uuid: z.string().uuid(), - datetime: z.string().datetime(), - ip: z.string().ip(), -}; -const extraFormattedStrings = { - base64: z.string().base64(), - base64url: z.string().base64url(), - cidr: z.string().cidr(), - cuid: z.string().cuid(), - cuid2: z.string().cuid2(), - date: z.string().date(), - duration: z.string().duration(), - emoji: z.string().emoji(), - nanoid: z.string().nanoid(), - time: z.string().time(), - ulid: z.string().ulid(), -}; -const boundedNumber = z.number().int().min(1).max(3).finite(); -print("schemaIntrospection", { - stringBounds: [boundedString.minLength, boundedString.maxLength], - stringFormats: Object.fromEntries( - Object.entries(formattedStrings).map(([name, schema]) => [ - name, - { - isEmail: schema.isEmail, - isURL: schema.isURL, - isUUID: schema.isUUID, - isDatetime: schema.isDatetime, - isIP: schema.isIP, - }, - ]), - ), - extraStringFormats: { - base64: extraFormattedStrings.base64.isBase64, - base64url: extraFormattedStrings.base64url.isBase64url, - cidr: extraFormattedStrings.cidr.isCIDR, - cuid: extraFormattedStrings.cuid.isCUID, - cuid2: extraFormattedStrings.cuid2.isCUID2, - date: extraFormattedStrings.date.isDate, - duration: extraFormattedStrings.duration.isDuration, - emoji: extraFormattedStrings.emoji.isEmoji, - nanoid: extraFormattedStrings.nanoid.isNANOID, - time: extraFormattedStrings.time.isTime, - ulid: extraFormattedStrings.ulid.isULID, - }, - numberBounds: [boundedNumber.minValue, boundedNumber.maxValue], - numberFlags: { isInt: boundedNumber.isInt, isFinite: boundedNumber.isFinite }, -}); - -print("numbers", { - finite: z.number().finite().safeParse(Number.POSITIVE_INFINITY).success, - min: z.number().min(2).safeParse(2).success, - max: z.number().max(2).safeParse(3).success, - gt: z.number().gt(1).safeParse(2).success, - gte: z.number().gte(2).safeParse(2).success, - lt: z.number().lt(3).safeParse(3).success, - lte: z.number().lte(3).safeParse(3).success, - multiple: z.number().multipleOf(5).safeParse(15).success, - step: z.number().step(0.5).safeParse(1.5).success, - positive: z.number().positive().safeParse(1).success, - nonpositive: z.number().nonpositive().safeParse(0).success, - negative: z.number().negative().safeParse(-1).success, - nonnegative: z.number().nonnegative().safeParse(0).success, - safe: z.number().safe().safeParse(Number.MAX_SAFE_INTEGER + 1).success, -}); - -print("bigints", { - min: z.bigint().min(2n).safeParse(2n).success, - max: z.bigint().max(2n).safeParse(3n).success, - gt: z.bigint().gt(1n).safeParse(2n).success, - gte: z.bigint().gte(2n).safeParse(2n).success, - lt: z.bigint().lt(3n).safeParse(3n).success, - lte: z.bigint().lte(3n).safeParse(3n).success, - multiple: z.bigint().multipleOf(5n).safeParse(15n).success, - positive: z.bigint().positive().safeParse(1n).success, - nonpositive: z.bigint().nonpositive().safeParse(0n).success, - negative: z.bigint().negative().safeParse(-1n).success, - nonnegative: z.bigint().nonnegative().safeParse(0n).success, -}); - -const eventSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal("text"), value: z.string() }), - z.object({ type: z.literal("count"), value: z.number().int() }), -]); -const multiValueDiscriminatedSchema = z.discriminatedUnion("kind", [ - z.object({ kind: z.enum(["text", "markdown"]), body: z.string() }), - z.object({ kind: z.literal("count"), value: z.number() }), -]); -const multiValueInvalidKind = multiValueDiscriminatedSchema.safeParse({ kind: "image", body: "x" }); -const multiValueInvalidBranch = multiValueDiscriminatedSchema.safeParse({ kind: "text", body: 1 }); -const inheritedDiscriminatedInput = Object.create({ type: "count", value: 3 }); -inheritedDiscriminatedInput.extra = "own"; -const inheritedDiscriminatedParsed = eventSchema.parse(inheritedDiscriminatedInput); -const inheritedDiscriminatedInvalidInput = Object.create({ type: "count", value: "bad" }); -inheritedDiscriminatedInvalidInput.extra = "own"; -const objectUnionSchema = z.union([ - z.object({ kind: z.literal("a"), value: z.string() }), - z.object({ kind: z.literal("b"), count: z.number() }), -]); -const unionBranchFailure = objectUnionSchema.safeParse({ kind: "a", value: 1 }); -const inheritedUnionInput = Object.create({ kind: "b", count: 2 }); -inheritedUnionInput.extra = "own"; -const inheritedUnionParsed = objectUnionSchema.parse(inheritedUnionInput); -print("discriminatedUnion", [ - eventSchema.parse({ type: "text", value: "hello" }), - eventSchema.parse({ type: "count", value: 3 }), -]); -print("discriminatedUnion.options", { - length: eventSchema.options.length, - discriminator: eventSchema.discriminator, - optionKeys: Array.from(eventSchema.optionsMap.keys()).join("|"), - typeName: eventSchema._def.typeName, - inherited: inheritedDiscriminatedParsed, - inheritedOwnType: Object.prototype.hasOwnProperty.call(inheritedDiscriminatedParsed, "type"), - inheritedOwnValue: Object.prototype.hasOwnProperty.call(inheritedDiscriminatedParsed, "value"), - inheritedInvalid: issueMetadataFromResult(eventSchema.safeParse(inheritedDiscriminatedInvalidInput)), - multiValue: [ - multiValueDiscriminatedSchema.parse({ kind: "text", body: "hello" }), - multiValueDiscriminatedSchema.parse({ kind: "markdown", body: "**hello**" }), - ], - multiValueOptionKeys: Array.from(multiValueDiscriminatedSchema.optionsMap.keys()).join("|"), - multiValueInvalidKind: issueMetadataFromResult(multiValueInvalidKind), - multiValueInvalidBranch: issueMetadataFromResult(multiValueInvalidBranch), -}); - -const primitiveSchema = z.union([z.string().regex(/^id-/), z.number().int()]); -print("union", { - results: [primitiveSchema.safeParse("id-42").success, primitiveSchema.safeParse(4.5).success], - options: primitiveSchema.options.length, - typeName: primitiveSchema._def.typeName, - optionTypes: primitiveSchema._def.options.map((option) => option.constructor.name), - branchFailure: unionBranchFailure.success - ? [] - : unionBranchFailure.error.issues[0].unionErrors.map((error) => - error.issues.map((issue) => `${issue.path.join(".")}:${issue.code}`), - ), - inheritedObject: inheritedUnionParsed, - inheritedObjectOwnKind: Object.prototype.hasOwnProperty.call(inheritedUnionParsed, "kind"), - inheritedObjectOwnCount: Object.prototype.hasOwnProperty.call(inheritedUnionParsed, "count"), -}); - -const objectBase = z.object({ id: z.number(), name: z.string(), active: z.boolean().optional() }); -const nestedObject = z.object({ nested: z.object({ label: z.string() }) }); -const defaultedOptionalObject = z.object({ defaulted: z.string().default("tuna").optional() }); -const maskedObjectBase = z.object({ - requiredName: z.string(), - optionalAge: z.number().optional(), - nested: z.object({ label: z.string() }), -}); -const maskedPartialObject = maskedObjectBase.partial({ requiredName: true }); -const maskedRequiredObject = maskedPartialObject.required({ requiredName: true }); -const inheritedObjectInput = Object.create({ inherited: "from-proto" }); -inheritedObjectInput.own = 1; -const inheritedObjectParsed = z.object({ own: z.number(), inherited: z.string() }).parse(inheritedObjectInput); -const inheritedPassthroughInput = Object.create({ inheritedExtra: "from-proto" }); -inheritedPassthroughInput.id = 1; -const inheritedPassthroughParsed = z.object({ id: z.number() }).passthrough().parse(inheritedPassthroughInput); -const nullProtoInput = Object.create(null) as Record; -nullProtoInput.id = 1; -nullProtoInput.extra = "x"; -const nullProtoPassthroughParsed = z.object({ id: z.number() }).passthrough().parse(nullProtoInput); -const nullProtoStrippedParsed = z.object({ id: z.number() }).parse(nullProtoInput); -let getterOwnReads = 0; -let getterInheritedReads = 0; -const getterInput = Object.create({ - get inherited() { - getterInheritedReads += 1; - return "from-getter"; - }, -}); -Object.defineProperty(getterInput, "id", { - enumerable: true, - get() { - getterOwnReads += 1; - return 7; - }, -}); -const getterObjectParsed = z.object({ id: z.number(), inherited: z.string() }).parse(getterInput); -const getterPassthroughParsed = z.object({ id: z.number() }).passthrough().parse(getterInput); -const partialObjectSchema = objectBase.partial(); -const partialNameObjectSchema = objectBase.partial({ name: true }); -const requiredObjectSchema = objectBase.required(); -const requiredActiveObjectSchema = objectBase.required({ active: true }); -const deepPartialObjectSchema = nestedObject.deepPartial(); -const objectShapeTypes = (schema: z.AnyZodObject) => - Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => [key, value.constructor.name])); -const categorySchema: z.ZodType = z.object({ - name: z.string(), - get subcategories() { - return z.array(categorySchema); - }, -}); -const authorSchema: z.ZodType = z.object({ - email: z.string().email(), - get posts() { - return z.array(postSchema); - }, -}); -const postSchema: z.ZodType = z.object({ - title: z.string(), - get author() { - return authorSchema; - }, -}); -const recursiveEventSchema: z.ZodType = z.discriminatedUnion("kind", [ - z.object({ kind: z.literal("leaf"), value: z.string() }), - z.object({ - kind: z.literal("branch"), - get children() { - return z.array(recursiveEventSchema); - }, - }), -]); -print("objects", { - strip: objectBase.parse({ id: 1, name: "a", extra: true } as unknown), - explicitStrip: objectBase.strip().parse({ id: 1, name: "a", extra: true } as unknown), - strict: objectBase.strict().safeParse({ id: 1, name: "a", extra: true }).success, - strictObject: z.strictObject({ name: z.string() }).safeParse({ name: "a", extra: true }).success, - nonstrict: objectBase.nonstrict().parse({ id: 1, name: "a", extra: true } as unknown), - passthrough: objectBase.passthrough().parse({ id: 1, name: "a", extra: true } as unknown), - catchall: z.object({ id: z.number() }).catchall(z.string()).safeParse({ id: 1, extra: "ok" }).success, - catchallFail: z.object({ id: z.number() }).catchall(z.string()).safeParse({ id: 1, extra: 1 }).success, - extend: objectBase.extend({ role: z.literal("admin") }).parse({ id: 1, name: "a", role: "admin" }), - augment: z.object({ id: z.number() }).augment({ name: z.string() }).parse({ id: 1, name: "a" }), - setKey: z.object({ id: z.number() }).setKey("name", z.string()).parse({ id: 1, name: "a" }), - merge: objectBase.merge(z.object({ role: z.string() })).parse({ id: 1, name: "a", role: "user" }), - strictMessage: z.object({ id: z.number() }).strict("no extras").safeParse({ id: 1, extra: true }).success, - keyof: objectBase.keyof().parse("name"), - shapeName: objectBase.shape.name.safeParse("a").success, - pick: objectBase.pick({ id: true }).parse({ id: 1 }), - omit: objectBase.omit({ active: true }).parse({ id: 1, name: "a" }), - spreadShape: z.object({ ...objectBase.shape, role: z.string() }).parse({ - id: 1, - name: "a", - role: "user", - }), - inheritedInput: inheritedObjectInput, - inheritedOutput: inheritedObjectParsed, - inheritedOwnOutput: Object.prototype.hasOwnProperty.call(inheritedObjectParsed, "inherited"), - inheritedOwnInput: Object.prototype.hasOwnProperty.call(inheritedObjectInput, "inherited"), - inheritedOutputPrototype: Object.getPrototypeOf(inheritedObjectParsed) === Object.prototype, - inheritedPassthrough: inheritedPassthroughParsed, - inheritedPassthroughOwn: Object.prototype.hasOwnProperty.call(inheritedPassthroughParsed, "inheritedExtra"), - inheritedStrict: z.object({ id: z.number() }).strict().safeParse(inheritedPassthroughInput).success, - nullProto: nullProtoPassthroughParsed, - nullProtoStripped: nullProtoStrippedParsed, - nullProtoInputPrototype: Object.getPrototypeOf(nullProtoInput) === null, - nullProtoOutputPrototype: Object.getPrototypeOf(nullProtoPassthroughParsed) === Object.prototype, - nullProtoStrict: z.object({ id: z.number() }).strict().safeParse(nullProtoInput).success, - getterInput: getterObjectParsed, - getterPassthrough: getterPassthroughParsed, - getterReads: [getterOwnReads, getterInheritedReads], - getterOutputOwnInherited: Object.prototype.hasOwnProperty.call(getterObjectParsed, "inherited"), - partial: partialObjectSchema.parse({ id: 1 }), - partialName: partialNameObjectSchema.safeParse({ id: 1 }).success, - deepPartial: deepPartialObjectSchema.parse({ nested: {} }), - required: requiredObjectSchema.safeParse({ id: 1, name: "a" }).success, - requiredActive: requiredActiveObjectSchema.safeParse({ id: 1, name: "a" }).success, - nestedRequired: nestedObject.required().safeParse({ nested: { label: "x" } }).success, - partialRequiredMask: { - baseMissingName: maskedObjectBase.safeParse({ nested: { label: "x" } }).success, - partialMissingName: maskedPartialObject.safeParse({ nested: { label: "x" } }).success, - partialMissingNested: maskedPartialObject.safeParse({}).success, - requiredMissingName: maskedRequiredObject.safeParse({ nested: { label: "x" } }).success, - requiredMissingOptionalAge: maskedRequiredObject.safeParse({ requiredName: "x", nested: { label: "x" } }).success, - base: objectShapeTypes(maskedObjectBase), - partial: objectShapeTypes(maskedPartialObject), - required: objectShapeTypes(maskedRequiredObject), - partialNameInner: maskedPartialObject.shape.requiredName._def.innerType.constructor.name, - }, - defaultedOptionalEmpty: defaultedOptionalObject.parse({}), - defaultedOptionalUndefined: defaultedOptionalObject.parse({ defaulted: undefined }), - anyUnknownMissing: z.object({ a: z.any(), b: z.unknown() }).safeParse({}).success, - unknownKeys: [ - objectBase._def.unknownKeys, - objectBase.passthrough()._def.unknownKeys, - objectBase.strict()._def.unknownKeys, - objectBase.strip()._def.unknownKeys, - ], - catchallType: z.object({ id: z.number() }).catchall(z.string())._def.catchall.constructor.name, - strictCreate: z.ZodObject.strictCreate({ id: z.number() }).safeParse({ id: 1, extra: true }).success, - strictCreateParse: z.ZodObject.strictCreate({ id: z.number() }).parse({ id: 1 }).id, - lazycreate: z.ZodObject.lazycreate(() => ({ name: z.string() })).parse({ name: "lazy" }).name, - shapeMetadata: { - partial: objectShapeTypes(partialObjectSchema), - partialName: objectShapeTypes(partialNameObjectSchema), - partialActiveInner: partialObjectSchema.shape.active._def.innerType.constructor.name, - required: objectShapeTypes(requiredObjectSchema), - requiredActive: objectShapeTypes(requiredActiveObjectSchema), - deepPartial: [ - deepPartialObjectSchema.shape.nested.constructor.name, - deepPartialObjectSchema.shape.nested._def.innerType.shape.label.constructor.name, - ], - }, -}); - -const mergeLeftPassthrough = z.object({ a: z.string() }).passthrough(); -const mergeRightStrict = z.object({ b: z.number() }).strict(); -const mergeRightPassthrough = z.object({ b: z.number() }).passthrough(); -const mergeLeftCatchall = z.object({ a: z.string() }).catchall(z.string()); -const mergeRightCatchall = z.object({ b: z.number() }).catchall(z.number()); -const mergedStrict = mergeLeftPassthrough.merge(mergeRightStrict); -const mergedPassthrough = mergeRightStrict.merge(mergeLeftPassthrough); -const mergedCatchall = mergeLeftCatchall.merge(mergeRightCatchall); -print("objectMergeSemantics", { - extendOverwrite: z.object({ id: z.string() }).extend({ id: z.number() }).parse({ id: 1 }).id, - strictWinsUnknownKeys: mergedStrict._def.unknownKeys, - strictRejectsExtra: mergedStrict.safeParse({ a: "x", b: 1, extra: true }).success, - passthroughWinsUnknownKeys: mergedPassthrough._def.unknownKeys, - passthroughAllowsExtra: mergedPassthrough.safeParse({ a: "x", b: 1, extra: true }).success, - catchallWinsType: mergedCatchall._def.catchall.constructor.name, - catchallAcceptsNumber: mergedCatchall.safeParse({ a: "x", b: 1, extra: 2 }).success, - catchallRejectsString: mergedCatchall.safeParse({ a: "x", b: 1, extra: "x" }).success, - mergedShapeKeys: Object.keys(mergedCatchall.shape).join("|"), -}); - -print("recursiveObjects", { - category: categorySchema.parse({ name: "root", subcategories: [{ name: "leaf", subcategories: [] }] }), - categoryIssue: issueMetadataFromResult(categorySchema.safeParse({ name: "root", subcategories: [{ name: 1 }] })), - mutual: postSchema.parse({ title: "post", author: { email: "a@example.com", posts: [] } }).author.email, - mutualNested: postSchema.parse({ - title: "post", - author: { - email: "a@example.com", - posts: [{ title: "nested", author: { email: "b@example.com", posts: [] } }], - }, - }).author.posts[0].author.email, - discriminatedRecursive: recursiveEventSchema.parse({ kind: "branch", children: [{ kind: "leaf", value: "x" }] }), - discriminatedRecursiveIssue: issueMetadataFromResult( - recursiveEventSchema.safeParse({ kind: "branch", children: [{ kind: "leaf", value: 1 }] }), - ), -}); - -const arrayElementIssue = z.array(z.string()).safeParse([1]); -const tupleLengthIssue = z.tuple([z.string()]).safeParse(["a", "b"]); -const boundedArray = z.array(z.string()).min(1).max(3).length(2); -const tupleWithRest = z.tuple([z.string(), z.number()]).rest(z.boolean()); -const arrayMinMessage = z.array(z.string()).min(2, "need two").safeParse(["a"]); -const arrayMaxMessage = z.array(z.string()).max(1, "too many").safeParse(["a", "b"]); -const arrayLengthMessage = z.array(z.string()).length(2, "exactly two").safeParse(["a"]); -const sparseArrayInput = ["a", , "c"] as (string | undefined)[]; -const sparseOptionalArrayParsed = z.array(z.string().optional()).parse(sparseArrayInput); -const sparseRequiredArrayResult = z.array(z.string()).safeParse(sparseArrayInput); -const inheritedArrayInput = ["a", , "c"] as string[]; -let inheritedArrayParsed: string[] = []; -let inheritedTupleParsed: [string, string, string] | [] = []; -Object.defineProperty(Array.prototype, "1", { - configurable: true, - enumerable: true, - writable: true, - value: "proto", -}); -try { - inheritedArrayParsed = z.array(z.string()).parse(inheritedArrayInput); - inheritedTupleParsed = z.tuple([z.string(), z.string(), z.string()]).parse(inheritedArrayInput); -} finally { - delete (Array.prototype as unknown as Record)["1"]; -} -print("arrays.tuples", { - array: z.array(z.number()).min(2).max(3).parse([1, 2]), - transformedArray: z.array(z.string().transform((value) => value.length)).parse(["aa", "bbb"]), - exactLength: z.array(z.string()).length(2).safeParse(["a", "b"]).success, - nonempty: z.array(z.string()).nonempty().safeParse([]).success, - elementDescription: z.array(z.string().describe("array item")).element.description, - elementIssuePath: arrayElementIssue.success ? "ok" : arrayElementIssue.error.issues[0].path.join("."), - typeName: boundedArray._def.typeName, - bounds: [ - boundedArray._def.minLength?.value, - boundedArray._def.maxLength?.value, - boundedArray._def.exactLength?.value, - ], - elementType: boundedArray.element.constructor.name, - minMessage: arrayMinMessage.success ? "ok" : arrayMinMessage.error.issues[0].message, - maxMessage: arrayMaxMessage.success ? "ok" : arrayMaxMessage.error.issues[0].message, - lengthMessage: arrayLengthMessage.success ? "ok" : arrayLengthMessage.error.issues[0].message, - sparseOptional: sparseOptionalArrayParsed, - sparseOptionalOwn1: Object.prototype.hasOwnProperty.call(sparseOptionalArrayParsed, "1"), - sparseRequiredIssue: issueMetadataFromResult(sparseRequiredArrayResult), - inheritedIndexArray: inheritedArrayParsed, - inheritedIndexArrayOwn1: Object.prototype.hasOwnProperty.call(inheritedArrayParsed, "1"), - inheritedIndexTuple: inheritedTupleParsed, - inheritedIndexTupleOwn1: Object.prototype.hasOwnProperty.call(inheritedTupleParsed, "1"), - tuple: z.tuple([z.string(), z.number()]).parse(["a", 1]), - tupleLengthIssue: tupleLengthIssue.success ? "ok" : tupleLengthIssue.error.issues[0].code, - tupleRest: z.tuple([z.string()]).rest(z.number()).parse(["a", 1, 2]), - tupleItems: tupleWithRest.items.map((item) => item.constructor.name), - tupleRestType: tupleWithRest._def.rest?.constructor.name, - tupleTypeName: tupleWithRest._def.typeName, -}); - -const parsedMap = z.map(z.string(), z.number()).parse(new Map([["a", 1], ["b", 2]])); -const parsedSet = z.set(z.string()).min(2).parse(new Set(["a", "b"])); -const transformedMap = z - .map(z.string(), z.string().transform((value) => value.length)) - .parse(new Map([["size", "abcd"]])); -const transformedSet = z - .set(z.string().transform((value) => value.length)) - .parse(new Set(["aa", "bbb"])); -const cloneInput = { nested: { label: "x" }, tags: ["a"] }; -const cloneParsed = z.object({ nested: z.object({ label: z.string() }), tags: z.array(z.string()) }).parse(cloneInput); -cloneParsed.nested.label = "changed"; -cloneParsed.tags.push("b"); -class FixtureMap extends Map {} -class FixtureSet extends Set<{ id: number }> {} -const cloneMapInput = new Map([["a", { count: 1 }]]); -const cloneMapParsed = z.map(z.string(), z.object({ count: z.number() })).parse(cloneMapInput); -cloneMapParsed.get("a")!.count = 2; -const subclassMapInput = new FixtureMap([["a", { count: 1 }]]); -const subclassMapParsed = z.map(z.string(), z.object({ count: z.number() })).parse(subclassMapInput); -const subclassMapParsedValue = subclassMapParsed.get("a")!; -subclassMapParsedValue.count = 2; -const cloneMapKeyValueKey = { id: 1 }; -const cloneMapKeyValueValue = { count: 1 }; -const cloneMapKeyValueInput = new Map([[cloneMapKeyValueKey, cloneMapKeyValueValue]]); -const cloneMapKeyValueParsed = z - .map(z.object({ id: z.number() }), z.object({ count: z.number() })) - .parse(cloneMapKeyValueInput); -const cloneMapKeyValueParsedKey = Array.from(cloneMapKeyValueParsed.keys())[0]; -const cloneMapKeyValueParsedValue = cloneMapKeyValueParsed.get(cloneMapKeyValueParsedKey)!; -cloneMapKeyValueParsedKey.id = 2; -cloneMapKeyValueParsedValue.count = 2; -const cloneSetValue = { id: 1 }; -const cloneSetInput = new Set([cloneSetValue]); -const cloneSetParsed = z.set(z.object({ id: z.number() })).parse(cloneSetInput); -const cloneSetParsedValue = Array.from(cloneSetParsed)[0]; -cloneSetParsedValue.id = 2; -const subclassSetValue = { id: 1 }; -const subclassSetInput = new FixtureSet([subclassSetValue]); -const subclassSetParsed = z.set(z.object({ id: z.number() })).parse(subclassSetInput); -const subclassSetParsedValue = Array.from(subclassSetParsed)[0]; -subclassSetParsedValue.id = 2; -const cloneDateInput = new Date("2020-01-02T00:00:00.000Z"); -const cloneDateParsed = z.date().parse(cloneDateInput); -cloneDateParsed.setUTCFullYear(2021); -const inheritedRecordInput = Object.create({ inherited: 2 }); -inheritedRecordInput.own = 1; -const inheritedRecordParsed = z.record(z.number()).parse(inheritedRecordInput); -const inheritedRecordInvalidInput = Object.create({ inherited: 2, badInherited: "x" }); -inheritedRecordInvalidInput.own = 1; -const recordHiddenSymbol = Symbol("record-hidden"); -const recordHiddenInput: Record = { own: 1 }; -Object.defineProperty(recordHiddenInput, "hidden", { value: 2, enumerable: false }); -recordHiddenInput[recordHiddenSymbol] = 3; -const recordHiddenParsed = z.record(z.number()).parse(recordHiddenInput); -const recordMetadataSchema = z.record(z.string(), z.number()); -const mapMetadataSchema = z.map(z.string(), z.number()); -const setMinMetadataSchema = z.set(z.string()).min(1); -const setMaxMetadataSchema = z.set(z.string()).max(3); -const setSizeMetadataSchema = z.set(z.string()).size(2); -const setMinMessage = z.set(z.string()).min(2, "need two").safeParse(new Set(["a"])); -const setMaxMessage = z.set(z.string()).max(1, "too many").safeParse(new Set(["a", "b"])); -const setSizeMessage = z.set(z.string()).size(2, "exactly two").safeParse(new Set(["a"])); -const collectionIssueSummary = (result: z.SafeParseReturnType) => - result.success ? [] : result.error.issues.map((issue) => ({ code: issue.code, path: issue.path.join(".") })); -print("collections", { - record: z.record(z.number()).safeParse({ a: 1, b: 2 }).success, - keyedRecord: z.record(z.enum(["a", "b"]), z.number()).safeParse({ a: 1, b: 2 }).success, - keyedRecordMissing: z.record(z.enum(["a", "b"]), z.number()).safeParse({ a: 1 }).success, - stringKeyRecord: z.record(z.string().min(1), z.number()).safeParse({ "": 1 }).success, - transformedRecord: z - .record(z.string().transform((value) => value.length)) - .parse({ a: "abcd" }), - recordFail: z.record(z.number()).refine((scores) => Object.values(scores).every((score) => score >= 0)).safeParse({ a: 1, b: -1 }).success, - recordNumberKey: collectionIssueSummary(z.record(z.number(), z.string()).safeParse({ 1: "one" })), - recordNumericStringKey: z.record(z.string().regex(/^\d+$/), z.string()).parse({ 1: "one" }), - recordNumericStringFail: collectionIssueSummary(z.record(z.string().regex(/^\d+$/), z.string()).safeParse({ abc: "bad" })), - inheritedRecord: inheritedRecordParsed, - inheritedRecordOwn: Object.prototype.hasOwnProperty.call(inheritedRecordParsed, "inherited"), - inheritedRecordIssue: collectionIssueSummary(z.record(z.number()).safeParse(inheritedRecordInvalidInput)), - nullProtoRecord: z.record(z.union([z.number(), z.string()])).parse(nullProtoInput), - hiddenSymbolRecord: recordHiddenParsed, - hiddenSymbolRecordHasHidden: Object.prototype.hasOwnProperty.call(recordHiddenParsed, "hidden"), - hiddenSymbolRecordSymbols: Object.getOwnPropertySymbols(recordHiddenParsed).length, - map: Array.from(parsedMap.entries()), - mapKeyFail: z.map(z.string().min(2), z.number()).safeParse(new Map([["a", 1]])).success, - mapFail: z.map(z.string(), z.number()).safeParse(new Map([["bad", "x"]])).success, - transformedMap: Array.from(transformedMap.entries()), - set: Array.from(parsedSet.values()), - setNonempty: z.set(z.string()).nonempty().safeParse(new Set()).success, - setFail: z.set(z.string()).min(2).safeParse(new Set(["a"])).success, - setMax: z.set(z.string()).max(2).safeParse(new Set(["a", "b", "c"])).success, - setSize: z.set(z.string()).size(2).safeParse(new Set(["a", "b"])).success, - transformedSet: Array.from(transformedSet.values()), - recordMeta: { - typeName: recordMetadataSchema._def.typeName, - key: recordMetadataSchema.keySchema.constructor.name, - value: recordMetadataSchema.valueSchema.constructor.name, - element: recordMetadataSchema.element.constructor.name, - }, - mapMeta: { - typeName: mapMetadataSchema._def.typeName, - key: mapMetadataSchema.keySchema.constructor.name, - value: mapMetadataSchema.valueSchema.constructor.name, - }, - setMeta: { - typeName: setSizeMetadataSchema._def.typeName, - value: setSizeMetadataSchema._def.valueType.constructor.name, - min: setMinMetadataSchema._def.minSize?.value, - max: setMaxMetadataSchema._def.maxSize?.value, - size: [ - setSizeMetadataSchema._def.minSize?.value, - setSizeMetadataSchema._def.maxSize?.value, - ], - }, - setMessages: [ - setMinMessage.success ? "ok" : setMinMessage.error.issues[0].message, - setMaxMessage.success ? "ok" : setMaxMessage.error.issues[0].message, - setSizeMessage.success ? "ok" : setSizeMessage.error.issues[0].message, - ], - recordIssues: collectionIssueSummary(z.record(z.number()).safeParse({ ok: 1, bad: "x" })), - mapKeyIssues: collectionIssueSummary(z.map(z.string().min(2), z.number()).safeParse(new Map([["a", 1]]))), - mapValueIssues: collectionIssueSummary(z.map(z.string(), z.number()).safeParse(new Map([["bad", "x"]]))), - setElementIssues: collectionIssueSummary(z.set(z.string().min(2)).safeParse(new Set(["a"]))), - setSizeIssues: collectionIssueSummary(z.set(z.string()).size(2).safeParse(new Set(["a"]))), -}); - -print("cloneSemantics", { - objectIdentity: cloneParsed !== cloneInput, - nestedIdentity: cloneParsed.nested !== cloneInput.nested, - arrayIdentity: cloneParsed.tags !== cloneInput.tags, - objectSource: cloneInput, - mapIdentity: cloneMapParsed !== cloneMapInput, - mapValueIdentity: cloneMapParsed.get("a") !== cloneMapInput.get("a"), - mapSource: Array.from(cloneMapInput.entries()), - subclassMapIsMap: subclassMapParsed instanceof Map, - subclassMapIsSubclass: subclassMapParsed instanceof FixtureMap, - subclassMapValueIdentity: subclassMapParsedValue !== subclassMapInput.get("a"), - subclassMapSource: Array.from(subclassMapInput.entries()), - subclassMapParsed: Array.from(subclassMapParsed.entries()), - mapObjectKeyIdentity: cloneMapKeyValueParsedKey !== cloneMapKeyValueKey, - mapObjectValueIdentity: cloneMapKeyValueParsedValue !== cloneMapKeyValueValue, - mapObjectSource: Array.from(cloneMapKeyValueInput.entries()), - mapObjectParsed: Array.from(cloneMapKeyValueParsed.entries()), - setIdentity: cloneSetParsed !== cloneSetInput, - setValueIdentity: cloneSetParsedValue !== cloneSetValue, - setSource: Array.from(cloneSetInput.values()), - subclassSetIsSet: subclassSetParsed instanceof Set, - subclassSetIsSubclass: subclassSetParsed instanceof FixtureSet, - subclassSetValueIdentity: subclassSetParsedValue !== subclassSetValue, - subclassSetSource: Array.from(subclassSetInput.values()), - subclassSetParsed: Array.from(subclassSetParsed.values()), - dateIdentity: cloneDateParsed !== cloneDateInput, - dateSource: cloneDateInput, - dateParsed: cloneDateParsed, -}); - -const intersectionObjectSchema = z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })); -const inheritedIntersectionInput = Object.create({ b: 2 }); -inheritedIntersectionInput.a = "x"; -const inheritedIntersectionParsed = intersectionObjectSchema.parse(inheritedIntersectionInput); -const nestedIntersectionSchema = z.intersection( - z.object({ nested: z.object({ a: z.string() }), shared: z.literal("same") }), - z.object({ nested: z.object({ b: z.number() }), shared: z.literal("same") }), -); -const transformedIntersectionSchema = z.intersection( - z.string().transform(() => ({ a: 1 })), - z.string().transform(() => ({ b: 2 })), -); -const conflictingIntersectionSchema = z.intersection( - z.string().transform(() => ({ shared: { value: 1 } })), - z.string().transform(() => ({ shared: { value: 2 } })), -); -const arrayIntersectionConflictSchema = z.intersection( - z.string().transform(() => [1, 2]), - z.string().transform(() => [1, 3]), -); -print("composition", { - intersection: intersectionObjectSchema.parse({ a: "x", b: 1 }), - inheritedIntersection: inheritedIntersectionParsed, - inheritedIntersectionOwnB: Object.prototype.hasOwnProperty.call(inheritedIntersectionParsed, "b"), - nestedIntersection: nestedIntersectionSchema.parse({ nested: { a: "x", b: 1 }, shared: "same" }), - conflictingIntersection: issueMetadataFromResult(conflictingIntersectionSchema.safeParse("x")), - arrayIntersectionConflict: issueMetadataFromResult(arrayIntersectionConflictSchema.safeParse("x")), - intersectionTypeName: intersectionObjectSchema._def.typeName, - intersectionSides: [ - intersectionObjectSchema._def.left.constructor.name, - intersectionObjectSchema._def.right.constructor.name, - ], - transformedIntersection: transformedIntersectionSchema.parse("x"), - or: z.string().or(z.number()).parse(5), - and: z.object({ a: z.string() }).and(z.object({ b: z.boolean() })).parse({ a: "x", b: true }), - strictObject: z.strictObject({ id: z.number() }).safeParse({ id: 1, extra: true }).success, - schemaArray: z.string().array().parse(["a", "b"]), - schemaOr: z.literal("a").or(z.literal("b")).parse("b"), - schemaOrIssue: issueMetadataFromResult(z.string().min(2).or(z.number().min(10)).safeParse(5)), - schemaAnd: z.object({ a: z.string() }).and(z.object({ b: z.number() })).parse({ a: "x", b: 1 }), - pipelineFactory: z.pipeline(z.string().transform((value) => value.length), z.number().min(2)).safeParse("abc").success, -}); - -const csvSchema = z - .string() - .transform((value) => value.split(",").map((item) => item.trim()).filter(Boolean)) - .refine((items) => items.length >= 2, { message: "need two values" }); -print("transform", csvSchema.parse("a, b, c")); -print("refine", csvSchema.safeParse("single").success); - -const preprocessSchema = z.preprocess((value) => (typeof value === "string" ? value.trim() : value), z.string().min(2)); -const metadataPreprocessSchema = z.preprocess((value) => value, z.string()); -const metadataTransformSchema = z.string().transform((value) => value.length); -const metadataRefineSchema = z.string().refine((value) => value.length > 1, "too short"); -const metadataPipelineSchema = z.string().transform((value) => value.length).pipe(z.number().min(1)); -const metadataPipelineFactorySchema = z.pipeline(z.string(), z.coerce.number()); -const matchingPasswords = z.object({ password: z.string(), confirm: z.string() }).refine((values) => values.password === values.confirm, { - path: ["confirm"], - message: "password mismatch", -}); -const superRefineSchema = z.array(z.number()).superRefine((values, ctx) => { - if (new Set(values).size !== values.length) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "duplicates" }); - } -}); -const fatalCalls: string[] = []; -const fatalRefineSchema = z - .string() - .superRefine((value, ctx) => { - fatalCalls.push(`first:${value}`); - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "stop", fatal: true }); - return z.NEVER; - }) - .superRefine(() => { - fatalCalls.push("second"); - }); -const neverTransformSchema = z - .string() - .transform((value, ctx) => { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: `bad transform:${value}` }); - return z.NEVER; - }) - .pipe(z.string()); -const matchingPasswordsResult = matchingPasswords.safeParse({ password: "a", confirm: "b" }); -const fatalRefineResult = fatalRefineSchema.safeParse("x"); -const neverTransformResult = neverTransformSchema.safeParse("x"); -const refinementAliasResult = z - .string() - .refinement((value) => value === "ok", { code: z.ZodIssueCode.custom, message: "bad refinement" }) - .safeParse("no"); -print("effects", { - preprocess: preprocessSchema.parse(" ok "), - pipe: z.string().transform((value) => value.length).pipe(z.number().min(2)).safeParse("abc").success, - superRefine: superRefineSchema.safeParse([1, 1]).success, - custom: z.custom((value) => value === "token").safeParse("token").success, - refinementAlias: refinementAliasResult.success ? "ok" : refinementAliasResult.error.issues[0].message, - innerType: z.string().transform((value) => value.length).innerType().parse("inner"), - sourceType: z.string().transform((value) => value.length).sourceType().parse("source"), - refinePath: matchingPasswordsResult.success ? "ok" : matchingPasswordsResult.error.issues[0].path.join("."), - refineMessage: matchingPasswordsResult.success ? "ok" : matchingPasswordsResult.error.issues[0].message, - fatalCalls, - fatalIssue: fatalRefineResult.success ? "ok" : fatalRefineResult.error.issues[0].message, - fatalFlag: fatalRefineResult.success ? false : fatalRefineResult.error.issues[0].fatal === true, - neverTransform: neverTransformResult.success ? "ok" : neverTransformResult.error.issues[0].message, - neverStatus: z.NEVER.status, - metadata: { - preprocess: [ - metadataPreprocessSchema._def.typeName, - metadataPreprocessSchema._def.effect.type, - metadataPreprocessSchema._def.schema.constructor.name, - metadataPreprocessSchema.innerType().constructor.name, - metadataPreprocessSchema.sourceType().constructor.name, - ], - transform: [ - metadataTransformSchema._def.typeName, - metadataTransformSchema._def.effect.type, - metadataTransformSchema._def.schema.constructor.name, - metadataTransformSchema.innerType().constructor.name, - metadataTransformSchema.sourceType().constructor.name, - ], - refine: [ - metadataRefineSchema._def.typeName, - metadataRefineSchema._def.effect.type, - metadataRefineSchema._def.schema.constructor.name, - ], - pipeline: [ - metadataPipelineSchema._def.typeName, - metadataPipelineSchema._def.in.constructor.name, - metadataPipelineSchema._def.out.constructor.name, - metadataPipelineFactorySchema._def.in.constructor.name, - metadataPipelineFactorySchema._def.out.constructor.name, - ], - }, -}); - -const customTokenResult = z.custom((value) => typeof value === "string" && value.startsWith("tok_")).safeParse("tok_123"); -const customTokenFailure = z.custom((value) => typeof value === "string" && value.startsWith("tok_"), { - message: "not a token", -}).safeParse("bad"); -print("customSchemas", { - unchecked: z.custom<{ id: number }>().parse({ id: 1 }).id, - predicate: customTokenResult.success, - message: customTokenFailure.success ? "ok" : customTokenFailure.error.issues[0].message, -}); - -const originalDescriptionSchema = z.string(); -const describedStringSchema = originalDescriptionSchema.describe("fixture string"); -const describedObjectSchema = z.object({ id: z.number().describe("identifier") }).describe("fixture object"); -const describedArraySchema = z.array(z.string().describe("array item")).describe("fixture array"); -const describedOptionalSchema = z.string().describe("inner").optional().describe("outer optional"); -print("descriptions", { - original: originalDescriptionSchema.description ?? null, - string: [describedStringSchema.description, describedStringSchema._def.description, describedStringSchema.parse("ok")], - object: [ - describedObjectSchema.description, - describedObjectSchema._def.description, - describedObjectSchema.shape.id.description, - describedObjectSchema.parse({ id: 1 }).id, - ], - array: [ - describedArraySchema.description, - describedArraySchema._def.description, - describedArraySchema.element.description, - describedArraySchema.parse(["x"]).length, - ], - optional: [ - describedOptionalSchema.description, - describedOptionalSchema._def.description, - describedOptionalSchema._def.innerType.description, - describedOptionalSchema.parse(undefined) === undefined, - ], -}); - -const optionalMetadataSchema = z.string().optional(); -const nullableMetadataSchema = z.string().nullable(); -const defaultMetadataSchema = z.string().default("fallback"); -const catchMetadataSchema = z.number().catch(9); -const promiseMetadataSchema = z.promise(z.number()); -const readonlyMetadataSchema = z.object({ id: z.number() }).readonly(); -const brandedMetadataSchema = z.string().brand<"FixtureId">(); -const readonlyObjectValue = z.object({ id: z.number() }).readonly().parse({ id: 1 }); -const readonlyNestedInput = { nested: { id: 1 }, tags: ["a"] }; -const readonlyNestedValue = z - .object({ nested: z.object({ id: z.number() }), tags: z.array(z.string()) }) - .readonly() - .parse(readonlyNestedInput); -const readonlyArrayValue = z.array(z.string()).readonly().parse(["a"]); -const readonlyTupleValue = z.tuple([z.string()]).readonly().parse(["a"]); -const readonlyMapValue = z.map(z.string(), z.number()).readonly().parse(new Map([["a", 1]])); -const readonlySetValue = z.set(z.string()).readonly().parse(new Set(["a"])); -let defaultFactoryCalls = 0; -let catchFactoryCalls = 0; -const countedDefaultSchema = z.string().default(() => `fallback-${++defaultFactoryCalls}`); -const countedCatchSchema = z.number().catch((ctx) => { - catchFactoryCalls += 1; - return ctx.error.issues.length + catchFactoryCalls; -}); -const tryErrorName = (fn: () => void) => { - try { - fn(); - return "ok"; - } catch (error) { - return error instanceof Error ? error.constructor.name : String(error); - } -}; -print("modifiers", { - ostring: z.ostring().parse(undefined) === undefined, - onumber: z.onumber().parse(undefined) === undefined, - oboolean: z.oboolean().parse(undefined) === undefined, - optional: z.string().optional().parse(undefined) === undefined, - nullable: z.string().nullable().parse(null) === null, - nullish: z.string().nullish().parse(undefined) === undefined, - default: z.string().default("fallback").parse(undefined), - defaultFunction: z.string().default(() => "factory").parse(undefined), - defaultSkipsPresent: z.string().default(() => "factory").parse("present"), - defaultFactorySequence: [ - countedDefaultSchema.parse(undefined), - countedDefaultSchema.parse(undefined), - countedDefaultSchema.parse("present"), - defaultFactoryCalls, - ], - defaultTransform: z.string().transform((value) => value.length).default("tuna").parse(undefined), - removeDefault: z.string().default("fallback").removeDefault().safeParse(undefined).success, - catch: z.number().catch(9).parse("bad"), - catchTransform: z.string().transform((value) => value.length).catch(9).parse(123), - catchFunction: z.number().catch((ctx) => ctx.error.issues.length).parse("bad"), - catchFactorySequence: [ - countedCatchSchema.parse("bad"), - countedCatchSchema.parse(3), - catchFactoryCalls, - ], - catchContext: z.number().catch((ctx) => `${ctx.input}:${ctx.error.issues[0].code}`).parse("bad"), - removeCatch: z.number().catch(9).removeCatch().safeParse("bad").success, - brand: z.string().brand<"FixtureId">().parse("id-1"), - described: z.string().describe("fixture string").description, - describedObject: z.object({ id: z.number() }).describe("fixture object").description, - describedParse: z.string().describe("parse still works").parse("described"), - optionalUnwrap: z.string().optional().unwrap().parse("wrapped"), - nullableUnwrap: z.string().nullable().unwrap().parse("wrapped"), - arrayElement: z.array(z.string()).element.parse("element"), - promiseUnwrap: z.promise(z.number()).unwrap().parse(3), - isOptional: z.string().optional().isOptional(), - isNullable: z.string().nullable().isNullable(), - readonlyFrozen: Object.isFrozen(z.object({ id: z.number() }).readonly().parse({ id: 1 })), - readonlyArray: Object.isFrozen(z.array(z.string()).readonly().parse(["a"])), - readonlyTuple: Object.isFrozen(z.tuple([z.string()]).readonly().parse(["a"])), - readonlyMap: Object.isFrozen(z.map(z.string(), z.number()).readonly().parse(new Map([["a", 1]]))), - readonlySet: Object.isFrozen(z.set(z.string()).readonly().parse(new Set(["a"]))), - readonlyMutations: { - object: [Reflect.set(readonlyObjectValue, "id", 2), readonlyObjectValue.id], - nestedObject: [ - Object.isFrozen(readonlyNestedValue), - Object.isFrozen(readonlyNestedValue.nested), - Object.isFrozen(readonlyNestedValue.tags), - Reflect.set(readonlyNestedValue, "extra", true), - Reflect.set(readonlyNestedValue.nested, "id", 2), - tryErrorName(() => readonlyNestedValue.tags.push("b")), - readonlyNestedValue, - readonlyNestedInput, - readonlyNestedValue.nested !== readonlyNestedInput.nested, - readonlyNestedValue.tags !== readonlyNestedInput.tags, - ], - array: [ - Reflect.set(readonlyArrayValue, "0", "b"), - tryErrorName(() => readonlyArrayValue.push("b")), - readonlyArrayValue[0], - readonlyArrayValue.length, - ], - tuple: [ - Reflect.set(readonlyTupleValue, "0", "b"), - tryErrorName(() => readonlyTupleValue.push("b")), - readonlyTupleValue[0], - readonlyTupleValue.length, - ], - map: [ - tryErrorName(() => readonlyMapValue.set("b", 2)), - readonlyMapValue.size, - readonlyMapValue.get("b"), - ], - set: [ - tryErrorName(() => readonlySetValue.add("b")), - readonlySetValue.size, - readonlySetValue.has("b"), - ], - }, - schemaArray: z.string().array().parse(["a", "b"]).length, - schemaOr: z.string().or(z.number()).parse(2), - schemaAnd: z.object({ a: z.string() }).and(z.object({ b: z.number() })).parse({ a: "x", b: 1 }), - metadata: { - optional: [optionalMetadataSchema._def.typeName, optionalMetadataSchema._def.innerType.constructor.name], - nullable: [nullableMetadataSchema._def.typeName, nullableMetadataSchema._def.innerType.constructor.name], - default: [defaultMetadataSchema._def.typeName, defaultMetadataSchema._def.defaultValue()], - catch: [catchMetadataSchema._def.typeName, catchMetadataSchema._def.catchValue({ error: null, input: "bad" })], - promise: [promiseMetadataSchema._def.typeName, promiseMetadataSchema._def.type.constructor.name], - readonly: [readonlyMetadataSchema._def.typeName, readonlyMetadataSchema._def.innerType.constructor.name], - branded: [ - brandedMetadataSchema._def.typeName, - brandedMetadataSchema.unwrap().constructor.name, - brandedMetadataSchema instanceof z.ZodBranded, - z.ZodBranded.name, - ], - }, -}); - -type Tree = { name: string; children?: Tree[] }; -const treeSchema: z.ZodType = z.lazy(() => z.object({ name: z.string(), children: z.array(treeSchema).optional() })); -const lazyTreeInput: Tree = { name: "root", children: [{ name: "leaf" }] }; -const lazyTreeParsed = treeSchema.parse(lazyTreeInput); -lazyTreeParsed.children![0].name = "changed"; -print("lazy", { - parsed: treeSchema.parse({ name: "root", children: [{ name: "leaf" }] }), - cloneIdentity: lazyTreeParsed !== lazyTreeInput, - childCloneIdentity: lazyTreeParsed.children![0] !== lazyTreeInput.children![0], - source: lazyTreeInput, - mutated: lazyTreeParsed, -}); - -const lateNodeSchema: z.ZodType = z.late.object(() => ({ - name: z.string(), - children: z.array(lateNodeSchema).default([]), -})); -const jsonLiteralSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -const jsonValueSchema: z.ZodType = z.lazy(() => - z.union([jsonLiteralSchema, z.array(jsonValueSchema), z.record(jsonValueSchema)]), -); -const jsonStringSchema = z.string().transform((value) => JSON.parse(value)).pipe(jsonValueSchema); -const mapSizeSchema = z.object({ - box: z.instanceof(Map).refine((value) => value.size === 1, { path: ["size"], message: "size one" }), -}); -const mergedStaticShape = z.objectUtil.mergeShapes( - { a: z.string(), shared: z.string() }, - { b: z.number(), shared: z.number() }, -); -const effectAliasSchema = z.effect(z.string(), { - type: "transform", - transform: (value: string) => value.length, -}); -const transformerAliasSchema = z.transformer(z.string(), { - type: "transform", - transform: (value: string) => value.toUpperCase(), -}); -print("factories", { - arrayFactory: z.array(z.string()).parse(["factory"]).join("|"), - optionalFactory: z.optional(z.string()).parse(undefined) === undefined, - nullableFactory: z.nullable(z.string()).parse(null) === null, - promiseFactory: await z.promise(z.number()).parse(Promise.resolve(4)), - ostringFactory: z.ostring().parse(undefined) === undefined, - onumberFactory: z.onumber().parse(3), - obooleanFactory: z.oboolean().parse(undefined) === undefined, - unionFactory: z.union([z.literal("left"), z.literal("right")]).parse("right"), - intersectionFactory: z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })).parse({ a: "x", b: 2 }), - lazyFactory: z.lazy(() => z.string()).parse("lazy"), - lateObject: lateNodeSchema.parse({ name: "root", children: [{ name: "leaf" }] }), -}); - -print("staticFactories", { - string: z.ZodString.create().parse("x"), - number: z.ZodNumber.create().parse(1), - nan: Number.isNaN(z.ZodNaN.create().parse(NaN)), - array: z.ZodArray.create(z.string()).parse(["a"]).length, - object: z.ZodObject.create({ id: z.number() }).parse({ id: 1 }).id, - tuple: z.ZodTuple.create([z.string(), z.number()]).parse(["a", 1])[1], - record: z.ZodRecord.create(z.string()).parse({ a: "x" }).a, - map: Array.from(z.ZodMap.create(z.string(), z.number()).parse(new Map([["a", 1]])).entries()), - set: Array.from(z.ZodSet.create(z.string()).parse(new Set(["a"])).values()), - optional: z.ZodOptional.create(z.string()).parse(undefined) === undefined, - nullable: z.ZodNullable.create(z.string()).parse(null) === null, - promise: await z.ZodPromise.create(z.number()).parse(Promise.resolve(2)), - readonly: Object.isFrozen(z.ZodReadonly.create(z.object({ id: z.number() })).parse({ id: 1 })), - effectAlias: effectAliasSchema.parse("tuna"), - transformerAlias: transformerAliasSchema.parse("tuna"), - objectUtil: z.object(mergedStaticShape).parse({ a: "x", b: 1, shared: 2 }), - literal: z.ZodLiteral.create("ready").parse("ready"), - enum: z.ZodEnum.create(["red", "blue"]).parse("blue"), - nativeEnum: z.ZodNativeEnum.create({ A: "a", B: "b" }).parse("a"), - date: z.ZodDate.create().parse(new Date("2020-01-02T00:00:00.000Z")).toISOString(), - bigint: z.ZodBigInt.create().parse(2n).toString(), - boolean: z.ZodBoolean.create().parse(true), - any: z.ZodAny.create().parse({ ok: true }).ok, - unknown: z.ZodUnknown.create().parse(["x"]).length, - never: z.ZodNever.create().safeParse("x").success, - undefined: z.ZodUndefined.create().parse(undefined) === undefined, - null: z.ZodNull.create().parse(null) === null, - void: z.ZodVoid.create().parse(undefined) === undefined, - symbol: typeof z.ZodSymbol.create().parse(Symbol.for("x")), - union: z.ZodUnion.create([z.string(), z.number()]).parse(3), - intersection: z.ZodIntersection.create(z.object({ a: z.string() }), z.object({ b: z.number() })).parse({ a: "x", b: 1 }), - discriminatedUnion: z.ZodDiscriminatedUnion.create("kind", [ - z.object({ kind: z.literal("a"), value: z.string() }), - z.object({ kind: z.literal("b"), value: z.number() }), - ]).parse({ kind: "b", value: 2 }).value, - function: z.ZodFunction.create() - .args(z.string()) - .returns(z.number()) - .implement((value) => value.length)("abc"), - lazy: z.ZodLazy.create(() => z.string()).parse("lazy"), - effects: z.ZodEffects.create(z.string(), { type: "transform", transform: (value: string) => value.length }).parse("fish"), - default: z.ZodDefault.create(z.string(), { default: () => "fallback" }).parse(undefined), - catch: z.ZodCatch.create(z.number(), { catch: () => 7 }).parse("bad"), - pipeline: z.ZodPipeline.create(z.string(), z.coerce.number()).parse("6"), -}); - -print("jsonLike", { - object: jsonValueSchema.safeParse({ nested: [1, "two", null] }).success, - rejectsFunction: jsonValueSchema.safeParse(() => 1).success, - rejectsSymbol: jsonValueSchema.safeParse(Symbol.for("x")).success, - rejectsBigint: jsonValueSchema.safeParse(1n).success, - badNested: issueMetadataFromResult(jsonValueSchema.safeParse({ ok: true, bad: () => 1 })), - fromString: jsonStringSchema.parse('{"a":[1,true,null]}'), - mapSize: mapSizeSchema.safeParse({ box: new Map([["a", 1]]) }).success, - mapSizeFail: issueMetadataFromResult(mapSizeSchema.safeParse({ box: new Map() })), - parsed: jsonValueSchema.parse({ ok: true, items: [1, "two"] }), -}); - -class Box { - value: string; - constructor(value: string) { - this.value = value; - } -} -const invalidBoxResult = z.instanceof(Box, { message: "not a box" }).safeParse({}); -const customDateSchema = z.date({ - required_error: "required date", - invalid_type_error: "not a date", -}); -const requiredDateResult = customDateSchema.safeParse(undefined); -const invalidDateResult = customDateSchema.safeParse("nope"); -print("instances.dates", { - instanceof: z.instanceof(Box).parse(new Box("ok")).value, - instanceMessage: invalidBoxResult.success ? "ok" : invalidBoxResult.error.issues[0].message, - date: z.date().parse(new Date("2020-01-02T00:00:00.000Z")).toISOString(), - dateRequired: requiredDateResult.success ? "ok" : requiredDateResult.error.issues[0].message, - dateInvalid: invalidDateResult.success ? "ok" : invalidDateResult.error.issues[0].message, - dateMin: z.date().min(new Date("2020-01-01T00:00:00.000Z")).safeParse(new Date("2020-01-02T00:00:00.000Z")).success, - dateMax: z.date().max(new Date("2020-01-03T00:00:00.000Z")).safeParse(new Date("2020-01-04T00:00:00.000Z")).success, -}); - -const validatedFn = z.function().args(z.string()).returns(z.number()).implement((value) => value.trim().length); -const strictValidatedFn = z.function().args(z.string()).returns(z.number()).strictImplement((value) => value.trim().length); -const invalidReturnFn = z.function().args(z.string()).returns(z.number()).implement(() => "bad" as unknown as number); -const strictInvalidReturnFn = z.function().args(z.string()).returns(z.number()).strictImplement(() => "bad" as unknown as number); -const validatedAliasFn = z.function().args(z.string()).returns(z.number()).validate((value) => value.trim().length); -const restValidatedFn = z - .function(z.tuple([z.string()]).rest(z.number()), z.number()) - .implement((label, ...values) => label.length + values.reduce((total, value) => total + value, 0)); -const functionSchema = z.function().args(z.string(), z.number()).returns(z.boolean()); -const functionMetadataSchema = z.function().args(z.string(), z.number()).returns(z.boolean()); -const defaultFunctionMetadataSchema = z.function(); -const argsOnlyFunctionMetadataSchema = z.function().args(z.string()); -const returnsOnlyFunctionMetadataSchema = z.function().returns(z.number()); -const asyncValidatedFn = z.function().args(z.string()).returns(z.promise(z.number())).implement(async (value) => value.trim().length); -const asyncInvalidReturnFn = z.function().args(z.string()).returns(z.promise(z.number())).implement(async () => "bad" as unknown as number); -const functionCloneInput = { nested: { id: 1 } }; -let functionCloneSeenArg: { nested: { id: number } } | undefined; -const functionCloneValidatedFn = z - .function() - .args(z.object({ nested: z.object({ id: z.number() }) })) - .returns(z.object({ nested: z.object({ id: z.number() }) })) - .implement((value) => { - functionCloneSeenArg = value; - value.nested.id = 2; - return value; - }); -const functionCloneReturned = functionCloneValidatedFn(functionCloneInput); -functionCloneReturned.nested.id = 3; -const functionNestedIssueCount = (issue: z.ZodIssue) => { - if (issue.code === "invalid_arguments") { - return issue.argumentsError.issues.length; - } - if (issue.code === "invalid_return_type") { - return issue.returnTypeError.issues.length; - } - return 0; -}; -const functionIssueSummary = (call: () => unknown) => { - try { - call(); - return []; - } catch (error) { - return (error as z.ZodError).issues.map((issue) => ({ - code: issue.code, - path: issue.path.join("."), - nestedCount: functionNestedIssueCount(issue), - })); - } -}; -const functionArgumentIssuePaths = (call: () => unknown) => { - try { - call(); - return []; - } catch (error) { - const issue = (error as z.ZodError).issues[0]; - return issue.code === "invalid_arguments" - ? issue.argumentsError.issues.map((nestedIssue) => `${nestedIssue.path.join(".")}:${nestedIssue.code}`) - : []; - } -}; -const invalidAsyncReturn = await (async () => { - try { - await asyncInvalidReturnFn("x"); - return false; - } catch { - return true; - } -})(); -print("function", { - valid: validatedFn(" tuna "), - strictValid: strictValidatedFn(" trout "), - validateAlias: validatedAliasFn(" bass "), - restValid: restValidatedFn("ab", 1, 2, 3), - restInvalid: functionArgumentIssuePaths(() => (restValidatedFn as unknown as (...values: unknown[]) => number)("ab", 1, "x")), - asyncValid: await asyncValidatedFn(" salmon "), - cloneSemantics: { - argIdentity: functionCloneSeenArg !== functionCloneInput, - nestedArgIdentity: functionCloneSeenArg?.nested !== functionCloneInput.nested, - source: functionCloneInput, - returnIdentity: functionCloneReturned !== functionCloneSeenArg, - nestedReturnIdentity: functionCloneReturned.nested !== functionCloneSeenArg?.nested, - returned: functionCloneReturned, - }, - parameters: functionSchema.parameters().items.length, - returnType: functionSchema.returnType().safeParse(true).success, - invalidArgs: (() => { - try { - (validatedFn as unknown as (value: number) => number)(1); - return false; - } catch { - return true; - } - })(), - invalidReturns: (() => { - try { - invalidReturnFn("x"); - return false; - } catch { - return true; - } - })(), - argumentIssues: functionIssueSummary(() => (validatedFn as unknown as (value: number) => number)(1)), - returnIssues: functionIssueSummary(() => invalidReturnFn("x")), - strictReturnIssues: functionIssueSummary(() => strictInvalidReturnFn("x")), - invalidAsyncReturn, - metadata: { - typed: { - typeName: functionMetadataSchema._def.typeName, - argsType: functionMetadataSchema._def.args.constructor.name, - argItems: functionMetadataSchema._def.args.items.map((item) => item.constructor.name), - argRest: functionMetadataSchema._def.args._def.rest?.constructor.name, - returns: functionMetadataSchema._def.returns.constructor.name, - parameters: functionMetadataSchema.parameters().items.map((item) => item.constructor.name), - returnType: functionMetadataSchema.returnType().constructor.name, - }, - defaults: { - args: defaultFunctionMetadataSchema._def.args.items.length, - rest: defaultFunctionMetadataSchema._def.args._def.rest?.constructor.name, - returns: defaultFunctionMetadataSchema._def.returns.constructor.name, - }, - argsOnly: { - args: argsOnlyFunctionMetadataSchema._def.args.items.map((item) => item.constructor.name), - returns: argsOnlyFunctionMetadataSchema._def.returns.constructor.name, - }, - returnsOnly: { - args: returnsOnlyFunctionMetadataSchema._def.args.items.length, - returns: returnsOnlyFunctionMetadataSchema._def.returns.constructor.name, - }, - }, -}); - -const asyncSchema = z.string().refine(async (value) => value === "ok"); -const asyncTransformSchema = z.string().transform(async (value) => value.trim().length); -const asyncPipeSchema = z - .string() - .transform(async (value) => value.length) - .pipe(z.number().min(2)); -const asyncSuperRefineSchema = z.string().superRefine(async (value, ctx) => { - if (value !== "ok") { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "bad async" }); - } -}); -const asyncPreprocessSchema = z.preprocess( - async (value) => (typeof value === "string" ? value.trim() : value), - z.string().min(2), -); -const numericPreprocessSchema = z.preprocess( - (value) => (typeof value === "string" ? Number(value) : value), - z.number().min(2), -); -const objectPipelineSchema = z.pipeline( - z.string().transform((value) => ({ len: value.length })), - z.object({ len: z.number().min(2) }), -); -const transformIssueSchema = z.string().transform((value, ctx) => { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "warn" }); - return value.length; -}); -const asyncPromiseMetadataSchema = z.promise(z.number()); -const promisedNumber = await z.promise(z.number()).parse(Promise.resolve(5)); -const methodPromisedNumber = await z.number().promise().parse(Promise.resolve(3)); -const asyncParsed = await asyncSchema.parseAsync("ok"); -const spaResult = await z.string().spa("ok"); -const promisedFailure = await (async () => { - try { - await z.promise(z.number()).parse(Promise.resolve("bad")); - return false; - } catch { - return true; - } -})(); -const rejectedReason = await (async () => { - try { - await z.promise(z.number()).parse(Promise.reject(new Error("promise boom"))); - return "ok"; - } catch (error) { - return error instanceof Error ? error.message : String(error); - } -})(); -print("async", { - refine: await asyncSchema.safeParseAsync("ok"), - parseAsync: asyncParsed, - spa: spaResult.success, - promise: promisedNumber, - methodPromise: methodPromisedNumber, - promiseRejects: promisedFailure, - rejectedReason, -}); -const asyncSuperRefineResult = await asyncSuperRefineSchema.safeParseAsync("bad"); -const asyncRefineMessageResult = await z - .string() - .refine(async (value) => value === "ok", { message: "not ok" }) - .safeParseAsync("bad"); -print("asyncEffects", { - transform: await asyncTransformSchema.parseAsync(" tuna "), - pipe: await asyncPipeSchema.safeParseAsync("abc"), - superRefineMessage: asyncSuperRefineResult.success ? "ok" : asyncSuperRefineResult.error.issues[0].message, - refineMessage: asyncRefineMessageResult.success ? "ok" : asyncRefineMessageResult.error.issues[0].message, - preprocess: await asyncPreprocessSchema.parseAsync(" ok "), - syncThrows: { - transformParse: thrownSummary(() => asyncTransformSchema.parse("x")), - transformSafeParse: thrownSummary(() => asyncTransformSchema.safeParse("x")), - refineParse: thrownSummary(() => asyncSchema.parse("bad")), - refineSafeParse: thrownSummary(() => asyncSchema.safeParse("bad")), - }, - metadata: { - refine: [asyncSchema._def.typeName, asyncSchema._def.effect.type, asyncSchema._def.schema.constructor.name], - transform: [ - asyncTransformSchema._def.typeName, - asyncTransformSchema._def.effect.type, - asyncTransformSchema._def.schema.constructor.name, - ], - pipe: [asyncPipeSchema._def.typeName, asyncPipeSchema._def.in.constructor.name, asyncPipeSchema._def.out.constructor.name], - superRefine: [ - asyncSuperRefineSchema._def.typeName, - asyncSuperRefineSchema._def.effect.type, - asyncSuperRefineSchema._def.schema.constructor.name, - ], - preprocess: [ - asyncPreprocessSchema._def.typeName, - asyncPreprocessSchema._def.effect.type, - asyncPreprocessSchema._def.schema.constructor.name, - ], - promise: [asyncPromiseMetadataSchema._def.typeName, asyncPromiseMetadataSchema._def.type.constructor.name], - }, -}); -print("asyncIssues", { - parseAsync: issueMetadataFromResult(await z.string().min(2).safeParseAsync("x")), - pipe: issueMetadataFromResult(await asyncPipeSchema.safeParseAsync("x")), - preprocess: issueMetadataFromResult(await asyncPreprocessSchema.safeParseAsync(" x ")), - promiseInner: await issueMetadataFromThrown(() => z.promise(z.number()).parse(Promise.resolve("bad"))), -}); - -const promiseSchema = z.promise(z.number()); -const promiseSafeResult = promiseSchema.safeParse(Promise.resolve(8)); -const promiseObjectValue = await z.promise(z.object({ id: z.number() })).parse(Promise.resolve({ id: 1 })); -const promiseCloneInput = { nested: { id: 1 } }; -const promiseCloneParsed = await z - .promise(z.object({ nested: z.object({ id: z.number() }) })) - .parse(Promise.resolve(promiseCloneInput)); -promiseCloneParsed.nested.id = 2; -print("promiseSchemas", { - safeParse: promiseSafeResult.success, - safeValue: promiseSafeResult.success ? await promiseSafeResult.data : null, - parseThen: typeof promiseSchema.parse(Promise.resolve(2)).then, - parseValue: await promiseSchema.parse(Promise.resolve(2)), - nonPromise: issueMetadataFromResult(promiseSchema.safeParse(1)), - inner: await issueMetadataFromThrown(() => promiseSchema.parse(Promise.resolve("x"))), - object: promiseObjectValue, - cloneIdentity: promiseCloneParsed !== promiseCloneInput, - cloneNestedIdentity: promiseCloneParsed.nested !== promiseCloneInput.nested, - cloneSource: promiseCloneInput, - cloneParsed: promiseCloneParsed, - methodType: z.number().promise()._def.type.constructor.name, -}); - -print("effectFactories", { - preprocessNumber: numericPreprocessSchema.parse("3"), - preprocessIssue: issueMetadataFromResult(numericPreprocessSchema.safeParse("1")), - pipelineObject: objectPipelineSchema.parse("abc"), - pipelineIssue: issueMetadataFromResult(objectPipelineSchema.safeParse("a")), - transformIssue: issueMetadataFromResult(transformIssueSchema.safeParse("abc")), -}); - -const formatted = objectBase.safeParse({ id: "x", name: 1 }); -if (!formatted.success) { - const nestedFormatted = z.object({ user: z.object({ tags: z.array(z.string().min(2)).min(2) }) }).safeParse({ user: { tags: ["x"] } }); - const unionFailure = z - .union([ - z.object({ kind: z.literal("a"), value: z.string() }), - z.object({ kind: z.literal("b"), count: z.number() }), - ]) - .safeParse({ kind: "a", value: 1 }); - const discriminatedFailure = eventSchema.safeParse({ type: "missing", value: true }); - const unionIssue = unionFailure.success ? null : unionFailure.error.issues[0]; - const discriminatedIssue = discriminatedFailure.success ? null : discriminatedFailure.error.issues[0]; - print("errors", { - formatKeys: Object.keys(formatted.error.format()).sort(), - flatten: formatted.error.flatten(), - nestedTagErrors: nestedFormatted.success ? [] : nestedFormatted.error.format().user?.tags?._errors, - nestedTagItemErrors: nestedFormatted.success ? [] : nestedFormatted.error.format().user?.tags?.[0]?._errors, - unionIssue: unionIssue?.code, - unionBranchIssueCounts: unionIssue?.code === "invalid_union" ? unionIssue.unionErrors.map((error) => error.issues.length) : [], - unionFirstBranchPath: unionIssue?.code === "invalid_union" ? unionIssue.unionErrors[0].issues[0].path.join(".") : "", - discriminatedIssue: discriminatedIssue?.code, - discriminatedPath: discriminatedIssue?.path.join("."), - }); -} - -print("issueMetadata", { - literal: issueMetadataFromResult(z.literal("ready").safeParse("no")), - invalidType: issueMetadataFromResult(z.number().safeParse("x")), - enum: issueMetadataFromResult(z.enum(["red", "blue"]).safeParse("green")), - invalidString: issueMetadataFromResult(z.string().email().safeParse("bad")), - invalidRegex: issueMetadataFromResult(z.string().regex(/^a+$/).safeParse("bbb")), - strict: issueMetadataFromResult(z.object({ id: z.number() }).strict().safeParse({ id: 1, extra: true })), - invalidDate: issueMetadataFromResult(z.date().safeParse(new Date("bad"))), - notFinite: issueMetadataFromResult(z.number().finite().safeParse(Infinity)), - dateTooSmall: issueMetadataFromResult( - z.date().min(new Date("2020-01-02T00:00:00.000Z")).safeParse(new Date("2020-01-01T00:00:00.000Z")), - ), - dateTooBig: issueMetadataFromResult( - z.date().max(new Date("2020-01-02T00:00:00.000Z")).safeParse(new Date("2020-01-03T00:00:00.000Z")), - ), - tooBigArray: issueMetadataFromResult(z.array(z.string()).max(1).safeParse(["a", "b"])), - tooSmallNumber: issueMetadataFromResult(z.number().min(2).safeParse(1)), - exactArray: issueMetadataFromResult(z.array(z.string()).length(2).safeParse(["a"])), - exactString: issueMetadataFromResult(z.string().length(2).safeParse("a")), - notMultiple: issueMetadataFromResult(z.number().multipleOf(5).safeParse(12)), - invalidIntersection: issueMetadataFromResult( - z.intersection(z.string().transform(() => 1), z.string().transform(() => 2)).safeParse("x"), - ), - custom: issueMetadataFromResult( - z.string() - .superRefine((value, ctx) => { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: `custom:${value}`, params: { kind: "fixture" } }); - }) - .safeParse("x"), - ), -}); - -const manualError = new z.ZodError([]); -manualError.addIssue({ code: z.ZodIssueCode.custom, path: ["manual"], message: "manual issue" }); -manualError.addIssues([{ code: z.ZodIssueCode.custom, path: ["more"], message: "more issue" }]); -const createdError = z.ZodError.create([ - { code: z.ZodIssueCode.custom, path: ["create"], message: "created issue" }, -]); -const mappedFormat = objectBase.safeParse({ id: "x", name: 1 }); -const unionFormatted = z - .union([z.object({ a: z.string() }), z.object({ b: z.number() })]) - .safeParse({ a: 1, b: "x" }); -print("errorMethods", { - manualCount: manualError.issues.length, - manualEmpty: manualError.isEmpty, - manualMessage: manualError.message.includes("manual issue"), - manualToString: manualError.toString().includes("more issue"), - manualErrorsAlias: manualError.errors === manualError.issues, - manualFormErrors: manualError.formErrors.fieldErrors.manual?.[0], - manualFlatten: manualError.flatten((issue) => `${issue.path.join(".")}:${issue.message}`), - created: createdError.issues[0].message, - mappedFormat: mappedFormat.success - ? null - : mappedFormat.error.format((issue) => `${issue.code}:${issue.message}`).id?._errors, - unionFormat: unionFormatted.success - ? null - : unionFormatted.error.format((issue) => `${issue.path.join(".")}:${issue.code}`), - unionFlatten: unionFormatted.success - ? null - : unionFormatted.error.flatten((issue) => `${issue.path.join(".")}:${issue.code}`), -}); - -const formattingResult = z - .object({ - user: z.object({ - name: z.string(), - tags: z.array(z.string().min(2)).min(2), - }), - }) - .strict() - .safeParse({ user: { name: 1, tags: ["x"] }, extra: true }); -if (!formattingResult.success) { - const formattedError = formattingResult.error.format(); - print("errorFormatting", { - issueCodes: formattingResult.error.issues.map((issue) => issue.code), - formatKeys: Object.keys(formattedError.user ?? {}).sort(), - formatRoot: formattedError._errors, - formatName: formattedError.user?.name?._errors, - formatTags: formattedError.user?.tags?._errors, - formatTag0: formattedError.user?.tags?.[0]?._errors, - flatten: formattingResult.error.flatten(), - flattenMapped: formattingResult.error.flatten((issue) => `${issue.path.join(".")}:${issue.code}`), - formErrors: formattingResult.error.formErrors, - }); -} - -const customMessageString = z.string({ - required_error: "required string", - invalid_type_error: "not a string", -}); -const requiredStringResult = customMessageString.safeParse(undefined); -const invalidStringResult = customMessageString.safeParse(1); -const minMessageResult = z.string().min(3, "too short").safeParse("x"); -const parsePathResult = z.string().safeParse(1, { path: ["root"] }); -const perParseMapResult = z.string().safeParse(1, { - errorMap: (issue) => ({ message: `local:${issue.code}` }), -}); -print("customErrors", { - required: requiredStringResult.success ? "ok" : requiredStringResult.error.issues[0].message, - invalidType: invalidStringResult.success ? "ok" : invalidStringResult.error.issues[0].message, - min: minMessageResult.success ? "ok" : minMessageResult.error.issues[0].message, - path: parsePathResult.success ? "ok" : parsePathResult.error.issues[0].path.join("."), - perParseMap: perParseMapResult.success ? "ok" : perParseMapResult.error.issues[0].message, -}); - -const parseOptionsObjectResult = z.object({ item: z.string().min(2) }).safeParse({ item: "x" }, { path: ["root"] }); -const parseOptionsUnionResult = z.union([z.string(), z.number()]).safeParse(true, { - path: ["value"], - errorMap: (issue, ctx) => ({ message: `union:${issue.code}:${ctx.defaultError}` }), -}); -const parseOptionsAsyncResult = await z.string().min(2).safeParseAsync("x", { - path: ["asyncRoot"], - errorMap: (issue) => ({ message: `async:${issue.code}` }), -}); -print("parseOptions", { - object: issueMetadataFromResult(parseOptionsObjectResult), - union: issueMetadataFromResult(parseOptionsUnionResult), - async: issueMetadataFromResult(parseOptionsAsyncResult), -}); - -const summarySchema = z.array(userSchema.pick({ id: true, role: true })).min(1); -print("array", summarySchema.parse([{ id: 1, role: "admin" }])); - -const standardOk = objectBase["~standard"].validate({ id: 1, name: "a" }); -const standardBad = objectBase["~standard"].validate({ id: "x", name: 1 }); -const legacyStandardOk = (objectBase as any)["~validate"]({ id: 1, name: "a" }); -const legacyStandardBad = (objectBase as any)["~validate"]({ id: "x", name: 1 }); -const legacyAsyncStandardOk = await (z.string().refine(async (value) => value === "ok") as any)["~validate"]("ok"); -const legacyAsyncStandardBad = await (z.string().refine(async (value) => value === "ok") as any)["~validate"]("bad"); -print("standard", { - vendor: objectBase["~standard"].vendor, - version: objectBase["~standard"].version, - ok: "value" in standardOk ? standardOk.value : null, - badIssues: "issues" in standardBad ? standardBad.issues?.length : 0, - legacyOk: "value" in legacyStandardOk ? legacyStandardOk.value : null, - legacyBadIssues: "issues" in legacyStandardBad ? legacyStandardBad.issues?.map((issue: z.ZodIssue) => `${issue.path.join(".")}:${issue.code}`) : [], - legacyAsyncOk: "value" in legacyAsyncStandardOk ? legacyAsyncStandardOk.value : null, - legacyAsyncBad: "issues" in legacyAsyncStandardBad ? legacyAsyncStandardBad.issues?.[0].message : null, -}); - -const z3SubpathResult = z3.object({ id: z3.number(), name: z3.string().default("subpath") }).parse({ id: 1 }); -const z3SubpathBad = z3.object({ id: z3.number() }).safeParse({ id: "bad" }); -print("packageSubpaths", { - v3Default: z3SubpathResult.name, - v3Issue: z3SubpathBad.success ? "ok" : z3SubpathBad.error.issues[0].code, - v3DefaultEqualsNamed: z3Default === z3, - v3NamespaceHasZ: z3Namespace.z === z3, - v3NamespaceDefaultEqualsNamed: z3Namespace.default === z3, - v3NamespaceParse: z3Namespace.string().parse("v3 namespace"), -}); - -const originalErrorMap = z.getErrorMap(); -z.setErrorMap((issue, ctx) => ({ message: `global:${issue.code}:${ctx.defaultError}` })); -const mappedError = z.string().safeParse(1); -const schemaMappedError = z.string({ invalid_type_error: "schema invalid" }).safeParse(1); -const localMappedError = z.string().safeParse(1, { - errorMap: (issue, ctx) => ({ message: `local:${issue.code}:${ctx.defaultError}` }), -}); -z.setErrorMap(originalErrorMap); -print("errorMap", { - mapped: mappedError.success ? "ok" : mappedError.error.issues[0].message, - schema: schemaMappedError.success ? "ok" : schemaMappedError.error.issues[0].message, - local: localMappedError.success ? "ok" : localMappedError.error.issues[0].message, - restored: z.getErrorMap() === originalErrorMap, -}); diff --git a/tests/release/packages/zod3-basic/expected.txt b/tests/release/packages/zod3-basic/expected.txt deleted file mode 100644 index 63958f0c1..000000000 --- a/tests/release/packages/zod3-basic/expected.txt +++ /dev/null @@ -1,52 +0,0 @@ -parse={"id":7,"name":"ADA","tags":[],"role":"user","meta":{"active":true,"source":"seed"}} -safeParse.success=false -safeParse.issues=[{"path":"id","code":"too_small","message":"Number must be greater than 0"},{"path":"name","code":"too_small","message":"String must contain at least 2 character(s)"},{"path":"tags.1","code":"invalid_type","message":"Expected string, received number"},{"path":"meta.active","code":"invalid_type","message":"Expected boolean, received string"}] -primitives={"string":"x","number":1.5,"int":false,"boolean":false,"bigint":"10","symbol":"symbol","nan":true,"void":true,"null":null,"undefined":true,"any":1,"unknown":2,"never":false} -packageExports={"parsed":{"string":"string","nan":"nan","map":"map","promise":"promise"},"status":{"ok":{"status":"valid","value":1},"dirty":{"status":"dirty","value":2},"invalid":{"status":"aborted"},"isValid":true,"isDirty":true,"isAborted":true},"parsedType":{"string":"string","nan":"nan","map":"map","promise":"promise"},"typeKind":{"object":"ZodObject","pipeline":"ZodPipeline","readonly":"ZodReadonly"},"aliases":{"schema":true,"schemaInstance":true,"brand":"symbol","brandDescription":"zod_brand","defaultEqualsNamed":true,"namespaceHasZ":true,"namespaceDefaultEqualsNamed":true,"namespaceString":"namespace"},"names":["ZodString","ZodNumber","ZodObject","ZodError"]} -packageUtilities={"isAsync":[true,false],"parseStatus":{"dirty":"dirty","merged":["a","b"],"aborted":"aborted"},"parsedTypes":["null","array","map","bigint","promise","nan"],"statusHelpers":{"valid":["valid",true,false,false],"dirty":["dirty",false,true,false],"aborted":["aborted",false,false,true],"never":"aborted","emptyPathLength":0},"util":{"arrayToEnum":"b","validEnumValues":["a",1],"joinValues":"'a' | 1 | true","objectKeys":"b|a","objectValues":[1,2],"find":2,"isInteger":[true,false],"jsonStringifyReplacer":"{\"n\":\"1\"}"},"quoteless":10,"datetimeRegex":[true,false],"defaultError":"Expected string, received number"} -parseHelpers={"direct":{"code":"custom","path":"root.leaf","message":"direct"},"mapped":{"path":"root","message":"Expected number, received string"},"context":[{"path":"ctx","message":"Expected number, received string"}],"aliases":{"schema":true,"type":true,"transformer":true,"transformerParse":4}} -coerce={"string":"42","number":12.5,"boolean":true,"bigint":"42","date":"2020-01-02T00:00:00.000Z"} -primitiveEdges={"boolean":true,"booleanRejectsString":false,"coerceBooleanString":true,"coerceNumberRejectsNaN":false,"coerceBigintRejectsText":false,"coerceDateEpoch":"1970-01-01T00:00:00.000Z","coerceDateRejectsInvalid":false} -primitiveMetadata={"types":["ZodString","ZodNumber","ZodBigInt","ZodBoolean","ZodNaN","ZodSymbol","ZodAny","ZodUnknown","ZodNever","ZodUndefined","ZodNull","ZodVoid"],"date":{"typeName":"ZodDate","coerce":false,"minDate":"2020-01-01T00:00:00.000Z","maxDate":"2020-12-31T00:00:00.000Z","checks":[{"kind":"min","value":1577836800000,"message":"too old"},{"kind":"max","value":1609372800000,"message":"too new"}]},"coerceDate":true,"instanceof":{"typeName":"ZodEffects","inner":"ZodAny","effect":"refinement"}} -literals.enums={"literal":true,"literalBigint":true,"literalBoolean":false,"literalNull":null,"literalFail":false,"literalValue":"ready","literalTypeName":"ZodLiteral","enum":"blue","enumBlue":"blue","enumValues":"green","enumEnum":"red","enumOptions":"red|blue|green","enumTypeName":"ZodEnum","enumDefValues":"red|blue|green","enumKeys":["blue","green","red"],"enumExtract":false,"enumExtractOk":"red","enumExtractOptions":"red|green","enumExclude":"green","enumExcludeOptions":"red|green","enumRequiredMessage":"need color","enumInvalidTypeMessage":"bad color","nativeEnum":"a","nativeEnumNumber":2,"nativeEnumMixed":["text",0,1],"nativeEnumReverseRejected":[{"code":"invalid_enum_value","path":"","received":"Zero","options":["text",0,1]}],"nativeEnumTypeName":"ZodNativeEnum","nativeEnumKeys":"A|B","nativeEnumMixedKeys":"0|1|Text|Zero|One"} -strings={"value":"AB-YZ","email":true,"uuid":true,"url":true,"datetime":true,"datetimeOffset":true,"datetimePrecision":true,"datetimeLocal":true,"ip":true,"ipv4":true,"ipv6":true,"length":true,"nonempty":false,"timePrecision":false,"includesPosition":true,"includesPositionFail":false,"startsWith":true,"startsWithFail":false,"endsWith":true,"endsWithFail":false,"trim":"tuna","uppercase":"TUNA","lowercase":"abc","chainedTransform":"TUNA","cuid":true,"cuid2":true,"ulid":true,"emoji":true,"nanoid":true,"base64":true,"base64url":true,"jwt":true,"date":true,"time":true,"duration":true,"cidr":true,"cidrv4":true,"cidrv6":true,"regexFail":false} -schemaIntrospection={"stringBounds":[2,5],"stringFormats":{"email":{"isEmail":true,"isURL":false,"isUUID":false,"isDatetime":false,"isIP":false},"url":{"isEmail":false,"isURL":true,"isUUID":false,"isDatetime":false,"isIP":false},"uuid":{"isEmail":false,"isURL":false,"isUUID":true,"isDatetime":false,"isIP":false},"datetime":{"isEmail":false,"isURL":false,"isUUID":false,"isDatetime":true,"isIP":false},"ip":{"isEmail":false,"isURL":false,"isUUID":false,"isDatetime":false,"isIP":true}},"extraStringFormats":{"base64":true,"base64url":true,"cidr":true,"cuid":true,"cuid2":true,"date":true,"duration":true,"emoji":true,"nanoid":true,"time":true,"ulid":true},"numberBounds":[1,3],"numberFlags":{"isInt":true,"isFinite":true}} -numbers={"finite":false,"min":true,"max":false,"gt":true,"gte":true,"lt":false,"lte":true,"multiple":true,"step":true,"positive":true,"nonpositive":true,"negative":true,"nonnegative":true,"safe":false} -bigints={"min":true,"max":false,"gt":true,"gte":true,"lt":false,"lte":true,"multiple":true,"positive":true,"nonpositive":true,"negative":true,"nonnegative":true} -discriminatedUnion=[{"type":"text","value":"hello"},{"type":"count","value":3}] -discriminatedUnion.options={"length":2,"discriminator":"type","optionKeys":"text|count","typeName":"ZodDiscriminatedUnion","inherited":{"type":"count","value":3},"inheritedOwnType":true,"inheritedOwnValue":true,"inheritedInvalid":[{"code":"invalid_type","path":"value","expected":"number","received":"string"}],"multiValue":[{"kind":"text","body":"hello"},{"kind":"markdown","body":"**hello**"}],"multiValueOptionKeys":"text|markdown|count","multiValueInvalidKind":[{"code":"invalid_union_discriminator","path":"kind","options":["text","markdown","count"]}],"multiValueInvalidBranch":[{"code":"invalid_type","path":"body","expected":"string","received":"number"}]} -union={"results":[true,false],"options":2,"typeName":"ZodUnion","optionTypes":["ZodString","ZodNumber"],"branchFailure":[["value:invalid_type"],["kind:invalid_literal","count:invalid_type"]],"inheritedObject":{"kind":"b","count":2},"inheritedObjectOwnKind":true,"inheritedObjectOwnCount":true} -objects={"strip":{"id":1,"name":"a"},"explicitStrip":{"id":1,"name":"a"},"strict":false,"strictObject":false,"nonstrict":{"id":1,"name":"a","extra":true},"passthrough":{"id":1,"name":"a","extra":true},"catchall":true,"catchallFail":false,"extend":{"id":1,"name":"a","role":"admin"},"augment":{"id":1,"name":"a"},"setKey":{"id":1,"name":"a"},"merge":{"id":1,"name":"a","role":"user"},"strictMessage":false,"keyof":"name","shapeName":true,"pick":{"id":1},"omit":{"id":1,"name":"a"},"spreadShape":{"id":1,"name":"a","role":"user"},"inheritedInput":{"own":1},"inheritedOutput":{"own":1,"inherited":"from-proto"},"inheritedOwnOutput":true,"inheritedOwnInput":false,"inheritedOutputPrototype":true,"inheritedPassthrough":{"id":1,"inheritedExtra":"from-proto"},"inheritedPassthroughOwn":true,"inheritedStrict":false,"nullProto":{"id":1,"extra":"x"},"nullProtoStripped":{"id":1},"nullProtoInputPrototype":true,"nullProtoOutputPrototype":true,"nullProtoStrict":false,"getterInput":{"id":7,"inherited":"from-getter"},"getterPassthrough":{"id":7,"inherited":"from-getter"},"getterReads":[2,2],"getterOutputOwnInherited":true,"partial":{"id":1},"partialName":true,"deepPartial":{"nested":{}},"required":false,"requiredActive":false,"nestedRequired":true,"partialRequiredMask":{"baseMissingName":false,"partialMissingName":true,"partialMissingNested":false,"requiredMissingName":false,"requiredMissingOptionalAge":true,"base":{"requiredName":"ZodString","optionalAge":"ZodOptional","nested":"ZodObject"},"partial":{"requiredName":"ZodOptional","optionalAge":"ZodOptional","nested":"ZodObject"},"required":{"requiredName":"ZodString","optionalAge":"ZodOptional","nested":"ZodObject"},"partialNameInner":"ZodString"},"defaultedOptionalEmpty":{},"defaultedOptionalUndefined":{},"anyUnknownMissing":true,"unknownKeys":["strip","passthrough","strict","strip"],"catchallType":"ZodString","strictCreate":false,"strictCreateParse":1,"lazycreate":"lazy","shapeMetadata":{"partial":{"id":"ZodOptional","name":"ZodOptional","active":"ZodOptional"},"partialName":{"id":"ZodNumber","name":"ZodOptional","active":"ZodOptional"},"partialActiveInner":"ZodOptional","required":{"id":"ZodNumber","name":"ZodString","active":"ZodBoolean"},"requiredActive":{"id":"ZodNumber","name":"ZodString","active":"ZodBoolean"},"deepPartial":["ZodOptional","ZodOptional"]}} -objectMergeSemantics={"extendOverwrite":1,"strictWinsUnknownKeys":"strict","strictRejectsExtra":false,"passthroughWinsUnknownKeys":"passthrough","passthroughAllowsExtra":true,"catchallWinsType":"ZodNumber","catchallAcceptsNumber":true,"catchallRejectsString":false,"mergedShapeKeys":"a|b"} -recursiveObjects={"category":{"name":"root","subcategories":[{"name":"leaf","subcategories":[]}]},"categoryIssue":[{"code":"invalid_type","path":"subcategories.0.name","expected":"string","received":"number"},{"code":"invalid_type","path":"subcategories.0.subcategories","expected":"array","received":"undefined"}],"mutual":"a@example.com","mutualNested":"b@example.com","discriminatedRecursive":{"kind":"branch","children":[{"kind":"leaf","value":"x"}]},"discriminatedRecursiveIssue":[{"code":"invalid_type","path":"children.0.value","expected":"string","received":"number"}]} -arrays.tuples={"array":[1,2],"transformedArray":[2,3],"exactLength":true,"nonempty":false,"elementDescription":"array item","elementIssuePath":"0","typeName":"ZodArray","bounds":[1,3,2],"elementType":"ZodString","minMessage":"need two","maxMessage":"too many","lengthMessage":"exactly two","sparseOptional":["a",null,"c"],"sparseOptionalOwn1":true,"sparseRequiredIssue":[{"code":"invalid_type","path":"1","expected":"string","received":"undefined"}],"inheritedIndexArray":["a","proto","c"],"inheritedIndexArrayOwn1":true,"inheritedIndexTuple":["a","proto","c"],"inheritedIndexTupleOwn1":true,"tuple":["a",1],"tupleLengthIssue":"too_big","tupleRest":["a",1,2],"tupleItems":["ZodString","ZodNumber"],"tupleRestType":"ZodBoolean","tupleTypeName":"ZodTuple"} -collections={"record":true,"keyedRecord":true,"keyedRecordMissing":true,"stringKeyRecord":false,"transformedRecord":{"a":4},"recordFail":false,"recordNumberKey":[{"code":"invalid_type","path":"1"}],"recordNumericStringKey":{"1":"one"},"recordNumericStringFail":[{"code":"invalid_string","path":"abc"}],"inheritedRecord":{"own":1,"inherited":2},"inheritedRecordOwn":true,"inheritedRecordIssue":[{"code":"invalid_type","path":"badInherited"}],"nullProtoRecord":{"id":1,"extra":"x"},"hiddenSymbolRecord":{"own":1},"hiddenSymbolRecordHasHidden":false,"hiddenSymbolRecordSymbols":0,"map":[["a",1],["b",2]],"mapKeyFail":false,"mapFail":false,"transformedMap":[["size",4]],"set":["a","b"],"setNonempty":false,"setFail":false,"setMax":false,"setSize":true,"transformedSet":[2,3],"recordMeta":{"typeName":"ZodRecord","key":"ZodString","value":"ZodNumber","element":"ZodNumber"},"mapMeta":{"typeName":"ZodMap","key":"ZodString","value":"ZodNumber"},"setMeta":{"typeName":"ZodSet","value":"ZodString","min":1,"max":3,"size":[2,2]},"setMessages":["need two","too many","exactly two"],"recordIssues":[{"code":"invalid_type","path":"bad"}],"mapKeyIssues":[{"code":"too_small","path":"0.key"}],"mapValueIssues":[{"code":"invalid_type","path":"0.value"}],"setElementIssues":[{"code":"too_small","path":"0"}],"setSizeIssues":[{"code":"too_small","path":""}]} -cloneSemantics={"objectIdentity":true,"nestedIdentity":true,"arrayIdentity":true,"objectSource":{"nested":{"label":"x"},"tags":["a"]},"mapIdentity":true,"mapValueIdentity":true,"mapSource":[["a",{"count":1}]],"subclassMapIsMap":true,"subclassMapIsSubclass":false,"subclassMapValueIdentity":true,"subclassMapSource":[["a",{"count":1}]],"subclassMapParsed":[["a",{"count":2}]],"mapObjectKeyIdentity":true,"mapObjectValueIdentity":true,"mapObjectSource":[[{"id":1},{"count":1}]],"mapObjectParsed":[[{"id":2},{"count":2}]],"setIdentity":true,"setValueIdentity":true,"setSource":[{"id":1}],"subclassSetIsSet":true,"subclassSetIsSubclass":false,"subclassSetValueIdentity":true,"subclassSetSource":[{"id":1}],"subclassSetParsed":[{"id":2}],"dateIdentity":true,"dateSource":"2020-01-02T00:00:00.000Z","dateParsed":"2021-01-02T00:00:00.000Z"} -composition={"intersection":{"a":"x","b":1},"inheritedIntersection":{"a":"x","b":2},"inheritedIntersectionOwnB":true,"nestedIntersection":{"nested":{"a":"x","b":1},"shared":"same"},"conflictingIntersection":[{"code":"invalid_intersection_types","path":""}],"arrayIntersectionConflict":[{"code":"invalid_intersection_types","path":""}],"intersectionTypeName":"ZodIntersection","intersectionSides":["ZodObject","ZodObject"],"transformedIntersection":{"a":1,"b":2},"or":5,"and":{"a":"x","b":true},"strictObject":false,"schemaArray":["a","b"],"schemaOr":"b","schemaOrIssue":[{"code":"too_small","path":"","minimum":10,"inclusive":true,"exact":false,"type":"number"}],"schemaAnd":{"a":"x","b":1},"pipelineFactory":true} -transform=["a","b","c"] -refine=false -effects={"preprocess":"ok","pipe":true,"superRefine":false,"custom":true,"refinementAlias":"bad refinement","innerType":"inner","sourceType":"source","refinePath":"confirm","refineMessage":"password mismatch","fatalCalls":["first:x"],"fatalIssue":"stop","fatalFlag":true,"neverTransform":"bad transform:x","neverStatus":"aborted","metadata":{"preprocess":["ZodEffects","preprocess","ZodString","ZodString","ZodString"],"transform":["ZodEffects","transform","ZodString","ZodString","ZodString"],"refine":["ZodEffects","refinement","ZodString"],"pipeline":["ZodPipeline","ZodEffects","ZodNumber","ZodString","ZodNumber"]}} -customSchemas={"unchecked":1,"predicate":true,"message":"not a token"} -descriptions={"original":null,"string":["fixture string","fixture string","ok"],"object":["fixture object","fixture object","identifier",1],"array":["fixture array","fixture array","array item",1],"optional":["outer optional","outer optional","inner",true]} -modifiers={"ostring":true,"onumber":true,"oboolean":true,"optional":true,"nullable":true,"nullish":true,"default":"fallback","defaultFunction":"factory","defaultSkipsPresent":"present","defaultFactorySequence":["fallback-1","fallback-2","present",2],"defaultTransform":4,"removeDefault":false,"catch":9,"catchTransform":9,"catchFunction":1,"catchFactorySequence":[2,3,1],"catchContext":"bad:invalid_type","removeCatch":false,"brand":"id-1","described":"fixture string","describedObject":"fixture object","describedParse":"described","optionalUnwrap":"wrapped","nullableUnwrap":"wrapped","arrayElement":"element","promiseUnwrap":3,"isOptional":true,"isNullable":true,"readonlyFrozen":true,"readonlyArray":true,"readonlyTuple":true,"readonlyMap":true,"readonlySet":true,"readonlyMutations":{"object":[false,1],"nestedObject":[true,false,false,false,true,"ok",{"nested":{"id":2},"tags":["a","b"]},{"nested":{"id":1},"tags":["a"]},true,true],"array":[false,"TypeError","a",1],"tuple":[false,"TypeError","a",1],"map":["ok",2,2],"set":["ok",2,true]},"schemaArray":2,"schemaOr":2,"schemaAnd":{"a":"x","b":1},"metadata":{"optional":["ZodOptional","ZodString"],"nullable":["ZodNullable","ZodString"],"default":["ZodDefault","fallback"],"catch":["ZodCatch",9],"promise":["ZodPromise","ZodNumber"],"readonly":["ZodReadonly","ZodObject"],"branded":["ZodBranded","ZodString",true,"ZodBranded"]}} -lazy={"parsed":{"name":"root","children":[{"name":"leaf"}]},"cloneIdentity":true,"childCloneIdentity":true,"source":{"name":"root","children":[{"name":"leaf"}]},"mutated":{"name":"root","children":[{"name":"changed"}]}} -factories={"arrayFactory":"factory","optionalFactory":true,"nullableFactory":true,"promiseFactory":4,"ostringFactory":true,"onumberFactory":3,"obooleanFactory":true,"unionFactory":"right","intersectionFactory":{"a":"x","b":2},"lazyFactory":"lazy","lateObject":{"name":"root","children":[{"name":"leaf","children":[]}]}} -staticFactories={"string":"x","number":1,"nan":true,"array":1,"object":1,"tuple":1,"record":"x","map":[["a",1]],"set":["a"],"optional":true,"nullable":true,"promise":2,"readonly":true,"effectAlias":4,"transformerAlias":"TUNA","objectUtil":{"a":"x","shared":2,"b":1},"literal":"ready","enum":"blue","nativeEnum":"a","date":"2020-01-02T00:00:00.000Z","bigint":"2","boolean":true,"any":true,"unknown":1,"never":false,"undefined":true,"null":true,"void":true,"symbol":"symbol","union":3,"intersection":{"a":"x","b":1},"discriminatedUnion":2,"function":3,"lazy":"lazy","effects":4,"default":"fallback","catch":7,"pipeline":6} -jsonLike={"object":true,"rejectsFunction":false,"rejectsSymbol":false,"rejectsBigint":false,"badNested":[{"code":"invalid_union","path":""}],"fromString":{"a":[1,true,null]},"mapSize":true,"mapSizeFail":[{"code":"custom","path":"box.size"}],"parsed":{"ok":true,"items":[1,"two"]}} -instances.dates={"instanceof":"ok","instanceMessage":"not a box","date":"2020-01-02T00:00:00.000Z","dateRequired":"required date","dateInvalid":"not a date","dateMin":true,"dateMax":false} -function={"valid":4,"strictValid":5,"validateAlias":4,"restValid":8,"restInvalid":["2:invalid_type"],"asyncValid":6,"cloneSemantics":{"argIdentity":true,"nestedArgIdentity":true,"source":{"nested":{"id":1}},"returnIdentity":true,"nestedReturnIdentity":true,"returned":{"nested":{"id":3}}},"parameters":2,"returnType":true,"invalidArgs":true,"invalidReturns":true,"argumentIssues":[{"code":"invalid_arguments","path":"","nestedCount":1}],"returnIssues":[{"code":"invalid_return_type","path":"","nestedCount":1}],"strictReturnIssues":[{"code":"invalid_return_type","path":"","nestedCount":1}],"invalidAsyncReturn":true,"metadata":{"typed":{"typeName":"ZodFunction","argsType":"ZodTuple","argItems":["ZodString","ZodNumber"],"argRest":"ZodUnknown","returns":"ZodBoolean","parameters":["ZodString","ZodNumber"],"returnType":"ZodBoolean"},"defaults":{"args":0,"rest":"ZodUnknown","returns":"ZodUnknown"},"argsOnly":{"args":["ZodString"],"returns":"ZodUnknown"},"returnsOnly":{"args":0,"returns":"ZodNumber"}}} -async={"refine":{"success":true,"data":"ok"},"parseAsync":"ok","spa":true,"promise":5,"methodPromise":3,"promiseRejects":true,"rejectedReason":"promise boom"} -asyncEffects={"transform":4,"pipe":{"success":true,"data":3},"superRefineMessage":"bad async","refineMessage":"not ok","preprocess":"ok","syncThrows":{"transformParse":"Error:Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.","transformSafeParse":"Error:Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.","refineParse":"Error:Async refinement encountered during synchronous parse operation. Use .parseAsync instead.","refineSafeParse":"Error:Async refinement encountered during synchronous parse operation. Use .parseAsync instead."},"metadata":{"refine":["ZodEffects","refinement","ZodString"],"transform":["ZodEffects","transform","ZodString"],"pipe":["ZodPipeline","ZodEffects","ZodNumber"],"superRefine":["ZodEffects","refinement","ZodString"],"preprocess":["ZodEffects","preprocess","ZodString"],"promise":["ZodPromise","ZodNumber"]}} -asyncIssues={"parseAsync":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":false,"type":"string"}],"pipe":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":false,"type":"number"}],"preprocess":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":false,"type":"string"}],"promiseInner":[{"code":"invalid_type","path":"","expected":"number","received":"string"}]} -promiseSchemas={"safeParse":true,"safeValue":8,"parseThen":"function","parseValue":2,"nonPromise":[{"code":"invalid_type","path":"","expected":"promise","received":"number"}],"inner":[{"code":"invalid_type","path":"","expected":"number","received":"string"}],"object":{"id":1},"cloneIdentity":true,"cloneNestedIdentity":true,"cloneSource":{"nested":{"id":1}},"cloneParsed":{"nested":{"id":2}},"methodType":"ZodNumber"} -effectFactories={"preprocessNumber":3,"preprocessIssue":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":false,"type":"number"}],"pipelineObject":{"len":3},"pipelineIssue":[{"code":"too_small","path":"len","minimum":2,"inclusive":true,"exact":false,"type":"number"}],"transformIssue":[{"code":"custom","path":""}]} -errors={"formatKeys":["_errors","id","name"],"flatten":{"formErrors":[],"fieldErrors":{"id":["Expected number, received string"],"name":["Expected string, received number"]}},"nestedTagErrors":["Array must contain at least 2 element(s)"],"nestedTagItemErrors":["String must contain at least 2 character(s)"],"unionIssue":"invalid_union","unionBranchIssueCounts":[1,2],"unionFirstBranchPath":"value","discriminatedIssue":"invalid_union_discriminator","discriminatedPath":"type"} -issueMetadata={"literal":[{"code":"invalid_literal","path":"","expected":"ready","received":"no"}],"invalidType":[{"code":"invalid_type","path":"","expected":"number","received":"string"}],"enum":[{"code":"invalid_enum_value","path":"","received":"green","options":["red","blue"]}],"invalidString":[{"code":"invalid_string","path":"","validation":"email"}],"invalidRegex":[{"code":"invalid_string","path":"","validation":"regex"}],"strict":[{"code":"unrecognized_keys","path":"","keys":["extra"]}],"invalidDate":[{"code":"invalid_date","path":""}],"notFinite":[{"code":"not_finite","path":""}],"dateTooSmall":[{"code":"too_small","path":"","minimum":1577923200000,"inclusive":true,"exact":false,"type":"date"}],"dateTooBig":[{"code":"too_big","path":"","maximum":1577923200000,"inclusive":true,"exact":false,"type":"date"}],"tooBigArray":[{"code":"too_big","path":"","maximum":1,"inclusive":true,"exact":false,"type":"array"}],"tooSmallNumber":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":false,"type":"number"}],"exactArray":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":true,"type":"array"}],"exactString":[{"code":"too_small","path":"","minimum":2,"inclusive":true,"exact":true,"type":"string"}],"notMultiple":[{"code":"not_multiple_of","path":"","multipleOf":5}],"invalidIntersection":[{"code":"invalid_intersection_types","path":""}],"custom":[{"code":"custom","path":"","params":{"kind":"fixture"}}]} -errorMethods={"manualCount":2,"manualEmpty":false,"manualMessage":true,"manualToString":true,"manualErrorsAlias":true,"manualFormErrors":"manual issue","manualFlatten":{"formErrors":[],"fieldErrors":{"manual":["manual:manual issue"],"more":["more:more issue"]}},"created":"created issue","mappedFormat":["invalid_type:Expected number, received string"],"unionFormat":{"_errors":[],"a":{"_errors":["a:invalid_type"]},"b":{"_errors":["b:invalid_type"]}},"unionFlatten":{"formErrors":[":invalid_union"],"fieldErrors":{}}} -errorFormatting={"issueCodes":["invalid_type","too_small","too_small","unrecognized_keys"],"formatKeys":["_errors","name","tags"],"formatRoot":["Unrecognized key(s) in object: 'extra'"],"formatName":["Expected string, received number"],"formatTags":["Array must contain at least 2 element(s)"],"formatTag0":["String must contain at least 2 character(s)"],"flatten":{"formErrors":["Unrecognized key(s) in object: 'extra'"],"fieldErrors":{"user":["Expected string, received number","Array must contain at least 2 element(s)","String must contain at least 2 character(s)"]}},"flattenMapped":{"formErrors":[":unrecognized_keys"],"fieldErrors":{"user":["user.name:invalid_type","user.tags:too_small","user.tags.0:too_small"]}},"formErrors":{"formErrors":["Unrecognized key(s) in object: 'extra'"],"fieldErrors":{"user":["Expected string, received number","Array must contain at least 2 element(s)","String must contain at least 2 character(s)"]}}} -customErrors={"required":"required string","invalidType":"not a string","min":"too short","path":"root","perParseMap":"local:invalid_type"} -parseOptions={"object":[{"code":"too_small","path":"root.item","minimum":2,"inclusive":true,"exact":false,"type":"string"}],"union":[{"code":"invalid_union","path":"value"}],"async":[{"code":"too_small","path":"asyncRoot","minimum":2,"inclusive":true,"exact":false,"type":"string"}]} -array=[{"id":1,"role":"admin"}] -standard={"vendor":"zod","version":1,"ok":{"id":1,"name":"a"},"badIssues":2,"legacyOk":{"id":1,"name":"a"},"legacyBadIssues":["id:invalid_type","name:invalid_type"],"legacyAsyncOk":"ok","legacyAsyncBad":"Invalid input"} -packageSubpaths={"v3Default":"subpath","v3Issue":"invalid_type","v3DefaultEqualsNamed":true,"v3NamespaceHasZ":true,"v3NamespaceDefaultEqualsNamed":true,"v3NamespaceParse":"v3 namespace"} -errorMap={"mapped":"global:invalid_type:Expected string, received number","schema":"schema invalid","local":"local:invalid_type:global:invalid_type:Expected string, received number","restored":true} diff --git a/tests/release/packages/zod3-basic/fixture.sh b/tests/release/packages/zod3-basic/fixture.sh deleted file mode 100755 index 3ea94e0de..000000000 --- a/tests/release/packages/zod3-basic/fixture.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail -cd "$(dirname "$0")" -. "$(dirname "$0")/../_fixture_lib.sh" - -NAME="zod3-basic" - -now_ms() { - python3 -c 'import time; print(time.perf_counter_ns() // 1000000)' -} - -fixture_setup "$NAME" || exit 1 - -node entry.ts > node-out.txt 2> node-run.log || { - echo "FAIL $NAME - node reference errored" - sed 's/^/ /' node-run.log | tail -40 - exit 1 -} -if ! diff -u expected.txt node-out.txt > node-diff.log; then - echo "FAIL $NAME - committed expected.txt differs from node output" - sed 's/^/ /' node-diff.log - exit 1 -fi - -start_ms="$(now_ms)" -echo " [perry compile] entry.ts" -if ! "$PERRY_BIN" compile entry.ts -o ./out > perry-compile.log 2>&1; then - echo "FAIL $NAME - perry compile errored" - sed 's/^/ /' perry-compile.log | tail -80 - exit 1 -fi -compile_done_ms="$(now_ms)" - -echo " [./out] (timeout=${PERRY_FIXTURE_TIMEOUT_SECS}s)" -set +e -_fixture_run_with_timeout "$PERRY_FIXTURE_TIMEOUT_SECS" ./out > perry-out.txt 2> perry-run.log -rc=$? -set -e -run_done_ms="$(now_ms)" - -cat > zod3-perf.txt < diff.log; then - echo "FAIL $NAME - output diverges" - sed 's/^/ /' diff.log - exit 1 -fi - -echo " [perf] $(tr '\n' ' ' < zod3-perf.txt)" -echo "PASS $NAME" diff --git a/tests/release/packages/zod3-basic/package-lock.json b/tests/release/packages/zod3-basic/package-lock.json deleted file mode 100644 index 76f3118cc..000000000 --- a/tests/release/packages/zod3-basic/package-lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "perry-release-fixture-zod3-basic", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "perry-release-fixture-zod3-basic", - "version": "0.0.0", - "dependencies": { - "zod": "3.25.76" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/tests/release/packages/zod3-basic/package.json b/tests/release/packages/zod3-basic/package.json deleted file mode 100644 index 76338df45..000000000 --- a/tests/release/packages/zod3-basic/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "perry-release-fixture-zod3-basic", - "version": "0.0.0", - "private": true, - "type": "module", - "description": "Tier-3 fixture: Zod 3 parse, safeParse, unions, defaults, transforms and refinements under Perry.", - "dependencies": { - "zod": "3.25.76" - }, - "perry": { - "compilePackages": ["zod"], - "allow": { - "compilePackages": ["zod"] - } - } -} diff --git a/tests/release/packages/zod4-basic/entry.ts b/tests/release/packages/zod4-basic/entry.ts deleted file mode 100644 index 8f30e33dc..000000000 --- a/tests/release/packages/zod4-basic/entry.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { z } from "zod"; - -// ---- object schema: parse, safeParse (ok + fail), defaults, optionals ---- -const User = z.object({ - name: z.string().min(2), - age: z.number().int().nonnegative(), - email: z.string().email().optional(), - tags: z.array(z.string()).default([]), - role: z.enum(["admin", "user", "guest"]), -}); - -const okUser = User.parse({ name: "Ada", age: 36, role: "admin" }); -const badUser = User.safeParse({ name: "A", age: -1, role: "root", email: "nope" }); - -// ---- unions / discriminated unions ---- -const Shape = z.discriminatedUnion("kind", [ - z.object({ kind: z.literal("circle"), r: z.number() }), - z.object({ kind: z.literal("rect"), w: z.number(), h: z.number() }), -]); -const circle = Shape.parse({ kind: "circle", r: 2 }); -const rectBad = Shape.safeParse({ kind: "rect", w: 3 }); - -// ---- transforms / refinements / pipes / coercion ---- -const Slug = z.string().transform((s) => s.trim().toLowerCase().replace(/\s+/g, "-")); -const Even = z.number().refine((n) => n % 2 === 0, { message: "must be even" }); -const AgeFromString = z.coerce.number().pipe(z.number().int().positive()); - -// ---- records / tuples / nullable ---- -const Scores = z.record(z.string(), z.number()); -const Pair = z.tuple([z.string(), z.number()]); -const MaybeName = z.string().nullable(); - -// ---- nested + partial-ish ---- -const Config = z.object({ - server: z.object({ host: z.string(), port: z.number().default(8080) }), - flags: z.array(z.boolean()), -}); - -const out = { - okUser, - badOk: badUser.success, - badPaths: badUser.success ? [] : badUser.error.issues.map((i) => i.path.join(".")).sort(), - badCount: badUser.success ? 0 : badUser.error.issues.length, - circle, - rectBadOk: rectBad.success, - slug: Slug.parse(" Hello World Foo "), - evenOk: Even.safeParse(10).success, - evenBad: Even.safeParse(7).success, - ageFromStr: AgeFromString.parse("42"), - scores: Scores.parse({ a: 1, b: 2 }), - pair: Pair.parse(["x", 9]), - maybeNull: MaybeName.parse(null), - config: Config.parse({ server: { host: "h" }, flags: [true, false] }), - isZodError: !badUser.success && badUser.error instanceof z.ZodError, -}; - -console.log(JSON.stringify(out)); diff --git a/tests/release/packages/zod4-basic/expected.txt b/tests/release/packages/zod4-basic/expected.txt deleted file mode 100644 index 1c2c3b3fb..000000000 --- a/tests/release/packages/zod4-basic/expected.txt +++ /dev/null @@ -1 +0,0 @@ -{"okUser":{"name":"Ada","age":36,"tags":[],"role":"admin"},"badOk":false,"badPaths":["age","email","name","role"],"badCount":4,"circle":{"kind":"circle","r":2},"rectBadOk":false,"slug":"hello-world-foo","evenOk":true,"evenBad":false,"ageFromStr":42,"scores":{"a":1,"b":2},"pair":["x",9],"maybeNull":null,"config":{"server":{"host":"h","port":8080},"flags":[true,false]},"isZodError":true} diff --git a/tests/release/packages/zod4-basic/fixture.sh b/tests/release/packages/zod4-basic/fixture.sh deleted file mode 100755 index 4235e637d..000000000 --- a/tests/release/packages/zod4-basic/fixture.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail -cd "$(dirname "$0")" -. "$(dirname "$0")/../_fixture_lib.sh" - -NAME="zod4-basic" - -now_ms() { - python3 -c 'import time; print(time.perf_counter_ns() // 1000000)' -} - -fixture_setup "$NAME" || exit 1 - -node entry.ts > node-out.txt 2> node-run.log || { - echo "FAIL $NAME - node reference errored" - sed 's/^/ /' node-run.log | tail -40 - exit 1 -} -if ! diff -u expected.txt node-out.txt > node-diff.log; then - echo "FAIL $NAME - committed expected.txt differs from node output" - sed 's/^/ /' node-diff.log - exit 1 -fi - -start_ms="$(now_ms)" -echo " [perry compile] entry.ts" -if ! "$PERRY_BIN" compile entry.ts -o ./out > perry-compile.log 2>&1; then - echo "FAIL $NAME - perry compile errored" - sed 's/^/ /' perry-compile.log | tail -80 - exit 1 -fi -compile_done_ms="$(now_ms)" - -echo " [./out] (timeout=${PERRY_FIXTURE_TIMEOUT_SECS}s)" -set +e -_fixture_run_with_timeout "$PERRY_FIXTURE_TIMEOUT_SECS" ./out > perry-out.txt 2> perry-run.log -rc=$? -set -e -run_done_ms="$(now_ms)" - -cat > zod4-perf.txt < diff.log; then - echo "FAIL $NAME - output diverges" - sed 's/^/ /' diff.log - exit 1 -fi - -echo " [perf] $(tr '\n' ' ' < zod4-perf.txt)" -echo "PASS $NAME" diff --git a/tests/release/packages/zod4-basic/package-lock.json b/tests/release/packages/zod4-basic/package-lock.json deleted file mode 100644 index 150211804..000000000 --- a/tests/release/packages/zod4-basic/package-lock.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "perry-release-fixture-zod4-basic", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "perry-release-fixture-zod4-basic", - "version": "0.0.0", - "dependencies": { - "zod": "4.4.3" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/tests/release/packages/zod4-basic/package.json b/tests/release/packages/zod4-basic/package.json deleted file mode 100644 index 0279b0c89..000000000 --- a/tests/release/packages/zod4-basic/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "perry-release-fixture-zod4-basic", - "version": "0.0.0", - "private": true, - "type": "module", - "description": "Tier-3 fixture: Zod 4 parse, safeParse, discriminated unions, transforms, refinements, coercion and pipes under Perry.", - "dependencies": { - "zod": "4.4.3" - }, - "perry": { - "compilePackages": ["zod"], - "allow": { - "compilePackages": ["zod"] - } - } -} diff --git a/tests/test_array_not_instanceof_error_subclass.sh b/tests/test_array_not_instanceof_error_subclass.sh deleted file mode 100755 index ccce0c29c..000000000 --- a/tests/test_array_not_instanceof_error_subclass.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/main.ts" <<'TS' -class ArraySubclass extends Array {} -class MapSubclass extends Map {} -class SetSubclass extends Set {} -class CustomA extends Error {} -class CustomB extends Error {} - -const arrays = [ - [], - [{}], - [{ code: "invalid_format", path: [], message: "Invalid URL", format: "url" }], - new Array(1), - new Array(2), - [new CustomA("x")], -]; - -for (let i = 0; i < arrays.length; i++) { - console.log(i, arrays[i] instanceof Error, arrays[i] instanceof CustomA, arrays[i] instanceof CustomB); -} - -const values = [ - ["array", new ArraySubclass(1, 2)], - ["map", new MapSubclass([["a", 1]])], - ["set", new SetSubclass([1])], - ["error", new CustomA("boom")], -] as const; - -for (const [name, value] of values) { - console.log(name, JSON.stringify({ - ArraySubclass: value instanceof ArraySubclass, - Array: value instanceof Array, - MapSubclass: value instanceof MapSubclass, - Map: value instanceof Map, - SetSubclass: value instanceof SetSubclass, - Set: value instanceof Set, - CustomA: value instanceof CustomA, - Error: value instanceof Error, - Object: value instanceof Object, - })); -} -TS - -cat >"$TMPDIR/expected.log" <<'EOF_EXPECTED' -0 false false false -1 false false false -2 false false false -3 false false false -4 false false false -5 false false false -array {"ArraySubclass":true,"Array":true,"MapSubclass":false,"Map":false,"SetSubclass":false,"Set":false,"CustomA":false,"Error":false,"Object":true} -map {"ArraySubclass":false,"Array":false,"MapSubclass":true,"Map":true,"SetSubclass":false,"Set":false,"CustomA":false,"Error":false,"Object":true} -set {"ArraySubclass":false,"Array":false,"MapSubclass":false,"Map":false,"SetSubclass":true,"Set":true,"CustomA":false,"Error":false,"Object":true} -error {"ArraySubclass":false,"Array":false,"MapSubclass":false,"Map":false,"SetSubclass":false,"Set":false,"CustomA":true,"Error":true,"Object":true} -EOF_EXPECTED - -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -COMPILE_OUT="$(PERRY_ALLOW_PERRY_FEATURES=1 "$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$TMPDIR/out" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} - -"$TMPDIR/out" >"$TMPDIR/stdout.log" 2>"$TMPDIR/stderr.log" || { - echo "FAIL: compiled binary errored" - sed 's/^/ /' "$TMPDIR/stdout.log" - sed 's/^/ /' "$TMPDIR/stderr.log" - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/stdout.log"; then - echo "FAIL: stdout mismatch" - exit 1 -fi - -if [[ -s "$TMPDIR/stderr.log" ]]; then - echo "FAIL: expected empty stderr" - sed 's/^/ /' "$TMPDIR/stderr.log" - exit 1 -fi - -echo "PASS: arrays are not Error subclass instances" diff --git a/tests/test_bigint_samevalue_map.sh b/tests/test_bigint_samevalue_map.sh deleted file mode 100644 index cade128ac..000000000 --- a/tests/test_bigint_samevalue_map.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -# BigInt keys must compare by value, not by allocation identity, in Map/WeakMap -# just like Set (test_bigint_samevalue_set.sh). The literal `1n` in the lookup -# is a distinct allocation from the one stored, so a bit-identity comparison -# would miss it. -cat > "$TMPDIR/main.ts" <<'TS' -const m = new Map([[1n, "a"], [2n, "b"]]); -const big = 9007199254740993n; -m.set(big, "big"); -console.log(JSON.stringify({ - get1: m.get(1n), - get2: m.get(2n), - getBig: m.get(9007199254740993n), - has: m.has(1n), - dedupes: new Map([[3n, "x"], [3n, "y"]]).size, - getMissing: m.get(99n) ?? "none", -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"get1":"a","get2":"b","getBig":"big","has":true,"dedupes":1,"getMissing":"none"} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: BigInt SameValue Map lookup" diff --git a/tests/test_bigint_samevalue_set.sh b/tests/test_bigint_samevalue_set.sh deleted file mode 100755 index f2b7534a5..000000000 --- a/tests/test_bigint_samevalue_set.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -console.log(JSON.stringify({ - objectIs: Object.is(2n, 2n), - setHas: new Set([2n]).has(2n), - setDedupes: new Set([2n, 2n]).size, - arrayIncludes: [2n].includes(2n), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"objectIs":true,"setHas":true,"setDedupes":1,"arrayIncludes":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: BigInt SameValue and Set lookup" diff --git a/tests/test_builtin_prototype_and_fetch_reflection.sh b/tests/test_builtin_prototype_and_fetch_reflection.sh deleted file mode 100755 index d97c8cc00..000000000 --- a/tests/test_builtin_prototype_and_fetch_reflection.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function protoOwn(name: string) { - const proto = (globalThis as any)[name]?.prototype; - if (!proto) return null; - return { - hasOwn: Object.prototype.hasOwnProperty.call(proto, "isPrototypeOf"), - hasOwnStatic: Object.hasOwn(proto, "isPrototypeOf"), - descriptorIsUndefined: Object.getOwnPropertyDescriptor(proto, "isPrototypeOf") === undefined, - namesHas: Object.getOwnPropertyNames(proto).includes("isPrototypeOf"), - }; -} - -const blob = new Blob(["hello"], { type: "text/plain" }); -const file = new File(["hello"], "hello.txt", { type: "text/plain" }); - -console.log(JSON.stringify({ - prototypes: { - Array: protoOwn("Array"), - Date: protoOwn("Date"), - RegExp: protoOwn("RegExp"), - Promise: protoOwn("Promise"), - Map: protoOwn("Map"), - Set: protoOwn("Set"), - Blob: protoOwn("Blob"), - File: protoOwn("File"), - Object: protoOwn("Object"), - }, - blob: { - toString: Object.prototype.toString.call(blob), - tag: (blob as any)[Symbol.toStringTag], - ctorName: (blob as any).constructor?.name, - hasCtor: "constructor" in (blob as any), - }, - file: { - toString: Object.prototype.toString.call(file), - tag: (file as any)[Symbol.toStringTag], - ctorName: (file as any).constructor?.name, - hasCtor: "constructor" in (file as any), - }, -})); -TS - -cat > "$TMPDIR/expected.log" <<'LOG' -{"prototypes":{"Array":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Date":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"RegExp":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Promise":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Map":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Set":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Blob":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"File":{"hasOwn":false,"hasOwnStatic":false,"descriptorIsUndefined":true,"namesHas":false},"Object":{"hasOwn":true,"hasOwnStatic":true,"descriptorIsUndefined":false,"namesHas":true}},"blob":{"toString":"[object Blob]","tag":"Blob","ctorName":"Blob","hasCtor":true},"file":{"toString":"[object File]","tag":"File","ctorName":"File","hasCtor":true}} -LOG - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -PERRY_DIR="$(cd "$(dirname "$PERRY")" && pwd)" -if [[ -f "$PERRY_DIR/libperry_runtime.a" && -f "$PERRY_DIR/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$PERRY_DIR" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: builtin prototype and fetch reflection" diff --git a/tests/test_builtin_subclass_implicit_ctor_fields.sh b/tests/test_builtin_subclass_implicit_ctor_fields.sh deleted file mode 100644 index 7ba295eb1..000000000 --- a/tests/test_builtin_subclass_implicit_ctor_fields.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -source "$(dirname "$0")/_perry_test_lib.sh" - -# A subclass of a builtin (Error/Map/Set/Array) with NO explicit constructor -# must still run its instance field initializers — they run right after the -# implicit super(). Regression: the synthesized default constructor dropped -# them (new E().tag was 0 instead of the initializer value). The explicit-ctor -# case (M2) already worked and guards against a regression the other way. -perry_run main.ts <<'TS' -class E1 extends Error { tag = "e1"; count = 1 + 2; } -class M1 extends Map { tag = "m1"; } -class S1 extends Set { tag = "s1"; } -class A1 extends Array { tag = "a1"; } -class M2 extends Map { tag = "m2"; constructor() { super(); } } - -const e = new E1(); -console.log(JSON.stringify({ - errTag: e.tag, - errCount: (e as any).count, - errIsError: e instanceof Error, - errMessage: (() => { try { throw new E1("boom"); } catch (x: any) { return x.message; } })(), - mapTag: new M1().tag, - setTag: new S1().tag, - arrTag: (new A1() as any).tag, - explicitCtorTag: new M2().tag, -})); -TS - -perry_expect_node -perry_pass "builtin subclass implicit-ctor field initializers" diff --git a/tests/test_class_define_property_name.sh b/tests/test_class_define_property_name.sh deleted file mode 100755 index 57ffbdc09..000000000 --- a/tests/test_class_define_property_name.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function f() {} -Object.defineProperty(f, "name", { value: "FunctionName" }); - -class PlainClass {} -Object.defineProperty(PlainClass, "name", { value: "PlainName" }); - -class Definition extends Error {} -Object.defineProperty(Definition, "name", { value: "$ZodError" }); -const err = new Definition("boom"); - -console.log(JSON.stringify({ - functionName: f.name, - functionDescriptor: Object.getOwnPropertyDescriptor(f, "name")?.value, - className: PlainClass.name, - classDescriptor: Object.getOwnPropertyDescriptor(PlainClass, "name")?.value, - errorCtor: err.constructor.name, - errorCtorDescriptor: Object.getOwnPropertyDescriptor(err.constructor, "name")?.value, -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"functionName":"FunctionName","functionDescriptor":"FunctionName","className":"PlainName","classDescriptor":"PlainName","errorCtor":"$ZodError","errorCtorDescriptor":"$ZodError"} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: class defineProperty name" diff --git a/tests/test_dynamic_arithmetic_bigint_any.sh b/tests/test_dynamic_arithmetic_bigint_any.sh deleted file mode 100755 index 698fbc388..000000000 --- a/tests/test_dynamic_arithmetic_bigint_any.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -class Parser { - _def: any; - constructor(value: bigint) { - this._def = { checks: [{ kind: "multipleOf", value }] }; - } - safeParse(data: any) { - const input: any = { data }; - for (const check of this._def.checks) { - if (check.kind === "multipleOf" && input.data % check.value !== BigInt(0)) { - return { success: false }; - } - } - return { success: true }; - } -} -const parser: any = new Parser(5n); -console.log(JSON.stringify({ - ok: parser.safeParse(15n).success, - bad: parser.safeParse(14n).success, - stringSub: ("6" as any) - ("2" as any), - stringMod: ("15" as any) % ("5" as any), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"ok":true,"bad":false,"stringSub":4,"stringMod":0} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: dynamic arithmetic bigint any" diff --git a/tests/test_dynamic_class_extends_once.sh b/tests/test_dynamic_class_extends_once.sh deleted file mode 100755 index ff395b629..000000000 --- a/tests/test_dynamic_class_extends_once.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -let count = 0; -const order: string[] = []; - -function parent() { - count++; - return class Base {}; -} - -function make() { - return class Child extends parent() { - static before = order.push("before"); - static { - order.push("block"); - } - static after = order.push("after"); - static observed = count; - }; -} - -const Child = make(); -console.log(JSON.stringify({ - count, - observed: (Child as any).observed, - order, - construct: new Child() instanceof Child, -})); -TS - -cat > "$TMPDIR/expected.log" <<'LOG' -{"count":1,"observed":1,"order":["before","block","after"],"construct":true} -LOG - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -PERRY_DIR="$(cd "$(dirname "$PERRY")" && pwd)" -if [[ -f "$PERRY_DIR/libperry_runtime.a" && -f "$PERRY_DIR/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$PERRY_DIR" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: dynamic class extends expression evaluated once" diff --git a/tests/test_dynamic_extends_class_binding.sh b/tests/test_dynamic_extends_class_binding.sh deleted file mode 100755 index 2b6807ec7..000000000 --- a/tests/test_dynamic_extends_class_binding.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function make(name: string, Parent: any) { - class Definition extends Parent {} - Object.defineProperty(Definition, "name", { value: name }); - - function create(value: string) { - const inst = new Definition(value); - inst.tag = name; - return inst; - } - - return create; -} - -const A = make("AName", Error); -const B = make("BName", Error); -const a = new A("a"); -const b = new B("b"); - -console.log(JSON.stringify({ - aCtor: a.constructor.name, - bCtor: b.constructor.name, - aName: a.name, - bName: b.name, - aTag: a.tag, - bTag: b.tag, -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"aCtor":"AName","bCtor":"BName","aName":"Error","bName":"Error","aTag":"AName","bTag":"BName"} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: dynamic extends class binding" diff --git a/tests/test_dynamic_function_probe_fallback_stderr.sh b/tests/test_dynamic_function_probe_fallback_stderr.sh deleted file mode 100755 index 99a1d1337..000000000 --- a/tests/test_dynamic_function_probe_fallback_stderr.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/main.ts" <<'TS' -let enabled = true; -try { - new Function(""); -} catch { - enabled = false; -} -console.log("empty feature", enabled ? "dynamic" : "fallback"); - -enabled = true; -try { - const body = String(Math.random()).slice(100); - const fn: any = new Function(body); - fn(); -} catch { - enabled = false; -} -console.log("feature", enabled ? "dynamic" : "fallback"); - -try { - const Ctor: any = Function; - const body = ["return", "7"].join(" "); - new Ctor(body); -} catch { - console.log("reflected fallback"); -} -TS - -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -COMPILE_OUT="$(PERRY_ALLOW_PERRY_FEATURES=1 "$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$TMPDIR/out" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} - -"$TMPDIR/out" >"$TMPDIR/stdout.log" 2>"$TMPDIR/stderr.log" || { - echo "FAIL: compiled binary errored" - sed 's/^/ /' "$TMPDIR/stdout.log" - sed 's/^/ /' "$TMPDIR/stderr.log" - exit 1 -} - -cat >"$TMPDIR/expected.log" <<'EOF_EXPECTED' -empty feature fallback -feature fallback -reflected fallback -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/stdout.log"; then - echo "FAIL: stdout mismatch" - exit 1 -fi - -if [[ -s "$TMPDIR/stderr.log" ]]; then - echo "FAIL: expected empty stderr" - sed 's/^/ /' "$TMPDIR/stderr.log" - exit 1 -fi - -echo "PASS: dynamic Function feature probe fallback is silent" diff --git a/tests/test_dynamic_import_package_registry.sh b/tests/test_dynamic_import_package_registry.sh deleted file mode 100755 index 373b3ad5e..000000000 --- a/tests/test_dynamic_import_package_registry.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -mkdir -p "$TMPDIR/node_modules/dynpkg" -cat >"$TMPDIR/package.json" <<'JSON' -{ - "type": "module", - "dependencies": { - "dynpkg": "1.0.0" - }, - "perry": { - "allow": { - "compilePackages": ["dynpkg"] - }, - "compilePackages": ["dynpkg"] - } -} -JSON -cat >"$TMPDIR/node_modules/dynpkg/package.json" <<'JSON' -{ - "name": "dynpkg", - "version": "1.0.0", - "type": "module", - "exports": { - "./a.js": "./a.js", - "./b.js": "./b.js", - "./c.js": "./c.js" - } -} -JSON -cat >"$TMPDIR/node_modules/dynpkg/a.js" <<'JS' -export default function () { return { name: "a" }; } -JS -cat >"$TMPDIR/node_modules/dynpkg/b.js" <<'JS' -export default function () { return { name: "b" }; } -JS -cat >"$TMPDIR/node_modules/dynpkg/c.js" <<'JS' -export default function () { return { name: "c" }; } -JS -cat >"$TMPDIR/main.ts" <<'TS' -async function main() { - const direct = await import("dynpkg/a.js"); - const registry = { b: "dynpkg/b.js" } as const; - const viaRegistry = await import(registry.b); - const loaders = { c: () => import("dynpkg/c.js") } as const; - const viaLoader = await loaders.c(); - const data = { name: "not-a-real-package", port: "3000" } as const; - let deferred = "no"; - try { - await import(data.name); - } catch { - deferred = "caught"; - } - console.log(JSON.stringify({ - direct: direct.default().name, - registry: viaRegistry.default().name, - loader: viaLoader.default().name, - deferred, - })); -} -main(); -TS - -BIN="$TMPDIR/main" -COMPILE_OUT="$($PERRY compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$BIN" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} -OUT="$($BIN 2>&1)" || { - echo "FAIL: compiled program errored" - echo "$OUT" - exit 1 -} -EXPECTED='{"direct":"a","registry":"b","loader":"c","deferred":"caught"}' -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: unexpected output" - echo "expected: $EXPECTED" - echo "actual: $OUT" - exit 1 -fi - -echo "PASS: dynamic import package registry" diff --git a/tests/test_dynamic_parent_builtin_no_downgrade.sh b/tests/test_dynamic_parent_builtin_no_downgrade.sh deleted file mode 100755 index beabb4b45..000000000 --- a/tests/test_dynamic_parent_builtin_no_downgrade.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function makeCtor(name: string, Parent?: any) { - const Base = Parent ?? Object; - class Definition extends Base {} - function _(this: any) { - const inst = Parent ? new Definition() : this; - inst.name = name; - return inst; - } - Object.defineProperty(_, "name", { value: name }); - Object.defineProperty(_, "prototype", { value: Definition.prototype }); - Object.defineProperty(Definition.prototype, "constructor", { value: _ }); - return _; -} -const Real: any = makeCtor("Real", Error); -makeCtor("Other"); -const error = new Real(); -const proto = Object.getPrototypeOf(error); -const parentProto = proto && Object.getPrototypeOf(proto); -console.log(JSON.stringify({ - error: error instanceof Error, - real: error instanceof Real, - // Pin the exact chain depth: error -> Real.prototype -> Error.prototype -> - // Object.prototype. A disjunctive `parentProto === Error.prototype || proto - // === Error.prototype` would accept a flattened (downgraded) chain too. - protoIsRealProto: proto === Real.prototype, - parentIsErrorProto: parentProto === Error.prototype, - protoNotErrorDirect: proto !== Error.prototype, - grandIsObjectProto: Object.getPrototypeOf(parentProto) === Object.prototype, -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"error":true,"real":true,"protoIsRealProto":true,"parentIsErrorProto":true,"protoNotErrorDirect":true,"grandIsObjectProto":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: dynamic builtin parent no downgrade" diff --git a/tests/test_error_subclass_arrow_field_init.sh b/tests/test_error_subclass_arrow_field_init.sh deleted file mode 100755 index 619b46c12..000000000 --- a/tests/test_error_subclass_arrow_field_init.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -SRC="$TMPDIR/error_subclass_arrow_field_init.ts" -BIN="$TMPDIR/error_subclass_arrow_field_init" - -cat > "$SRC" <<'TS' -class MyError extends Error { - items: string[] = []; - - add = (item: string) => { - this.items = [...this.items, item]; - }; - - constructor() { - super(); - Object.setPrototypeOf(this, new.target.prototype); - } -} - -const error = new MyError(); -console.log( - typeof error.add, - Object.keys(error).filter((key) => key !== "name").sort().join(","), - error instanceof MyError, - error instanceof Error, -); -error.add("item"); -console.log(JSON.stringify(error.items)); -TS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$SRC" > "$TMPDIR/expected.txt" - -if [ -x "$ROOT/target/debug/perry" ]; then - PERRY="$ROOT/target/debug/perry" - export PERRY_RUNTIME_DIR="$ROOT/target/debug" -else - PERRY="$ROOT/target/release/perry" - export PERRY_RUNTIME_DIR="$ROOT/target/release" -fi - -"$PERRY" compile --no-cache --no-auto-optimize "$SRC" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - cat "$TMPDIR/compile.log" - exit 1 -} - -"$BIN" > "$TMPDIR/actual.txt" 2> "$TMPDIR/run.log" || { - cat "$TMPDIR/run.log" - exit 1 -} - -diff -u "$TMPDIR/expected.txt" "$TMPDIR/actual.txt" diff --git a/tests/test_export_enum_runtime_object.sh b/tests/test_export_enum_runtime_object.sh deleted file mode 100755 index a83610c2a..000000000 --- a/tests/test_export_enum_runtime_object.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/source.ts" <<'TS' -export enum Kind { - Object = "Object", - Readonly = "Readonly", -} - -export enum Color { - Red, - Green, -} -TS - -cat >"$TMPDIR/barrel.ts" <<'TS' -import * as z from "./source.js"; -export * from "./source.js"; -export { z }; -TS - -cat >"$TMPDIR/main.ts" <<'TS' -import { z, Kind, Color } from "./barrel.js"; - -const actual = { - directKindType: typeof Kind, - directKind: Kind.Object, - namespaceKindType: typeof z.Kind, - namespaceKind: z.Kind.Object, - directColorType: typeof Color, - directColor: Color.Green, - reverseColor: Color[1], - namespaceColorType: typeof z.Color, - namespaceColor: z.Color.Red, - keys: Object.keys(z.Color).sort(), -}; - -console.log(JSON.stringify(actual)); -TS - -"$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" \ - -o "$TMPDIR/main" >"$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$TMPDIR/main" >"$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -EXPECTED='{"directKindType":"object","directKind":"Object","namespaceKindType":"object","namespaceKind":"Object","directColorType":"object","directColor":1,"reverseColor":"Green","namespaceColorType":"object","namespaceColor":0,"keys":["0","1","Green","Red"]}' -ACTUAL="$(cat "$TMPDIR/run.log")" - -if [[ "$ACTUAL" != "$EXPECTED" ]]; then - echo "FAIL: output mismatch" - echo "expected: $EXPECTED" - echo "actual: $ACTUAL" - exit 1 -fi - -echo "PASS: export enum runtime object" diff --git a/tests/test_file_instanceof.sh b/tests/test_file_instanceof.sh deleted file mode 100755 index 01fcc8101..000000000 --- a/tests/test_file_instanceof.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const file = new File(["a"], "a.txt", { type: "text/plain" }); -const blob = new Blob(["a"], { type: "text/plain" }); -console.log(JSON.stringify({ - fileType: typeof File, - fileInstance: file instanceof File, - fileIsBlob: file instanceof Blob, - blobIsFile: blob instanceof File, - name: file.name, - size: file.size, - type: file.type, -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"fileType":"function","fileInstance":true,"fileIsBlob":true,"blobIsFile":false,"name":"a.txt","size":1,"type":"text/plain"} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: File instanceof" diff --git a/tests/test_forward_captured_let_box.sh b/tests/test_forward_captured_let_box.sh deleted file mode 100755 index 07b45267e..000000000 --- a/tests/test_forward_captured_let_box.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -SRC="$TMPDIR/forward_captured_let_box.ts" -BIN="$TMPDIR/forward_captured_let_box" - -cat > "$SRC" <<'TS' -function makeGetter() { - const getter = () => value; - const value = { ok: true, nested: [1, "x"] }; - return getter; -} - -const getter = makeGetter(); -console.log(JSON.stringify(getter())); -TS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$SRC" > "$TMPDIR/expected.txt" - -if [ -x "$ROOT/target/debug/perry" ]; then - PERRY="$ROOT/target/debug/perry" - export PERRY_RUNTIME_DIR="$ROOT/target/debug" -else - PERRY="$ROOT/target/release/perry" - export PERRY_RUNTIME_DIR="$ROOT/target/release" -fi - -"$PERRY" compile --no-cache --no-auto-optimize "$SRC" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - cat "$TMPDIR/compile.log" - exit 1 -} - -"$BIN" > "$TMPDIR/actual.txt" 2> "$TMPDIR/run.log" || { - cat "$TMPDIR/run.log" - exit 1 -} - -diff -u "$TMPDIR/expected.txt" "$TMPDIR/actual.txt" diff --git a/tests/test_function_prototype_instanceof.sh b/tests/test_function_prototype_instanceof.sh deleted file mode 100755 index 2783fff04..000000000 --- a/tests/test_function_prototype_instanceof.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function Ctor() {} -const proto: any = { marker: true }; -Object.defineProperty(Ctor, "prototype", { value: proto }); -const value = Object.create(proto); -console.log(JSON.stringify({ - custom: value instanceof Ctor, - proto: Object.getPrototypeOf(value) === Ctor.prototype, -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"custom":true,"proto":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: function prototype instanceof" diff --git a/tests/test_imported_default_named_reexport.sh b/tests/test_imported_default_named_reexport.sh deleted file mode 100755 index ce92e347c..000000000 --- a/tests/test_imported_default_named_reexport.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/locales.js" <<'JS' -export const Code = { invalid_type: "invalid_type" }; - -const errorMap = (issue, ctx) => { - let message; - switch (issue.code) { - case Code.invalid_type: - message = `Expected ${issue.expected}, received ${issue.received}`; - break; - default: - message = ctx.defaultError; - } - return { message }; -}; - -export default errorMap; -JS - -cat >"$TMPDIR/errors.js" <<'JS' -import defaultErrorMap from "./locales.js"; -export { defaultErrorMap }; -JS - -cat >"$TMPDIR/index.js" <<'JS' -export * from "./errors.js"; -JS - -cat >"$TMPDIR/main.js" <<'JS' -import defaultMap from "./locales.js"; -import { defaultErrorMap } from "./index.js"; - -const issue = { code: "invalid_type", expected: "string", received: "number" }; -const ctx = { defaultError: "fallback" }; -console.log(JSON.stringify({ direct: defaultMap(issue, ctx), reexport: defaultErrorMap(issue, ctx) })); -JS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$TMPDIR/main.js" >"$TMPDIR/node.txt" -"$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.js" \ - -o "$TMPDIR/main" >"$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} -"$TMPDIR/main" >"$TMPDIR/perry.txt" 2>"$TMPDIR/run.log" || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/node.txt" "$TMPDIR/perry.txt" >"$TMPDIR/diff.log"; then - echo "FAIL: output mismatch" - sed 's/^/ /' "$TMPDIR/diff.log" - exit 1 -fi - -echo "PASS: imported default named re-export" diff --git a/tests/test_imported_error_new_target.sh b/tests/test_imported_error_new_target.sh deleted file mode 100755 index 8f0cac038..000000000 --- a/tests/test_imported_error_new_target.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/err.ts" <<'TS' -export class MyError extends Error { - issues: string[]; - constructor(issues: string[]) { - super("bad"); - const actualProto = new.target.prototype; - Object.setPrototypeOf(this, actualProto); - this.issues = issues; - } -} -TS - -cat > "$TMPDIR/main.ts" <<'TS' -import { MyError } from "./err"; - -const result = { - success: false, - get error() { - return new MyError(["issue"]); - }, -}; - -console.log("success", result.success); -const error = result.error; -console.log("issues", error.issues.length); -console.log("instanceof", error instanceof MyError); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -success false -issues 1 -instanceof true -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: imported Error subclass new.target" diff --git a/tests/test_imported_static_field_alias_method.sh b/tests/test_imported_static_field_alias_method.sh deleted file mode 100755 index 7cf85ebe3..000000000 --- a/tests/test_imported_static_field_alias_method.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/lib.ts" <<'TS' -class Holder { - static create = (value: string) => ({ value }); -} -const mapType = Holder.create; -export { Holder, mapType as map }; -export const z = { map: mapType }; -TS - -cat > "$TMPDIR/main.ts" <<'TS' -import { Holder, z } from "./lib"; - -function run(label: string, fn: () => unknown) { - try { - console.log(label, JSON.stringify(fn())); - } catch (e: any) { - console.log(`${label}.ERR`, e?.message); - } -} - -run("z.map", () => z.map("ok")); -run("Holder.create", () => Holder.create("ok")); -run("sameRef", () => z.map === Holder.create); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -z.map {"value":"ok"} -Holder.create {"value":"ok"} -sameRef true -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: imported static field alias method" diff --git a/tests/test_json_stringify_replacer_key_order.sh b/tests/test_json_stringify_replacer_key_order.sh deleted file mode 100755 index 38d62a519..000000000 --- a/tests/test_json_stringify_replacer_key_order.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/main.ts" <<'TS' -const formatted: any = { _errors: [] }; -formatted.b = { _errors: [] }; -formatted.b[0] = { _errors: ["x"] }; -console.log(JSON.stringify({ formatted }, (_key, value) => value)); -TS - -BIN="$TMPDIR/main" -COMPILE_OUT="$($PERRY compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$BIN" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} -OUT="$($BIN 2>&1)" || { - echo "FAIL: compiled program errored" - echo "$OUT" - exit 1 -} -EXPECTED='{"formatted":{"_errors":[],"b":{"0":{"_errors":["x"]},"_errors":[]}}}' -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: unexpected output" - echo "expected: $EXPECTED" - echo "actual: $OUT" - exit 1 -fi - -echo "PASS: JSON.stringify replacer key order" diff --git a/tests/test_map_set_subclass_constructor_iterable.sh b/tests/test_map_set_subclass_constructor_iterable.sh deleted file mode 100755 index fd87a7b82..000000000 --- a/tests/test_map_set_subclass_constructor_iterable.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -class FixtureMap extends Map {} -class FixtureSet extends Set<{ id: number }> {} -class ChildMap extends FixtureMap {} -class ChildSet extends FixtureSet {} - -const mapInput = new FixtureMap([["a", { count: 1 }]]); -const copiedMap = new Map(mapInput); -const childMap = new ChildMap([["b", { count: 2 }]]); -const setValue = { id: 1 }; -const setInput = new FixtureSet([setValue]); -const copiedSet = new Set(setInput); -const childSetValue = { id: 2 }; -const childSet = new ChildSet([childSetValue]); - -console.log(JSON.stringify({ - mapInputIsMap: mapInput instanceof Map, - mapInputIsSubclass: mapInput instanceof FixtureMap, - mapInputSize: mapInput.size, - mapInputGet: mapInput.get("a"), - mapInputEntries: Array.from(mapInput.entries()), - copiedMapSize: copiedMap.size, - copiedMapGet: copiedMap.get("a"), - copiedMapEntries: Array.from(copiedMap.entries()), - childMapIsSubclass: childMap instanceof ChildMap, - childMapEntries: Array.from(childMap.entries()), - setInputIsSet: setInput instanceof Set, - setInputIsSubclass: setInput instanceof FixtureSet, - setInputSize: setInput.size, - setInputValues: Array.from(setInput.values()), - copiedSetSize: copiedSet.size, - copiedSetValues: Array.from(copiedSet.values()), - copiedSetHasOriginal: copiedSet.has(setValue), - childSetIsSubclass: childSet instanceof ChildSet, - childSetValues: Array.from(childSet.values()), - childSetHasOriginal: childSet.has(childSetValue), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"mapInputIsMap":true,"mapInputIsSubclass":true,"mapInputSize":1,"mapInputGet":{"count":1},"mapInputEntries":[["a",{"count":1}]],"copiedMapSize":1,"copiedMapGet":{"count":1},"copiedMapEntries":[["a",{"count":1}]],"childMapIsSubclass":true,"childMapEntries":[["b",{"count":2}]],"setInputIsSet":true,"setInputIsSubclass":true,"setInputSize":1,"setInputValues":[{"id":1}],"copiedSetSize":1,"copiedSetValues":[{"id":1}],"copiedSetHasOriginal":true,"childSetIsSubclass":true,"childSetValues":[{"id":2}],"childSetHasOriginal":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: Map/Set subclass constructor iterable" diff --git a/tests/test_missing_error_property_undefined.sh b/tests/test_missing_error_property_undefined.sh deleted file mode 100755 index c1298dc34..000000000 --- a/tests/test_missing_error_property_undefined.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/main.js" <<'JS' -class FixtureError extends Error { - constructor() { - super("boom"); - this.name = "FixtureError"; - } -} - -const error = new FixtureError(); -const aggregate = new AggregateError([1, 2], "many"); -console.log(JSON.stringify({ - own: Object.prototype.hasOwnProperty.call(error, "errors"), - inValue: "errors" in error, - type: typeof error.errors, - undef: error.errors === undefined, - aggregateLength: aggregate.errors.length, - aggregateFirst: aggregate.errors[0], -})); -JS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$TMPDIR/main.js" >"$TMPDIR/node.txt" -"$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.js" \ - -o "$TMPDIR/main" >"$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} -"$TMPDIR/main" >"$TMPDIR/perry.txt" 2>"$TMPDIR/run.log" || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/node.txt" "$TMPDIR/perry.txt" >"$TMPDIR/diff.log"; then - echo "FAIL: output mismatch" - sed 's/^/ /' "$TMPDIR/diff.log" - exit 1 -fi - -echo "PASS: missing Error.errors property is undefined" diff --git a/tests/test_namespace_default_alias_call.sh b/tests/test_namespace_default_alias_call.sh deleted file mode 100755 index 939b0d669..000000000 --- a/tests/test_namespace_default_alias_call.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -mkdir -p "$TMPDIR/locales" -cat >"$TMPDIR/locales/es.ts" <<'TS' -function build() { - return () => "locale-es"; -} -export default function () { - return { localeError: build() }; -} -TS -cat >"$TMPDIR/locales/en.ts" <<'TS' -export default function () { - return { localeError: () => "locale-en" }; -} -TS -cat >"$TMPDIR/locales/index.ts" <<'TS' -export { default as en } from "./en.js"; -export { default as es } from "./es.js"; -TS -cat >"$TMPDIR/defaultVar.ts" <<'TS' -const defaultValue = () => "default-var"; -export default defaultValue; -TS -cat >"$TMPDIR/main.ts" <<'TS' -import defaultValue from "./defaultVar.js"; -import * as locales from "./locales/index.js"; -console.log(JSON.stringify({ defaultValue: defaultValue(), en: locales.en().localeError({}), es: locales.es().localeError({}) })); -TS - -BIN="$TMPDIR/main" -COMPILE_OUT="$($PERRY compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$BIN" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} -OUT="$($BIN 2>&1)" || { - echo "FAIL: compiled program errored" - echo "$OUT" - exit 1 -} -EXPECTED='{"defaultValue":"default-var","en":"locale-en","es":"locale-es"}' -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: unexpected output" - echo "expected: $EXPECTED" - echo "actual: $OUT" - exit 1 -fi - -echo "PASS: namespace default alias call" diff --git a/tests/test_namespace_homonymous_class_export.sh b/tests/test_namespace_homonymous_class_export.sh deleted file mode 100755 index ab45f0c89..000000000 --- a/tests/test_namespace_homonymous_class_export.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/ranges.ts" <<'TS' -export const NUMBER_FORMAT_RANGES = { safeint: [-1, 1] }; -export function normalizeParams(value?: unknown) { - return value ?? {}; -} -TS -cat >"$TMPDIR/legacy.ts" <<'TS' -export class util { - static objectKeys(value: object) { - return Object.keys(value); - } -} -TS -cat >"$TMPDIR/checks.ts" <<'TS' -import * as util from "./ranges.js"; -import { util as legacyUtil } from "./legacy.js"; - -export function readRange() { - return { - keys: Object.keys(util).sort(), - range: util.NUMBER_FORMAT_RANGES.safeint, - normalized: util.normalizeParams(), - legacyType: typeof legacyUtil, - legacyKeys: legacyUtil.objectKeys({ a: 1, b: 2 }), - }; -} -TS -cat >"$TMPDIR/main.ts" <<'TS' -import { readRange } from "./checks.js"; -console.log(JSON.stringify(readRange())); -TS - -EXPECTED='{"keys":["NUMBER_FORMAT_RANGES","normalizeParams"],"range":[-1,1],"normalized":{},"legacyType":"function","legacyKeys":["a","b"]}' -COMPILE_OUT="$(PERRY_ALLOW_PERRY_FEATURES=1 PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" \ - "$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$TMPDIR/out" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} - -OUT="$($TMPDIR/out 2>&1)" || { - echo "FAIL: compiled binary errored" - echo "$OUT" - exit 1 -} - -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: expected $EXPECTED, got:" - echo "$OUT" - exit 1 -fi - -echo "PASS: namespace homonymous class export" diff --git a/tests/test_namespace_homonymous_var_function_exports.sh b/tests/test_namespace_homonymous_var_function_exports.sh deleted file mode 100755 index def38ef68..000000000 --- a/tests/test_namespace_homonymous_var_function_exports.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/function-source.ts" <<'TS' -export function string() { - return "function-export"; -} -TS -cat >"$TMPDIR/var-source.ts" <<'TS' -const stringType = () => "var-export"; -export { stringType as string }; -TS -cat >"$TMPDIR/main.ts" <<'TS' -import * as current from "./function-source.js"; -import * as legacy from "./var-source.js"; - -const out = { - currentType: typeof current.string, - legacyType: typeof legacy.string, - currentCall: current.string(), - legacyCall: legacy.string(), -}; -console.log(JSON.stringify(out)); -TS - -EXPECTED='{"currentType":"function","legacyType":"function","currentCall":"function-export","legacyCall":"var-export"}' -COMPILE_OUT="$(PERRY_ALLOW_PERRY_FEATURES=1 PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" \ - "$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$TMPDIR/out" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} - -OUT="$($TMPDIR/out 2>&1)" || { - echo "FAIL: compiled binary errored" - echo "$OUT" - exit 1 -} - -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: expected $EXPECTED, got:" - echo "$OUT" - exit 1 -fi - -echo "PASS: namespace homonymous var/function exports" diff --git a/tests/test_namespace_reexport_binding.sh b/tests/test_namespace_reexport_binding.sh index 5eaf2f747..f9b6b0f43 100755 --- a/tests/test_namespace_reexport_binding.sh +++ b/tests/test_namespace_reexport_binding.sh @@ -28,12 +28,9 @@ trap 'rm -rf "$TMPDIR"' EXIT cat >"$TMPDIR/sub.ts" <<'TS' export function object() { return "obj"; } TS -cat >"$TMPDIR/coerce.ts" <<'TS' -export function number() { return 42; } -TS cat >"$TMPDIR/external.ts" <<'TS' export * from "./sub.js"; -export * as coerce from "./coerce.js"; +export const coerce = { number: () => 42 }; TS cat >"$TMPDIR/barrel.ts" <<'TS' import * as z from "./external.js"; @@ -41,11 +38,6 @@ export * from "./external.js"; export { z }; export default z; TS -cat >"$TMPDIR/index.ts" <<'TS' -import z4 from "./barrel.js"; -export * from "./barrel.js"; -export default z4; -TS cat >"$TMPDIR/main.ts" <<'TS' import { z } from "./barrel.js"; if (typeof z !== "object") throw new Error(`typeof z: ${typeof z}`); @@ -53,13 +45,6 @@ if (typeof (z as any).object !== "function") throw new Error("z.object missing") if (typeof (z as any).coerce !== "object") throw new Error("z.coerce missing"); if ((z as any).coerce.number() !== 42) throw new Error("z.coerce.number() wrong"); if ((z as any).object() !== "obj") throw new Error("z.object() wrong"); -import { z as zViaExportAll } from "./index.js"; -if (typeof zViaExportAll !== "object") throw new Error(`typeof zViaExportAll: ${typeof zViaExportAll}`); -if ((zViaExportAll as any).coerce.number() !== 42) throw new Error("zViaExportAll.coerce.number() wrong"); -if ((zViaExportAll as any).object() !== "obj") throw new Error("zViaExportAll.object() wrong"); -import * as pkg from "./barrel.js"; -if (!Object.keys(pkg).includes("coerce")) throw new Error("pkg.coerce not enumerable"); -if ((pkg as any).coerce.number() !== 42) throw new Error("pkg.coerce.number() wrong"); console.log("OK"); TS diff --git a/tests/test_namespace_reexport_optional_arg.sh b/tests/test_namespace_reexport_optional_arg.sh deleted file mode 100755 index 638d27088..000000000 --- a/tests/test_namespace_reexport_optional_arg.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/core.ts" <<'TS' -export function _make(params?: any) { - return { check: "string_format", format: "lowercase", ...(!params ? {} : params) }; -} -TS -cat >"$TMPDIR/external.ts" <<'TS' -export { _make as make } from "./core.js"; -export function string() { - return { format: null, parse() {} }; -} -TS -cat >"$TMPDIR/main.ts" <<'TS' -import * as z from "./external.js"; -const schema = z.string(); -const check = z.make(); -console.log(JSON.stringify({ schemaFormat: schema.format, checkKeys: Object.keys(check), checkFormat: check.format, hasParse: "parse" in check })); -TS - -BIN="$TMPDIR/main" -COMPILE_OUT="$($PERRY compile --no-cache --no-auto-optimize "$TMPDIR/main.ts" -o "$BIN" 2>&1)" || { - echo "FAIL: perry compile errored" - echo "$COMPILE_OUT" - exit 1 -} -OUT="$($BIN 2>&1)" || { - echo "FAIL: compiled program errored" - echo "$OUT" - exit 1 -} -EXPECTED='{"schemaFormat":null,"checkKeys":["check","format"],"checkFormat":"lowercase","hasParse":false}' -if [[ "$OUT" != "$EXPECTED" ]]; then - echo "FAIL: unexpected output" - echo "expected: $EXPECTED" - echo "actual: $OUT" - exit 1 -fi - -echo "PASS: namespace re-export optional arg" diff --git a/tests/test_namespace_subobject_method_call.sh b/tests/test_namespace_subobject_method_call.sh deleted file mode 100755 index e792ddb6a..000000000 --- a/tests/test_namespace_subobject_method_call.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/external.js" <<'JS' -export const util = { - find(arr, checker) { - for (const item of arr) { - if (checker(item)) return item; - } - return undefined; - }, -}; -JS - -cat >"$TMPDIR/index.js" <<'JS' -import * as z from "./external.js"; -export * from "./external.js"; -export { z }; -export default z; -JS - -cat >"$TMPDIR/main.js" <<'JS' -import { z } from "./index.js"; - -console.log(JSON.stringify({ - type: typeof z.util.find, - found: z.util.find([1, 2, 3], (value) => value > 1), -})); -JS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$TMPDIR/main.js" >"$TMPDIR/node.txt" -"$PERRY" compile --no-cache --no-auto-optimize "$TMPDIR/main.js" \ - -o "$TMPDIR/main" >"$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} -"$TMPDIR/main" >"$TMPDIR/perry.txt" 2>"$TMPDIR/run.log" || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/node.txt" "$TMPDIR/perry.txt" >"$TMPDIR/diff.log"; then - echo "FAIL: output mismatch" - sed 's/^/ /' "$TMPDIR/diff.log" - exit 1 -fi - -echo "PASS: namespace subobject method call" diff --git a/tests/test_new_parameter_shadows_class.sh b/tests/test_new_parameter_shadows_class.sh deleted file mode 100755 index 95718410f..000000000 --- a/tests/test_new_parameter_shadows_class.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -export abstract class Class { - constructor(..._args: any[]) {} -} - -function makeCtor(name: string, initFn: (inst: any, def: any) => void) { - function _(this: any, def: any) { - initFn(this, def); - return this; - } - Object.defineProperty(_, "name", { value: name }); - return _ as any; -} - -const Real = makeCtor("Real", (inst, def) => { - inst.tag = def.tag; -}); - -function factory(Class: any) { - return new Class({ tag: "ok" }); -} - -const value = factory(Real); -console.log(JSON.stringify({ - proto: Object.getPrototypeOf(value)?.constructor?.name, - tag: value.tag, - inst: value instanceof (Real as any), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"proto":"Real","tag":"ok","inst":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: new parameter shadows same-named class" diff --git a/tests/test_number_to_string_large_exponent.sh b/tests/test_number_to_string_large_exponent.sh deleted file mode 100755 index 5ae7c51e7..000000000 --- a/tests/test_number_to_string_large_exponent.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const n = 3.4028234663852886e38; -const box: any = { maximum: n }; -const wrapped = new Number(n); - -console.log(JSON.stringify([ - String(n), - n.toString(), - box.maximum.toString(), - `${box.maximum}`, - String(box.maximum), - wrapped.toString(), - String(1e21), - String(1e-7), -])); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -["3.4028234663852886e+38","3.4028234663852886e+38","3.4028234663852886e+38","3.4028234663852886e+38","3.4028234663852886e+38","3.4028234663852886e+38","1e+21","1e-7"] -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: number toString large exponent" diff --git a/tests/test_object_create_for_in_prototype.sh b/tests/test_object_create_for_in_prototype.sh deleted file mode 100755 index 33f2f621c..000000000 --- a/tests/test_object_create_for_in_prototype.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -source "$(dirname "$0")/_perry_test_lib.sh" - -perry_run main.ts <<'TS' -const proto: any = { inherited: 2 }; -const input: any = Object.create(proto); -input.own = 1; - -const forInKeys: string[] = []; -for (const key in input) { - forInKeys.push(key); -} - -console.log(JSON.stringify({ - protoIdentity: Object.getPrototypeOf(input) === proto, - protoKeys: Object.keys(Object.getPrototypeOf(input)), - forInKeys, - keys: Object.keys(input), - spread: { ...input }, - assign: Object.assign({}, input), - ctorIsObject: input.constructor === Object, -})); -TS - -perry_expect '{"protoIdentity":true,"protoKeys":["inherited"],"forInKeys":["own","inherited"],"keys":["own"],"spread":{"own":1},"assign":{"own":1},"ctorIsObject":true}' -perry_pass "Object.create for-in prototype chain" diff --git a/tests/test_object_create_in_operator_prototype.sh b/tests/test_object_create_in_operator_prototype.sh deleted file mode 100755 index 4a9708129..000000000 --- a/tests/test_object_create_in_operator_prototype.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const input: any = Object.create({ inherited: "from-proto", undef: undefined }); -input.own = 1; - -console.log(JSON.stringify({ - inherited: input.inherited, - bracket: input["inherited"], - inheritedIn: "inherited" in input, - undefinedIn: "undef" in input, - missingIn: "missing" in input, - ownInherited: Object.prototype.hasOwnProperty.call(input, "inherited"), - own: input.own, - keys: Object.keys(input), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"inherited":"from-proto","bracket":"from-proto","inheritedIn":true,"undefinedIn":true,"missingIn":false,"ownInherited":false,"own":1,"keys":["own"]} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: Object.create in operator prototype chain" diff --git a/tests/test_object_create_inherited_constructor.sh b/tests/test_object_create_inherited_constructor.sh deleted file mode 100755 index ec7516b31..000000000 --- a/tests/test_object_create_inherited_constructor.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const input: any = Object.create({ inherited: 2 }); -input.own = 1; -const ctor = input.constructor; -const prot = ctor.prototype; -console.log(JSON.stringify({ - ctorType: typeof ctor, - ctorName: ctor.name, - ctorIsObject: ctor === Object, - protIsObjectPrototype: prot === Object.prototype, - protHasOwnIsPrototypeOf: Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf"), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"ctorType":"function","ctorName":"Object","ctorIsObject":true,"protIsObjectPrototype":true,"protHasOwnIsPrototypeOf":true} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: Object.create inherited constructor" diff --git a/tests/test_object_prototype_method_call_fallback.sh b/tests/test_object_prototype_method_call_fallback.sh deleted file mode 100755 index 7c5631d54..000000000 --- a/tests/test_object_prototype_method_call_fallback.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.js" <<'JS' -function run(label, fn) { - try { - console.log(label, JSON.stringify(fn())); - } catch (e) { - console.log(label, e.name + ":" + e.message); - } -} - -run("objectToString", () => ({}).toString()); -run("objectToLocaleString", () => ({}).toLocaleString()); -run("objectValueOfObject", () => ({}).valueOf() instanceof Object); -run("arrayToString", () => [1, 2].toString()); -run("mapToString", () => new Map().toString()); -run("mapToLocaleString", () => new Map().toLocaleString()); -run("mapValueOfSelf", () => { - const m = new Map(); - return m.valueOf() === m; -}); -run("setToString", () => new Set().toString()); -run("setToLocaleString", () => new Set().toLocaleString()); -run("setValueOfSelf", () => { - const s = new Set(); - return s.valueOf() === s; -}); -run("ownToString", () => ({ toString() { return "own"; } }).toString()); -run("classToString", () => new (class { toString() { return "class"; } })().toString()); -JS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.js" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -objectToString "[object Object]" -objectToLocaleString "[object Object]" -objectValueOfObject true -arrayToString "1,2" -mapToString "[object Map]" -mapToLocaleString "[object Map]" -mapValueOfSelf true -setToString "[object Set]" -setToLocaleString "[object Set]" -setValueOfSelf true -ownToString "own" -classToString "class" -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: Object.prototype method call fallback" diff --git a/tests/test_own_to_string_method.sh b/tests/test_own_to_string_method.sh deleted file mode 100755 index f20e0d285..000000000 --- a/tests/test_own_to_string_method.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const errorUtil: any = {}; -errorUtil.toString = (message: any) => typeof message === "string" ? message : message?.message; -const value = errorUtil.toString(undefined); -console.log(value, typeof value, value === undefined); -console.log(JSON.stringify({ message: value })); - -const builder: any = { - toUpperCase() { - return { parse: () => "schema-method" }; - }, - trim() { - return { parse: () => "trim-method" }; - }, -}; -console.log(builder.toUpperCase().parse()); -console.log(builder.trim().parse()); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -undefined undefined true -{} -schema-method -trim-method -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: own toString method call" diff --git a/tests/test_prototype_cycle_has_property.sh b/tests/test_prototype_cycle_has_property.sh deleted file mode 100644 index b3a1d5a85..000000000 --- a/tests/test_prototype_cycle_has_property.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -# The `in` operator walks the [[Prototype]] chain recursively. Two hazards: -# 1. A pathologically deep (non-cyclic) chain must not overflow the stack — -# a missing key returns false (bounded like V8's own chain walk). -# 2. `Object.setPrototypeOf` must reject a cycle with a TypeError, exactly -# like Node, so a cycle can never be built in the first place. -cat > "$TMPDIR/main.ts" <<'TS' -let deep: any = { bottomKey: 1 }; -for (let i = 0; i < 3000; i++) { - deep = Object.create(deep); -} -let cycleThrew = false; -try { - const a: any = {}; - const b: any = {}; - Object.setPrototypeOf(a, b); - Object.setPrototypeOf(b, a); // would form a 2-cycle -> must throw -} catch (e) { - cycleThrew = e instanceof TypeError; -} -console.log(JSON.stringify({ - missingOnDeep: "nope" in deep, // false, no stack overflow - shallowPresent: "x" in Object.create({ x: 1 }), // true - shallowMissing: "y" in Object.create({ x: 1 }), // false - cycleThrew, // true (matches Node) -})); -TS - -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi -node "$TMPDIR/main.ts" > "$TMPDIR/expected.log" - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed (stack overflow on deep prototype chain?)" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch vs node" - exit 1 -fi - -echo "PASS: deep prototype chain has-property terminates and cycles throw" diff --git a/tests/test_reflect_set_frozen_array.sh b/tests/test_reflect_set_frozen_array.sh deleted file mode 100755 index 482144659..000000000 --- a/tests/test_reflect_set_frozen_array.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" -if [[ ! -x "$PERRY" ]]; then PERRY="$REPO_ROOT/target/debug/perry"; fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat >"$TMPDIR/main.ts" <<'TS' -const array = Object.freeze(["a"]); -const tupleLike = Object.freeze(["x"] as [string]); -const object = Object.freeze({ id: 1 }); - -function check(value: boolean, label: string) { - if (!value) { - throw new Error(label); - } -} - -check(Reflect.set(array, "0", "b") === false, "frozen array index Reflect.set result"); -check(array[0] === "a", "frozen array index unchanged"); -check(Reflect.set(tupleLike, "0", "y") === false, "frozen tuple-like index Reflect.set result"); -check(tupleLike[0] === "x", "frozen tuple-like index unchanged"); -check(Reflect.set(object, "id", 2) === false, "frozen object property Reflect.set result"); -check(object.id === 1, "frozen object property unchanged"); -console.log("OK"); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -PERRY_DIR="$(cd "$(dirname "$PERRY")" && pwd)" -if [[ -f "$PERRY_DIR/libperry_runtime.a" && -f "$PERRY_DIR/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$PERRY_DIR" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} -if ! grep -q "^OK$" "$TMPDIR/run.log"; then - echo "FAIL: expected OK, got:" - cat "$TMPDIR/run.log" - exit 1 -fi -echo "PASS: Reflect.set reports false for frozen array indices" diff --git a/tests/test_string_computed_length.sh b/tests/test_string_computed_length.sh deleted file mode 100755 index bc62e653f..000000000 --- a/tests/test_string_computed_length.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -function read(value: any, key: any) { - return value[key]; -} - -const short = "abc"; -const long = "abcdefghijklmnopqrstuvwxyz"; -const lengthKey = "length"; -const indexKey = "1"; -const nonCanonicalIndexKey = "01"; - -console.log(JSON.stringify({ - shortLength: read(short, lengthKey), - longLength: read(long, lengthKey), - directLength: short["length"], - boxedLength: read(new String(short), lengthKey), - char: read(short, indexKey), - nonCanonical: String(read(short, nonCanonicalIndexKey)), -})); -TS - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -if [[ -f "$REPO_ROOT/target/debug/libperry_runtime.a" && -f "$REPO_ROOT/target/debug/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/debug" - COMPILE_ARGS+=(--no-auto-optimize) -elif [[ -f "$REPO_ROOT/target/release/libperry_runtime.a" && -f "$REPO_ROOT/target/release/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$REPO_ROOT/target/release" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -cat > "$TMPDIR/expected.log" <<'EOF_EXPECTED' -{"shortLength":3,"longLength":26,"directLength":3,"boxedLength":3,"char":"b","nonCanonical":"undefined"} -EOF_EXPECTED - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: string computed length" diff --git a/tests/test_switch_bigint_case.sh b/tests/test_switch_bigint_case.sh deleted file mode 100644 index 0ea41787c..000000000 --- a/tests/test_switch_bigint_case.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -source "$(dirname "$0")/_perry_test_lib.sh" - -# A `switch` on a BigInt must match `case 1n` by value, not allocation -# identity — the same rule the Set/Map key and `===` paths already follow. -perry_run main.ts <<'TS' -function classify(x: bigint): string { - switch (x) { - case 0n: return "zero"; - case 1n: return "one"; - case 9007199254740993n: return "big"; - default: return "other"; - } -} -console.log(JSON.stringify({ - computed: classify(1n + 0n), - big: classify(9007199254740992n + 1n), - miss: classify(5n), - mixedType: (1n as any) === 1 ? "eqNumber" : "neqNumber", -})); -TS - -perry_expect_node -perry_pass "switch matches BigInt case by value" diff --git a/tests/test_unicode_regex_literal.sh b/tests/test_unicode_regex_literal.sh deleted file mode 100755 index c881a0a5d..000000000 --- a/tests/test_unicode_regex_literal.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const composed = "CAFE\u0301!".toLowerCase().normalize("NFC"); -const literal = /^café!$/; -const constructor = new RegExp("^café!$"); - -console.log(JSON.stringify({ - direct: literal.test("café!"), - composed, - composedCodepoints: [...composed].map((char) => char.codePointAt(0)), - literalSource: literal.source, - literalMatch: literal.test(composed), - constructorSource: constructor.source, - constructorMatch: constructor.test(composed), -})); -TS - -node "$TMPDIR/main.ts" > "$TMPDIR/expected.log" - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -PERRY_DIR="$(cd "$(dirname "$PERRY")" && pwd)" -if [[ -f "$PERRY_DIR/libperry_runtime.a" && -f "$PERRY_DIR/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$PERRY_DIR" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: Unicode regex literal" diff --git a/tests/test_url_href_normalization.sh b/tests/test_url_href_normalization.sh deleted file mode 100755 index 21298604d..000000000 --- a/tests/test_url_href_normalization.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PERRY="${PERRY_BIN:-${PERRY:-$REPO_ROOT/target/release/perry}}" - -if [[ ! -x "$PERRY" ]]; then - PERRY="$REPO_ROOT/target/debug/perry" -fi -if [[ ! -x "$PERRY" ]]; then - echo "SKIP: perry binary not found (build with cargo build -p perry)" - exit 0 -fi -if ! command -v node >/dev/null 2>&1; then - echo "SKIP: node binary not found" - exit 0 -fi - -TMPDIR="$(mktemp -d)" -trap 'rm -rf "$TMPDIR"' EXIT - -cat > "$TMPDIR/main.ts" <<'TS' -const url = new URL("HTTP://ExAmPle.com:80/./a/../b?X=1#f oo"); -console.log(JSON.stringify({ - href: url.href, - protocol: url.protocol, - hostname: url.hostname, - port: url.port, - pathname: url.pathname, - hash: url.hash, -})); -TS - -node "$TMPDIR/main.ts" > "$TMPDIR/expected.log" - -BIN="$TMPDIR/out" -COMPILE_ARGS=(compile --no-cache) -PERRY_DIR="$(cd "$(dirname "$PERRY")" && pwd)" -if [[ -f "$PERRY_DIR/libperry_runtime.a" && -f "$PERRY_DIR/libperry_stdlib.a" ]]; then - export PERRY_RUNTIME_DIR="$PERRY_DIR" - COMPILE_ARGS+=(--no-auto-optimize) -fi - -"$PERRY" "${COMPILE_ARGS[@]}" "$TMPDIR/main.ts" -o "$BIN" > "$TMPDIR/compile.log" 2>&1 || { - echo "FAIL: compile failed" - sed 's/^/ /' "$TMPDIR/compile.log" | tail -80 - exit 1 -} - -"$BIN" > "$TMPDIR/run.log" 2>&1 || { - echo "FAIL: program failed" - sed 's/^/ /' "$TMPDIR/run.log" | tail -80 - exit 1 -} - -if ! diff -u "$TMPDIR/expected.log" "$TMPDIR/run.log"; then - echo "FAIL: output mismatch" - exit 1 -fi - -echo "PASS: URL href normalization" diff --git a/tests/test_url_instanceof_reflection.sh b/tests/test_url_instanceof_reflection.sh deleted file mode 100755 index 1b6cee9ad..000000000 --- a/tests/test_url_instanceof_reflection.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -source "$(dirname "$0")/_perry_test_lib.sh" - -perry_run main.ts <<'TS' -const url = new URL("https://example.com/path?q=1"); -const C = URL; - -console.log(JSON.stringify({ - direct: url instanceof URL, - dynamic: url instanceof C, - hasInstance: Function.prototype[Symbol.hasInstance].call(URL, url), - protoIs: Object.getPrototypeOf(url) === URL.prototype, - ctorName: (url as any).constructor?.name, - tag: (url as any)[Symbol.toStringTag], - objectTag: Object.prototype.toString.call(url), - protocol: url.protocol, -})); -TS - -perry_expect_node -perry_pass "URL instanceof reflection"