diff --git a/CLAUDE.md b/CLAUDE.md index 7e9c8918ff..413ad68af3 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.1341 +**Current Version:** 0.5.1342 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index b732dcbef5..aaf2ae27db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1341" +version = "0.5.1342" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1341" +version = "0.5.1342" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1341" +version = "0.5.1342" [[package]] name = "perry-ui-tvos" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1341" +version = "0.5.1342" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index d8bb7d5111..d285e5ca38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1341" +version = "0.5.1342" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7600-discarded-push-length-elision.md b/changelog.d/7600-discarded-push-length-elision.md new file mode 100644 index 0000000000..262eb75389 --- /dev/null +++ b/changelog.d/7600-discarded-push-length-elision.md @@ -0,0 +1,20 @@ +**perf(codegen): elide the length computation of a statement-position push** + +`arr.push(x)` evaluates to the new length, computed by `js_array_length` — +which is not a field read: it resolves Proxy arrays through the `get` trap and +probes the registered-Set/Map side tables. A statement-position `arr.push(x);` +discards that result, so push-heavy code paid an out-of-line runtime call per +push for a number nobody reads. + +The elision is gated on the `mem::take`n per-expression signal from +`dispatch::lower_expr` (#7590/#7591), which reaches exactly the statement's own +expression and never an operand — `n = arr.push(x)` and every other consuming +position still computes the real length, covered by a consuming-position test +matrix (call-argument / assignment / arithmetic / conditional / nested / +spread / boxed) that is byte-identical to Node. + +Measured: 2.77× on a pure statement-position push loop (9.0 → 3.25 ns/push — +the out-of-line call was also blocking loop optimization around it); ~3 % on +the GC-bound `json_pipeline` at 200k records. Verified live in traced IR: the +discarded push emits zero `js_array_length` calls, the consumed push exactly +one per specialization. diff --git a/crates/perry-codegen/src/expr/array_push.rs b/crates/perry-codegen/src/expr/array_push.rs index 82b2c07ba1..998798b378 100644 --- a/crates/perry-codegen/src/expr/array_push.rs +++ b/crates/perry-codegen/src/expr/array_push.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Result}; use perry_hir::Expr; +use crate::nanbox::double_literal; use crate::native_value::{ BoundsState, BufferAccessMode, ExpectedNativeRep, LoweredValue, MaterializationReason, NativeRep, SemanticKind, @@ -24,16 +25,37 @@ use super::{ TypedFeedbackContract, TypedFeedbackKind, }; -fn emit_array_handle_length(ctx: &mut FnCtx<'_>, array_handle: &str) -> String { +/// The expression's result: the new length per ES2024 `Array.prototype.push`. +/// +/// `js_array_length` is NOT a field read — it resolves Proxy arrays through +/// the `get` trap and probes the registered-Set/Map side tables — and a +/// statement-position `arr.push(x);` discards its result, so on push-heavy +/// workloads it was 8–13% of the run computing a number nobody reads. +/// `value_discarded` is the `mem::take`n per-expression signal from +/// `dispatch::lower_expr` (#7590: it reaches exactly the statement's own +/// expression, never an operand — a consumed `n = arr.push(x)` always +/// computes the real length). When set, the placeholder constant is returned +/// without emitting the call. +fn emit_array_handle_length( + ctx: &mut FnCtx<'_>, + array_handle: &str, + value_discarded: bool, +) -> String { + if value_discarded { + return double_literal(0.0); + } let blk = ctx.block(); let len_i32 = blk.call(I32, "js_array_length", &[(I64, array_handle)]); blk.sitofp(I32, &len_i32, DOUBLE) } -fn emit_array_box_length(ctx: &mut FnCtx<'_>, array_box: &str) -> String { +fn emit_array_box_length(ctx: &mut FnCtx<'_>, array_box: &str, value_discarded: bool) -> String { + if value_discarded { + return double_literal(0.0); + } let blk = ctx.block(); let array_handle = unbox_to_i64(blk, array_box); - emit_array_handle_length(ctx, &array_handle) + emit_array_handle_length(ctx, &array_handle, false) } fn lower_array_push_value( @@ -69,7 +91,7 @@ fn lower_array_push_value( Ok((value_double, Some(value_bits))) } -pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { +pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> Result { match expr { Expr::ArrayPush { array_id, value } => { // Resolve the array storage in priority order: closure @@ -254,8 +276,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ); ctx.current_block = merge_idx; + if value_discarded { + // Skip the slot reload too — it only feeds the length. + return Ok(double_literal(0.0)); + } let current_box = ctx.block().load(DOUBLE, &slot); - return Ok(emit_array_box_length(ctx, ¤t_box)); + return Ok(emit_array_box_length(ctx, ¤t_box, false)); } // Fast path: local-bound, non-captured, non-boxed array. @@ -527,8 +553,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } ctx.current_block = merge_idx; + if value_discarded { + // Skip the slot reload too — it only feeds the length. + return Ok(double_literal(0.0)); + } let current_box = ctx.block().load(DOUBLE, &slot); - return Ok(emit_array_box_length(ctx, ¤t_box)); + return Ok(emit_array_box_length(ctx, ¤t_box, false)); } let blk = ctx.block(); @@ -566,7 +596,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // which would clobber the box pointer in the capture slot with // the array pointer, so the next push would treat the array as // the box and silently lose the realloc write-back. - return Ok(emit_array_handle_length(ctx, &new_handle)); + return Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)); } else if let Some(slot) = ctx.locals.get(array_id).cloned() { let blk = ctx.block(); let box_ptr = blk.load(I64, &slot); @@ -577,7 +607,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // The slot holds the BOX pointer — the box is the shared // storage. Return so the slot keeps pointing at the box (see // the captured branch above). - return Ok(emit_array_handle_length(ctx, &new_handle)); + return Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)); } // #5459: `array_id` is in `boxed_vars` but has no box location in // THIS context — it's a module-level global accessed directly from @@ -609,7 +639,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } else { return Err(anyhow!("ArrayPush({}): local not in scope", array_id)); } - Ok(emit_array_handle_length(ctx, &new_handle)) + Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)) } // `arr.push(...src)` — HIR variant carrying the destination @@ -654,7 +684,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // pointing at the box. Return so we don't fall through to the // capture-slot store, which would clobber the box pointer (see // the matching note in `Expr::ArrayPush`). - return Ok(emit_array_handle_length(ctx, &new_handle)); + return Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)); } else if let Some(slot) = ctx.locals.get(array_id).cloned() { let blk = ctx.block(); let box_ptr = blk.load(I64, &slot); @@ -662,7 +692,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { blk.call_void("js_box_set_bits", &[(I64, &box_ptr), (I64, &new_bits)]); // Gen-GC Phase C2: barrier the box parent (see capture path). emit_write_barrier(ctx, &box_ptr, &new_bits); - return Ok(emit_array_handle_length(ctx, &new_handle)); + return Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)); } // #5459: in `boxed_vars` but no box location here — a module-level // global accessed directly from a nested function. Fall through to @@ -691,7 +721,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } else { return Err(anyhow!("ArrayPushSpread({}): local not in scope", array_id)); } - Ok(emit_array_handle_length(ctx, &new_handle)) + Ok(emit_array_handle_length(ctx, &new_handle, value_discarded)) } // -------- Closures (Phase D.1) -------- diff --git a/crates/perry-codegen/src/expr/dispatch.rs b/crates/perry-codegen/src/expr/dispatch.rs index 401f0686e5..ac2c45f308 100644 --- a/crates/perry-codegen/src/expr/dispatch.rs +++ b/crates/perry-codegen/src/expr/dispatch.rs @@ -64,7 +64,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::PropertyGet { .. } => super::property_get::lower(ctx, expr), Expr::Conditional { .. } => super::conditional::lower(ctx, expr), Expr::ArrayPush { .. } | Expr::ArrayPushSpread { .. } => { - super::array_push::lower(ctx, expr) + super::array_push::lower(ctx, expr, value_discarded) } Expr::Closure { .. } => super::closure::lower(ctx, expr), Expr::New { .. } | Expr::NewDynamic { .. } | Expr::NewDynamicSpread { .. } => { diff --git a/test-files/test_array_push_expression_value.ts b/test-files/test_array_push_expression_value.ts new file mode 100644 index 0000000000..7f5054afd6 --- /dev/null +++ b/test-files/test_array_push_expression_value.ts @@ -0,0 +1,61 @@ +// `arr.push(x)` used as an EXPRESSION must evaluate to the new length +// (ES2024 Array.prototype.push step 5), even though the statement-position +// form's length computation is elided as dead (#7592 follow-up to #7590). +// +// The elision is gated on the same `mem::take`n per-expression signal that +// #7591 introduced, so it reaches exactly the statement's own expression and +// never an operand. Every consuming position below would return 0 if that +// signal ever leaked into operand lowering again — and the discarded forms at +// the end must keep pushing correctly, which is why a "does it still run" +// smoke test cannot catch a regression here. +const out: string[] = []; + +function check(label: string, got: number, want: number): void { + out.push(got === want ? label + ":ok" : label + ":WRONG got=" + got + " want=" + want); +} + +const a: number[] = []; +// consumed as a call argument +check("arg", a.push(10), 1); +// consumed by an assignment +let n = 0; +n = a.push(20); +check("assign", n, 2); +// consumed by arithmetic +check("binary", a.push(30) + 100, 103); +// consumed by a condition +check("ternary", a.push(40) > 0 ? 7 : 9, 7); +// consumed nested inside another push's argument +const b: number[] = []; +check("nested", b.push(a.push(50)), 1); +check("nested_value", b[0], 5); + +// pointer elements: same expression positions through the all-pointer tier +const objs: { v: number }[] = []; +check("obj_arg", objs.push({ v: 1 }), 1); +let m = 0; +m = objs.push({ v: 2 }); +check("obj_assign", m, 2); + +// spread form consumed +const c: number[] = [1, 2]; +const d: number[] = [3, 4, 5]; +check("spread", c.push(...d), 5); + +// the ordinary discarded forms still push correctly +a.push(60); +a.push(70); +objs.push({ v: 3 }); +c.push(9); + +console.log(out.join(" ")); +console.log("a", a.length, a[0], a[4], a[5], a[6]); +console.log("objs", objs.length, objs[0].v, objs[2].v); +console.log("c", c.length, c[5]); + +// a captured / boxed array takes the runtime fall-through path — cover it +let boxed: number[] = []; +const pushBoxed = (): number => boxed.push(1); +check("boxed_arg", pushBoxed(), 1); +boxed.push(2); +console.log("boxed", boxed.length);