diff --git a/CLAUDE.md b/CLAUDE.md index 9b08a61640..b1bb8eb2a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1571 +**Current Version:** 0.5.1572 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 7142aaaa5d..3e72f191c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5583,7 +5583,7 @@ checksum = "1542e48011813fbdf3c075da4a4ed53ee93c816eef62e36eb5064a6fd2be10a5" [[package]] name = "perry" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "base64 0.22.1", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-dispatch", "serde", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "cc", "libc", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "aho-corasick", "anyhow", @@ -5681,7 +5681,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-hir", @@ -5689,7 +5689,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-hir", @@ -5697,7 +5697,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-dispatch", @@ -5706,7 +5706,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-hir", @@ -5714,7 +5714,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "base64 0.22.1", @@ -5726,7 +5726,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-hir", @@ -5734,7 +5734,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "async-trait", "clap", @@ -5758,14 +5758,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "serde", "serde_json", @@ -5773,7 +5773,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1571" +version = "0.5.1572" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5784,7 +5784,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "clap", @@ -5799,7 +5799,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "block2", "objc2", @@ -5809,7 +5809,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "argon2", "perry-ffi", @@ -5818,7 +5818,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "reqwest", @@ -5827,7 +5827,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "bcrypt", "perry-ffi", @@ -5835,7 +5835,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "rusqlite", @@ -5843,7 +5843,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "scraper", @@ -5851,7 +5851,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "perry-runtime", @@ -5859,7 +5859,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "chrono", "cron", @@ -5869,7 +5869,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "chrono", "perry-ffi", @@ -5877,7 +5877,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "rust_decimal", @@ -5885,7 +5885,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "serde_json", @@ -5893,7 +5893,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5901,7 +5901,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "perry-runtime", @@ -5909,14 +5909,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "bytes", "http-body-util", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "bytes", "lazy_static", @@ -5946,7 +5946,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "bytes", @@ -5978,7 +5978,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "lazy_static", "perry-ffi", @@ -5988,7 +5988,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -5999,7 +5999,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "lru", "perry-ffi", @@ -6008,7 +6008,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "chrono", "perry-ffi", @@ -6016,7 +6016,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "bson", "futures-util", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "chrono", "perry-ffi", @@ -6040,7 +6040,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "nanoid", "perry-ffi", @@ -6049,7 +6049,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "bytes", "perry-ffi", @@ -6064,7 +6064,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6083,7 +6083,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "lettre", "perry-ffi", @@ -6093,7 +6093,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "notify", "perry-ffi", @@ -6105,7 +6105,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "printpdf", @@ -6113,7 +6113,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "sqlx", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "perry-runtime", @@ -6131,7 +6131,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "governor", "perry-ffi", @@ -6139,7 +6139,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "fast_image_resize", "image", @@ -6150,7 +6150,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "lazy_static", "perry-ffi", @@ -6159,7 +6159,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "perry-runtime", @@ -6188,7 +6188,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "uuid", @@ -6196,7 +6196,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-ffi", "perry-validation", @@ -6205,7 +6205,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "futures-util", "lazy_static", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "brotli", "flate2", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6238,7 +6238,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-api-manifest", @@ -6258,11 +6258,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1571" +version = "0.5.1572" [[package]] name = "perry-parser" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "perry-diagnostics", @@ -6275,7 +6275,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perex", "regex", @@ -6283,7 +6283,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "ahash", "base64 0.22.1", @@ -6341,14 +6341,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6437,21 +6437,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "dirs", "perry-ffi", @@ -6461,7 +6461,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "jni", @@ -6476,7 +6476,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "rand 0.10.2", "serde", @@ -6486,7 +6486,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6509,7 +6509,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "block2", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "block2", @@ -6543,7 +6543,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1571" +version = "0.5.1572" [[package]] name = "perry-ui-test" @@ -6554,11 +6554,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1571" +version = "0.5.1572" [[package]] name = "perry-ui-tvos" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "block2", @@ -6575,7 +6575,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "block2", @@ -6592,7 +6592,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "block2", "libc", @@ -6606,7 +6606,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "libc", @@ -6625,7 +6625,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "base64 0.22.1", "libc", @@ -6638,7 +6638,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "anyhow", "base64 0.22.1", @@ -6653,7 +6653,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "idna", "regex", @@ -6663,7 +6663,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1571" +version = "0.5.1572" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 1703902254..3735806113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1571" +version = "0.5.1572" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10279-deferred-cycle-init.md b/changelog.d/10279-deferred-cycle-init.md new file mode 100644 index 0000000000..a8aff81229 --- /dev/null +++ b/changelog.d/10279-deferred-cycle-init.md @@ -0,0 +1,11 @@ +Initialize the cycle partner of a module that is reached only through a dynamic +`import()`. Perry drops init-call back-edges from a module's `__init` wrapper so +that a cycle member the entry's eager init loop already ran is not re-entered +early, but that loop skips Deferred modules, so dropping the edge to one left it +with no caller at all: its body never ran and every export it assigns at run time +stayed undefined, surfacing as `TypeError: value is not a function` on the first +call through such a binding. The positional drop now applies only to Eager deps. +An Eager module's static imports are themselves statically reachable from the +entry and so are Eager, which is why the ordering this rule protects is +unaffected, and the existing per-module init guard keeps the extra call +idempotent and cycle-safe. diff --git a/changelog.d/10280-proxy-value-shapes.md b/changelog.d/10280-proxy-value-shapes.md new file mode 100644 index 0000000000..195cd7b323 --- /dev/null +++ b/changelog.d/10280-proxy-value-shapes.md @@ -0,0 +1,24 @@ +Fix Proxy values in `Array.from` and dynamic `RequestInit.headers` (#10270, +#10274; OpenCode bring-up tracker #10107). + +- Route array proxies through iterator materialization before inspecting heap + headers, including mapped `Array.from`, Headers iterable pairs, call spread, + and collection constructors. Preserve the proxy receiver when a get trap + forwards the default iterator, and safely dispatch array-like proxy indices. + Keep concat on trapped indexed reads, preserving + holes and ignoring a custom iterator. +- Convert dynamic Request header records/iterables with + `js_headers_init_from_value`; retain the existing inline literal-header path. +- Keep proxy registry lookups behind the existing proxy-id band predicate; + Array.from checks only within its existing small-handle branch. +- Add compiled regression probes for both issue reproductions, nested proxies, + get/ownKeys traps, custom iterators, mapped conversion, and sibling consumers. + +Read a Proxy source’s iterator method once before constructing the result, and +cache the iterator’s next method. Keep the method, iterator, intermediate values, +and pending mapping errors rooted across user callbacks and iterator closing. + +Pass an existing Headers handle straight to the Request constructor, which +already clones its entries, instead of copying it into a temporary Headers +store first. Records, iterables, Proxies, `undefined` and `null` still take the +full conversion path. diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 3996ef9dab..236b982c67 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -1374,7 +1374,8 @@ pub(super) fn lower_builtin_new<'a>( if let Some(hprops) = extract_options_fields(ctx, vexpr) { headers_handle = build_headers_from_object(ctx, &hprops)?; } else { - headers_handle = lower_expr(ctx, vexpr)?; + headers_handle = + super::options::build_headers_from_value(ctx, vexpr)?; } } "referrer" => { diff --git a/crates/perry-codegen/src/lower_call/options/mod.rs b/crates/perry-codegen/src/lower_call/options/mod.rs index 281d1d9fcb..4574cbaa12 100644 --- a/crates/perry-codegen/src/lower_call/options/mod.rs +++ b/crates/perry-codegen/src/lower_call/options/mod.rs @@ -67,6 +67,23 @@ pub(in crate::lower_call) fn build_headers_from_object( }) } +/// Convert a dynamic HeadersInit with the same helper as `new Headers(init)`. +/// The literal path stays in `build_headers_from_object`; a runtime value is +/// not necessarily a Headers registry handle (it may be a record or iterable). +pub(in crate::lower_call) fn build_headers_from_value( + ctx: &mut FnCtx<'_>, + init: &Expr, +) -> Result { + with_rooted_group(ctx, 1, |ctx, group| { + let value = lower_expr(ctx, init)?; + let h = ctx + .block() + .call(DOUBLE, "js_headers_from_value", &[(DOUBLE, &value)]); + let h_root = group.adopt_emitted(ctx, Repr::Boxed, &h, true); + Ok(group.reread_emitted(ctx, h_root)) + }) +} + /// Phase 3 compat: extract `{key: value, ...}` pairs from an options /// argument in a form that works whether the options literal reached us /// as a plain `Expr::Object(props)` (pre-Phase-3 / spread/dynamic shapes) diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 14d9ac9516..0316269426 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -970,6 +970,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { module.declare_function("js_response_body_init_ptr", I64, &[DOUBLE]); // new Headers() -> f64 module.declare_function("js_headers_new", DOUBLE, &[]); + module.declare_function("js_headers_from_value", DOUBLE, &[DOUBLE]); // headers.set(handle_f64, key_ptr, val_ptr) -> f64 (undefined-tag) module.declare_function("js_headers_set", DOUBLE, &[DOUBLE, I64, I64]); // headers.append(handle_f64, key_ptr, val_ptr) -> f64 (undefined-tag) diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index c69be29769..2365a2c250 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -458,7 +458,7 @@ pub extern "C" fn js_array_clone(src: *const ArrayHeader) -> *mut ArrayHeader { let top16 = (src as u64) >> 48; if top16 == 0x7FFF { true - } else if raw_addr >= crate::gc::GC_HEADER_SIZE + 0x1000 { + } else if crate::value::addr_class::is_above_handle_band(raw_addr) { unsafe { let hdr = (raw_addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; @@ -478,6 +478,15 @@ pub extern "C" fn js_array_clone(src: *const ArrayHeader) -> *mut ArrayHeader { // helper after codegen strips the tag, so ask the generic iterator resolver // before treating the id as a non-array and returning []. if crate::value::addr_class::is_small_handle(raw_addr) { + // #10270: a Proxy id has no GC/ArrayHeader. Only the existing handle + // arm probes the registry; plain arrays keep their classification and + // copy path. Array.from permits array-like proxies as well as iterables. + if let Some(proxy) = array_ptr_as_proxy(raw_addr as *const ArrayHeader) { + let undefined = f64::from_bits(crate::value::TAG_UNDEFINED); + let result = + super::from_concat::array_from_full(undefined, proxy, undefined, undefined); + return crate::value::js_nanbox_get_pointer(result) as *mut ArrayHeader; + } if let Some(dispatch) = crate::object::handle_property_dispatch() { let method = b"@@iterator"; let iter_fn = unsafe { dispatch(raw_addr as i64, method.as_ptr(), method.len()) }; diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index 0903c98cd0..a2c5ae57b7 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -12,6 +12,8 @@ use super::{ use crate::closure::ClosureHeader; use crate::value::JSValue; +mod proxy_from; + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; @@ -720,6 +722,13 @@ pub fn array_from_full(c: f64, items: f64, mapfn: f64, this_arg: f64) -> f64 { throw_not_iterable("object null"); } + // Proxy GetMethod is observable. Resolve it once, retain it across + // construction, and root iterator state across callbacks that can collect. + if let Some(proxy) = crate::array::array_ptr_as_proxy( + crate::value::js_nanbox_get_pointer(items) as *const ArrayHeader, + ) { + return proxy_from::array_from_proxy(c, proxy, mapfn, this_arg, mapping); + } let is_ctor = is_constructor_value(c); if items_is_iterable(items) { @@ -1037,8 +1046,10 @@ unsafe fn try_append_spread_array_dense( src: *const ArrayHeader, ) -> Option<*mut ArrayHeader> { // A masked proxy id is not a dereferenceable ArrayHeader. - if crate::array::array_ptr_as_proxy(src).is_some() { - return None; + if let Some(proxy) = crate::array::array_ptr_as_proxy(src) { + // Concat uses HasProperty/Get, never @@iterator. Resolve this inside + // the existing proxy guard so the ordinary bulk-copy path is unchanged. + return Some(append_concat_proxy(result, proxy)); } let src = clean_arr_ptr(src); if src.is_null() { @@ -1131,6 +1142,32 @@ unsafe fn try_append_spread_array_dense( Some(result) } +/// Indexed concat of a proxy, preserving holes and observing its traps. +unsafe fn append_concat_proxy(result: *mut ArrayHeader, proxy: f64) -> *mut ArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let result = scope.root_raw_mut_ptr(result); + let proxy = scope.root_nanbox_f64(proxy); + let (_, result) = result.across_mut(|| { + let len = array_like_length(proxy.get_nanbox_f64()); + for index in 0..len { + let entry_scope = crate::gc::RuntimeHandleScope::new(); + let name = index.to_string(); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key = entry_scope.root_nanbox_f64(crate::value::js_nanbox_string(key as i64)); + let present = + crate::object::js_object_has_property(proxy.get_nanbox_f64(), key.get_nanbox_f64()); + let value = if crate::value::js_is_truthy(present) != 0 { + crate::proxy::js_proxy_get(proxy.get_nanbox_f64(), key.get_nanbox_f64()) + } else { + f64::from_bits(crate::value::TAG_HOLE) + }; + let grown = result.with_mut_ptr(|ptr| js_array_push_f64(ptr, value)); + result.set_raw_mut_ptr(grown); + } + }); + result +} + /// Append every element of the (already-materializable) source array `src` /// into `result`, returning the (possibly reallocated) result. `src` is /// materialized via `js_array_clone` so sets/maps/typed-arrays/buffers spread diff --git a/crates/perry-runtime/src/array/from_concat/proxy_from.rs b/crates/perry-runtime/src/array/from_concat/proxy_from.rs new file mode 100644 index 0000000000..a65cfab5b3 --- /dev/null +++ b/crates/perry-runtime/src/array/from_concat/proxy_from.rs @@ -0,0 +1,181 @@ +//! Proxy-only Array.from: one GetMethod, with roots across user callbacks. +use super::*; +use crate::gc::{RuntimeHandle, RuntimeHandleScope}; + +fn get(value: &RuntimeHandle<'_>, name: &[u8]) -> f64 { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let raw = crate::value::js_nanbox_get_pointer(value.get_nanbox_f64()); + crate::object::js_object_get_field_by_name_f64(raw as *const crate::ObjectHeader, key) +} + +fn invoke(method: &RuntimeHandle<'_>, receiver: &RuntimeHandle<'_>) -> Result { + let scope = RuntimeHandleScope::new(); + let rebound = crate::closure::clone_closure_rebind_this( + method.get_nanbox_f64().to_bits(), + receiver.get_nanbox_f64(), + ); + let rebound = scope.root_nanbox_f64(f64::from_bits(rebound)); + crate::collection_iter::call_with_this_capturing_throw( + rebound.get_nanbox_f64(), + receiver.get_nanbox_f64(), + &[], + ) +} + +fn define(result: &RuntimeHandle<'_>, index: usize, value: f64, fresh_array: bool) { + if fresh_array { + let raw = crate::value::js_nanbox_get_pointer(result.get_nanbox_f64()); + let grown = js_array_set_f64_extend(raw as *mut ArrayHeader, index as u32, value); + result.set_nanbox_f64(crate::value::js_nanbox_pointer(grown as i64)); + return; + } + let scope = RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(value); + let desc = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 4)); + for name in [ + b"value".as_slice(), + b"writable", + b"enumerable", + b"configurable", + ] { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let field = if name == b"value" { + value.get_nanbox_f64() + } else { + f64::from_bits(TAG_TRUE) + }; + desc.with_mut_ptr(|ptr| crate::object::js_object_set_field_by_name(ptr, key, field)); + } + let name = index.to_string(); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key = scope.root_nanbox_f64(crate::value::js_nanbox_string(key as i64)); + let descriptor = desc.with_const_ptr::(|ptr| { + crate::value::js_nanbox_pointer(ptr as i64) + }); + if crate::proxy::js_reflect_define_property( + result.get_nanbox_f64(), + key.get_nanbox_f64(), + descriptor, + ) + .to_bits() + != TAG_TRUE + { + throw_cannot_define_property(index); + } +} + +fn finish(result: &RuntimeHandle<'_>, len: usize, fresh_array: bool) -> f64 { + if fresh_array { + let raw = crate::value::js_nanbox_get_pointer(result.get_nanbox_f64()); + crate::array::js_array_set_length(raw as *mut ArrayHeader, len as f64); + } else { + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let key = crate::value::js_nanbox_string(key as i64); + let current = result.get_nanbox_f64(); + crate::proxy::js_put_value_set(current, key, len as f64, current, 1); + } + result.get_nanbox_f64() +} + +pub(super) fn array_from_proxy( + c: f64, + items: f64, + mapfn: f64, + this_arg: f64, + mapping: bool, +) -> f64 { + let scope = RuntimeHandleScope::new(); + let constructor = scope.root_nanbox_f64(c); + let items = scope.root_nanbox_f64(items); + let mapfn = scope.root_nanbox_f64(mapfn); + let this_arg = scope.root_nanbox_f64(this_arg); + let symbol = crate::symbol::well_known_symbol("iterator"); + let method = unsafe { + crate::symbol::js_object_get_symbol_property( + items.get_nanbox_f64(), + crate::value::js_nanbox_pointer(symbol as i64), + ) + }; + let method = scope.root_nanbox_f64(method); + let iterable = !matches!(method.get_nanbox_f64().to_bits(), TAG_UNDEFINED | TAG_NULL); + if iterable { + resolve_callable(method.get_nanbox_f64()); + } + let fresh_array = !is_constructor_value(constructor.get_nanbox_f64()); + let len = if iterable { + 0 + } else { + array_like_length(items.get_nanbox_f64()) + }; + let result = if fresh_array { + crate::value::js_nanbox_pointer(js_array_alloc(0) as i64) + } else { + let args = [len as f64]; + unsafe { + crate::object::js_new_function_construct( + constructor.get_nanbox_f64(), + args.as_ptr(), + if iterable { 0 } else { 1 }, + ) + } + }; + let result = scope.root_nanbox_f64(result); + if !iterable { + for index in 0..len { + let value = crate::object::js_object_get_index_polymorphic( + items.get_nanbox_f64().to_bits() as i64, + index as f64, + ); + let value = if mapping { + call_map_fn( + mapfn.get_nanbox_f64(), + this_arg.get_nanbox_f64(), + value, + index, + ) + } else { + value + }; + define(&result, index, value, fresh_array); + } + return finish(&result, len, fresh_array); + } + // GetIteratorFromMethod uses the saved method even if construction mutates + // @@iterator. The iterator record likewise reads its next method only once. + let iter = invoke(&method, &items).unwrap_or_else(|error| crate::exception::js_throw(error)); + crate::symbol::js_iterator_result_validate(iter); + let iter = scope.root_nanbox_f64(iter); + let next = scope.root_nanbox_f64(get(&iter, b"next")); + let mut index = 0; + loop { + let step_scope = RuntimeHandleScope::new(); + let step = invoke(&next, &iter).unwrap_or_else(|error| crate::exception::js_throw(error)); + crate::symbol::js_iterator_result_validate(step); + let step = step_scope.root_nanbox_f64(step); + if crate::value::js_is_truthy(get(&step, b"done")) != 0 { + break; + } + let value = step_scope.root_nanbox_f64(get(&step, b"value")); + let completion = crate::collection_iter::call_capturing_throw(|| { + let mapped = if mapping { + call_map_fn( + mapfn.get_nanbox_f64(), + this_arg.get_nanbox_f64(), + value.get_nanbox_f64(), + index, + ) + } else { + value.get_nanbox_f64() + }; + define(&result, index, mapped, fresh_array); + f64::from_bits(TAG_UNDEFINED) + }); + if let Err(error) = completion { + let error = step_scope.root_nanbox_f64(error); + crate::collection_iter::iterator_close(iter.get_nanbox_f64()); + crate::exception::js_throw(error.get_nanbox_f64()); + } + index += 1; + } + finish(&result, index, fresh_array) +} diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index bc277d6b8d..79ed386779 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -102,6 +102,13 @@ pub extern "C" fn js_for_of_to_array(val_f64: f64) -> f64 { throw_not_iterable(val_f64); } + // Proxy ids are not heap headers. Use the same trapped iterator as spread. + if let Some(proxy) = array_ptr_as_proxy(raw_ptr as *const ArrayHeader) { + return js_nanbox_pointer( + js_iterator_to_array(crate::symbol::js_get_iterator(proxy)) as i64 + ); + } + // Inspect the GC header's object kind to dispatch Array / Map / Set // without consulting any static type. let obj_type = unsafe { diff --git a/crates/perry-runtime/src/collection_iter.rs b/crates/perry-runtime/src/collection_iter.rs index 44d5c801b4..1e64d79e15 100644 --- a/crates/perry-runtime/src/collection_iter.rs +++ b/crates/perry-runtime/src/collection_iter.rs @@ -254,6 +254,13 @@ pub(crate) fn constructor_iter(value: f64) -> ConstructorIter { let jsv = JSValue::from_bits(value.to_bits()); let is_array = crate::array::js_array_is_array(value).to_bits() == crate::value::TAG_TRUE; if is_array { + if crate::array::array_ptr_as_proxy( + js_nanbox_get_pointer(value) as *const crate::array::ArrayHeader + ) + .is_some() + { + return ConstructorIter::Iterator(crate::symbol::js_get_iterator(value)); + } return ConstructorIter::Array(value); } if jsv.is_any_string() { diff --git a/crates/perry-runtime/src/object/arguments.rs b/crates/perry-runtime/src/object/arguments.rs index cf1ad044f6..41cf23d17f 100644 --- a/crates/perry-runtime/src/object/arguments.rs +++ b/crates/perry-runtime/src/object/arguments.rs @@ -717,7 +717,9 @@ pub extern "C" fn js_array_like_to_array(value: f64) -> *mut ArrayHeader { // sticky flag that is false until user code writes the prototype slot, // so the fast path is untouched in every ordinary program. if crate::array::js_array_is_array(value).to_bits() == crate::value::TAG_TRUE { - if crate::array::array_proto_iterator_modified() { + if crate::array::array_proto_iterator_modified() + || crate::array::array_ptr_as_proxy(raw as *const ArrayHeader).is_some() + { return crate::array::js_array_clone_for_spread(value); } return crate::array::clean_arr_ptr(raw as *const ArrayHeader) as *mut ArrayHeader; diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 0468240fe0..bbb7daa4aa 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -174,7 +174,15 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> } else { obj_handle as u64 }; - if raw < 0x1000 { + // Array.from's array-like fallback can receive a Proxy record. Its id is + // not a heap header; route it inside the existing low-address guard so + // ordinary heap receivers do not perform a proxy registry lookup. + if crate::value::addr_class::is_handle_band(raw as usize) { + if let Some(proxy) = + crate::array::array_ptr_as_proxy(raw as *const crate::array::ArrayHeader) + { + return crate::proxy::js_proxy_get(proxy, idx); + } return f64::from_bits(crate::value::TAG_UNDEFINED); } // Symbols share GC_TYPE_STRING storage for tracing, but they are primitive diff --git a/crates/perry-runtime/src/stdlib_stubs.rs b/crates/perry-runtime/src/stdlib_stubs.rs index dd9fa2fa3e..7779a1ab34 100644 --- a/crates/perry-runtime/src/stdlib_stubs.rs +++ b/crates/perry-runtime/src/stdlib_stubs.rs @@ -192,6 +192,13 @@ pub extern "C" fn js_headers_new() -> f64 { f64::from_bits(crate::value::TAG_UNDEFINED) } +#[cfg(not(feature = "external-fetch-symbols"))] +#[no_mangle] +pub extern "C" fn js_headers_from_value(_init: f64) -> f64 { + perry_stub_warn("js_headers_from_value", FETCH_REASON, None); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + #[cfg(not(feature = "external-fetch-symbols"))] #[no_mangle] pub extern "C" fn js_headers_init_from_value(_handle: f64, _init: f64) -> f64 { diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 326f73dee6..ed72157767 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -1141,22 +1141,19 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 } } } - // #321: arrays expose `Symbol.iterator`. perry has no standalone array - // iterator object (for-of is special-cased), but `arr[Symbol.iterator]` - // must resolve to a callable so `Symbol.iterator in arr` is true - // (effect's `Predicate.isIterable`) and `typeof arr[Symbol.iterator]` is - // "function". Bind the array's `values` method as that callable. Pre-fix - // the symbol key fell through to the numeric/string paths and read back a - // number, so `isIterable([...])` was false and `Effect.all`'s - // predicate-`dual` `forEach` went data-last (returned a function). + // Return the actual prototype iterator, whose thunk reads call-time this. + // Binding `values` to this array makes a Proxy get trap returning + // target[Symbol.iterator] iterate the target, bypassing length/index traps + // even when GetIterator calls the method with the Proxy receiver (#10270). if crate::array::js_array_is_array(obj_f64).to_bits() == crate::value::TAG_TRUE { let iter_wk = well_known_symbol("iterator"); if !iter_wk.is_null() { let iter_f64 = f64::from_bits(crate::value::JSValue::pointer(iter_wk as *const u8).bits()); if sym_key_from_f64(sym_f64) == sym_key_from_f64(iter_f64) { - let mname = b"values"; - return crate::object::js_class_method_bind(obj_f64, mname.as_ptr(), mname.len()); + let proto = crate::object::builtin_prototype_value("Array"); + return own_symbol_property(proto, sym_f64) + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); } } } diff --git a/crates/perry-stdlib/src/fetch/headers.rs b/crates/perry-stdlib/src/fetch/headers.rs index eab946c46c..1192b2dcc4 100644 --- a/crates/perry-stdlib/src/fetch/headers.rs +++ b/crates/perry-stdlib/src/fetch/headers.rs @@ -21,6 +21,31 @@ pub extern "C" fn js_headers_new() -> f64 { handle_to_f64(alloc_headers(HeadersStore::default())) } +/// Normalize a runtime `HeadersInit` value to a Headers handle. +/// +/// A genuine Headers handle is already in the representation consumed by the +/// Request constructor, which clones its entries into the Request record. Keep +/// that handle instead of allocating and filling a redundant intermediate +/// store. Records, iterables, and Proxies still take the full constructor path. +#[no_mangle] +pub extern "C" fn js_headers_from_value(init: f64) -> f64 { + let scope = perry_runtime::gc::RuntimeHandleScope::new(); + let init = scope.root_nanbox_f64(init); + let current = init.get_nanbox_f64(); + if perry_runtime::proxy::js_proxy_is_proxy(current) == 0 { + let id = handle_id(current); + if HEADERS_REGISTRY.lock().unwrap().contains_key(&id) { + return current; + } + } + + let handle = js_headers_new(); + unsafe { + js_headers_init_from_value(handle, init.get_nanbox_f64()); + } + handle +} + unsafe fn header_init_string(value: f64) -> String { let ptr = perry_runtime::value::js_jsvalue_to_string(value); string_from_header(ptr as *const StringHeader).unwrap_or_default() @@ -293,7 +318,13 @@ unsafe fn materialize_header_pair( if !is_headers_init_iterable(pair_value) { headers_init_type_error("Headers constructor: expected name/value pair"); } - let pair_array_value = if perry_runtime::js_array_is_array(pair_value).to_bits() == TAG_TRUE { + // IsArray also accepts proxy ids and object-backed Array subclasses; only + // a real array header may bypass iterable materialization (#10270). + let raw = perry_runtime::js_nanbox_get_pointer(pair_value); + let pair_array_value = if matches!( + gc_type_for_raw_ptr(raw), + Some(perry_runtime::gc::GC_TYPE_ARRAY | perry_runtime::gc::GC_TYPE_LAZY_ARRAY) + ) { pair_value } else { perry_runtime::array::js_for_of_to_array(pair_value) diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index f751ace6dc..e25bd6ea3f 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -3319,8 +3319,29 @@ pub fn run_with_parse_cache( .enumerate() .map(|(i, name)| (sanitize_name(name), i)) .collect(); + // + // A Deferred dep is the exception (#10278). The back-edge drop is + // sound only because the entry's eager init loop runs every Eager + // module in `init_pos` order, so a dep positioned before this + // module has already initialized by the time this wrapper runs. + // Deferred modules are filtered OUT of that loop — nothing runs + // them but a dynamic-import dispatch site or another wrapper — so + // dropping the edge to one strands it: its body never runs and + // every export the body assigns at runtime stays undefined. A + // static `import()` entry into a Deferred cycle reproduced it + // (`useAssigned()` returned undefined, `obj.method` threw). Keep + // the edge whenever the dep is Deferred; the `__init` guard + // already makes the call idempotent and cycle-safe, and it + // reproduces ESM's order (the partner body runs first, the + // re-entrant call returns immediately). This cannot perturb the + // Eager ordering #6463 fixed: a module statically imported by an + // Eager module is itself statically reachable from the entry, so + // it is Eager, so this arm never fires for it. if let Some(&self_pos) = init_pos.get(&sanitize_name(&hir_module.name)) { - deps.retain(|dep| init_pos.get(dep).map_or(true, |&p| p < self_pos)); + deps.retain(|dep| { + deferred_module_prefixes.contains(dep) + || init_pos.get(dep).map_or(true, |&p| p < self_pos) + }); } deps }; diff --git a/crates/perry/src/commands/compile/strip_dedup/stub_symbols.rs b/crates/perry/src/commands/compile/strip_dedup/stub_symbols.rs index 35b22442f6..762f0d5376 100644 --- a/crates/perry/src/commands/compile/strip_dedup/stub_symbols.rs +++ b/crates/perry/src/commands/compile/strip_dedup/stub_symbols.rs @@ -23,6 +23,7 @@ const STDLIB_STUB_SYMBOLS: &[&str] = &[ "js_fetch_with_options", "js_blob_new", "js_headers_new", + "js_headers_from_value", "js_headers_init_from_value", "js_request_new", "js_response_new", diff --git a/crates/perry/tests/issue_10270_proxy_array_from.rs b/crates/perry/tests/issue_10270_proxy_array_from.rs new file mode 100644 index 0000000000..25de2c03da --- /dev/null +++ b/crates/perry/tests/issue_10270_proxy_array_from.rs @@ -0,0 +1,88 @@ +//! #10270: IsArray is not proof that a Proxy id is an ArrayHeader. + +mod support; + +const SOURCE: &str = r#" +const t = (name: string, f: () => any) => { try { console.log(name, JSON.stringify(f())) } catch (e: any) { console.log(name, "THROW", e.message) } } +const pa = () => new Proxy([["x-a", "1"]], {}) +t("Q1 [...proxyOverArray]", () => [...(pa() as any)].length) +t("Q2 for..of proxyOverArray", () => { let n = 0; for (const _ of pa() as any) n++; return n }) +t("Q3 Array.isArray(proxyOverArray)", () => Array.isArray(pa())) +t("Q4 proxy[Symbol.iterator] typeof", () => typeof (pa() as any)[Symbol.iterator]) +t("Q5 Array.from(proxyOverArray)", () => Array.from(pa() as any).length) +t("nested", () => Array.from(new Proxy(new Proxy([1, 2], {}), {}))) +t("trapped indices", () => Array.from(new Proxy([1, 2], { get(t: any, k: any) { return k === "0" ? 9 : t[k]; } }))) +t("custom iterator", () => Array.from(new Proxy([1, 2], { get(t: any, k: any) { return k === Symbol.iterator ? () => [7, 8, 9].values() : t[k]; } }))) +t("mapped", () => Array.from(new Proxy([1, 2], {}), (v: number, i: number) => v + i)) +t("mapped custom iterator", () => Array.from(new Proxy([1, 2], { get(t: any, k: any) { return k === Symbol.iterator ? () => [7, 8, 9].values() : t[k]; } }), (v: number) => v * 2)) +t("arraylike", () => Array.from(new Proxy({ 0: "a", 1: "b", length: 2 }, {}))) +t("removed iterator", () => Array.from(new Proxy([1, 2], { get(t: any, k: any) { return k === Symbol.iterator ? undefined : t[k]; } }))) +t("record iterator", () => Array.from(new Proxy({}, { get(t: any, k: any) { return k === Symbol.iterator ? () => [5, 6].values() : t[k]; } }))) +t("headers outer", () => new Headers(new Proxy([["x-a", "1"]], {}) as any).get("x-a")) +t("headers pair", () => new Headers([new Proxy(["x-a", "1"], {})] as any).get("x-a")) +t("plain array", () => Array.from([3, 4])) +t("of preserves proxy", () => { const p = new Proxy([1, 2], {}); return Array.of(p)[0] === p; }) +t("concat indices", () => [0].concat(new Proxy([1, 2], { get(t: any, k: any) { return k === Symbol.iterator ? () => [9].values() : k === "0" ? 7 : t[k]; } }))) +t("concat holes", () => { const a = [0].concat(new Proxy([, 2], {})); return [a.length, 1 in a, a[2]]; }) +t("call spread", () => { const count = (...xs: any[]) => xs.length; return count(...new Proxy([1, 2], {})); }) +t("set iterator", () => Array.from(new Set(new Proxy([1, 2], { get(t: any, k: any) { return k === Symbol.iterator ? () => [7, 8].values() : t[k]; } })))) +Promise.all(new Proxy([Promise.resolve(1), Promise.resolve(2)], {})).then(v => console.log("all", JSON.stringify(v))) +"#; + +#[test] +fn proxy_array_from_and_headers_iterables() { + assert_eq!( + support::compile_and_run(SOURCE), + concat!( + "Q1 [...proxyOverArray] 1\n", + "Q2 for..of proxyOverArray 1\n", + "Q3 Array.isArray(proxyOverArray) true\n", + "Q4 proxy[Symbol.iterator] typeof \"function\"\n", + "Q5 Array.from(proxyOverArray) 1\n", + "nested [1,2]\n", + "trapped indices [9,2]\n", + "custom iterator [7,8,9]\n", + "mapped [1,3]\n", + "mapped custom iterator [14,16,18]\n", + "arraylike [\"a\",\"b\"]\n", + "removed iterator [1,2]\n", + "record iterator [5,6]\n", + "headers outer \"1\"\n", + "headers pair \"1\"\n", + "plain array [3,4]\n", + "of preserves proxy true\n", + "concat indices [0,7,2]\n", + "concat holes [3,false,2]\n", + "call spread 2\n", + "set iterator [7,8]\n", + "all [1,2]\n", + ) + ); +} + +#[test] +fn proxy_array_from_reads_iterator_methods_once_and_closes_on_mapping_throw() { + let source = r#" +let reads = 0; +const p = new Proxy([1, 2], { get(t: any, k: any) { + if (k === Symbol.iterator) { reads++; return reads === 1 ? function() { return [4, 5].values(); } : undefined; } + return t[k]; +}}); +console.log("method", JSON.stringify(Array.from(p)), reads); +let nextReads = 0, steps = 0, closed = 0; +const it = { get next() { nextReads++; return function() { return steps++ < 2 ? {done:false, value:7} : {done:true}; }; }, return() { closed++; return {done:true}; } }; +const q = new Proxy({}, {get(t:any,k:any) { return k === Symbol.iterator ? () => it : undefined; }}); +console.log("next", JSON.stringify(Array.from(q)), nextReads); +steps = 0; +try { Array.from(q, () => { throw new Error("mapped"); }); } catch(e:any) { console.log("close",e.message,closed); } +let selected = false; +const r = new Proxy([1], {get(t:any,k:any) { if(k === Symbol.iterator) { selected = true; return function() { return [8].values(); }; } return t[k]; }}); +function C() { console.log("constructor", selected); } +const result:any = Array.from.call(C, r); +console.log("constructed", result[0], result.length); +"#; + assert_eq!( + support::compile_and_run(source), + "method [4,5] 1\nnext [7,7] 1\nclose mapped 1\nconstructor true\nconstructed 8 1\n" + ); +} diff --git a/crates/perry/tests/issue_10274_request_proxy_headers.rs b/crates/perry/tests/issue_10274_request_proxy_headers.rs new file mode 100644 index 0000000000..de3c09438d --- /dev/null +++ b/crates/perry/tests/issue_10274_request_proxy_headers.rs @@ -0,0 +1,51 @@ +//! #10274: a dynamic HeadersInit in a literal RequestInit needs conversion. + +mod support; + +const SOURCE: &str = r#" +const dump = (h: any) => { const out: string[] = []; h.forEach((v: string, k: string) => out.push(k + "=" + v)); return out.sort() } +const proxy = new Proxy({ "x-a": "1" }, {}) +console.log("direct", JSON.stringify(dump(new Headers(proxy as any)))) +console.log("request", JSON.stringify(dump(new Request("https://x.dev", { headers: proxy as any }).headers))) +function make(headers: any) { return new Request("https://x.dev", { headers }).headers; } +function init(headers: any): any { return { headers }; } +console.log("parameter", JSON.stringify(dump(make(proxy)))) +console.log("runtime init", JSON.stringify(dump(new Request("https://x.dev", init(proxy)).headers))) +const trapped = new Proxy({ "authorization": "secret", "x-hidden": "hidden" }, { + ownKeys() { return ["authorization"]; }, + get(t: any, k: any) { return k === "authorization" ? "Bearer token" : t[k]; } +}); +console.log("traps", JSON.stringify(dump(make(trapped)))) +console.log("record", JSON.stringify(dump(make({ "x-a": "1" })))) +console.log("pairs", JSON.stringify(dump(make([["x-a", "1"], ["x-a", "2"]])))) +console.log("handle", JSON.stringify(dump(make(new Headers({ "x-a": "1" }))))) +console.log("literal", JSON.stringify(dump(new Request("https://x.dev", { headers: { "x-a": "1" } }).headers))) +const source = new Headers({ "x-a": "1" }) +const copied = make(source) +source.set("x-b", "2") +copied.set("x-c", "3") +console.log("independent", JSON.stringify(dump(source)), JSON.stringify(dump(copied)), copied !== source) +console.log("absent", JSON.stringify(dump(make(undefined)))) +try { make(null); console.log("null accepted") } catch (e) { console.log("null", e instanceof TypeError) } +"#; + +#[test] +fn request_converts_dynamic_headers_init() { + assert_eq!( + support::compile_and_run(SOURCE), + concat!( + "direct [\"x-a=1\"]\n", + "request [\"x-a=1\"]\n", + "parameter [\"x-a=1\"]\n", + "runtime init [\"x-a=1\"]\n", + "traps [\"authorization=Bearer token\"]\n", + "record [\"x-a=1\"]\n", + "pairs [\"x-a=1, 2\"]\n", + "handle [\"x-a=1\"]\n", + "literal [\"x-a=1\"]\n", + "independent [\"x-a=1\",\"x-b=2\"] [\"x-a=1\",\"x-c=3\"] true\n", + "absent []\n", + "null true\n", + ) + ); +} diff --git a/crates/perry/tests/issue_10278_dynamic_import_cycle_init.rs b/crates/perry/tests/issue_10278_dynamic_import_cycle_init.rs new file mode 100644 index 0000000000..f56e39ed8e --- /dev/null +++ b/crates/perry/tests/issue_10278_dynamic_import_cycle_init.rs @@ -0,0 +1,145 @@ +//! Regression test for #10278: a module reached only through a dynamic +//! `import()` (`ModuleInitKind::Deferred`) that takes part in an import cycle +//! must still initialize its cycle partner. +//! +//! `module_init_deps` (run_pipeline) drops init-call back-edges so that an +//! Eager module's `__init` wrapper never re-enters a cycle member the entry's +//! eager init loop has already run — that is #6463, and dropping the edge is +//! only sound *because* that loop exists. Deferred modules are filtered out of +//! it (`codegen/entry.rs`), so nothing else runs them: dropping a wrapper's +//! edge to a Deferred dep strands the dep forever. Its body never runs and +//! every export the body assigns at runtime stays undefined, which surfaces as +//! `TypeError: value is not a function` on the first call through such a slot. +//! +//! The two tests are deliberately a pair. `dynamic` is the defect: it fails on +//! the pre-fix compiler with `assigned-UNDEFINED`. `static_control` pins the +//! #6463 ordering it must not disturb — the same cycle entered statically, +//! where the eager loop is what initializes both members. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn runtime_dir() -> PathBuf { + std::env::var_os("PERRY_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| { + perry_bin() + .parent() + .expect("compiler directory") + .to_path_buf() + }) +} + +/// Cycle member whose body assigns two exports at run time. A function +/// declaration alone would not witness the bug: those are reachable without the +/// body ever running. +const CYCLE_A: &str = "\ +console.log(\"[init] a body start\"); +import { bFn } from \"./b.js\"; +export let assigned; +assigned = () => \"a-assigned-ok\"; +export const obj = {}; +obj.method = () => \"a-obj-method-ok\"; +export function aFn() { return \"a:\" + bFn(); } +console.log(\"[init] a body end\"); +"; + +const CYCLE_B: &str = "\ +console.log(\"[init] b body start\"); +import { aFn, assigned, obj } from \"./a.js\"; +export function bFn() { return \"b\"; } +export function useA() { return aFn(); } +export function useAssigned() { return assigned ? assigned() : \"assigned-UNDEFINED\"; } +export function useObj() { + return typeof obj.method === \"function\" ? obj.method() : \"obj-method-NOT-A-FUNCTION\"; +} +console.log(\"[init] b body end\"); +"; + +const DYNAMIC_ENTRY: &str = "\ +const m = await import(\"./b.js\"); +console.log(m.bFn(), m.useA(), m.useAssigned(), m.useObj()); +"; + +const STATIC_ENTRY: &str = "\ +import { bFn, useA, useAssigned, useObj } from \"./b.js\"; +console.log(bFn(), useA(), useAssigned(), useObj()); +"; + +/// Node and bun print the cycle partner's body first in both entry shapes. +const EXPECTED: &str = "\ +[init] a body start +[init] a body end +[init] b body start +[init] b body end +b a:b a-assigned-ok a-obj-method-ok +"; + +fn compile_and_run(entry_src: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + std::fs::write(root.join("a.js"), CYCLE_A).unwrap(); + std::fs::write(root.join("b.js"), CYCLE_B).unwrap(); + std::fs::write(root.join("entry.js"), entry_src).unwrap(); + + let entry = root.join("entry.js"); + let output = root.join("entry_bin"); + let out = Command::new(perry_bin()) + .current_dir(root) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .arg("--no-codegen") + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RUNTIME_DIR", runtime_dir()) + .output() + .expect("run perry compile"); + assert!( + out.status.success(), + "import cycle must compile; stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + let run = Command::new(&output).output().expect("run compiled binary"); + assert!( + run.status.success(), + "compiled cycle binary must run; stdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8(run.stdout).expect("UTF-8 stdout") +} + +#[test] +fn dynamic_import_into_cycle_initializes_the_partner() { + let stdout = compile_and_run(DYNAMIC_ENTRY); + assert!( + stdout.contains("[init] a body start"), + "the cycle partner's body must run when the cycle is entered through \ + a dynamic import; stdout:\n{stdout}" + ); + assert!( + !stdout.contains("assigned-UNDEFINED") && !stdout.contains("obj-method-NOT-A-FUNCTION"), + "run-time-assigned exports of the cycle partner must be live; stdout:\n{stdout}" + ); + assert_eq!( + stdout, EXPECTED, + "dynamic-import cycle output must match node/bun byte-for-byte" + ); +} + +#[test] +fn static_import_into_cycle_keeps_the_6463_order() { + let stdout = compile_and_run(STATIC_ENTRY); + assert_eq!( + stdout, EXPECTED, + "static-import cycle output must match node/bun byte-for-byte" + ); +} diff --git a/crates/perry/tests/support/mod.rs b/crates/perry/tests/support/mod.rs new file mode 100644 index 0000000000..b3bdd36e36 --- /dev/null +++ b/crates/perry/tests/support/mod.rs @@ -0,0 +1,2 @@ +mod proxy_value_probe; +pub use proxy_value_probe::compile_and_run; diff --git a/crates/perry/tests/support/proxy_value_probe.rs b/crates/perry/tests/support/proxy_value_probe.rs new file mode 100644 index 0000000000..c3d913c277 --- /dev/null +++ b/crates/perry/tests/support/proxy_value_probe.rs @@ -0,0 +1,44 @@ +use std::path::PathBuf; +use std::process::Command; + +pub fn compile_and_run(source: &str) -> String { + let compiler = PathBuf::from(env!("CARGO_BIN_EXE_perry")); + let runtime = std::env::var_os("PERRY_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| compiler.parent().expect("compiler directory").to_path_buf()); + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + let compile = Command::new(compiler) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .arg("--no-codegen") + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RUNTIME_DIR", runtime) + .output() + .expect("compile probe"); + assert!( + compile.status.success(), + "compile failed: {:?}\n{}\n{}", + compile.status, + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + let run = Command::new(output) + .current_dir(dir.path()) + .output() + .expect("run compiled probe"); + assert!( + run.status.success(), + "probe failed: {:?}\n{}\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8(run.stdout).expect("UTF-8 output") +} diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index 0562872ae5..32642c62a4 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -29,7 +29,7 @@ handle-floor | crates/perry-ext-http/src/agent.rs | 3 handle-floor | crates/perry-ext-http/src/lib.rs | 2 handle-floor | crates/perry-runtime/src/array/alloc.rs | 2 handle-floor | crates/perry-runtime/src/array/concat_reverse.rs | 1 -handle-floor | crates/perry-runtime/src/array/flat_clone.rs | 4 +handle-floor | crates/perry-runtime/src/array/flat_clone.rs | 3 handle-floor | crates/perry-runtime/src/array/generic.rs | 4 handle-floor | crates/perry-runtime/src/array/header.rs | 3 handle-floor | crates/perry-runtime/src/array/indexing.rs | 2