diff --git a/CLAUDE.md b/CLAUDE.md index 6167e558ae..59c9e2bede 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.1318 +**Current Version:** 0.5.1319 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 0e185080bd..bf3f1c6496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1318" +version = "0.5.1319" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1318" +version = "0.5.1319" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1318" +version = "0.5.1319" [[package]] name = "perry-ui-tvos" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1318" +version = "0.5.1319" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index db95b4cfb8..c29e72e799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1318" +version = "0.5.1319" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7565-iterator-no-silent-truncation.md b/changelog.d/7565-iterator-no-silent-truncation.md new file mode 100644 index 0000000000..f4b8108f34 --- /dev/null +++ b/changelog.d/7565-iterator-no-silent-truncation.md @@ -0,0 +1,28 @@ +### Iterators: spread / `Array.from` silently truncated at 100,000 elements + +`[...m.values()]` on a 250,000-entry Map returned **100,000 elements** — no +error, no warning, a plausible-looking wrong answer that a caller would ship. +Node returns all 250,000. + +Three drain loops in `array/iterator.rs` carried a hardcoded +`for _ in 0..100_000` "safety limit" and simply fell out of the loop when it +was hit, returning whatever had accumulated. Every spread, `Array.from`, and +iterator-protocol drain was affected — Maps, Sets, generators, and user +iterables alike. + +Two changes, and the second matters as much as the first: + +- The bound is now `MAX_ITERATOR_DRAIN` = JavaScript's own maximum array + length (`u32::MAX - 1`), so every realistic workload is unaffected. +- **Exhausting it throws a `RangeError` instead of truncating.** Node applies + no limit at all — `[...it]` runs until the iterator finishes or memory runs + out — so matching it exactly would trade silent truncation for an unbounded + loop. A visible, recoverable error is the right third option; returning + short data is strictly worse than either. + +`test_gap_7562_iterator_no_truncation.ts` covers all three drain paths (Map +values/keys/entries, Set, and a generator) past the old bound, byte-identical +to node. Sabotage-verified: restoring the 100,000 bound makes it exit 1. + +Found by the `map_1m` investigation (#7561) and filed as #7562; pre-existing, +confirmed at `969b447cc`. diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index 0a09faceeb..ba11ca3a4d 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -1009,6 +1009,20 @@ fn is_object_like_value(value: f64) -> bool { raw >= 0x10000 && !crate::symbol::is_registered_symbol(raw) } +/// #7562: the drain hit [`MAX_ITERATOR_DRAIN`] without the iterator finishing. +/// +/// Throwing is the point. The previous behaviour was to fall out of the loop +/// and return whatever had accumulated, so `[...it]` handed back a short array +/// that looked entirely valid — a `RangeError` is recoverable and visible, +/// silent truncation is neither. +#[cold] +fn throw_iterator_too_long() -> ! { + let msg = b"Iterator produced more than the maximum supported number of elements"; + let msg_str = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err = crate::error::js_rangeerror_new(msg_str); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + #[cold] fn throw_iterator_result_not_object() -> ! { iter_bt_dump( @@ -1038,6 +1052,21 @@ pub extern "C" fn js_iterator_next_result(iter_f64: f64) -> f64 { result } +/// Convert any iterator-protocol object (has `.next()` method) to an array. +/// #7562: the drain loops below were bounded at a hardcoded 100,000 and +/// **silently returned a short array** when a longer iterator hit it — +/// `[...m.values()]` on a 250,000-entry Map produced 100,000 elements with no +/// error, no warning, and a plausible-looking result. Wrong data a caller +/// would ship, which is strictly worse than either hanging or throwing. +/// +/// Node applies no such limit: `[...it]` runs until the iterator finishes or +/// the process runs out of memory. Matching that exactly would trade silent +/// truncation for an unbounded loop, so the bound stays — but it is raised to +/// JavaScript's own maximum array length and **throws** on exhaustion instead +/// of truncating. Every real workload is unaffected; a runaway iterator now +/// reports itself rather than corrupting a result. +const MAX_ITERATOR_DRAIN: usize = u32::MAX as usize - 1; + /// `IteratorClose(iterator)` when destructuring exits before the iterator is done. #[no_mangle] pub extern "C" fn js_iterator_close_if_not_done(iter_f64: f64, done_f64: f64) -> f64 { @@ -1106,7 +1135,12 @@ pub(crate) fn sync_iterator_to_array_if_not_async(iter_f64: f64) -> Option<*mut let value_key = js_string_from_bytes(b"value".as_ptr(), 5); let mut result = arr; - for _ in 0..100_000 { + for drained in 0..MAX_ITERATOR_DRAIN { + if drained == MAX_ITERATOR_DRAIN - 1 { + // Reached the cap with the iterator still producing: refuse rather + // than return a short array (#7562). + throw_iterator_too_long(); + } let step = if use_method_dispatch { unsafe { crate::object::js_native_call_method( @@ -1187,7 +1221,6 @@ fn settled_promise_value(value: f64) -> Option { } } -/// Convert any iterator-protocol object (has `.next()` method) to an array. /// Used by spread on generators, Array.from on generators, etc. /// Calls `.next()` in a loop until `.done` is true, collecting `.value` entries. #[no_mangle] @@ -1278,7 +1311,12 @@ pub extern "C" fn js_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { // Reusable scratch slot for the `{ value, done }` object `.next()` returns. let step_h = scope.root_nanbox_f64(f64::from_bits(TAG_UNDEFINED)); - for _ in 0..100_000 { + for drained in 0..MAX_ITERATOR_DRAIN { + if drained == MAX_ITERATOR_DRAIN - 1 { + // Reached the cap with the iterator still producing: refuse rather + // than return a short array (#7562). + throw_iterator_too_long(); + } // safety limit // Call next() — stored-closure fast path, or class-id method dispatch. // Both addresses are read fresh from their roots at the callsite: the @@ -1390,7 +1428,12 @@ fn js_async_iterator_to_array(iter_f64: f64) -> *mut ArrayHeader { let value_key = js_string_from_bytes(b"value".as_ptr(), 5); let mut result = arr; - for _ in 0..100_000 { + for drained in 0..MAX_ITERATOR_DRAIN { + if drained == MAX_ITERATOR_DRAIN - 1 { + // Reached the cap with the iterator still producing: refuse rather + // than return a short array (#7562). + throw_iterator_too_long(); + } let step = if use_method_dispatch { unsafe { crate::object::js_native_call_method( diff --git a/test-files/test_gap_7562_iterator_no_truncation.ts b/test-files/test_gap_7562_iterator_no_truncation.ts new file mode 100644 index 0000000000..17fbda9160 --- /dev/null +++ b/test-files/test_gap_7562_iterator_no_truncation.ts @@ -0,0 +1,24 @@ +// #7562: spread / Array.from must not silently truncate a long iterator. +// The drain loops carried a hardcoded 100,000-element "safety limit" and +// returned a SHORT array when it was hit — no error, no warning, a +// plausible-looking wrong answer. Compared byte-for-byte against node. +const m = new Map(); +for (let i = 0; i < 150000; i++) m.set("k" + i, i); +console.log("map size:", m.size); +console.log("spread values:", [...m.values()].length); +console.log("spread keys:", [...m.keys()].length); +console.log("Array.from values:", Array.from(m.values()).length); +console.log("spread map:", [...m].length); + +const s = new Set(); +for (let i = 0; i < 130000; i++) s.add(i); +console.log("set size:", s.size); +console.log("spread set:", [...s].length); + +// A generator past the old bound. +function* gen(n: number) { for (let i = 0; i < n; i++) yield i; } +const g = [...gen(120000)]; +console.log("generator spread:", g.length, "last:", g[g.length - 1]); + +// Small iterators must be untouched. +console.log("small:", [...new Map([["a", 1], ["b", 2]]).values()].join(","));