From 71e8cfdb068404ddeb075f6c409b5fa07258be58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 10:58:06 +0000 Subject: [PATCH 01/11] perf(regex): run the pre-search safepoint poll on one search in 64 (#10166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The poll before each search costs 502 of the 4,792 instructions a hoisted `.test()` call takes, and measurement says it buys very little. It cannot cancel. `host::poll` returns `Ok(())` unconditionally, and `EngineError::Cancelled` has no producer anywhere in perry-runtime outside tests, where a test supplies its own cancelling closure to prove the engine's paths clean up. It performs no cycle stepping in practice either. With the poll removed entirely, `cycle_starts`, `completions` and `steps` were IDENTICAL across 48,000,000 allocation-free `.test()` calls interleaved with allocation churn — 5,286 steps on both arms. Every step comes from an allocation-site assist; the What it does retain is the one thing no witness could rule out: the option of servicing a due collection from a loop that allocates nothing, which is how a non-allocating mutator participates in an incremental cycle. Three witness designs failed to construct a program where that mattered, but "could not construct" is not "cannot happen". So the poll is strided rather than removed, keeping a participation point every 64 searches. The stride is a fixed constant, not an environment knob, so it does not owe the GC knob policy an OFF-state CI arm. Measured on perrymaster, both arms from one commit: hoisted .test() 4,792 -> 4,3xx instructions per call regex-replace-callback unchanged at n=700,000, all four GC counters within n=700,000 noise, checksum 203210458 on both arms --- .../tests/runtime_roots/perex_construction.rs | 55 +++++++++++++++++++ .../perry-runtime/src/regex/perex_runtime.rs | 50 +++++++++++++++-- scripts/gc_runtime_root_holders.json | 12 ++++ 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs index 08ec365e66..fbf11f0a06 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_construction.rs @@ -15,6 +15,61 @@ fn bytes(ptr: *const StringHeader) -> Vec { .to_vec() } } +/// The pre-search safepoint poll runs on one search in 64, not on every one. +/// +/// It costs 502 of the 4,792 instructions a hoisted `.test()` call takes and +/// buys very little (#10166): it cannot cancel — nothing in production +/// constructs `EngineError::Cancelled` and `host::poll` returns `Ok(())` +/// unconditionally — and removing it entirely left `cycle_starts`, +/// `completions` and `steps` identical across 48,000,000 allocation-free +/// calls. Striding keeps a participation point for a loop that allocates +/// nothing while recovering most of the cost. +/// +/// This asserts the poll path was taken AND skipped by counting it, rather +/// than asserting "nothing broke" — which a stride that never polls at all +/// would also satisfy. +/// +/// The expected count is a literal, not `SEARCHES / PRE_SEARCH_POLL_STRIDE`. +/// Deriving it made the test self-consistent at any stride — it passed +/// unchanged with the stride set to 1, which is to say it asserted nothing. +/// +/// Sabotage-proved after that fix: `PRE_SEARCH_POLL_STRIDE = 1` fails on the +/// pinned stride and on 128 polls against an expected 2; removing the +/// `poll_on_stride` call fails with 0. +#[test] +fn the_pre_search_poll_runs_on_one_search_in_sixty_four() { + use crate::regex::perex_runtime::{PRE_SEARCH_POLLS_RUN, PRE_SEARCH_POLL_STRIDE}; + + let scope = RuntimeHandleScope::new(); + let re = construct(&scope, b"[0-9]+", b""); + let subject = text(&scope, b"ab12 cd345;"); + + // One search per call: unanchored and matching, so it returns on its first + // attempt and the tick advances exactly once. + const SEARCHES: usize = 128; + let before = PRE_SEARCH_POLLS_RUN.with(std::cell::Cell::get); + for _ in 0..SEARCHES { + assert_eq!( + re.with_const_ptr(|re| subject.with_const_ptr(|s| crate::regex::js_regexp_test(re, s))), + 1, + "fixture: every call must run a search that matches" + ); + } + let ran = PRE_SEARCH_POLLS_RUN.with(std::cell::Cell::get) - before; + + // Pinned to literals on purpose. Deriving the expectation from + // PRE_SEARCH_POLL_STRIDE makes the test self-consistent at ANY stride: it + // passed unchanged with the stride set to 1, asserting nothing. + assert_eq!( + PRE_SEARCH_POLL_STRIDE, 64, + "this test pins the stride at 64; change both together" + ); + assert_eq!( + ran, 2, + "128 searches must run the poll twice — once per 64, not on every call" + ); +} + fn construct<'s>(scope: &'s RuntimeHandleScope, pattern: &[u8], flags: &[u8]) -> RuntimeHandle<'s> { let p = text(scope, pattern); let f = text(scope, flags); diff --git a/crates/perry-runtime/src/regex/perex_runtime.rs b/crates/perry-runtime/src/regex/perex_runtime.rs index 441f2bd95d..a4853ba871 100644 --- a/crates/perry-runtime/src/regex/perex_runtime.rs +++ b/crates/perry-runtime/src/regex/perex_runtime.rs @@ -240,6 +240,51 @@ crate::perry_thread_local! { }; } +/// One call in `PRE_SEARCH_POLL_STRIDE` runs the pre-search safepoint poll. +/// +/// The poll costs 502 instructions of the 4,792 a hoisted `.test()` call takes, +/// and measurement says it buys very little (#10166). It cannot cancel: nothing +/// in production constructs `EngineError::Cancelled`, and `host::poll` returns +/// `Ok(())` unconditionally. It performs no cycle stepping in practice either — +/// with it removed entirely, `cycle_starts`, `completions` and `steps` were +/// identical across 48,000,000 allocation-free calls interleaved with churn. +/// +/// What it does retain is the one thing a witness could not rule out: the +/// option of servicing a due collection from a loop that allocates nothing, +/// which is how a non-allocating mutator participates in an incremental cycle. +/// That is why it is strided rather than removed. A stride of 64 keeps a +/// participation point every 64 searches while recovering most of the cost. +pub(crate) const PRE_SEARCH_POLL_STRIDE: usize = 64; + +crate::perry_thread_local! { + /// Counts searches for the stride above. A tick count, never an address. + static PRE_SEARCH_POLL_TICK: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +crate::perry_thread_local! { + /// Test-only: how many pre-search polls actually ran, so a test can assert + /// the stride took the poll path rather than infer it from a timing. + pub(crate) static PRE_SEARCH_POLLS_RUN: std::cell::Cell = + const { std::cell::Cell::new(0) }; +} + +/// Run the pre-search poll on one call in `PRE_SEARCH_POLL_STRIDE`. +#[inline] +fn poll_on_stride(poll: &mut impl FnMut() -> Result<(), EngineError>) -> Result<(), EngineError> { + let due = PRE_SEARCH_POLL_TICK.with(|tick| { + let next = tick.get().wrapping_add(1); + tick.set(next); + next % PRE_SEARCH_POLL_STRIDE == 0 + }); + if !due { + return Ok(()); + } + #[cfg(test)] + PRE_SEARCH_POLLS_RUN.with(|n| n.set(n.get() + 1)); + poll() +} + /// What a lent attempt produced: an answer, or a reason to run the owned path. enum Lent<'mem> { Done(Option>, Position), @@ -336,10 +381,7 @@ fn find_near_lent<'mem, S: ImmutableSubject>( frames: &mut cell.frames[..], undo: &mut cell.undo[..], }; - // PROBE ONLY (#10166 poll experiment) — NEVER MERGE. Prices the - // pre-search safepoint poll by removing it. Unsafe by construction: in - // a loop that allocates nothing this is the only safepoint, so an open - // budgeted cycle can go unstepped with its mark barrier armed. + poll_on_stride(poll)?; let mut search = match near { Some(near) => Search::new_near(resources, start, near, scratch, *budget), None => Search::new(resources, start, scratch, *budget), diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 9ea5aa6c2e..0e3ed1f219 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -979,6 +979,18 @@ "verdict": "not_a_gc_pointer", "why": "Per-thread match scratch lent to one search at a time (#10166): registers are subject offsets in UTF-16 units, frames and undo entries are perex's own opaque evaluator scratch. No GC pointer is ever stored, exactly as for the per-call MatchBuffers it replaces (this module's header and perex_memory.rs). The cell is borrowed for one search and holds no program, subject or result: those stay in GcProgram/HeapSubject roots and in the operation's MemoryBudget-charged slots." }, + { + "file": "crates/perry-runtime/src/regex/perex_runtime.rs", + "name": "PRE_SEARCH_POLLS_RUN", + "verdict": "test_only", + "why": "#[cfg(test)] Cell counting pre-search polls that actually ran, so a test can assert the stride took and skipped the poll path rather than infer it from a timing (#10166). A count, never a pointer; absent from shipped binaries." + }, + { + "file": "crates/perry-runtime/src/regex/perex_runtime.rs", + "name": "PRE_SEARCH_POLL_TICK", + "verdict": "not_a_gc_pointer", + "why": "Cell search counter for the pre-search safepoint poll's stride (#10166): one search in PRE_SEARCH_POLL_STRIDE runs the poll. It holds a wrapping tick count, never an address, and nothing reads it but the stride test in poll_on_stride." + }, { "file": "crates/perry-runtime/src/regex/perex_split.rs", "name": "FORWARD_SPLITS", From 5a1552a7367b1c0318cd16789496e5a2ecb4f939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 12:07:53 +0000 Subject: [PATCH 02/11] docs(changelog): fragment for #10494 --- changelog.d/10494-pre-search-poll-stride.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/10494-pre-search-poll-stride.md diff --git a/changelog.d/10494-pre-search-poll-stride.md b/changelog.d/10494-pre-search-poll-stride.md new file mode 100644 index 0000000000..02ad943f72 --- /dev/null +++ b/changelog.d/10494-pre-search-poll-stride.md @@ -0,0 +1,3 @@ +### Faster + +- Regex calls spend about 9% fewer instructions. The GC safepoint check each search ran now happens on one search in 64, which measurement showed was doing no collection work on the other 63 (#10166). From 4893c4181c24a52f9725d26b947da95c0f0261f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 10:51:44 +0000 Subject: [PATCH 03/11] fix(codegen,runtime): remove the argument-count ceilings on dynamic calls Calling a function VALUE with more than 16 arguments was a hard codegen error (`closure call with 18 args (max 16)`) at three sites, and dynamic calls silently dropped arguments past several fixed widths: - `__perry_wrap_*` function-value wrappers took at most 16 params, so apply/call/spread/object-literal-method calls of an 18-param function passed 0 for params 17 and 18 (class-method wrappers stopped at 32); - `dispatch_with_arity` (> 32 declared params) and `dispatch_rest_bundled` (>= 16 fixed params, including `arguments` users) returned `undefined` without calling the body; - `Reflect.apply` dispatched every list of 4+ arguments through `js_closure_call4`; - setTimeout/setInterval/process.nextTick clamped trailing args to 9. Closure-value call sites now share `emit_closure_handle_call`: up to 16 args keep `js_closure_call{N}`, wider calls marshal a stack buffer into `js_closure_call_array`. Wrappers take every declared param. Bodies wider than the exact runtime arms are called through a padded ladder of widths (64/256/1024 slots, undefined-filled) in `closure::wide_call`, and the wide `js_closure_call_array` arm resolves its route with one memoized strategy probe. qs 6.15.3 compiles from source. --- crates/perry-codegen/src/codegen/artifacts.rs | 31 +- crates/perry-codegen/src/codegen/helpers.rs | 2 +- .../perry-codegen/src/expr/static_method.rs | 26 +- .../lower_call/closure_call_arity_tests.rs | 142 +++ .../src/lower_call/console_promise.rs | 39 +- .../src/lower_call/early_branches.rs | 56 +- .../src/lower_call/extern_func.rs | 40 +- crates/perry-codegen/src/lower_call/mod.rs | 49 + .../src/lower_call/namespace_call.rs | 26 +- crates/perry-runtime/src/builtins/globals.rs | 14 +- crates/perry-runtime/src/closure/dispatch.rs | 1 - .../src/closure/dispatch/value_call.rs | 38 +- crates/perry-runtime/src/closure/mod.rs | 2 + crates/perry-runtime/src/closure/registry.rs | 35 +- crates/perry-runtime/src/closure/wide_call.rs | 270 +++++ crates/perry-runtime/src/proxy.rs | 9 +- .../perry-runtime/src/proxy/reflect_misc.rs | 29 + crates/perry-runtime/src/timer.rs | 19 +- test-files/_helpers/call_arity_10420.ts | 133 +++ .../test_gap_10420_call_arity_limits.ts | 993 ++++++++++++++++++ 20 files changed, 1747 insertions(+), 207 deletions(-) create mode 100644 crates/perry-codegen/src/lower_call/closure_call_arity_tests.rs create mode 100644 crates/perry-runtime/src/closure/wide_call.rs create mode 100644 test-files/_helpers/call_arity_10420.ts create mode 100644 test-files/test_gap_10420_call_arity_limits.ts diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 4bb32f4e03..695ecc65a0 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -777,16 +777,17 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { // dead-code elimination at link time will remove unused ones. for f in &hir.functions { let original_name = func_names.get(&f.id).cloned().unwrap(); - // Wrapper signature: i64 closure_ptr + N doubles for args. Cap at 16 to - // match the `js_closure_call0..16` dispatch family (the closure-call ABI - // tops out at 16 positional args; a function with more must be reached - // via a rest-bundling path). Pre-fix this was capped at 5 with a stale - // "js_closure_call only goes up to 5 args" comment, so any function - // invoked as a closure value (object-literal method, callback, `apply` - // target) with 6+ params silently dropped every argument past the 5th - // — e.g. test262's `TemporalHelpers.assertDuration(d, y, mo, w, d, h, …)` - // (11 args) read `hours` onward as 0. - let arity = f.params.len().min(16); + // Wrapper signature: i64 closure_ptr + one double per declared param — + // the full ABI arity `user_fn_wrapper_arity` registers for dynamic + // dispatch. Any cap here silently drops arguments: at 5, test262's + // `TemporalHelpers.assertDuration(d, y, mo, w, d, h, …)` (11 args) read + // `hours` onward as 0; at 16 (#10420), every `apply`/`call`/spread or + // object-literal-method call of an 18-param function passed `0` for + // params 17 and 18, because the runtime dispatched the registered + // 18-slot signature into a 16-param wrapper. Calls wider than the + // `js_closure_call0..16` family reach the wrapper through + // `js_closure_call_array`. + let arity = f.params.len(); let arg_names: Vec = (0..arity).map(|i| format!("%a{}", i)).collect(); let mut wrap_params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; for name in &arg_names { @@ -839,7 +840,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { continue; } - let arity = f.params.len().min(16); + let arity = f.params.len(); let mut params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; params.extend((0..arity).map(|i| (DOUBLE, format!("%a{}", i)))); let alias = llmod.define_function(&alias_wrap, DOUBLE, params); @@ -1064,9 +1065,9 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { let target_wrap = format!("__perry_wrap_{}", target); if !llmod.has_function(&raw_wrap) && emitted_aliases.insert(raw_wrap.clone()) { if llmod.has_function(&target_wrap) { - // Match the canonical wrapper's closure-call ABI (up to - // 16 positional arguments), including renamed exports. - let arity = function.map(|f| f.params.len().min(16)).unwrap_or(5); + // Match the canonical wrapper's closure-call ABI (one + // double per declared param), including renamed exports. + let arity = function.map(|f| f.params.len()).unwrap_or(5); let mut params = vec![(I64, "%this_closure".to_string())]; params.extend((0..arity).map(|i| (DOUBLE, format!("%a{}", i)))); let wf = llmod.define_function(&raw_wrap, DOUBLE, params.clone()); @@ -1234,7 +1235,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { if !emitted_wrappers.insert(wrap_name.clone()) { continue; } - let arity = method.params.len().min(32); + let arity = method.params.len(); let mut wrap_params: Vec<(LlvmType, String)> = vec![(I64, "%this_closure".to_string())]; for i in 0..arity { wrap_params.push((DOUBLE, format!("%a{}", i))); diff --git a/crates/perry-codegen/src/codegen/helpers.rs b/crates/perry-codegen/src/codegen/helpers.rs index aa7ff04a61..5f8b1e1b16 100644 --- a/crates/perry-codegen/src/codegen/helpers.rs +++ b/crates/perry-codegen/src/codegen/helpers.rs @@ -1494,7 +1494,7 @@ pub(super) fn emit_namespace_populator( // not be collapsed to `_constructor` here (#7964). sanitize_member(source_local) ); - let arity = (*param_count).min(16); + let arity = *param_count; let mut wrapper_params: Vec = vec![I64]; wrapper_params.extend(std::iter::repeat_n(DOUBLE, arity)); ctx.pending_declares diff --git a/crates/perry-codegen/src/expr/static_method.rs b/crates/perry-codegen/src/expr/static_method.rs index ce0c5dd37b..f3c6d2d1cf 100644 --- a/crates/perry-codegen/src/expr/static_method.rs +++ b/crates/perry-codegen/src/expr/static_method.rs @@ -360,33 +360,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let blk = ctx.block(); unbox_to_i64(blk, closure_box) }; - if lowered.len() <= 16 { - let runtime_fn = format!("js_closure_call{}", lowered.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for value in lowered.iter() { - call_args.push((DOUBLE, value.as_str())); - } - return Ok(ctx.block().call(DOUBLE, &runtime_fn, &call_args)); - } - // #3527: namespace members backed by exported // closure getters need the same arbitrary-arity // array dispatch as ordinary closure values. // Effect's `Layer.mergeAll` is a rest closure // and OpenCode passes 18 layers here. - let n = lowered.len(); - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - let blk = ctx.block(); - for (i, value) in lowered.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]); - blk.store(DOUBLE, value, &slot); - } - let argc = n.to_string(); - Ok(blk.call( - DOUBLE, - "js_closure_call_array", - &[(I64, &closure_handle), (PTR, &buf), (I64, &argc)], + Ok(crate::lower_call::emit_closure_handle_call( + ctx, + &closure_handle, + &lowered, )) }, )?; diff --git a/crates/perry-codegen/src/lower_call/closure_call_arity_tests.rs b/crates/perry-codegen/src/lower_call/closure_call_arity_tests.rs new file mode 100644 index 0000000000..b517a1a2ea --- /dev/null +++ b/crates/perry-codegen/src/lower_call/closure_call_arity_tests.rs @@ -0,0 +1,142 @@ +//! #10420: calls through a function VALUE have no argument-count ceiling. +//! +//! A closure-typed local called with more than 16 arguments used to fail to +//! compile (`closure call with 18 args (max 16)`), and the `__perry_wrap_*` +//! value wrappers of top-level functions were capped at 16 params, so every +//! dynamic call of an 18-param function (`apply`/`call`/spread/object-literal +//! method) passed `0` for params 17 and 18. Up to 16 arguments must keep the +//! per-arity `js_closure_call{N}` fast path; wider calls marshal a stack buffer +//! into `js_closure_call_array`. + +use crate::{compile_module, CompileOptions}; +use perry_hir::types::{FunctionType, Type}; +use perry_hir::{Expr, Function, Module, Param, Stmt}; + +fn param(id: u32) -> Param { + Param { + id, + name: format!("p{id}"), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + } +} + +fn params(base: u32, count: usize) -> Vec { + (0..count as u32).map(|i| param(base + i)).collect() +} + +fn function_type(count: usize) -> Type { + Type::Function(FunctionType { + params: (0..count) + .map(|i| (format!("p{i}"), Type::Any, false)) + .collect(), + return_type: Box::new(Type::Any), + is_async: false, + is_generator: false, + }) +} + +fn ir(module: &Module) -> String { + let opts = CompileOptions { + emit_ir_only: true, + ..Default::default() + }; + String::from_utf8(compile_module(module, opts).expect("fixture must compile")) + .expect("LLVM IR is UTF-8") +} + +/// `const f: (p0, …) => any = (p0, …) => p; f(1, 2, …, argc)`. +fn closure_value_call_ir(file: &str, argc: usize) -> String { + let mut module = Module::new(file); + module.init.push(Stmt::Let { + id: 1, + name: "f".to_string(), + ty: function_type(argc), + mutable: false, + init: Some(Expr::Closure { + func_id: 7, + params: params(100, argc), + return_type: Type::Any, + body: vec![Stmt::Return(Some(Expr::LocalGet(100 + argc as u32 - 1)))], + captures: Vec::new(), + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: true, + is_async: false, + is_generator: false, + is_strict: true, + }), + }); + module.init.push(Stmt::Expr(Expr::Call { + callee: Box::new(Expr::LocalGet(1)), + args: (1..=argc).map(|i| Expr::Number(i as f64)).collect(), + type_args: Vec::new(), + byte_offset: 0, + })); + ir(&module) +} + +#[test] +fn a_wide_closure_value_call_compiles_through_the_array_path() { + let ir = closure_value_call_ir("closure_call_arity_wide.ts", 18); + assert!( + ir.contains("call double @js_closure_call_array(i64 ") && ir.contains(", i64 18)"), + "an 18-argument closure-value call must dispatch through \ + js_closure_call_array with argc 18:\n{ir}" + ); + assert!( + !ir.contains("@js_closure_call18("), + "no fixed-arity entry point exists past 16:\n{ir}" + ); +} + +#[test] +fn a_narrow_closure_value_call_keeps_the_fixed_arity_fast_path() { + let ir = closure_value_call_ir("closure_call_arity_narrow.ts", 3); + assert!( + ir.contains("call double @js_closure_call3(i64 "), + "a 3-argument closure-value call must keep js_closure_call3:\n{ir}" + ); + assert!( + !ir.contains("call double @js_closure_call_array("), + "a 3-argument call must not take the array path:\n{ir}" + ); +} + +#[test] +fn function_value_wrappers_take_every_declared_param() { + let mut module = Module::new("closure_call_arity_wrapper.ts"); + module.functions = vec![Function { + id: 1, + name: "wide".to_string(), + type_params: Vec::new(), + params: params(10, 18), + return_type: Type::Any, + body: vec![Stmt::Return(Some(Expr::LocalGet(27)))], + is_async: false, + is_generator: false, + is_strict: true, + was_plain_async: false, + was_unrolled: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + }]; + let ir = ir(&module); + let define = ir + .lines() + .find(|line| { + line.starts_with("define") && line.contains("@__perry_wrap_") && line.contains("wide(") + }) + .unwrap_or_else(|| panic!("expected the value wrapper of `wide`:\n{ir}")); + assert_eq!( + define.matches("double %a").count(), + 18, + "the wrapper must forward all 18 params, not a 16-param prefix: {define}" + ); +} diff --git a/crates/perry-codegen/src/lower_call/console_promise.rs b/crates/perry-codegen/src/lower_call/console_promise.rs index e0240a0a19..2961323cbb 100644 --- a/crates/perry-codegen/src/lower_call/console_promise.rs +++ b/crates/perry-codegen/src/lower_call/console_promise.rs @@ -1640,43 +1640,22 @@ fn lower_closure_call_rooted<'a>( lowered_args.push(group.reread(ctx, arg_base + i)?); } - let result = if lowered_args.len() <= 16 { - let runtime_fn = if receiverless_one_arg { - "js_closure_call1_receiverless".to_string() - } else { - format!("js_closure_call{}", lowered_args.len()) - }; - let blk = ctx.block(); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = vec![(I64, &closure_handle)]; - for v in &lowered_args { - call_args.push((DOUBLE, v.as_str())); - } - blk.call(DOUBLE, &runtime_fn, &call_args) + let result = if receiverless_one_arg { + ctx.block().call( + DOUBLE, + "js_closure_call1_receiverless", + &[(I64, &closure_handle), (DOUBLE, &lowered_args[0])], + ) } else { - // #3527: > 16 args — stack-allocate a `[N x double]` array (entry-block - // alloca, see #167), store each lowered arg, and dispatch through the - // variadic `js_closure_call_array(closure_i64, args_ptr, argc)`. This - // mirrors the `js_native_call_value` marshaling used elsewhere in - // lower_call. `args_ptr` is non-null here since argc > 16 > 0. + // #3527: > 16 args marshal into an entry-block `[N x double]` buffer and + // dispatch through the variadic `js_closure_call_array`. // // #7154: the stores happen below the unbox now. A stack buffer is not a // GC root, so filling it above an allocating rebind would freeze // pre-move addresses into it — the same staleness one indirection // further out. The stores have no observable effect, so moving them // below the throw-capable unbox changes nothing else. - let n = lowered_args.len(); - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - let blk = ctx.block(); - for (i, v) in lowered_args.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf, &[(I64, &format!("{}", i))]); - blk.store(DOUBLE, v, &slot); - } - let argc = n.to_string(); - blk.call( - DOUBLE, - "js_closure_call_array", - &[(I64, &closure_handle), (PTR, &buf), (I64, &argc)], - ) + super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args) }; // #7211: re-read the saved implicit `this` from its slot. Mandatory, not diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index cfe395a87d..88c93a3785 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -10,7 +10,7 @@ //! Each `try_lower_*` returns `Ok(Some(s))` when it handled the call, //! `Ok(None)` to let the caller try the next branch. -use anyhow::{bail, Result}; +use anyhow::Result; use perry_hir::types::Type as HirType; use perry_hir::Expr; @@ -351,20 +351,12 @@ pub fn try_lower_current_step_closure_call( for a in args { lowered_args.push(lower_expr(ctx, a)?); } - if lowered_args.len() > 16 { - bail!( - "perry-codegen Phase D.1: CurrentStepClosure call with {} args (max 16)", - lowered_args.len() - ); - } - let blk = ctx.block(); - let closure_handle = unbox_to_i64(blk, &recv_box); - let runtime_fn = format!("js_closure_call{}", lowered_args.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = vec![(I64, &closure_handle)]; - for v in &lowered_args { - call_args.push((DOUBLE, v.as_str())); - } - return Ok(Some(blk.call(DOUBLE, &runtime_fn, &call_args))); + let closure_handle = unbox_to_i64(ctx.block(), &recv_box); + return Ok(Some(super::emit_closure_handle_call( + ctx, + &closure_handle, + &lowered_args, + ))); } Ok(None) } @@ -445,12 +437,10 @@ pub fn try_lower_closure_typed_local_call( // FuncRef calls (direct function-symbol dispatch) keep their // static-bundling at lower_call.rs:444+ because they don't go // through js_closure_callN. - if lowered_args.len() > 16 { - bail!( - "perry-codegen Phase D.1: closure call with {} args (max 16)", - lowered_args.len() - ); - } + // + // #10420: no arity ceiling here. More than 16 arguments dispatch + // through `js_closure_call_array` (`emit_closure_handle_call`); the + // exact-closure direct arm below is an ordinary N-argument call. // Re-read below the argument lowering, THEN unmask: the unmask // must consume the post-relocation address. let recv_box = callee_group.reread(ctx, callee_root)?; @@ -531,12 +521,8 @@ pub fn try_lower_closure_typed_local_call( ctx.current_block = fallback_idx; let prev_this = crate::rooting::implicit_this_save(ctx, &undef_this); - let runtime_fn = format!("js_closure_call{}", lowered_args.len()); - let mut fallback_args: Vec<(crate::types::LlvmType, &str)> = - Vec::with_capacity(lowered_args.len() + 1); - fallback_args.push((I64, &closure_handle)); - fallback_args.extend(lowered_args.iter().map(|value| (DOUBLE, value.as_str()))); - let fallback_value = ctx.block().call(DOUBLE, &runtime_fn, &fallback_args); + let fallback_value = + super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); crate::rooting::implicit_this_restore(ctx, prev_this); let after_fallback = ctx.block().label.clone(); if !ctx.block().is_terminated() { @@ -1183,13 +1169,8 @@ pub fn try_lower_closure_typed_local_call( } else { None }; - let runtime_fn = format!("js_closure_call{}", lowered_args.len()); - let mut fallback_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in &lowered_args { - fallback_args.push((DOUBLE, v.as_str())); - } - let fallback_value = ctx.block().call(DOUBLE, &runtime_fn, &fallback_args); + let fallback_value = + super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); // Inner save, released inside its own arm — so the outer // slot (restored in the merge block) is still live and the // temp-root depth matches on both paths into the merge. @@ -1223,12 +1204,7 @@ pub fn try_lower_closure_typed_local_call( // read `this`, so the reset is unconditional here. // #7211: rooted save/restore across the runtime-resolved callee. let prev_this = crate::rooting::implicit_this_save(ctx, &undef_this); - let runtime_fn = format!("js_closure_call{}", lowered_args.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = vec![(I64, &closure_handle)]; - for v in &lowered_args { - call_args.push((DOUBLE, v.as_str())); - } - let result = ctx.block().call(DOUBLE, &runtime_fn, &call_args); + let result = super::emit_closure_handle_call(ctx, &closure_handle, &lowered_args); crate::rooting::implicit_this_restore(ctx, prev_this); callee_group.release(ctx); return Ok(Some(result)); diff --git a/crates/perry-codegen/src/lower_call/extern_func.rs b/crates/perry-codegen/src/lower_call/extern_func.rs index c04d15dad8..bbbee2bdb7 100644 --- a/crates/perry-codegen/src/lower_call/extern_func.rs +++ b/crates/perry-codegen/src/lower_call/extern_func.rs @@ -1355,18 +1355,12 @@ pub fn try_lower_extern_func_call( let blk = ctx.block(); let closure_bits = blk.bitcast_double_to_i64(&closure_value); let closure_handle = blk.and(I64, &closure_bits, POINTER_MASK_I64); - let call_name = format!("js_closure_call{}", lowered_args.len().min(16)); - let mut decl_types = vec![I64]; - decl_types.extend(std::iter::repeat_n(DOUBLE, lowered_args.len().min(16))); - ctx.pending_declares - .push((call_name.clone(), DOUBLE, decl_types)); - let mut call_args: Vec<(crate::types::LlvmType, String)> = vec![(I64, closure_handle)]; - for arg in lowered_args.into_iter().take(16) { - call_args.push((DOUBLE, arg)); - } - let arg_refs: Vec<(crate::types::LlvmType, &str)> = - call_args.iter().map(|(t, s)| (*t, s.as_str())).collect(); - return Ok(Some(ctx.block().call(DOUBLE, &call_name, &arg_refs))); + // #10420: no 16-argument truncation — wider calls take the array path. + return Ok(Some(super::emit_closure_handle_call( + ctx, + &closure_handle, + &lowered_args, + ))); } // perry/system dispatch: map JS names (isDarkMode, getDeviceIdiom, // keychainSave, etc.) to their perry_system_* / perry_* C symbols. @@ -1866,12 +1860,6 @@ pub fn try_lower_extern_func_call( // an arrow-bound exported value (hono's `mergePath` from utils/url.js, // any `export const foo = () => …` cross-module use). if ctx.imported_vars.contains(name) { - if args.len() > 16 { - anyhow::bail!( - "perry-codegen Phase D.1: closure call with {} args (max 16)", - args.len() - ); - } ctx.pending_declares.push((fname.clone(), DOUBLE, vec![])); // Fetch the callee before evaluating arguments, as JavaScript requires, // but keep that closure rooted while argument expressions run. Next's @@ -1895,16 +1883,14 @@ pub fn try_lower_extern_func_call( |ctx, closure_box| { // Re-read and unbox only after every collecting argument and // after the argument group's own re-reads have completed. + // #10420: more than 16 arguments dispatch through the array path. let lowered = lowered_args.borrow(); - let blk = ctx.block(); - let closure_handle = unbox_to_i64(blk, closure_box); - let runtime_fn = format!("js_closure_call{}", lowered.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for value in lowered.iter() { - call_args.push((DOUBLE, value.as_str())); - } - Ok(blk.call(DOUBLE, &runtime_fn, &call_args)) + let closure_handle = unbox_to_i64(ctx.block(), closure_box); + Ok(super::emit_closure_handle_call( + ctx, + &closure_handle, + &lowered, + )) }, )?; return Ok(Some(result)); diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index deee0dc456..77cffef49f 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -39,6 +39,10 @@ mod builtin; mod builtin_table_gate; mod capture_writeback; mod closure_analysis; +/// #10420: closure-value calls with more than 16 arguments compile and take +/// the array path; function-value wrappers keep every declared param. +#[cfg(test)] +mod closure_call_arity_tests; mod console_promise; /// Rooting and evaluation-order coverage for the `console.*` arms slice 6 /// repaired (#7649) — see the module header for why these assert on IR. @@ -257,6 +261,51 @@ pub(crate) fn emit_rooted_call( result } +/// Widest call the per-arity `js_closure_call{N}` runtime entry points take. +pub(crate) const MAX_FIXED_CLOSURE_CALL_ARGS: usize = 16; + +/// Dispatch an unboxed closure handle over already-lowered arguments through +/// the closure-call ABI. +/// +/// Up to [`MAX_FIXED_CLOSURE_CALL_ARGS`] arguments use the per-arity +/// `js_closure_call{N}` register entry points — the fast path, unchanged. +/// Wider calls marshal the arguments into an entry-block `[N x double]` buffer +/// and dispatch through the variadic `js_closure_call_array(closure, args_ptr, +/// argc)`, which owns arbitrary-arity dispatch including rest bundling (#3527). +/// #10420: every closure-value call site used to either reject a 17th argument +/// at compile time or truncate the list to 16; they all route here now. +/// +/// The buffer is NOT a GC root, so callers pass values that are already valid +/// below their last collection point; nothing emitted between the stores and +/// the call can collect. +pub(crate) fn emit_closure_handle_call( + ctx: &mut FnCtx<'_>, + closure_handle: &str, + args: &[String], +) -> String { + use crate::types::{DOUBLE, I64, PTR}; + if args.len() <= MAX_FIXED_CLOSURE_CALL_ARGS { + let runtime_fn = format!("js_closure_call{}", args.len()); + let mut call_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(args.len() + 1); + call_args.push((I64, closure_handle)); + call_args.extend(args.iter().map(|value| (DOUBLE, value.as_str()))); + return ctx.block().call(DOUBLE, &runtime_fn, &call_args); + } + let n = args.len(); + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + let blk = ctx.block(); + for (i, value) in args.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]); + blk.store(DOUBLE, value, &slot); + } + let argc = n.to_string(); + blk.call( + DOUBLE, + "js_closure_call_array", + &[(I64, closure_handle), (PTR, &buf), (I64, &argc)], + ) +} + /// One array a rest/`arguments` call has to materialize from its argument /// list: every argument from `from` onwards, optionally flagged as an /// `arguments` object. diff --git a/crates/perry-codegen/src/lower_call/namespace_call.rs b/crates/perry-codegen/src/lower_call/namespace_call.rs index 3d5a91e551..70260d7f1b 100644 --- a/crates/perry-codegen/src/lower_call/namespace_call.rs +++ b/crates/perry-codegen/src/lower_call/namespace_call.rs @@ -349,32 +349,14 @@ pub fn try_lower_namespace_member_call( let blk = ctx.block(); unbox_to_i64(blk, closure_box) }; - if lowered.len() <= 16 { - let runtime_fn = format!("js_closure_call{}", lowered.len()); - let mut call_args: Vec<(crate::types::LlvmType, &str)> = - vec![(I64, &closure_handle)]; - for v in lowered.iter() { - call_args.push((DOUBLE, v.as_str())); - } - return Ok(ctx.block().call(DOUBLE, &runtime_fn, &call_args)); - } - // #3527: the runtime's array dispatcher owns arbitrary-arity // closure calls (including rest bundling). Keep the fixed-N // entry points as the small fast path and marshal wider calls // into a stack buffer after all rooted operand re-reads. - let n = lowered.len(); - let buf = ctx.func.alloca_entry_array(DOUBLE, n); - let blk = ctx.block(); - for (i, value) in lowered.iter().enumerate() { - let slot = blk.gep(DOUBLE, &buf, &[(I64, &i.to_string())]); - blk.store(DOUBLE, value, &slot); - } - let argc = n.to_string(); - Ok(blk.call( - DOUBLE, - "js_closure_call_array", - &[(I64, &closure_handle), (PTR, &buf), (I64, &argc)], + Ok(super::emit_closure_handle_call( + ctx, + &closure_handle, + &lowered, )) }, )?; diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index 6fc89dac9e..acbded0288 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -1151,7 +1151,7 @@ pub extern "C" fn js_drain_queued_microtasks() { pub(crate) fn drain_queued_microtasks_count() -> i32 { use crate::closure::{ js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3, js_closure_call4, - js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, js_closure_call9, + js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, }; let mut ran = 0; loop { @@ -1203,14 +1203,10 @@ pub(crate) fn drain_queued_microtasks_count() -> i32 { 8 => { js_closure_call8(cb_ptr, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); } - _ => { - // >= 9 args: clamp to 9. Mirrors the setTimeout - // dispatch fallback; real-world nextTick rarely - // exceeds 1-2 trailing args. - js_closure_call9( - cb_ptr, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], - ); - } + // #10420: more than 8 trailing args used to clamp to 9. + n => unsafe { + crate::closure::js_closure_call_array(cb_ptr as i64, a.as_ptr(), n as i64); + }, } crate::async_hooks::after(async_id); crate::async_hooks::destroy(async_id); diff --git a/crates/perry-runtime/src/closure/dispatch.rs b/crates/perry-runtime/src/closure/dispatch.rs index 54ec8dc8e4..b9c829849a 100644 --- a/crates/perry-runtime/src/closure/dispatch.rs +++ b/crates/perry-runtime/src/closure/dispatch.rs @@ -31,7 +31,6 @@ pub use errors::throw_not_callable; pub use validate::{clean_closure_ptr, dispatch_proxy_callee_or_throw, get_valid_func_ptr}; -pub(crate) use calln::{dispatch_registered_call, dispatch_rest_or_declared_arity}; pub use calln::{ js_closure_call0, js_closure_call1, js_closure_call10, js_closure_call11, js_closure_call12, js_closure_call13, js_closure_call14, js_closure_call15, js_closure_call16, diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index f2ecef6f9f..329d1381ab 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -655,10 +655,16 @@ pub unsafe extern "C" fn js_closure_call_array( // arg slice and dispatch through the strategy resolver so the // closure body is called with ALL its args (the old `_ => // js_closure_call16(...)` silently dropped args 16.. — breaking - // qs's recursive `stringify`, which self-calls with 18 args). For - // a plain (Direct) closure with no registered rest/arity, dispatch - // through `dispatch_with_arity` with the provided count so the body - // is transmuted to its real N-arg signature. + // qs's recursive `stringify`, which self-calls with 18 args). + // + // #10420: ONE memoized strategy probe decides the route, as in + // `js_closure_callN` — the registry helpers this arm used to chain + // re-read the body record on every call. A body with a registered + // arity is called at exactly that width: padded when it declares + // more than `n`, and never handed slots it does not declare (so a + // `fn.apply(null, arr)` with thousands of elements costs the body's + // own width). An unregistered body is a runtime-provided callee with + // a handful of params; clamp it to the widest dynamic call. _ => { let mut full: Vec = Vec::with_capacity(n); for i in 0..n { @@ -668,18 +674,20 @@ pub unsafe extern "C" fn js_closure_call_array( if func_ptr.is_null() { throw_not_callable(); } - if let Some(result) = dispatch_registered_call(closure, func_ptr, &full) { - return result; + match resolve_strategy(func_ptr).kind() { + DispatchKind::BoundMethod => dispatch_bound_method(closure, &full), + DispatchKind::BoundFunction => dispatch_bound_function(closure, &full), + DispatchKind::Rest(fixed_arity, synth) => { + dispatch_rest_bundled(closure, func_ptr, &full, fixed_arity, synth) + } + DispatchKind::Arity(declared) => { + dispatch_with_arity(closure, func_ptr, &full, declared) + } + DispatchKind::Direct => { + let width = n.min(crate::closure::MAX_DYNAMIC_CALL_WIDTH) as u32; + dispatch_with_arity(closure, func_ptr, &full, width) + } } - if let Some(result) = - dispatch_rest_or_declared_arity(closure, func_ptr, &full, n as u32) - { - return result; - } - // Direct closure: declared arity == provided count. Reuse the - // arity dispatcher (it transmutes to the concrete N-arg fn and - // forwards the slice unchanged when provided == declared). - dispatch_with_arity(closure, func_ptr, &full, n as u32) } } } diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index 4effa0b213..a7e4210220 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -12,6 +12,7 @@ mod dynamic_props; mod registry; mod unbox; mod v8_stubs; +mod wide_call; #[cfg(test)] mod tests; @@ -30,6 +31,7 @@ pub use alloc::{ pub(crate) use registry::closure_registry_census; pub(crate) use registry::DispatchKind; +pub(crate) use wide_call::{dispatch_wide_abi, MAX_DYNAMIC_CALL_WIDTH}; /// `PERRY_GC_CENSUS`: every closure-keyed side table outside the registries. pub(crate) fn closure_side_table_census() -> Vec { diff --git a/crates/perry-runtime/src/closure/registry.rs b/crates/perry-runtime/src/closure/registry.rs index 2ed6d2ba16..23fe0b38ef 100644 --- a/crates/perry-runtime/src/closure/registry.rs +++ b/crates/perry-runtime/src/closure/registry.rs @@ -1002,10 +1002,8 @@ pub unsafe fn build_rest_array(values: &[f64], arguments_object: bool) -> f64 { /// `fixed_arity` onwards is bundled into a fresh JS Array passed as the /// last arg. The body is then invoked with exactly `fixed_arity + 1` doubles. /// -/// Currently supports `fixed_arity` in `0..=15` — the same ceiling as -/// `js_closure_callN`. A program that defines a rest closure with more than -/// 15 fixed params before the rest is unsupported (and would already trip -/// the `Phase D.1: closure call with N args (max 16)` guard in lower_call). +/// `fixed_arity` 0..=15 have exact arms; wider bodies go through the padded +/// ladder in `wide_call` (#10420). #[inline(never)] pub unsafe fn dispatch_rest_bundled( closure: *const ClosureHeader, @@ -1282,10 +1280,18 @@ pub unsafe fn dispatch_rest_bundled( 14 => rest_arm!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13), 15 => rest_arm!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14), _ => { - // Unsupported arity — fall back to undefined so we don't - // mis-call the body and trigger UB. This mirrors the upper - // bound that lower_call's static-bundling path enforces. - f64::from_bits(crate::value::TAG_UNDEFINED) + // #10420: 16+ fixed params used to return `undefined` without + // calling the body. Lay out the full ABI — fixed params, rest + // array, then the `arguments` object when the body has one — and + // call through the padded ladder. + let mut slots: Vec = Vec::with_capacity(k + 2); + slots.extend((0..k).map(|i| a!(i))); + slots.push(rest_double); + if let Some(arguments_double) = all_arguments_double { + slots.push(arguments_double); + } + let width = slots.len(); + super::dispatch_wide_abi(closure, func_ptr, &slots, width) } } } @@ -1326,10 +1332,11 @@ pub unsafe fn dispatch_with_arity( } // One match arm per declared arity. Each arm transmutes `func_ptr` to // the concrete `(closure, f64 x N)` signature and forwards the (padded) - // args. Arities up to 32 are supported so high-arity closures dispatched + // args. Arities up to 32 have exact arms so high-arity closures dispatched // dynamically — e.g. qs's recursive `stringify`, which declares 18 // params and self-calls with 18 args (#3527) — call their body - // correctly instead of mis-calling and corrupting registers. The + // correctly instead of mis-calling and corrupting registers; wider + // bodies take the padded ladder in `wide_call` (#10420). The // `arm!` macro builds the fn type and the (padded) call args from the // arg-index token list; `arm!(@ty $i)` maps any index token to `f64`. macro_rules! arm { @@ -1405,11 +1412,9 @@ pub unsafe fn dispatch_with_arity( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 ), - _ => { - // Unsupported arity (>32 declared params). Fall back to - // undefined rather than mis-calling and triggering UB. - f64::from_bits(crate::value::TAG_UNDEFINED) - } + // #10420: more than 32 declared params used to return `undefined` + // without calling the body. + _ => super::dispatch_wide_abi(closure, func_ptr, args, k), } } diff --git a/crates/perry-runtime/src/closure/wide_call.rs b/crates/perry-runtime/src/closure/wide_call.rs new file mode 100644 index 0000000000..841e81a0c8 --- /dev/null +++ b/crates/perry-runtime/src/closure/wide_call.rs @@ -0,0 +1,270 @@ +//! Closure-body calls wider than the exact per-arity dispatch arms (#10420). +//! +//! Dynamic dispatch reaches a closure body by transmuting its `func_ptr` to +//! `extern "C" fn(*const ClosureHeader, f64, …) -> f64` with the body's ABI +//! width: its declared params, plus the rest array and the `arguments` slot +//! when it has them. Rust has no variadic function types, so every width needs +//! its own transmute — `dispatch_with_arity` spells out 0..=32 and +//! `dispatch_rest_bundled` 0..=15 fixed params. Past those, both returned +//! `undefined` WITHOUT calling the body. +//! +//! Wider bodies go through a short ladder of PADDED widths instead: the slots +//! are copied into a buffer of the next ladder width, the tail is filled with +//! `undefined`, and the body is called through that wider signature. That is +//! sound for the same reason every `js_closure_callN` call with more arguments +//! than the body declares already is: on each ABI Perry targets (SysV x86-64, +//! AAPCS64 including Apple's variant, Win64) scalar arguments are assigned +//! left to right and the stack argument area belongs to the caller, so a body +//! declaring N doubles reads exactly the first N slots and ignores the rest. + +use super::ClosureHeader; + +/// Widest closure-body ABI dynamic dispatch can reach. A body wider than this +/// declares more than a thousand parameters; calling it through `apply`/`call`/ +/// spread/a closure value throws a `RangeError` instead of silently skipping +/// the body. +pub(crate) const MAX_DYNAMIC_CALL_WIDTH: usize = 1024; + +/// Call `func_ptr` as a closure body whose ABI takes `width` doubles, passing +/// `args[i]` for `i < min(args.len(), width)` and `undefined` for the rest. +/// +/// Callers use this only past their exact arms, so `width` is at least 16. +/// `width` above [`MAX_DYNAMIC_CALL_WIDTH`] throws `RangeError`. +/// +/// # Safety +/// `func_ptr` must be a validated, non-sentinel closure body whose ABI width +/// is at most `width`. +#[inline(never)] +pub(crate) unsafe fn dispatch_wide_abi( + closure: *const ClosureHeader, + func_ptr: *const u8, + args: &[f64], + width: usize, +) -> f64 { + let undef = f64::from_bits(crate::value::TAG_UNDEFINED); + let provided = args.len().min(width); + + // `padded!(slots, [0], 1, + + …)` doubles the index list once per `+` + // (six doublings = 64 indices) and emits the transmuted call with one + // `f64` parameter per index. The indices are constant expressions into a + // fixed-size array, so the loads carry no bounds checks. + macro_rules! padded { + (@f64 $i:expr) => { f64 }; + ($slots:ident, [$($i:expr),+], $step:expr, + $($more:tt)*) => { + padded!($slots, [$($i,)+ $($i + $step),+], $step * 2, $($more)*) + }; + ($slots:ident, [$($i:expr),+], $step:expr,) => {{ + #[cfg(panic = "abort")] + let f: extern "C" fn(*const ClosureHeader $(, padded!(@f64 $i))+) -> f64 = + std::mem::transmute(func_ptr); + #[cfg(not(panic = "abort"))] + let f: extern "C-unwind" fn(*const ClosureHeader $(, padded!(@f64 $i))+) -> f64 = + std::mem::transmute(func_ptr); + f(closure $(, $slots[$i])+) + }}; + } + macro_rules! fill { + ($len:literal) => {{ + let mut slots = [undef; $len]; + slots[..provided].copy_from_slice(&args[..provided]); + slots + }}; + } + + match width { + 0..=64 => { + let slots = fill!(64); + padded!(slots, [0usize], 1usize, + + + + + +) + } + 65..=256 => { + let slots = fill!(256); + padded!(slots, [0usize], 1usize, + + + + + + + +) + } + 257..=MAX_DYNAMIC_CALL_WIDTH => { + let slots = fill!(1024); + padded!(slots, [0usize], 1usize, + + + + + + + + + +) + } + _ => throw_too_wide(width), + } +} + +#[cold] +#[inline(never)] +fn throw_too_wide(width: usize) -> ! { + let message = format!( + "Maximum call width exceeded: the callee takes {width} parameter slots \ + (at most {MAX_DYNAMIC_CALL_WIDTH} can be passed dynamically)" + ); + let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); + let err = crate::error::js_rangeerror_new(msg); + crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const UNDEF: u64 = crate::value::TAG_UNDEFINED; + + extern "C" fn body_40( + _: *const ClosureHeader, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, + a13: f64, + a14: f64, + a15: f64, + a16: f64, + a17: f64, + a18: f64, + a19: f64, + a20: f64, + a21: f64, + a22: f64, + a23: f64, + a24: f64, + a25: f64, + a26: f64, + a27: f64, + a28: f64, + a29: f64, + a30: f64, + a31: f64, + a32: f64, + a33: f64, + a34: f64, + a35: f64, + a36: f64, + a37: f64, + a38: f64, + a39: f64, + ) -> f64 { + let all = [ + a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, + a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32, a33, a34, a35, + a36, a37, a38, a39, + ]; + // Encode "which slots are undefined" and the sum of the defined ones so + // a dropped, shifted, or zero-filled slot changes the result. + let mut sum = 0.0; + let mut undefined_mask = 0u64; + for (i, v) in all.iter().enumerate() { + if v.to_bits() == UNDEF { + undefined_mask |= 1 << i; + } else { + sum += *v * (i as f64 + 1.0); + } + } + sum + (undefined_mask as f64) * 1.0e6 + } + + fn expected(provided: usize) -> f64 { + let mut sum = 0.0; + let mut undefined_mask = 0u64; + for i in 0..40 { + if i < provided { + sum += (i as f64 + 100.0) * (i as f64 + 1.0); + } else { + undefined_mask |= 1 << i; + } + } + sum + (undefined_mask as f64) * 1.0e6 + } + + #[test] + fn padded_widths_deliver_every_slot_in_order() { + let body = body_40 as *const u8; + let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); + // Exactly the declared width, through each ladder rung. + for width in [40usize, 64, 65, 256, 257, 1024] { + let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, width) }; + assert_eq!(result, expected(40), "width {width}"); + } + } + + #[test] + fn missing_slots_are_padded_with_undefined() { + let body = body_40 as *const u8; + let args: Vec = (0..23).map(|i| i as f64 + 100.0).collect(); + let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, 40) }; + assert_eq!(result, expected(23)); + } + + /// `dispatch_with_arity` past its 32 exact arms used to return `undefined` + /// without calling the body. + #[test] + fn declared_arity_past_the_exact_arms_reaches_the_body() { + let body = body_40 as *const u8; + let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); + let result = + unsafe { crate::closure::dispatch_with_arity(std::ptr::null(), body, &args, 40) }; + assert_eq!(result, expected(40)); + } + + extern "C" fn rest_after_16( + _: *const ClosureHeader, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + a6: f64, + a7: f64, + a8: f64, + a9: f64, + a10: f64, + a11: f64, + a12: f64, + a13: f64, + a14: f64, + a15: f64, + rest: f64, + ) -> f64 { + let fixed = [ + a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, + ]; + let rest = crate::value::js_nanbox_get_pointer(rest) as *const crate::array::ArrayHeader; + let rest_len = crate::array::js_array_length(rest); + let last = crate::array::js_array_get_f64(rest, rest_len - 1); + fixed.iter().sum::() * 1_000.0 + f64::from(rest_len) * 100.0 + last + } + + /// `dispatch_rest_bundled` with 16+ fixed params used to return `undefined` + /// without calling the body. + #[test] + fn rest_bundling_past_fifteen_fixed_params_reaches_the_body() { + let body = rest_after_16 as *const u8; + let args: Vec = (1..=20).map(f64::from).collect(); + let result = unsafe { + crate::closure::dispatch_rest_bundled( + std::ptr::null(), + body, + &args, + 16, + crate::closure::registry::RestDispatchKind::UserRest, + ) + }; + // fixed = 1..=16 (sum 136), rest = [17, 18, 19, 20]. + assert_eq!(result, 136.0 * 1_000.0 + 4.0 * 100.0 + 20.0); + } + + #[test] + fn slots_past_the_width_are_not_forwarded() { + let body = body_40 as *const u8; + let args: Vec = (0..40).map(|i| i as f64 + 100.0).collect(); + // A width of 30 means the body only owns 30 slots; 30..40 read as undefined. + let result = unsafe { dispatch_wide_abi(std::ptr::null(), body, &args, 30) }; + assert_eq!(result, expected(30)); + } +} diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 5251672421..f90c9d087c 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -920,7 +920,14 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { 1 => js_closure_call1(closure, a(0)), 2 => js_closure_call2(closure, a(0), a(1)), 3 => js_closure_call3(closure, a(0), a(1), a(2)), - _ => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), + 4 => crate::closure::js_closure_call4(closure, a(0), a(1), a(2), a(3)), + // #10425: this arm was `_ => js_closure_call4(…)`, so every argument + // after the fourth was dropped. The variadic entry point owns + // arbitrary-arity dispatch, rest bundling included. (`call_trap` + // above keeps its catch-all: a proxy trap receives at most four.) + n => unsafe { + crate::closure::js_closure_call_array(closure as i64, args.as_ptr(), n as i64) + }, }; crate::object::js_implicit_this_set(prev.get_nanbox_f64()); result diff --git a/crates/perry-runtime/src/proxy/reflect_misc.rs b/crates/perry-runtime/src/proxy/reflect_misc.rs index 9435174a02..a79c6ec259 100644 --- a/crates/perry-runtime/src/proxy/reflect_misc.rs +++ b/crates/perry-runtime/src/proxy/reflect_misc.rs @@ -366,3 +366,32 @@ pub(super) extern "C" fn proxy_revoke_trampoline( js_proxy_revoke(proxy); f64::from_bits(TAG_UNDEFINED) } + +#[cfg(test)] +mod tests { + use super::*; + + extern "C" fn weighted_six( + _: *const crate::closure::ClosureHeader, + a0: f64, + a1: f64, + a2: f64, + a3: f64, + a4: f64, + a5: f64, + ) -> f64 { + a0 + 10.0 * a1 + 100.0 * a2 + 1_000.0 * a3 + 10_000.0 * a4 + 100_000.0 * a5 + } + + /// #10425: `Reflect.apply` dispatched every list of four or more arguments + /// through `js_closure_call4`, so the body read whatever the fifth and sixth + /// argument registers held. + #[test] + fn reflect_apply_forwards_every_argument() { + let closure = crate::closure::js_closure_alloc(weighted_six as *const u8, 0); + let f = crate::value::js_nanbox_pointer(closure as i64); + let args = array_from_args(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + let result = js_reflect_apply(f, f64::from_bits(TAG_UNDEFINED), args); + assert_eq!(result, 654_321.0); + } +} diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index e99d6bb8c0..7e5941ad83 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -1238,7 +1238,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { crate::perf_hooks::note_event_loop_start(); use crate::closure::{ js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3, js_closure_call4, - js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, js_closure_call9, + js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, }; if in_timer_callback_dispatch() { @@ -1349,12 +1349,10 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { 8 => { js_closure_call8(cb, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); } - _ => { - // >= 9 args: clamp to 9. Real-world setTimeout - // rarely exceeds 1-2 trailing args; this is a - // conservative safety net rather than spec coverage. - js_closure_call9(cb, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]); - } + // #10420: more than 8 trailing args used to clamp to 9. + n => unsafe { + crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), n as i64); + }, } }); // #3870: Node runs a microtask checkpoint after *each* timer @@ -1679,7 +1677,7 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { crate::promise::bump(&PROFILE_INTERVAL_TIMER_TICKS); use crate::closure::{ js_closure_call0, js_closure_call1, js_closure_call2, js_closure_call3, js_closure_call4, - js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, js_closure_call9, + js_closure_call5, js_closure_call6, js_closure_call7, js_closure_call8, }; if in_timer_callback_dispatch() { @@ -1746,7 +1744,10 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { 6 => js_closure_call6(cb, a[0], a[1], a[2], a[3], a[4], a[5]), 7 => js_closure_call7(cb, a[0], a[1], a[2], a[3], a[4], a[5], a[6]), 8 => js_closure_call8(cb, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]), - _ => js_closure_call9(cb, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]), + // #10420: more than 8 trailing args used to clamp to 9. + n => unsafe { + crate::closure::js_closure_call_array(cb as i64, a.as_ptr(), n as i64) + }, }; }); crate::async_hooks::after(async_id); diff --git a/test-files/_helpers/call_arity_10420.ts b/test-files/_helpers/call_arity_10420.ts new file mode 100644 index 0000000000..2595752b54 --- /dev/null +++ b/test-files/_helpers/call_arity_10420.ts @@ -0,0 +1,133 @@ +// Helper module for test_gap_10420_call_arity_limits.ts (#10420): function +// VALUES exported across a module boundary -- an arrow `const`, a qs-shaped +// named function expression bound to a `var`, and a rest arrow. + +export function sig(values: any[]): string { + let sum = 0; + for (let i = 0; i < values.length; i++) sum += values[i]; + return `${values.length}:${values[0]}:${values[values.length - 1]}:${sum}`; +} + +export const importedArrow3 = ( + p0: any, p1: any, p2: any, +): string => + sig([ + p0, p1, p2, + ]); + +export var importedFexpr3 = function importedFexpr3( + p0: any, p1: any, p2: any, +): string { + return sig([ + p0, p1, p2, + ]); +}; + +export const importedArrow16 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +): string => + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); + +export var importedFexpr16 = function importedFexpr16( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); +}; + +export const importedArrow17 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +): string => + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); + +export var importedFexpr17 = function importedFexpr17( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); +}; + +export const importedArrow18 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, +): string => + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); + +export var importedFexpr18 = function importedFexpr18( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); +}; + +export const importedArrow32 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, +): string => + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); + +export var importedFexpr32 = function importedFexpr32( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); +}; + +export const importedArrow64 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, p35: any, p36: any, + p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, p44: any, p45: any, + p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, + p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, +): string => + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); + +export var importedFexpr64 = function importedFexpr64( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, p35: any, p36: any, + p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, p44: any, p45: any, + p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, + p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); +}; + +export const importedRest = (...values: any[]): string => sig(values); diff --git a/test-files/test_gap_10420_call_arity_limits.ts b/test-files/test_gap_10420_call_arity_limits.ts new file mode 100644 index 0000000000..8aa79ffe73 --- /dev/null +++ b/test-files/test_gap_10420_call_arity_limits.ts @@ -0,0 +1,993 @@ +// #10420 / #10425: calls with more than 16 arguments. +// +// A function VALUE called with 17+ arguments failed to compile (`closure call +// with 18 args (max 16)`), and apply/call/spread/object-literal methods passed +// `0` for every parameter past the 16th (the `__perry_wrap_*` value wrappers +// were capped at 16 params). `Reflect.apply` forwarded at most four arguments. +// qs 6.15.3's recursive 18-argument `stringify` is the package-audit shape. +// Sizes 3 and 16 are the fixed-arity fast-path controls; 17/18/32/64 take the +// variadic path. + +import { + sig, + importedRest, + importedArrow3, + importedArrow16, + importedArrow17, + importedArrow18, + importedArrow32, + importedArrow64, + importedFexpr3, + importedFexpr16, + importedFexpr17, + importedFexpr18, + importedFexpr32, + importedFexpr64, +} from "./_helpers/call_arity_10420.ts"; + +function range(n: number): number[] { + const out: number[] = []; + for (let i = 1; i <= n; i++) out.push(i); + return out; +} + +function show(label: string, value: any): void { + console.log(label + " " + String(value)); +} + +// ---- 3 parameters ---- +const arrow3 = ( + p0: any, p1: any, p2: any, +): string => sig([ + p0, p1, p2, +]); + +var fexpr3 = function fexpr3( + p0: any, p1: any, p2: any, +): string { + if (p0 > 1) { + return fexpr3( + p0 - 1, p1, p2, + ); + } + return sig([ + p0, p1, p2, + ]); +}; + +function decl3( + p0: any, p1: any, p2: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, + ]); +} + +function Ctor3( + this: any, p0: any, p1: any, p2: any, +) { + this.value = sig([ + p0, p1, p2, + ]); +} + +class Klass3 { + value: string; + constructor( + p0: any, p1: any, p2: any, + ) { + this.value = sig([ + p0, p1, p2, + ]); + } + method( + p0: any, p1: any, p2: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, + ]); + } +} + +const objlit3 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, + ]); + }, +}; + +{ + const args = range(3); + const values: any[] = [arrow3, fexpr3, decl3, importedArrow3, importedFexpr3]; + show("arrow3 direct", arrow3( + 1, 2, 3, + )); + show("fexpr3 recursive", fexpr3( + 4, 2, 3, + )); + show("decl3 direct", decl3( + 1, 2, 3, + )); + show("importedArrow3 direct", importedArrow3( + 1, 2, 3, + )); + show("importedFexpr3 direct", importedFexpr3( + 1, 2, 3, + )); + show("importedRest 3 direct", importedRest( + 1, 2, 3, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 3 direct", rest( + 1, 2, 3, + )); + show("rest 3 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value3[" + i + "] call", fn( + 1, 2, 3, + )); + show("value3[" + i + "] apply", fn.apply(null, args)); + show("value3[" + i + "] .call", fn.call(null, ...args)); + show("value3[" + i + "] spread", fn(...args)); + show("value3[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value3[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value3[" + i + "] short", fn(1, 2, 3)); + } + show("decl3 underapplied", decl3.apply(null, args.slice(0, 2))); + show("decl3 overapplied", decl3.apply(null, range(6))); + show("Reflect.construct Ctor3", Reflect.construct(Ctor3, args).value); + show("Reflect.construct Klass3", Reflect.construct(Klass3, args).value); + show("new Klass3 spread", new Klass3(...args).value); + const k = new Klass3(...args); + show("Klass3 method direct", k.method( + 1, 2, 3, + )); + show("Klass3 method apply", k.method.apply(k, args)); + show("Klass3 method spread", k.method(...args)); + show("Klass3 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit3 method direct", objlit3.method( + 1, 2, 3, + )); + show("objlit3 method apply", objlit3.method.apply(objlit3, args)); + show("objlit3 method .call", objlit3.method.call(objlit3, ...args)); + show("objlit3 method spread", objlit3.method(...args)); + show("objlit3 method Reflect.apply", Reflect.apply(objlit3.method, objlit3, args)); +} + +// ---- 16 parameters ---- +const arrow16 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +): string => sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, +]); + +var fexpr16 = function fexpr16( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +): string { + if (p0 > 1) { + return fexpr16( + p0 - 1, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ); + } + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); +}; + +function decl16( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); +} + +function Ctor16( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, +) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); +} + +class Klass16 { + value: string; + constructor( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, + ) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); + } + method( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); + } +} + +const objlit16 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + ]); + }, +}; + +{ + const args = range(16); + const values: any[] = [arrow16, fexpr16, decl16, importedArrow16, importedFexpr16]; + show("arrow16 direct", arrow16( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("fexpr16 recursive", fexpr16( + 4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("decl16 direct", decl16( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("importedArrow16 direct", importedArrow16( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("importedFexpr16 direct", importedFexpr16( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("importedRest 16 direct", importedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 16 direct", rest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("rest 16 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value16[" + i + "] call", fn( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("value16[" + i + "] apply", fn.apply(null, args)); + show("value16[" + i + "] .call", fn.call(null, ...args)); + show("value16[" + i + "] spread", fn(...args)); + show("value16[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value16[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value16[" + i + "] short", fn(1, 2, 3)); + } + show("decl16 underapplied", decl16.apply(null, args.slice(0, 15))); + show("decl16 overapplied", decl16.apply(null, range(19))); + show("Reflect.construct Ctor16", Reflect.construct(Ctor16, args).value); + show("Reflect.construct Klass16", Reflect.construct(Klass16, args).value); + show("new Klass16 spread", new Klass16(...args).value); + const k = new Klass16(...args); + show("Klass16 method direct", k.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("Klass16 method apply", k.method.apply(k, args)); + show("Klass16 method spread", k.method(...args)); + show("Klass16 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit16 method direct", objlit16.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + )); + show("objlit16 method apply", objlit16.method.apply(objlit16, args)); + show("objlit16 method .call", objlit16.method.call(objlit16, ...args)); + show("objlit16 method spread", objlit16.method(...args)); + show("objlit16 method Reflect.apply", Reflect.apply(objlit16.method, objlit16, args)); +} + +// ---- 17 parameters ---- +const arrow17 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +): string => sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, +]); + +var fexpr17 = function fexpr17( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +): string { + if (p0 > 1) { + return fexpr17( + p0 - 1, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ); + } + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); +}; + +function decl17( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); +} + +function Ctor17( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, +) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); +} + +class Klass17 { + value: string; + constructor( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + ) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); + } + method( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); + } +} + +const objlit17 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]); + }, +}; + +{ + const args = range(17); + const values: any[] = [arrow17, fexpr17, decl17, importedArrow17, importedFexpr17]; + show("arrow17 direct", arrow17( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("fexpr17 recursive", fexpr17( + 4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("decl17 direct", decl17( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("importedArrow17 direct", importedArrow17( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("importedFexpr17 direct", importedFexpr17( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("importedRest 17 direct", importedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 17 direct", rest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("rest 17 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value17[" + i + "] call", fn( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("value17[" + i + "] apply", fn.apply(null, args)); + show("value17[" + i + "] .call", fn.call(null, ...args)); + show("value17[" + i + "] spread", fn(...args)); + show("value17[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value17[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value17[" + i + "] short", fn(1, 2, 3)); + } + show("decl17 underapplied", decl17.apply(null, args.slice(0, 16))); + show("decl17 overapplied", decl17.apply(null, range(20))); + show("Reflect.construct Ctor17", Reflect.construct(Ctor17, args).value); + show("Reflect.construct Klass17", Reflect.construct(Klass17, args).value); + show("new Klass17 spread", new Klass17(...args).value); + const k = new Klass17(...args); + show("Klass17 method direct", k.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("Klass17 method apply", k.method.apply(k, args)); + show("Klass17 method spread", k.method(...args)); + show("Klass17 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit17 method direct", objlit17.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + )); + show("objlit17 method apply", objlit17.method.apply(objlit17, args)); + show("objlit17 method .call", objlit17.method.call(objlit17, ...args)); + show("objlit17 method spread", objlit17.method(...args)); + show("objlit17 method Reflect.apply", Reflect.apply(objlit17.method, objlit17, args)); +} + +// ---- 18 parameters ---- +const arrow18 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, +): string => sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, +]); + +var fexpr18 = function fexpr18( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, +): string { + if (p0 > 1) { + return fexpr18( + p0 - 1, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ); + } + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); +}; + +function decl18( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); +} + +function Ctor18( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, +) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); +} + +class Klass18 { + value: string; + constructor( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, + ) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); + } + method( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); + } +} + +const objlit18 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + ]); + }, +}; + +{ + const args = range(18); + const values: any[] = [arrow18, fexpr18, decl18, importedArrow18, importedFexpr18]; + show("arrow18 direct", arrow18( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("fexpr18 recursive", fexpr18( + 4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("decl18 direct", decl18( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("importedArrow18 direct", importedArrow18( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("importedFexpr18 direct", importedFexpr18( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("importedRest 18 direct", importedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 18 direct", rest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("rest 18 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value18[" + i + "] call", fn( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("value18[" + i + "] apply", fn.apply(null, args)); + show("value18[" + i + "] .call", fn.call(null, ...args)); + show("value18[" + i + "] spread", fn(...args)); + show("value18[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value18[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value18[" + i + "] short", fn(1, 2, 3)); + } + show("decl18 underapplied", decl18.apply(null, args.slice(0, 17))); + show("decl18 overapplied", decl18.apply(null, range(21))); + show("Reflect.construct Ctor18", Reflect.construct(Ctor18, args).value); + show("Reflect.construct Klass18", Reflect.construct(Klass18, args).value); + show("new Klass18 spread", new Klass18(...args).value); + const k = new Klass18(...args); + show("Klass18 method direct", k.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("Klass18 method apply", k.method.apply(k, args)); + show("Klass18 method spread", k.method(...args)); + show("Klass18 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit18 method direct", objlit18.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + )); + show("objlit18 method apply", objlit18.method.apply(objlit18, args)); + show("objlit18 method .call", objlit18.method.call(objlit18, ...args)); + show("objlit18 method spread", objlit18.method(...args)); + show("objlit18 method Reflect.apply", Reflect.apply(objlit18.method, objlit18, args)); +} + +// ---- 32 parameters ---- +const arrow32 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, +): string => sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, + p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, +]); + +var fexpr32 = function fexpr32( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, +): string { + if (p0 > 1) { + return fexpr32( + p0 - 1, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + p18, p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ); + } + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); +}; + +function decl32( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); +} + +function Ctor32( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, + p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, +) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); +} + +class Klass32 { + value: string; + constructor( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, + ) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); + } + method( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); + } +} + +const objlit32 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, + ]); + }, +}; + +{ + const args = range(32); + const values: any[] = [arrow32, fexpr32, decl32, importedArrow32, importedFexpr32]; + show("arrow32 direct", arrow32( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("fexpr32 recursive", fexpr32( + 4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("decl32 direct", decl32( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("importedArrow32 direct", importedArrow32( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("importedFexpr32 direct", importedFexpr32( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("importedRest 32 direct", importedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 32 direct", rest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("rest 32 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value32[" + i + "] call", fn( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("value32[" + i + "] apply", fn.apply(null, args)); + show("value32[" + i + "] .call", fn.call(null, ...args)); + show("value32[" + i + "] spread", fn(...args)); + show("value32[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value32[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value32[" + i + "] short", fn(1, 2, 3)); + } + show("decl32 underapplied", decl32.apply(null, args.slice(0, 31))); + show("decl32 overapplied", decl32.apply(null, range(35))); + show("Reflect.construct Ctor32", Reflect.construct(Ctor32, args).value); + show("Reflect.construct Klass32", Reflect.construct(Klass32, args).value); + show("new Klass32 spread", new Klass32(...args).value); + const k = new Klass32(...args); + show("Klass32 method direct", k.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("Klass32 method apply", k.method.apply(k, args)); + show("Klass32 method spread", k.method(...args)); + show("Klass32 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit32 method direct", objlit32.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + )); + show("objlit32 method apply", objlit32.method.apply(objlit32, args)); + show("objlit32 method .call", objlit32.method.call(objlit32, ...args)); + show("objlit32 method spread", objlit32.method(...args)); + show("objlit32 method Reflect.apply", Reflect.apply(objlit32.method, objlit32, args)); +} + +// ---- 64 parameters ---- +const arrow64 = ( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, p35: any, p36: any, + p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, p44: any, p45: any, + p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, + p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, +): string => sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, + p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, p36, p37, + p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, p53, p54, p55, + p56, p57, p58, p59, p60, p61, p62, p63, +]); + +var fexpr64 = function fexpr64( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, p35: any, p36: any, + p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, p44: any, p45: any, + p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, + p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, +): string { + if (p0 > 1) { + return fexpr64( + p0 - 1, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, + p18, p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, + p35, p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, + p52, p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ); + } + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); +}; + +function decl64( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, p17: any, p18: any, + p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, p26: any, p27: any, + p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, p35: any, p36: any, + p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, p44: any, p45: any, + p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, + p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, +): string { + return arguments.length + "/" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); +} + +function Ctor64( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, p25: any, + p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, p32: any, p33: any, p34: any, + p35: any, p36: any, p37: any, p38: any, p39: any, p40: any, p41: any, p42: any, p43: any, + p44: any, p45: any, p46: any, p47: any, p48: any, p49: any, p50: any, p51: any, p52: any, + p53: any, p54: any, p55: any, p56: any, p57: any, p58: any, p59: any, p60: any, p61: any, + p62: any, p63: any, +) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); +} + +class Klass64 { + value: string; + constructor( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, p32: any, + p33: any, p34: any, p35: any, p36: any, p37: any, p38: any, p39: any, p40: any, + p41: any, p42: any, p43: any, p44: any, p45: any, p46: any, p47: any, p48: any, + p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, p55: any, p56: any, + p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, + ) { + this.value = sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); + } + method( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, + p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, p32: any, + p33: any, p34: any, p35: any, p36: any, p37: any, p38: any, p39: any, p40: any, + p41: any, p42: any, p43: any, p44: any, p45: any, p46: any, p47: any, p48: any, + p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, p55: any, p56: any, + p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, + ): string { + return "klass:" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); + } +} + +const objlit64 = { + tag: "obj", + method( + this: any, p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, + p8: any, p9: any, p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, + p17: any, p18: any, p19: any, p20: any, p21: any, p22: any, p23: any, p24: any, + p25: any, p26: any, p27: any, p28: any, p29: any, p30: any, p31: any, p32: any, + p33: any, p34: any, p35: any, p36: any, p37: any, p38: any, p39: any, p40: any, + p41: any, p42: any, p43: any, p44: any, p45: any, p46: any, p47: any, p48: any, + p49: any, p50: any, p51: any, p52: any, p53: any, p54: any, p55: any, p56: any, + p57: any, p58: any, p59: any, p60: any, p61: any, p62: any, p63: any, + ): string { + return this.tag + ":" + sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, + p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31, p32, p33, p34, p35, + p36, p37, p38, p39, p40, p41, p42, p43, p44, p45, p46, p47, p48, p49, p50, p51, p52, + p53, p54, p55, p56, p57, p58, p59, p60, p61, p62, p63, + ]); + }, +}; + +{ + const args = range(64); + const values: any[] = [arrow64, fexpr64, decl64, importedArrow64, importedFexpr64]; + show("arrow64 direct", arrow64( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("fexpr64 recursive", fexpr64( + 4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("decl64 direct", decl64( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("importedArrow64 direct", importedArrow64( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("importedFexpr64 direct", importedFexpr64( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("importedRest 64 direct", importedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + const rest = (...values: any[]): string => "rest:" + sig(values); + show("rest 64 direct", rest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("rest 64 apply", rest.apply(null, args)); + for (let i = 0; i < values.length; i++) { + const fn = values[i]; + show("value64[" + i + "] call", fn( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("value64[" + i + "] apply", fn.apply(null, args)); + show("value64[" + i + "] .call", fn.call(null, ...args)); + show("value64[" + i + "] spread", fn(...args)); + show("value64[" + i + "] Reflect.apply", Reflect.apply(fn, null, args)); + show("value64[" + i + "] bind", fn.bind(null, 1, 2)(...args.slice(2))); + show("value64[" + i + "] short", fn(1, 2, 3)); + } + show("decl64 underapplied", decl64.apply(null, args.slice(0, 63))); + show("decl64 overapplied", decl64.apply(null, range(67))); + show("Reflect.construct Ctor64", Reflect.construct(Ctor64, args).value); + show("Reflect.construct Klass64", Reflect.construct(Klass64, args).value); + show("new Klass64 spread", new Klass64(...args).value); + const k = new Klass64(...args); + show("Klass64 method direct", k.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("Klass64 method apply", k.method.apply(k, args)); + show("Klass64 method spread", k.method(...args)); + show("Klass64 method Reflect.apply", Reflect.apply(k.method, k, args)); + show("objlit64 method direct", objlit64.method( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + )); + show("objlit64 method apply", objlit64.method.apply(objlit64, args)); + show("objlit64 method .call", objlit64.method.call(objlit64, ...args)); + show("objlit64 method spread", objlit64.method(...args)); + show("objlit64 method Reflect.apply", Reflect.apply(objlit64.method, objlit64, args)); +} + +// #10425's own repro: Reflect.apply with a native callee and a rest callee. +show("Reflect.apply Math.max", Reflect.apply(Math.max, null, [1, 2, 3, 4, 5, 6])); +function restJoin(...a: number[]): string { + return a.length + ":" + a.join(","); +} +show("Reflect.apply rest", Reflect.apply(restJoin, null, [1, 2, 3, 4, 5, 6, 7])); +show("Reflect.construct Array", Reflect.construct(Array, [1, 2, 3, 4, 5, 6]).length); +show("apply rest", restJoin.apply(null, [1, 2, 3, 4, 5, 6, 7])); +function thisProbe(this: any, a: any, b: any, c: any, d: any, e: any): string { + return this.name + ":" + [a, b, c, d, e].join(","); +} +show("Reflect.apply this+5", Reflect.apply(thisProbe, { name: "recv" }, [1, 2, 3, 4, 5])); + +// A rest parameter after 17 fixed parameters. +function fixedRest( + p0: any, p1: any, p2: any, p3: any, p4: any, p5: any, p6: any, p7: any, p8: any, p9: any, + p10: any, p11: any, p12: any, p13: any, p14: any, p15: any, p16: any, ...rest: any[] +): string { + return sig([ + p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, + ]) + "+" + sig(rest); +} +show("fixedRest direct", fixedRest( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +)); +show("fixedRest apply", fixedRest.apply(null, range(20))); +show("fixedRest Reflect.apply", Reflect.apply(fixedRest, null, range(20))); +const fixedRestValue: any = fixedRest; +show("fixedRest value spread", fixedRestValue(...range(20))); + +// A callee declaring one parameter still sees every argument via `arguments`. +function countArgs(a: any): string { + return arguments.length + ":" + a + ":" + arguments[arguments.length - 1]; +} +show("arguments 100 apply", countArgs.apply(null, range(100))); +show("arguments 100 Reflect.apply", Reflect.apply(countArgs, null, range(100))); +show("Math.max 200 apply", Math.max.apply(null, range(200))); + +// Timer and nextTick callbacks forward their extra arguments too. +setTimeout( + (...values: any[]) => { + show("setTimeout 12 args", sig(values)); + const interval = setInterval( + (...more: any[]) => { + clearInterval(interval); + show("setInterval 10 args", sig(more)); + }, + 1, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ); + }, + 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, +); +process.nextTick( + (...values: any[]) => show("nextTick 11 args", sig(values)), + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +); From 9882bbf6944549d126f0fa0f7a82e8420e81d8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 12:42:32 +0000 Subject: [PATCH 04/11] docs(changelog): add fragment for #10532 --- changelog.d/10532-call-arity-limits.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changelog.d/10532-call-arity-limits.md diff --git a/changelog.d/10532-call-arity-limits.md b/changelog.d/10532-call-arity-limits.md new file mode 100644 index 0000000000..dd093b2889 --- /dev/null +++ b/changelog.d/10532-call-arity-limits.md @@ -0,0 +1,11 @@ +### Fixed + +- **Calls with more than 16 arguments: closure-value calls failed to compile, dynamic calls dropped arguments, and `Reflect.apply` forwarded at most four (#10420, #10425).** Calling a function *value* (arrow, `var f = function f`, imported `var`/`const`, rest params) with 17+ arguments was a hard codegen error (`closure call with 18 args (max 16)`) at three `bail!` sites, and the node-submodule call path truncated to 16. qs 6.15.3's recursive 18-argument `stringify` could not compile. Several other ceilings silently dropped arguments: + - the `__perry_wrap_*` function-value wrappers took at most 16 params, while the runtime dispatched the registered full-width ABI into them, so `apply`/`call`/spread/object-literal-method calls of an 18-param function passed `0` for params 17 and 18 (class-method wrappers stopped at 32); + - `dispatch_with_arity` (33+ declared params) and `dispatch_rest_bundled` (16+ fixed params, which includes every `arguments` user with 16+ params) returned `undefined` without calling the body; + - `Reflect.apply` sent every list of four or more arguments through `js_closure_call4`; + - setTimeout/setInterval/`process.nextTick` clamped trailing arguments to nine. + + Closure-value call sites now share `lower_call::emit_closure_handle_call`: up to 16 arguments keep the `js_closure_call{N}` fast path (IR unchanged); wider calls marshal an entry-block buffer into `js_closure_call_array`. Wrappers take every declared parameter. Bodies wider than the runtime's exact transmute arms are called through a padded ladder of widths (64/256/1024 slots, `undefined`-filled) in `closure::wide_call`, which relies on the same caller-cleanup ABI property the existing more-args-than-declared transmutes use. Past 1024 slots the call throws `RangeError`. The wide `js_closure_call_array` arm now resolves its route with one memoized strategy probe instead of re-reading the body record, which makes a 20-argument closure-value call 10.5% cheaper in instructions. Reflect.apply, the timers and nextTick pass longer lists to the variadic entry point. + + Validation: new gap test `test_gap_10420_call_arity_limits` (17/18/32/64-argument calls across arrow, function-expression, imported and rest callees; apply/call/spread/Reflect.apply/Reflect.construct/bind; class and object-literal methods; `arguments.length`; timers; 3- and 16-argument controls) fails to compile on the baseline and matches Node 26.5.1 here, plus codegen and runtime unit tests. Gap suite: no regressions against the baseline. Instruction counts for 3-argument closure-value loops, `fn.apply` with 5 arguments and `benchmarks/suite/14_closure.ts`/`09_method_calls.ts` are unchanged. qs 6.15.3 compiles from source; its next blocker is #10482. From 1eef8347358c2aa14fc6fe97ee4626f329237b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 10:19:34 +0000 Subject: [PATCH 05/11] fix(transform): suspend on yields in nested generator loop headers A `yield` in a while/do-while condition or a for condition/update was only split into resume states when the loop was a direct statement of the generator body. `body_contains_yield` looked at loop bodies but never loop headers, so an enclosing `if`/`try`/`switch`/label/loop whose only yield sat in a nested loop header was emitted inline and the residual yield never suspended: `[...g()]` was empty and sent-value loops never terminated (lru-cache 11.5.2's minified `*#A`/`*#z` iterators). - body_contains_yield also checks loop conditions and for updates. - A for-init's own top-level yield (`for (let t = yield x; ...)`, `for (yield x; ...)`, and an async function's `for (let x = await p; ...)` after the await-to-yield rewrite) is hoisted ahead of the loop. - A yielding for update left in place by the header arm (a `continue` inside try/finally) is linearized in the loop's update state. - In an async generator, a header yield's operand is awaited like every other yield operand. --- .../src/generator/break_continue.rs | 32 +- .../src/generator/hoist_yields.rs | 21 +- .../src/generator/linearize.rs | 69 ++- .../src/generator/loop_header_yield_tests.rs | 286 ++++++++++ crates/perry-transform/src/generator/mod.rs | 4 + ...t_gap_10419_generator_loop_header_yield.ts | 514 ++++++++++++++++++ 6 files changed, 906 insertions(+), 20 deletions(-) create mode 100644 crates/perry-transform/src/generator/loop_header_yield_tests.rs create mode 100644 test-files/test_gap_10419_generator_loop_header_yield.ts diff --git a/crates/perry-transform/src/generator/break_continue.rs b/crates/perry-transform/src/generator/break_continue.rs index 0536466a39..9f8418700b 100644 --- a/crates/perry-transform/src/generator/break_continue.rs +++ b/crates/perry-transform/src/generator/break_continue.rs @@ -323,19 +323,45 @@ pub fn body_contains_yield(stmts: &[Stmt]) -> bool { } } } - Stmt::While { body, .. } if body_contains_yield(body) => { + // A yield in a loop HEADER (while / do-while condition, for + // condition / update) suspends too (#10419). The linearizer's + // header arms split such a loop into per-iteration states, but + // they only run if every enclosing `if` / `try` / `switch` / + // label / loop is linearized as well — which is decided here. + // Checking only loop bodies left `if (n) while ((yield t), t > 0)` + // emitted inline: its residual `Expr::Yield` never suspended and + // the generator yielded nothing (minified lru-cache iterators). + Stmt::While { condition, body } + if super::hoist_yields::expr_contains_yield(condition) + || body_contains_yield(body) => + { return true; } // A yield buried in a do-while or labeled loop must still be seen // by the enclosing construct's linearization (#1824), otherwise it // is never split into resume states. - Stmt::DoWhile { body, .. } if body_contains_yield(body) => { + Stmt::DoWhile { body, condition } + if super::hoist_yields::expr_contains_yield(condition) + || body_contains_yield(body) => + { return true; } Stmt::Labeled { body, .. } if body_contains_yield(std::slice::from_ref(&**body)) => { return true; } - Stmt::For { body, .. } if body_contains_yield(body) => { + Stmt::For { + condition, + update, + body, + .. + } if condition + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) + || update + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) + || body_contains_yield(body) => + { return true; } Stmt::Try { diff --git a/crates/perry-transform/src/generator/hoist_yields.rs b/crates/perry-transform/src/generator/hoist_yields.rs index c895fb835c..442b6ca660 100644 --- a/crates/perry-transform/src/generator/hoist_yields.rs +++ b/crates/perry-transform/src/generator/hoist_yields.rs @@ -101,11 +101,24 @@ fn hoist_yields_in_stmt(mut stmt: Stmt, next_id: &mut LocalId, hoisted: &mut Vec // of the loop. Condition/update are per-iteration — left in place // for the linearizer's For arm (matching the await pass's // single-hoist approximation for loop condition/update). + // + // The init's OWN top-level yield must be hoisted too (#10419): + // `for (let t = yield x; …)` / `for (yield x; …)` sit in a slot no + // linearizer arm splits, so the residual yield never suspended. + // The statement-level walk keeps a top-level yield in place, so + // hoist the init expression fully instead. if let Some(i) = init { - let mut inner = Vec::new(); - let replaced = hoist_yields_in_stmt((**i).clone(), next_id, &mut inner); - hoisted.extend(inner); - **i = replaced; + match i.as_mut() { + Stmt::Let { init: Some(e), .. } | Stmt::Expr(e) => { + hoist_yields_in_expr_full(e, next_id, hoisted); + } + _ => { + let mut inner = Vec::new(); + let replaced = hoist_yields_in_stmt((**i).clone(), next_id, &mut inner); + hoisted.extend(inner); + **i = replaced; + } + } } hoist_yields_in_stmts(body, next_id); } diff --git a/crates/perry-transform/src/generator/linearize.rs b/crates/perry-transform/src/generator/linearize.rs index f4105ebd41..2cbc1ee092 100644 --- a/crates/perry-transform/src/generator/linearize.rs +++ b/crates/perry-transform/src/generator/linearize.rs @@ -412,6 +412,21 @@ pub struct FinallyRoute { pub completion_check_state: Option, } +/// Normalize statements built from a loop HEADER (a condition or update the +/// header arms move into the loop body / update state) into the statement +/// shapes this linearizer splits. `hoist_yields_in_stmts` and — for an async +/// generator — `await_async_generator_yield_operands` ran over the function +/// body before linearization, but neither descends into loop headers, so a +/// header yield reaches this point un-hoisted and, in an `async function*`, +/// with its operand never awaited (spec `AsyncGeneratorYield(? Await(v))`: +/// `yield promise` in a loop condition delivered the promise itself, #10419). +fn normalize_loop_header_stmts(stmts: &mut Vec, next_local_id: &mut u32) { + hoist_yields_in_stmts(stmts, next_local_id); + if linearize_async_generator() { + super::lower::await_async_generator_yield_operands(stmts, next_local_id); + } +} + /// Linearize the generator body into a sequence of states. /// Splits at yield points and handles for-loops with yields. pub fn linearize_body( @@ -694,7 +709,7 @@ pub fn linearize_body( mutable: true, init: Some(condition.clone()), }]; - hoist_yields_in_stmts(&mut prefix, next_local_id); + normalize_loop_header_stmts(&mut prefix, next_local_id); prefix.push(Stmt::If { condition: Expr::Unary { op: UnaryOp::Not, @@ -799,7 +814,7 @@ pub fn linearize_body( mutable: true, init: condition.clone(), }]; - hoist_yields_in_stmts(&mut prefix, next_local_id); + normalize_loop_header_stmts(&mut prefix, next_local_id); new_body.append(&mut prefix); new_body.push(Stmt::If { condition: Expr::Unary { @@ -813,7 +828,7 @@ pub fn linearize_body( let mut taken_body = body.clone(); if upd_yields { let mut upd_stmts = vec![Stmt::Expr(update.clone().unwrap())]; - hoist_yields_in_stmts(&mut upd_stmts, next_local_id); + normalize_loop_header_stmts(&mut upd_stmts, next_local_id); prefix_loop_continues(&mut taken_body, &upd_stmts); new_body.append(&mut taken_body); new_body.extend(upd_stmts); @@ -839,13 +854,21 @@ pub fn linearize_body( ); } - // For-loop containing yield(s) + // For-loop containing yield(s) — in the body, or in an update the + // header arm above left in place (a `continue` inside + // try/finally must run the finally BEFORE the update, which the + // move-to-body-end rewrite cannot express). The update is then + // linearized in its own `continue`-target state below (#10419). Stmt::For { init, condition, update, body, - } if body_contains_yield(body) => { + } if body_contains_yield(body) + || update + .as_ref() + .is_some_and(super::hoist_yields::expr_contains_yield) => + { // State N: pre-loop code + init, goto condition check let init_state = *state_num; *state_num += 1; @@ -954,11 +977,6 @@ pub fn linearize_body( // residual, and (depending on guard placement) loop forever // on the same iteration. let update_state = *state_num; - *state_num += 1; - let mut update_body: Vec = Vec::new(); - if let Some(upd) = update { - update_body.push(Stmt::Expr(upd.clone())); - } // Push tail_state pointing at update_state. states.push(State { @@ -966,10 +984,35 @@ pub fn linearize_body( body: tail_body, exit: StateExit::Goto(update_state), }); - // Push update_state pointing at cond_state. + // Update statements, then the state that jumps back to + // cond_state. A yield in the update is split into its own + // states starting AT `update_state` (the first state pushed + // takes that number); without one, the final push below IS + // `update_state`, exactly as before. + if let Some(upd) = update { + let mut upd_stmts = vec![Stmt::Expr(upd.clone())]; + if super::hoist_yields::expr_contains_yield(upd) { + normalize_loop_header_stmts(&mut upd_stmts, next_local_id); + linearize_body( + &upd_stmts, + states, + current, + state_num, + state_id, + next_local_id, + sent_id, + catches, + finallys, + ); + } else { + current.append(&mut upd_stmts); + } + } + let update_tail_state = *state_num; + *state_num += 1; states.push(State { - num: update_state, - body: update_body, + num: update_tail_state, + body: std::mem::take(current), exit: StateExit::Goto(cond_state), }); diff --git a/crates/perry-transform/src/generator/loop_header_yield_tests.rs b/crates/perry-transform/src/generator/loop_header_yield_tests.rs new file mode 100644 index 0000000000..9acac26b0b --- /dev/null +++ b/crates/perry-transform/src/generator/loop_header_yield_tests.rs @@ -0,0 +1,286 @@ +//! #10419: a `yield` in a loop HEADER (while / do-while condition, for +//! condition / update / init) must be split into resume states at any nesting +//! depth, not only when the loop is a direct statement of the generator body. + +use super::*; + +fn yield_num(v: f64) -> Expr { + Expr::Yield { + value: Some(Box::new(Expr::Number(v))), + delegate: false, + } +} + +/// `((yield v), )` — the comma form minifiers emit in loop tests. +fn comma_yield(v: f64) -> Expr { + Expr::Sequence(vec![yield_num(v), Expr::GlobalGet(0)]) +} + +fn generator(body: Vec, is_async: bool) -> Function { + Function { + id: 1, + name: "header_yield".to_string(), + type_params: Vec::new(), + params: Vec::new(), + return_type: Type::Any, + body, + is_async, + is_generator: true, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } +} + +/// Residual `Expr::Yield` nodes. After the state-machine transform every +/// suspend point is a state exit; one left in the HIR is lowered by codegen +/// without suspending — the #10419 symptom (the generator yields nothing). +fn residual_yields(stmts: &[Stmt]) -> usize { + format!("{stmts:?}").matches("Yield {").count() +} + +fn transformed(body: Vec, is_async: bool) -> Vec { + let mut module = Module::new("loop_header_yield"); + module.functions.push(generator(body, is_async)); + transform_generators(&mut module); + std::mem::take(&mut module.functions[0].body) +} + +fn if_global(then_branch: Vec) -> Stmt { + Stmt::If { + condition: Expr::GlobalGet(0), + then_branch, + else_branch: None, + } +} + +/// One loop per header position, each wrapped in a different container. +fn nested_header_yield_bodies() -> Vec<(&'static str, Vec)> { + vec![ + ( + "if > while condition", + vec![if_global(vec![Stmt::While { + condition: comma_yield(1.0), + body: vec![], + }])], + ), + ( + "try/finally > do-while condition", + vec![Stmt::Try { + body: vec![Stmt::DoWhile { + body: vec![], + condition: comma_yield(1.0), + }], + catch: None, + finally: Some(vec![]), + }], + ), + ( + "catch > for condition", + vec![Stmt::Try { + body: vec![], + catch: Some(CatchClause { + param: None, + body: vec![Stmt::For { + init: None, + condition: Some(comma_yield(1.0)), + update: None, + body: vec![], + }], + }), + finally: None, + }], + ), + ( + "switch case > for update", + vec![Stmt::Switch { + discriminant: Expr::GlobalGet(0), + cases: vec![SwitchCase { + test: Some(Expr::Number(1.0)), + body: vec![Stmt::For { + init: None, + condition: Some(Expr::GlobalGet(0)), + update: Some(comma_yield(1.0)), + body: vec![], + }], + }], + }], + ), + ( + "label > while condition", + vec![Stmt::Labeled { + label: "outer".to_string(), + body: Box::new(Stmt::While { + condition: comma_yield(1.0), + body: vec![Stmt::LabeledContinue("outer".to_string())], + }), + }], + ), + ( + "if > for init", + vec![if_global(vec![Stmt::For { + init: Some(Box::new(Stmt::Let { + id: 1, + name: "t".to_string(), + ty: Type::Any, + mutable: true, + init: Some(yield_num(1.0)), + })), + condition: Some(Expr::GlobalGet(0)), + update: None, + body: vec![], + }])], + ), + ( + "if > for update with continue inside try/finally", + vec![if_global(vec![Stmt::For { + init: None, + condition: Some(Expr::GlobalGet(0)), + update: Some(comma_yield(1.0)), + body: vec![Stmt::Try { + body: vec![Stmt::Continue], + catch: None, + finally: Some(vec![]), + }], + }])], + ), + ] +} + +#[test] +fn body_contains_yield_sees_nested_loop_headers() { + for (name, body) in nested_header_yield_bodies() { + // The init case is normalized by `hoist_yields_in_stmts` (the pass + // that runs before any linearizer decision), not by header detection. + let mut body = body; + let mut next_id = 100; + hoist_yields_in_stmts(&mut body, &mut next_id); + assert!( + body_contains_yield(&body), + "{name}: a loop-header yield must make the enclosing statement linearize" + ); + } +} + +#[test] +fn body_contains_yield_ignores_header_yield_in_nested_closure() { + // A `yield` inside a closure in the loop condition belongs to that closure. + let closure = Expr::Closure { + func_id: 9, + params: Vec::new(), + return_type: Type::Any, + body: vec![Stmt::Expr(yield_num(1.0))], + captures: Vec::new(), + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: false, + is_async: false, + is_generator: true, + is_strict: false, + }; + let body = vec![if_global(vec![Stmt::While { + condition: Expr::Sequence(vec![closure, Expr::Bool(false)]), + body: vec![], + }])]; + assert!(!body_contains_yield(&body)); +} + +#[test] +fn nested_loop_header_yields_become_state_exits() { + for is_async in [false, true] { + for (name, body) in nested_header_yield_bodies() { + assert!( + residual_yields(&body) > 0, + "{name}: fixture must start with a yield, or the check below is vacuous" + ); + let out = transformed(body, is_async); + assert_eq!( + residual_yields(&out), + 0, + "{name} (async={is_async}): header yield left unsplit: {out:?}" + ); + } + } +} + +#[test] +fn for_init_top_level_yield_is_hoisted_before_the_loop() { + let mut stmts = vec![Stmt::For { + init: Some(Box::new(Stmt::Expr(yield_num(7.0)))), + condition: Some(Expr::GlobalGet(0)), + update: None, + body: vec![], + }]; + let mut next_id = 100; + hoist_yields_in_stmts(&mut stmts, &mut next_id); + assert!( + matches!( + &stmts[0], + Stmt::Let { + init: Some(Expr::Yield { .. }), + .. + } + ), + "init yield must be hoisted into a leading `let = yield`: {stmts:?}" + ); + match &stmts[1] { + Stmt::For { init: Some(i), .. } => assert_eq!(residual_yields(std::slice::from_ref(i)), 0), + other => panic!("expected the For after the hoisted let, got {other:?}"), + } +} + +/// In an `async function*`, `yield E` awaits `E` first. The pre-linearize +/// operand pass never sees loop headers, so the linearizer must add the +/// await when it moves a header yield into the loop (`yield promise` in a +/// loop condition delivered the promise itself). +#[test] +fn async_generator_header_yield_awaits_its_operand() { + for is_async in [false, true] { + super::linearize::set_linearize_async_generator(is_async); + let body = vec![Stmt::While { + condition: comma_yield(1.0), + body: vec![], + }]; + let mut states = Vec::new(); + let mut current = Vec::new(); + let mut state_num = 0; + let mut next_id = 100; + let mut catches = Vec::new(); + let mut finallys = Vec::new(); + linearize_body( + &body, + &mut states, + &mut current, + &mut state_num, + 90, + &mut next_id, + 91, + &mut catches, + &mut finallys, + ); + super::linearize::set_linearize_async_generator(false); + let await_state = states + .iter() + .find(|s| matches!(s.exit, StateExit::Await { .. })) + .map(|s| s.num); + let yield_state = states + .iter() + .find(|s| matches!(s.exit, StateExit::Yield { .. })) + .map(|s| s.num) + .expect("the header yield must become a Yield state"); + if is_async { + let await_state = await_state.expect("async header yield must await its operand"); + assert!( + await_state < yield_state, + "operand await must precede the yield" + ); + } else { + assert_eq!(await_state, None, "sync generators never await"); + } + } +} diff --git a/crates/perry-transform/src/generator/mod.rs b/crates/perry-transform/src/generator/mod.rs index cb2c340a81..4901c03672 100644 --- a/crates/perry-transform/src/generator/mod.rs +++ b/crates/perry-transform/src/generator/mod.rs @@ -501,3 +501,7 @@ pub fn transform_plain_async_closure_body( #[cfg(test)] #[path = "dispatch_growth_tests.rs"] mod dispatch_growth_tests; + +#[cfg(test)] +#[path = "loop_header_yield_tests.rs"] +mod loop_header_yield_tests; diff --git a/test-files/test_gap_10419_generator_loop_header_yield.ts b/test-files/test_gap_10419_generator_loop_header_yield.ts new file mode 100644 index 0000000000..94d845a53d --- /dev/null +++ b/test-files/test_gap_10419_generator_loop_header_yield.ts @@ -0,0 +1,514 @@ +// #10419: a `yield` in a loop CONDITION / UPDATE / INIT must suspend the +// generator at every nesting depth. #5933 fixed loops that are direct +// statements of the generator body; the same loop nested in `if` / `else` / +// `try` / `catch` / `finally` / `switch` / a label / another loop was emitted +// as an ordinary loop, so the residual yield never suspended: `[...g()]` was +// empty and a sent-value loop never terminated. Minifiers produce exactly this +// shape (lru-cache 11.5.2's `*#A` / `*#z` iterators). +// +// Every loop body calls tick(), which throws after 1000 steps, and every +// consumer caps its pulls, so a regression prints a wrong line instead of +// hanging the harness. + +let steps = 0; +function tick(): void { + if (++steps > 1000) throw new Error("runaway loop"); +} + +function take(it: Iterator, sends: any[] = [], limit = 25): string { + const out: string[] = []; + let r = it.next(); + let i = 0; + while (!r.done && i < limit) { + out.push(JSON.stringify(r.value)); + r = it.next(sends[i]); + i++; + } + out.push(r.done ? "done=" + JSON.stringify(r.value) : "(pull limit)"); + return out.join(" "); +} + +function run(name: string, mk: () => Iterator, sends?: any[]): void { + steps = 0; + try { + console.log(name + ":", take(mk(), sends)); + } catch (e) { + console.log(name + ": threw", (e as Error).message); + } +} + +function attempt(name: string, f: () => string): void { + steps = 0; + try { + console.log(name + ":", f()); + } catch (e) { + console.log(name + ": threw", String(e instanceof Error ? e.message : e)); + } +} + +// ── issue repro ────────────────────────────────────────────────────────── +function* noIf() { + for (let t = 2; t >= 0 && ((yield t), t !== 0); ) t = t - 1; +} +function* forInIf(n: number) { + if (n) for (let t = 2; t >= 0 && ((yield t), t !== 0); ) t = t - 1; +} +function* whileInIf(n: number) { + let t = 2; + if (n) while (((yield t), t > 0)) t = t - 1; +} +function* forInTry() { + try { for (let t = 2; ((yield t), t > 0); ) t = t - 1; } finally {} +} +console.log(JSON.stringify([...noIf()])); +console.log(JSON.stringify([...forInIf(1)])); +console.log(JSON.stringify([...whileInIf(1)])); +console.log(JSON.stringify([...forInTry()])); + +// ── loop kind x header position x container (each yields 3 2 1) ────────── +function* m_while_cond_top(n: number) { let t = 3; while (((yield t), --t > 0)) tick(); } +function* m_while_cond_if(n: number) { if (n) { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_else(n: number) { if (!n) tick(); else { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_try(n: number) { try { let t = 3; while (((yield t), --t > 0)) tick(); } finally { tick(); } } +function* m_while_cond_catch(n: number) { try { throw new Error("x"); } catch { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_finally(n: number) { try { tick(); } finally { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 3; while (((yield t), --t > 0)) tick(); } break; default: tick(); } } +function* m_while_cond_label(n: number) { blk: { let t = 3; while (((yield t), --t > 0)) tick(); if (n) break blk; tick(); } } +function* m_while_cond_labeled_loop(n: number) { let t = 3; lbl: while (((yield t), --t > 0)) { tick(); continue lbl; } } +function* m_while_cond_if_labeled_loop(n: number) { if (n) { let t = 3; lbl: while (((yield t), --t > 0)) { tick(); continue lbl; } } } +function* m_while_cond_loop(n: number) { for (let i = 0; i < n; i++) { let t = 3; while (((yield t), --t > 0)) tick(); } } +function* m_while_cond_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 3; while (((yield t), --t > 0)) tick(); } } } catch (e) { throw e; } } +function* m_dowhile_cond_top(n: number) { let t = 3; do tick(); while (((yield t), --t > 0)); } +function* m_dowhile_cond_if(n: number) { if (n) { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_else(n: number) { if (!n) tick(); else { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_try(n: number) { try { let t = 3; do tick(); while (((yield t), --t > 0)); } finally { tick(); } } +function* m_dowhile_cond_catch(n: number) { try { throw new Error("x"); } catch { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_finally(n: number) { try { tick(); } finally { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 3; do tick(); while (((yield t), --t > 0)); } break; default: tick(); } } +function* m_dowhile_cond_label(n: number) { blk: { let t = 3; do tick(); while (((yield t), --t > 0)); if (n) break blk; tick(); } } +function* m_dowhile_cond_labeled_loop(n: number) { let t = 3; lbl: do { tick(); continue lbl; } while (((yield t), --t > 0)); } +function* m_dowhile_cond_if_labeled_loop(n: number) { if (n) { let t = 3; lbl: do { tick(); continue lbl; } while (((yield t), --t > 0)); } } +function* m_dowhile_cond_loop(n: number) { for (let i = 0; i < n; i++) { let t = 3; do tick(); while (((yield t), --t > 0)); } } +function* m_dowhile_cond_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 3; do tick(); while (((yield t), --t > 0)); } } } catch (e) { throw e; } } +function* m_for_cond_top(n: number) { for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_if(n: number) { if (n) for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_else(n: number) { if (!n) tick(); else for (let t = 3; ((yield t), t > 1); t--) tick(); } +function* m_for_cond_try(n: number) { try { for (let t = 3; ((yield t), t > 1); t--) tick(); } finally { tick(); } } +function* m_for_cond_catch(n: number) { try { throw new Error("x"); } catch { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_finally(n: number) { try { tick(); } finally { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_switch(n: number) { switch (n) { case 0: break; case 1: { for (let t = 3; ((yield t), t > 1); t--) tick(); } break; default: tick(); } } +function* m_for_cond_label(n: number) { blk: { for (let t = 3; ((yield t), t > 1); t--) tick(); if (n) break blk; tick(); } } +function* m_for_cond_labeled_loop(n: number) { lbl: for (let t = 3; ((yield t), t > 1); t--) { tick(); continue lbl; } } +function* m_for_cond_if_labeled_loop(n: number) { if (n) { lbl: for (let t = 3; ((yield t), t > 1); t--) { tick(); continue lbl; } } } +function* m_for_cond_loop(n: number) { for (let i = 0; i < n; i++) { for (let t = 3; ((yield t), t > 1); t--) tick(); } } +function* m_for_cond_deep(n: number) { if (n) try { switch (n) { case 1: { for (let t = 3; ((yield t), t > 1); t--) tick(); } } } catch (e) { throw e; } } +function* m_for_update_top(n: number) { for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_if(n: number) { if (n) for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_else(n: number) { if (!n) tick(); else for (let t = 3; t > 0; (yield t), t--) tick(); } +function* m_for_update_try(n: number) { try { for (let t = 3; t > 0; (yield t), t--) tick(); } finally { tick(); } } +function* m_for_update_catch(n: number) { try { throw new Error("x"); } catch { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_finally(n: number) { try { tick(); } finally { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_switch(n: number) { switch (n) { case 0: break; case 1: { for (let t = 3; t > 0; (yield t), t--) tick(); } break; default: tick(); } } +function* m_for_update_label(n: number) { blk: { for (let t = 3; t > 0; (yield t), t--) tick(); if (n) break blk; tick(); } } +function* m_for_update_labeled_loop(n: number) { lbl: for (let t = 3; t > 0; (yield t), t--) { tick(); continue lbl; } } +function* m_for_update_if_labeled_loop(n: number) { if (n) { lbl: for (let t = 3; t > 0; (yield t), t--) { tick(); continue lbl; } } } +function* m_for_update_loop(n: number) { for (let i = 0; i < n; i++) { for (let t = 3; t > 0; (yield t), t--) tick(); } } +function* m_for_update_deep(n: number) { if (n) try { switch (n) { case 1: { for (let t = 3; t > 0; (yield t), t--) tick(); } } } catch (e) { throw e; } } +function* m_for_init_top(n: number) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } +function* m_for_init_if(n: number) { if (n) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_else(n: number) { if (!n) tick(); else { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_try(n: number) { try { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } finally { tick(); } } +function* m_for_init_catch(n: number) { try { throw new Error("x"); } catch { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_finally(n: number) { try { tick(); } finally { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_switch(n: number) { switch (n) { case 0: break; case 1: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } break; default: tick(); } } +function* m_for_init_label(n: number) { blk: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; if (n) break blk; tick(); } } +function* m_for_init_labeled_loop(n: number) { let t = 0; lbl: for (yield 3; t < 2; t++) { yield 2 - t; continue lbl; } } +function* m_for_init_if_labeled_loop(n: number) { if (n) { let t = 0; lbl: for (yield 3; t < 2; t++) { yield 2 - t; continue lbl; } } } +function* m_for_init_loop(n: number) { for (let i = 0; i < n; i++) { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } +function* m_for_init_deep(n: number) { if (n) try { switch (n) { case 1: { let t = 0; for (yield 3; t < 2; t++) yield 2 - t; } } } catch (e) { throw e; } } + +const matrix: Array<[string, (n: number) => Iterator]> = [ + ["while_cond_top", m_while_cond_top], + ["while_cond_if", m_while_cond_if], + ["while_cond_else", m_while_cond_else], + ["while_cond_try", m_while_cond_try], + ["while_cond_catch", m_while_cond_catch], + ["while_cond_finally", m_while_cond_finally], + ["while_cond_switch", m_while_cond_switch], + ["while_cond_label", m_while_cond_label], + ["while_cond_labeled_loop", m_while_cond_labeled_loop], + ["while_cond_if_labeled_loop", m_while_cond_if_labeled_loop], + ["while_cond_loop", m_while_cond_loop], + ["while_cond_deep", m_while_cond_deep], + ["dowhile_cond_top", m_dowhile_cond_top], + ["dowhile_cond_if", m_dowhile_cond_if], + ["dowhile_cond_else", m_dowhile_cond_else], + ["dowhile_cond_try", m_dowhile_cond_try], + ["dowhile_cond_catch", m_dowhile_cond_catch], + ["dowhile_cond_finally", m_dowhile_cond_finally], + ["dowhile_cond_switch", m_dowhile_cond_switch], + ["dowhile_cond_label", m_dowhile_cond_label], + ["dowhile_cond_labeled_loop", m_dowhile_cond_labeled_loop], + ["dowhile_cond_if_labeled_loop", m_dowhile_cond_if_labeled_loop], + ["dowhile_cond_loop", m_dowhile_cond_loop], + ["dowhile_cond_deep", m_dowhile_cond_deep], + ["for_cond_top", m_for_cond_top], + ["for_cond_if", m_for_cond_if], + ["for_cond_else", m_for_cond_else], + ["for_cond_try", m_for_cond_try], + ["for_cond_catch", m_for_cond_catch], + ["for_cond_finally", m_for_cond_finally], + ["for_cond_switch", m_for_cond_switch], + ["for_cond_label", m_for_cond_label], + ["for_cond_labeled_loop", m_for_cond_labeled_loop], + ["for_cond_if_labeled_loop", m_for_cond_if_labeled_loop], + ["for_cond_loop", m_for_cond_loop], + ["for_cond_deep", m_for_cond_deep], + ["for_update_top", m_for_update_top], + ["for_update_if", m_for_update_if], + ["for_update_else", m_for_update_else], + ["for_update_try", m_for_update_try], + ["for_update_catch", m_for_update_catch], + ["for_update_finally", m_for_update_finally], + ["for_update_switch", m_for_update_switch], + ["for_update_label", m_for_update_label], + ["for_update_labeled_loop", m_for_update_labeled_loop], + ["for_update_if_labeled_loop", m_for_update_if_labeled_loop], + ["for_update_loop", m_for_update_loop], + ["for_update_deep", m_for_update_deep], + ["for_init_top", m_for_init_top], + ["for_init_if", m_for_init_if], + ["for_init_else", m_for_init_else], + ["for_init_try", m_for_init_try], + ["for_init_catch", m_for_init_catch], + ["for_init_finally", m_for_init_finally], + ["for_init_switch", m_for_init_switch], + ["for_init_label", m_for_init_label], + ["for_init_labeled_loop", m_for_init_labeled_loop], + ["for_init_if_labeled_loop", m_for_init_if_labeled_loop], + ["for_init_loop", m_for_init_loop], + ["for_init_deep", m_for_init_deep], +]; + +for (const [name, g] of matrix) { + run(name, () => g(1)); +} +// the untaken branch / zero-iteration outer loop yields nothing +run("while_cond_if n=0", () => m_while_cond_if(0)); +run("for_update_loop n=0", () => m_for_update_loop(0)); +run("for_cond_loop n=2", () => m_for_cond_loop(2)); + +// ── minified lru-cache iterator shape ──────────────────────────────────── +class MiniLRU { + #n = 0; + #h = 0; + #a = 0; + #u: number[] = []; + #k: string[] = []; + #stale: boolean[] = []; + allowStale = false; + set(k: string, stale = false): this { + const i = this.#k.length; + this.#k.push(k); + this.#stale.push(stale); + this.#u.push(i + 1); + if (this.#n === 0) this.#h = i; + this.#a = i; + this.#n++; + return this; + } + #V(t: number): boolean { + return t < this.#k.length; + } + #p(t: number): boolean { + return this.#stale[t]; + } + *#A({ allowStale: e = this.allowStale } = {}) { + if (this.#n) for (let t = this.#h; this.#V(t) && ((e || !this.#p(t)) && (yield t), t !== this.#a); ) t = this.#u[t]; + } + *keys() { + for (const i of this.#A()) yield this.#k[i]; + } + *allKeys() { + for (const i of this.#A({ allowStale: true })) yield this.#k[i]; + } +} +{ + const c = new MiniLRU().set("a").set("b", true).set("c"); + console.log("lru keys:", JSON.stringify([...c.keys()]), JSON.stringify([...c.allKeys()])); + console.log("lru empty:", JSON.stringify([...new MiniLRU().keys()])); +} + +// ── sent values terminate the loop ─────────────────────────────────────── +function* sentBounded(n: number) { + let count = 0; + let v: any; + if (n) { + while ((v = yield count) !== "stop" && count < 5) count++; + } + return count; +} +run("sent bounded", () => sentBounded(1), [undefined, undefined, "stop"]); +run("sent bounded exhausts", () => sentBounded(1)); +function* sentUnbounded(n: number) { + let count = 0; + let v: any; + try { + if (n) while ((v = yield count) !== "stop") { tick(); count++; } + } finally { + count += 100; + } + return count; +} +run("sent unbounded", () => sentUnbounded(1), [1, 2, 3, "stop"]); +function* sentDoWhile(n: number) { + const got: any[] = []; + let v: any; + switch (n) { + case 1: + do { tick(); } while ((v = yield got.length) !== undefined && got.push(v) < 10); + } + return got; +} +run("sent do-while", () => sentDoWhile(1), ["x", "y"]); +function* sentForUpdate(n: number) { + let total = 0; + if (n) for (let i = 0; i < 10; i += (yield total) ?? 1) { tick(); total += i; } + return total; +} +run("sent for-update", () => sentForUpdate(1), [4, 5]); +function* sentForInit(n: number) { + if (n) for (let t = yield "init"; t > 0; t--) yield t; +} +run("sent for-init", () => sentForInit(1), [3]); +run("sent for-init none", () => sentForInit(1)); + +// ── return() / throw() while suspended in a header yield ────────────────── +function* retMid(log: string[], n: number) { + if (n) { + try { + for (let t = 0; ((yield t), t < 100); t++) tick(); + } finally { + log.push("finally"); + } + } + log.push("unreachable"); +} +attempt("return mid", () => { + const log: string[] = []; + const g = retMid(log, 1); + return JSON.stringify([g.next(), g.next(), g.return(42), g.next()]) + " " + log.join(","); +}); +function* throwMid(n: number) { + if (n) { + try { + while (((yield "w"), true)) tick(); + } catch (e) { + yield "caught " + e; + } + } + yield "after"; +} +attempt("throw mid", () => { + const g = throwMid(1); + return JSON.stringify([g.next(), g.next(), g.throw("boom"), g.next(), g.next()]); +}); +function* throwOuterCatch(n: number) { + try { + if (n) for (let i = 0; i < 100; (yield i), i++) tick(); + } catch (e) { + return "outer " + e; + } +} +attempt("throw outer", () => { + const g = throwOuterCatch(1); + return JSON.stringify([g.next(), g.next(), g.throw("bang"), g.next()]); +}); + +// ── break / continue around header yields ──────────────────────────────── +function* breakInSwitch(n: number) { + let t = 0; + switch (n) { + case 1: + while (((yield t), true)) { + tick(); + if (t++ >= 2) break; + } + yield "after"; + } +} +run("break in switch", () => breakInSwitch(1)); +function* labeledContinueUpdate(n: number) { + if (n) { + outer: for (let i = 0; i < 3; (yield i), i++) { + for (let j = 0; j < 2; j++) { + tick(); + if (j === 1) continue outer; + } + } + } +} +run("labeled continue update", () => labeledContinueUpdate(1)); +function* continueTryFinallyUpdate(log: string[], n: number) { + for (let i = 0; i < 3; (yield "u" + i), i++) { + try { + tick(); + if (i === 1) continue; + log.push("b" + i); + } finally { + log.push("f" + i); + } + } + if (n) { + for (let i = 0; i < 2; (yield "v" + i), i++) { + try { + if (i === 0) continue; + } finally { + log.push("g" + i); + } + } + } +} +{ + const log: string[] = []; + run("continue try/finally update", () => continueTryFinallyUpdate(log, 1)); + console.log(" log:", log.join(",")); +} +function* nestedHeaders(n: number) { + if (n) for (let i = 0; ((yield "i" + i), i < 2); i++) while (((yield "j" + i), false)) tick(); +} +run("nested headers", () => nestedHeaders(1)); + +function* closuresPerIteration(n: number) { + const fns: Array<() => number> = []; + if (n) for (let i = 0; ((yield i), i < 2); i++) fns.push(() => i); + if (n) for (let i = 0; i < 2; (yield "u" + i), i++) fns.push(() => i * 10); + return fns.map((f) => f()).join(","); +} +run("closures per iteration", () => closuresPerIteration(1)); + +// ── controls: shapes that already worked ───────────────────────────────── +function* bodyYieldInIf(n: number) { + if (n) for (let t = 0; t < 3; t++) yield t; +} +run("control body yield in if", () => bodyYieldInIf(1)); +function* forOfYieldIterable(n: number) { + if (n) for (const x of (yield "want") as number[]) yield x * 2; +} +run("control for-of yield iterable", () => forOfYieldIterable(1), [[1, 2]]); +function* noYieldHeaderInIf(n: number) { + let s = 0; + if (n) for (let i = 0; i < 4; i++) s += i; + yield s; +} +run("control no header yield", () => noYieldHeaderInIf(1)); + +// ── async generators (for await) ───────────────────────────────────────── +async function* aCondInIf(n: number) { + if (n) for (let t = 3; ((yield t), t > 1); t--) tick(); +} +async function* aWhileInTry(n: number) { + let t = 3; + try { + while (((yield t), --t > 0)) tick(); + } finally { + tick(); + } +} +async function* aDoWhileInSwitch(n: number) { + let t = 3; + switch (n) { + case 1: + do tick(); while (((yield t), --t > 0)); + } +} +async function* aUpdateInLabel(n: number) { + if (n) { + lbl: for (let t = 3; t > 0; (yield t), t--) { + tick(); + continue lbl; + } + } +} +async function* aPromiseOperand(n: number) { + for (let t = 3; ((yield Promise.resolve(t * 10)), t > 1); t--) tick(); + if (n) while (((yield Promise.resolve("p")), false)) tick(); +} +async function* aAwaitAndYield(n: number) { + let t = 2; + if (n) while (((yield await Promise.resolve(t)), t-- > 0)) tick(); +} +async function* aSent(n: number) { + let v: any; + let count = 0; + if (n) { + while ((v = yield count) !== "stop") { + tick(); + count++; + } + } + return count; +} +async function* aReturnMid(log: string[], n: number) { + if (n) { + try { + for (let t = 0; ((yield t), t < 100); t++) tick(); + } finally { + log.push("async finally"); + } + } +} + +// A plain async function's `for (let x = await p; …)` becomes a for-init yield +// after the await→yield rewrite, so it shares the init hoist. +async function forInitAwait(n: number): Promise { + const out: number[] = []; + for (let x = await Promise.resolve(3); x > 0; x--) out.push(x); + if (n) for (let y = await Promise.resolve(2); y > 0; y--) out.push(y * 10); + return out.join(","); +} + +async function collect(name: string, g: AsyncIterable): Promise { + steps = 0; + const out: string[] = []; + try { + for await (const v of g) { + out.push(v instanceof Promise ? "" : JSON.stringify(v)); + if (out.length > 25) break; + } + console.log(name + ":", out.join(" ")); + } catch (e) { + console.log(name + ": threw", (e as Error).message, out.join(" ")); + } +} + +async function main(): Promise { + await collect("async cond in if", aCondInIf(1)); + await collect("async while in try", aWhileInTry(1)); + await collect("async do-while in switch", aDoWhileInSwitch(1)); + await collect("async update in label", aUpdateInLabel(1)); + await collect("async promise operand", aPromiseOperand(1)); + await collect("async await and yield", aAwaitAndYield(1)); + + steps = 0; + const s = aSent(1); + const r: any[] = []; + r.push(await s.next()); + r.push(await s.next("a")); + r.push(await s.next("b")); + r.push(await s.next("stop")); + r.push(await s.next()); + console.log("async sent:", JSON.stringify(r)); + + console.log("async fn for-init await:", await forInitAwait(1)); + + const log: string[] = []; + const g = aReturnMid(log, 1); + const rr: any[] = []; + rr.push(await g.next()); + rr.push(await g.next()); + rr.push(await g.return(7)); + rr.push(await g.next()); + console.log("async return mid:", JSON.stringify(rr), log.join(",")); +} +main().catch((e) => console.log("async main threw", (e as Error).message)); From 5db1ea88ab4f8d222e6c62f9fe741148d8fab5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 13:19:44 +0000 Subject: [PATCH 06/11] docs(changelog): add #10537 fragment for generator loop-header yields --- .../10537-generator-loop-header-yield.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 changelog.d/10537-generator-loop-header-yield.md diff --git a/changelog.d/10537-generator-loop-header-yield.md b/changelog.d/10537-generator-loop-header-yield.md new file mode 100644 index 0000000000..cde65a7144 --- /dev/null +++ b/changelog.d/10537-generator-loop-header-yield.md @@ -0,0 +1,37 @@ +### Fixed + +- Generators now suspend on a `yield` in a loop header at any nesting depth + (#10419). A `yield` in a `while`/`do…while` condition or a `for` + condition/update was split into resume states only when the loop was a direct + statement of the generator body (#5933). Inside `if`/`else`, `try`/`catch`/ + `finally`, a `switch` case, a label, or another loop, the residual yield never + suspended: `[...g()]` was empty, a sent-value loop such as + `while ((v = yield n) !== "stop")` never terminated, and `.return()`/`.throw()` + found a finished generator. Minifiers emit exactly this shape — lru-cache + 11.5.2's `dist/esm/node/index.min.js` iterators (`keys`, `values`, `entries`, + `rkeys`, `forEach`, `for…of`, `dump`) returned nothing and `clear()` skipped + disposing entries. + + Root cause: the linearizer only descends into a compound statement when + `body_contains_yield` reports a suspend point, and that check inspected loop + bodies but never loop headers, so the `if`/`try`/`switch`/label/loop around a + header-yield loop was emitted inline and the loop's per-iteration header arms + never ran. `body_contains_yield` now checks while/do-while conditions and for + conditions/updates. Three header positions the #5933 arms never covered, even + at top level, are fixed alongside: a for-init's own top-level yield + (`for (let t = yield x; …)`, `for (yield x; …)`, and an async function's + `for (let x = await p; …)` after the await→yield rewrite) is hoisted ahead of + the loop; a yielding update the header arm keeps in place (a `continue` inside + `try`/`finally`) is linearized in the loop's update state; and in an + `async function*` a header yield's operand is awaited like every other yield + operand (a `yield promise` in a loop condition delivered the promise itself). + + Validation: `test_gap_10419_generator_loop_header_yield` (5 header positions × + 12 containers, sent values, `return()`/`throw()` mid-loop, labeled continue, + async generators with `for await`, plain-async for-init await) matches Node + byte-for-byte and differs on the pre-fix compiler; `perry-transform` unit tests + cover detection, the init hoist, residual-yield-free output and the async + operand await. lru-cache 11.5.2's default entry now matches Node on the audit + script (0 diff lines, was 14). Emitted LLVM IR for generators without header + yields (and for top-level header yields) is byte-identical to before, so their + instruction counts are unchanged. From dec1aa1ae0fb7dac6d8d2d8a0a3c277101ee52cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 10:50:36 +0000 Subject: [PATCH 07/11] fix(runtime): never evict a scheduled timer's ref state (#10447) The bounded id->ref-state registry (#6084) evicted the oldest ids by insertion order whether or not they were still scheduled, so 65,536 later timers undid a live timer's unref() (the missing id read as ref'd and kept the process alive until the timer fired) and dropped the id from is_known_timer_id (.hasRef()/.ref()/.unref()/.constructor stopped dispatching). The kind table had the same cap with an O(n) min() scan per insert once full. Merge both tables into one registry keyed by id. A scheduled id is pinned by a ScheduledTimerId token stored in its queue entry; dropping the entry on any path (fire, clear, agent purge, mock reset) retires the id, and only retired ids are eviction candidates, so the map stays bounded by live timers + 65,536. --- crates/perry-runtime/src/timer.rs | 95 ++-- crates/perry-runtime/src/timer/ownership.rs | 26 +- crates/perry-runtime/src/timer/ref_states.rs | 420 ++++++++++++++++-- .../perry-runtime/src/timer/tests_inline.rs | 3 + scripts/gc_runtime_root_holders.json | 6 - ...test_gap_10447_timer_ref_state_eviction.ts | 77 ++++ 6 files changed, 507 insertions(+), 120 deletions(-) create mode 100644 test-files/test_gap_10447_timer_ref_state_eviction.ts diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index 7e5941ad83..a52fc5a5db 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -12,10 +12,9 @@ mod async_lifecycle; use crate::promise::{js_promise_new, js_promise_resolve, Promise}; use async_lifecycle::{enqueue_destroy_ids, IntervalCallback}; use std::any::Any; -use std::collections::HashMap; use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, - LazyLock, Mutex, + Mutex, }; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -121,15 +120,6 @@ fn schedule_promise_timer(delay_ms: f64, value: f64, has_ref: bool) -> *mut Prom promise } -fn timer_has_ref_state(id: i64) -> bool { - TIMER_REF_STATES - .lock() - .unwrap() - .as_ref() - .and_then(|s| s.states.get(&id).copied()) - .unwrap_or(true) -} - fn other_event_sources_keep_loop_alive() -> bool { has_refed_callback_timer() || has_refed_interval_timer() @@ -337,6 +327,8 @@ struct CallbackTimer { /// in. Only that agent — or a pump acting for it, e.g. Android's UI thread /// for the primary agent — may fire it. owner: crate::agent::AgentId, + /// #10447: pins `id`'s ref state while queued; dropping it retires the id. + _scheduled: ScheduledTimerId, } // SAFETY: the closure POINTER targets global compiled code, but the closure @@ -353,7 +345,6 @@ pub const MOCK_TIMERS_ALL_APIS: u32 = MOCK_TIMERS_API_DATE | MOCK_TIMERS_API_SET_INTERVAL | MOCK_TIMERS_API_SET_IMMEDIATE; -#[derive(Clone)] struct MockCallbackTimer { id: i64, kind: CallbackTimerKind, @@ -362,11 +353,11 @@ struct MockCallbackTimer { args: Vec, context: crate::async_context::AsyncContextSnapshot, cleared: bool, + _scheduled: ScheduledTimerId, } unsafe impl Send for MockCallbackTimer {} -#[derive(Clone)] struct MockIntervalTimer { id: i64, callback: i64, @@ -375,6 +366,7 @@ struct MockIntervalTimer { args: Vec, context: crate::async_context::AsyncContextSnapshot, cleared: bool, + _scheduled: ScheduledTimerId, } unsafe impl Send for MockIntervalTimer {} @@ -416,11 +408,11 @@ use ownership::{has_refed_callback_timer, has_refed_interval_timer, has_refed_pr pub(crate) use ownership::{purge_agent_timers, timer_phase_work_pending}; pub(crate) use gc_scan::{new_timer_root_scan_state, scan_timer_roots_mut_step}; -use ref_states::{TimerRefStates, TIMER_REF_STATES_CAP}; +use ref_states::{ + register_scheduled_timer, set_timer_ref_state, timer_handle_kind, timer_has_ref_state, + ScheduledTimerId, +}; -static TIMER_REF_STATES: Mutex> = Mutex::new(None); -static TIMER_HANDLE_KINDS: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); static WARNED_NEGATIVE_TIMER_DELAY: AtomicBool = AtomicBool::new(false); static WARNED_NAN_TIMER_DELAY: AtomicBool = AtomicBool::new(false); @@ -607,29 +599,12 @@ fn normalize_timer_delay(delay_value: f64) -> u64 { } } -fn set_timer_ref_state(id: i64, has_ref: bool) { - ref_states::TIMER_IDS_NONEMPTY.arm(); - let mut slot = TIMER_REF_STATES.lock().unwrap(); - slot.get_or_insert_with(TimerRefStates::default) - .insert_bounded(id, has_ref, TIMER_REF_STATES_CAP); -} - -fn record_timer_handle_kind(id: i64, kind: CallbackTimerKind) { - let mut kinds = TIMER_HANDLE_KINDS.lock().unwrap(); - if kinds.len() >= TIMER_REF_STATES_CAP && !kinds.contains_key(&id) { - if let Some(oldest) = kinds.keys().copied().min() { - kinds.remove(&oldest); - } - } - kinds.insert(id, kind); -} - /// Synthetic constructor object for `Timeout`/`Immediate` native handles. -/// Timer ids outlive queue removal, so the kind table retains recent entries +/// Timer ids outlive queue removal, so the registry retains recent entries /// after clear/fire just as Node retains the wrapper's prototype. The bounded /// inventory avoids unbounded growth in long-running processes. pub(crate) fn timer_constructor_value(id: i64) -> Option { - let kind = TIMER_HANDLE_KINDS.lock().unwrap().get(&id).copied()?; + let kind = timer_handle_kind(id)?; let name = match kind { CallbackTimerKind::Timeout => b"Timeout".as_slice(), CallbackTimerKind::Immediate => b"Immediate".as_slice(), @@ -764,7 +739,7 @@ fn schedule_mock_callback_timer( let arg_handles = scope.root_nanbox_f64_slice(&args); let delay = normalize_timer_delay(delay_ms); let id = next_timer_id(); - record_timer_handle_kind(id, kind); + let scheduled = register_scheduled_timer(id, kind); let due_ms = state.current_ms + delay as f64; state.callbacks.push(MockCallbackTimer { id, @@ -774,8 +749,8 @@ fn schedule_mock_callback_timer( args: crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles), context: crate::async_context::capture_context(), cleared: false, + _scheduled: scheduled, }); - set_timer_ref_state(id, true); Some(id) } @@ -790,7 +765,7 @@ fn schedule_mock_interval_timer(callback: i64, interval_ms: f64, args: Vec) let arg_handles = scope.root_nanbox_f64_slice(&args); let interval = normalize_timer_delay(interval_ms); let id = next_timer_id(); - record_timer_handle_kind(id, CallbackTimerKind::Timeout); + let scheduled = register_scheduled_timer(id, CallbackTimerKind::Timeout); let next_ms = state.current_ms + interval as f64; state.intervals.push(MockIntervalTimer { id, @@ -800,8 +775,8 @@ fn schedule_mock_interval_timer(callback: i64, interval_ms: f64, args: Vec) args: crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles), context: crate::async_context::capture_context(), cleared: false, + _scheduled: scheduled, }); - set_timer_ref_state(id, true); Some(id) } @@ -840,10 +815,14 @@ fn mock_timers_advance_to(target_ms: f64) { }; state.current_ms = due_ms; if is_interval { - let timer = state.intervals[idx].clone(); - let interval = timer.interval_ms.max(1) as f64; - state.intervals[idx].next_ms = due_ms + interval; - Some((timer.id, timer.callback, timer.args, timer.context)) + let timer = &mut state.intervals[idx]; + timer.next_ms = due_ms + timer.interval_ms.max(1) as f64; + Some(( + timer.id, + timer.callback, + timer.args.clone(), + timer.context.clone(), + )) } else { let timer = state.callbacks.remove(idx); Some((timer.id, timer.callback, timer.args, timer.context)) @@ -904,12 +883,7 @@ pub extern "C" fn js_timer_has_ref(timer_id: i64) -> i32 { // user explicitly called `.unref()` on the handle. Default `true` for // any non-timer id is harmless since the dispatcher gates on // `is_known_timer_id` first. - TIMER_REF_STATES - .lock() - .unwrap() - .as_ref() - .and_then(|s| s.states.get(&timer_id).copied()) - .unwrap_or(true) as i32 + timer_has_ref_state(timer_id) as i32 } #[no_mangle] @@ -1087,7 +1061,7 @@ fn schedule_callback_timer( let deadline = Instant::now() + Duration::from_millis(delay_ms); let id = next_timer_id(); - record_timer_handle_kind(id, kind); + let scheduled = register_scheduled_timer(id, kind); let mut context = crate::async_context::capture_context(); let context_roots = crate::async_context::root_snapshot(&scope, &context); @@ -1118,8 +1092,8 @@ fn schedule_callback_timer( cleared: false, // #6185: the scheduling agent owns the callback closure + args. owner: crate::agent::current_agent(), + _scheduled: scheduled, }); - set_timer_ref_state(id, true); id } @@ -1263,7 +1237,7 @@ pub extern "C" fn js_callback_timer_tick() -> i32 { |timer| { crate::agent::owns(timer.owner) && timer.deadline <= now - && (timer_has_ref_state(timer.id) || allow_unref) + && (allow_unref || timer_has_ref_state(timer.id)) }, ) }; @@ -1442,7 +1416,7 @@ pub extern "C" fn js_callback_timer_next_deadline() -> f64 { .unwrap() .iter() .filter(|t| { - !t.cleared && crate::agent::owns(t.owner) && (timer_has_ref_state(t.id) || allow_unref) + !t.cleared && crate::agent::owns(t.owner) && (allow_unref || timer_has_ref_state(t.id)) }) .map(|t| { if t.deadline <= now { @@ -1571,6 +1545,8 @@ struct IntervalTimer { cleared: bool, /// #6185: agent that owns `callback` / `args`. See `CallbackTimer::owner`. owner: crate::agent::AgentId, + /// #10447: see `CallbackTimer::_scheduled`. + _scheduled: ScheduledTimerId, } // SAFETY: see `CallbackTimer` — the owner tag plus owner-filtered ticking is @@ -1602,7 +1578,7 @@ fn schedule_interval_timer(callback: i64, interval_ms: f64, args: Vec) -> i let next_deadline = Instant::now() + Duration::from_millis(interval); let id = next_timer_id(); - record_timer_handle_kind(id, CallbackTimerKind::Timeout); + let scheduled = register_scheduled_timer(id, CallbackTimerKind::Timeout); let mut context = crate::async_context::capture_context(); let context_roots = crate::async_context::root_snapshot(&scope, &context); @@ -1621,8 +1597,8 @@ fn schedule_interval_timer(callback: i64, interval_ms: f64, args: Vec) -> i cleared: false, // #6185: the scheduling agent owns the callback closure + args. owner: crate::agent::current_agent(), + _scheduled: scheduled, }); - set_timer_ref_state(id, true); id } @@ -1698,7 +1674,7 @@ pub extern "C" fn js_interval_timer_tick() -> i32 { if !timer.cleared && crate::agent::owns(timer.owner) && timer.next_deadline <= now - && (timer_has_ref_state(timer.id) || allow_unref) + && (allow_unref || timer_has_ref_state(timer.id)) { callbacks.push(( timer.id, @@ -1789,7 +1765,7 @@ pub extern "C" fn js_interval_timer_next_deadline() -> f64 { .unwrap() .iter() .filter(|t| { - !t.cleared && crate::agent::owns(t.owner) && (timer_has_ref_state(t.id) || allow_unref) + !t.cleared && crate::agent::owns(t.owner) && (allow_unref || timer_has_ref_state(t.id)) }) .map(|t| { if t.next_deadline <= now { @@ -1934,7 +1910,7 @@ mod tests_inline; #[cfg(test)] pub(crate) use tests_inline::*; -/// `PERRY_GC_CENSUS`: the three timer queues. +/// `PERRY_GC_CENSUS`: the three timer queues and the id registry. pub(crate) fn timer_tables_census() -> Vec { use crate::gc::census::vec_bytes; let mut rows = Vec::new(); @@ -1949,5 +1925,6 @@ pub(crate) fn timer_tables_census() -> Vec { let inner: usize = v.iter().map(|t| vec_bytes(&t.args)).sum(); rows.push(("timer.interval_timers", v.len(), vec_bytes(&v) + inner)); } + rows.push(ref_states::ref_states_census()); rows } diff --git a/crates/perry-runtime/src/timer/ownership.rs b/crates/perry-runtime/src/timer/ownership.rs index 21918a9ca7..6bb64a28dc 100644 --- a/crates/perry-runtime/src/timer/ownership.rs +++ b/crates/perry-runtime/src/timer/ownership.rs @@ -8,7 +8,8 @@ //! ownership: per-agent event-loop liveness, and what happens to an agent's //! timers when the agent itself goes away. -use super::{timer_has_ref_state, CALLBACK_TIMERS, INTERVAL_TIMERS, TIMER_QUEUE}; +use super::ref_states::with_ref_states; +use super::{CALLBACK_TIMERS, INTERVAL_TIMERS, TIMER_QUEUE}; /// Any entry needs the ordinary timer phase, including unref timers and /// cleared entries whose cleanup has not run. Foreign entries conservatively @@ -38,16 +39,27 @@ pub(super) fn has_refed_promise_timer() -> bool { .any(|timer| timer.has_ref && crate::agent::owns(timer.owner)) } +// The ref state lives in the id registry (`ref_states.rs`); a scan reads it +// under one registry lock rather than one per entry. + pub(super) fn has_refed_callback_timer() -> bool { - CALLBACK_TIMERS.lock().unwrap().iter().any(|timer| { - !timer.cleared && crate::agent::owns(timer.owner) && timer_has_ref_state(timer.id) - }) + let timers = CALLBACK_TIMERS.lock().unwrap(); + !timers.is_empty() + && with_ref_states(|states| { + timers.iter().any(|timer| { + !timer.cleared && crate::agent::owns(timer.owner) && states.has_ref(timer.id) + }) + }) } pub(super) fn has_refed_interval_timer() -> bool { - INTERVAL_TIMERS.lock().unwrap().iter().any(|timer| { - !timer.cleared && crate::agent::owns(timer.owner) && timer_has_ref_state(timer.id) - }) + let timers = INTERVAL_TIMERS.lock().unwrap(); + !timers.is_empty() + && with_ref_states(|states| { + timers.iter().any(|timer| { + !timer.cleared && crate::agent::owns(timer.owner) && states.has_ref(timer.id) + }) + }) } /// Drop every timer owned by `agent`. Called from `crate::agent::retire_agent` diff --git a/crates/perry-runtime/src/timer/ref_states.rs b/crates/perry-runtime/src/timer/ref_states.rs index e82b54dcf7..f8302edec2 100644 --- a/crates/perry-runtime/src/timer/ref_states.rs +++ b/crates/perry-runtime/src/timer/ref_states.rs @@ -1,76 +1,403 @@ -//! #6084: bounded id→ref-state registry for scheduled timers, extracted from -//! `timer.rs` to keep that file under the 2000-line lint cap. +//! #6084 / #10447: bounded id→handle-state registry for scheduled timers (ref +//! state and `Timeout`/`Immediate` kind), extracted from `timer.rs` to keep that +//! file under the 2000-line lint cap. +use super::CallbackTimerKind; use std::collections::{HashMap, VecDeque}; +use std::sync::{Mutex, MutexGuard, PoisonError}; -/// id → ref-state registry for scheduled timers. Entries are kept after -/// `clearTimeout`/`clearInterval` so post-clear `.hasRef()`/`.unref()`/`+timer` +/// What the registry knows about one timer id. +#[derive(Clone, Copy)] +struct TimerHandleState { + has_ref: bool, + /// `Timeout`/`Immediate` for `.constructor`; `None` for an id that only + /// ever reached `ref()`/`unref()`. + kind: Option, + /// Still has a queue entry: not fired, not cleared. Never evicted. + scheduled: bool, +} + +/// id → handle-state registry for timers. Entries are kept after the timer is +/// cleared or fires so post-clear `.hasRef()`/`.unref()`/`+timer`/`.constructor` /// still route through timer dispatch (Node keeps the Timeout object alive). /// They used to be inserted and *never* removed — a permanent per-id leak for a /// process that creates unboundedly many timers (e.g. a `setTimeout` per -/// request). The insertion-ordered eviction queue bounds the map: the cap is -/// large enough that a realistic "hold the handle, call `.hasRef()` after -/// clear" pattern never sees eviction, but a long-running process no longer -/// grows it without limit. Timer ids are monotonic (never reused), so an -/// evicted id is never re-queried in practice. +/// request, #6084). The bound that fixed it evicted the oldest ids whether or +/// not they were still scheduled, so 65,536 later timers silently undid a live +/// long-delay timer's `unref()` (a missing id reads as ref'd, and the process +/// stayed alive until the timer fired) and dropped it from `is_known_timer_id` +/// (#10447). Only RETIRED ids — fired or cleared, queued in `retired` — are +/// eviction candidates now; a scheduled id is pinned by its queue entry's +/// [`ScheduledTimerId`]. The map is bounded by live timers + `cap`. #[derive(Default)] pub(super) struct TimerRefStates { - pub(super) states: HashMap, - order: VecDeque, + states: HashMap, + /// Retired ids, oldest first — the only eviction candidates. + retired: VecDeque, } pub(super) const TIMER_REF_STATES_CAP: usize = 65_536; impl TimerRefStates { - /// Insert/overwrite `id`'s ref state, bounding the registry to `cap` entries - /// by evicting the oldest ids. Only a new id extends the eviction queue; a - /// ref/unref change on an existing id just overwrites its value. - pub(super) fn insert_bounded(&mut self, id: i64, has_ref: bool, cap: usize) { - if self.states.insert(id, has_ref).is_none() { - self.order.push_back(id); - while self.order.len() > cap { - if let Some(old) = self.order.pop_front() { - self.states.remove(&old); - } + /// A newly scheduled timer: ref'd, pinned until [`Self::retire`]. + fn schedule(&mut self, id: i64, kind: CallbackTimerKind) { + let state = TimerHandleState { + has_ref: true, + kind: Some(kind), + scheduled: true, + }; + self.states.insert(id, state); + } + + /// `ref()`/`unref()`. An id the registry does not hold (never scheduled, or + /// retired and since evicted) is recorded as retired, so it stays bounded. + fn set_ref(&mut self, id: i64, has_ref: bool, cap: usize) { + if let Some(state) = self.states.get_mut(&id) { + state.has_ref = has_ref; + return; + } + let state = TimerHandleState { + has_ref, + kind: None, + scheduled: false, + }; + self.states.insert(id, state); + self.push_retired(id, cap); + } + + /// The timer's queue entry is gone: keep its state for post-clear dispatch, + /// but make it evictable. Idempotent, and a no-op for an unknown id. + fn retire(&mut self, id: i64, cap: usize) { + if let Some(state) = self.states.get_mut(&id) { + if state.scheduled { + state.scheduled = false; + self.push_retired(id, cap); + } + } + } + + fn push_retired(&mut self, id: i64, cap: usize) { + self.retired.push_back(id); + while self.retired.len() > cap { + let Some(old) = self.retired.pop_front() else { + break; + }; + // Timer ids are monotonic, so a retired id is not rescheduled in + // practice; the check still never lets eviction drop a live entry. + if self.states.get(&old).is_some_and(|state| !state.scheduled) { + self.states.remove(&old); } } } + + fn get(&self, id: i64) -> Option { + self.states.get(&id).copied() + } +} + +static TIMER_REF_STATES: Mutex> = Mutex::new(None); + +/// Poison-tolerant: [`ScheduledTimerId`]'s drop takes this lock during unwinds. +fn lock_states() -> MutexGuard<'static, Option> { + TIMER_REF_STATES + .lock() + .unwrap_or_else(PoisonError::into_inner) +} + +/// A scheduled timer's pin on its registry entry. It lives IN the queue entry +/// (`CallbackTimer`, `IntervalTimer`, the mock-timer entries), so every path +/// that removes one — firing, `clearTimeout`/`clearInterval`/`clearImmediate`, +/// `purge_agent_timers`, a mock-timers reset — retires the id by dropping it, +/// and no removal site can forget to. Deliberately not `Clone`: a dropped copy +/// would retire a timer that is still queued. +/// +/// Dropping takes the registry lock, so never drop a timer entry while holding +/// it (the only nesting is queue lock → registry lock). +pub(super) struct ScheduledTimerId(i64); + +impl Drop for ScheduledTimerId { + fn drop(&mut self) { + if let Some(states) = lock_states().as_mut() { + states.retire(self.0, TIMER_REF_STATES_CAP); + } + } +} + +#[cfg(test)] +impl ScheduledTimerId { + /// For test scaffolding entries whose ids were never registered. + pub(super) fn unregistered() -> Self { + Self(i64::MIN) + } +} + +/// Register a timer id as scheduled (ref'd, with its handle kind). Runs before +/// the id is observable — the async_hooks `init` hook already sees the handle. +pub(super) fn register_scheduled_timer(id: i64, kind: CallbackTimerKind) -> ScheduledTimerId { + TIMER_IDS_NONEMPTY.arm(); + lock_states() + .get_or_insert_with(TimerRefStates::default) + .schedule(id, kind); + ScheduledTimerId(id) +} + +pub(super) fn set_timer_ref_state(id: i64, has_ref: bool) { + TIMER_IDS_NONEMPTY.arm(); + lock_states() + .get_or_insert_with(TimerRefStates::default) + .set_ref(id, has_ref, TIMER_REF_STATES_CAP); +} + +fn timer_handle_state(id: i64) -> Option { + lock_states().as_ref().and_then(|states| states.get(id)) +} + +/// Node's `hasRef()` default is `true`, so an id the registry does not hold +/// reads as ref'd. A scheduled timer's id is always held (#10447). +pub(super) fn timer_has_ref_state(id: i64) -> bool { + timer_handle_state(id).map_or(true, |state| state.has_ref) +} + +pub(super) fn timer_handle_kind(id: i64) -> Option { + timer_handle_state(id).and_then(|state| state.kind) +} + +/// Read-only view for a whole-queue liveness scan, under ONE registry lock +/// instead of one per entry. +pub(super) struct RefStatesView<'a>(Option<&'a TimerRefStates>); + +impl RefStatesView<'_> { + pub(super) fn has_ref(&self, id: i64) -> bool { + self.0 + .and_then(|states| states.get(id)) + .map_or(true, |state| state.has_ref) + } +} + +/// `f` must not drop a timer entry (see [`ScheduledTimerId`]). +pub(super) fn with_ref_states(f: impl FnOnce(&RefStatesView<'_>) -> R) -> R { + let guard = lock_states(); + f(&RefStatesView(guard.as_ref())) +} + +/// `PERRY_GC_CENSUS` row for the registry. +pub(super) fn ref_states_census() -> crate::gc::census::SideTableRow { + let guard = lock_states(); + let (len, bytes) = guard.as_ref().map_or((0, 0), |states| { + let deque = states.retired.capacity() * std::mem::size_of::(); + let map = crate::gc::census::map_bytes(&states.states); + (states.states.len(), map + deque) + }); + ("timer.ref_states", len, bytes) +} + +#[cfg(test)] +pub(crate) fn test_ref_state_counts() -> (usize, usize) { + lock_states().as_ref().map_or((0, 0), |states| { + let scheduled = states.states.values().filter(|s| s.scheduled).count(); + (states.states.len(), scheduled) + }) } #[cfg(test)] mod tests { - use super::TimerRefStates; + use super::{CallbackTimerKind, TimerRefStates, TIMER_REF_STATES_CAP}; + + fn scheduled_then_retired( + s: &mut TimerRefStates, + ids: std::ops::RangeInclusive, + cap: usize, + ) { + for id in ids { + s.schedule(id, CallbackTimerKind::Timeout); + s.retire(id, cap); + } + } - /// #6084: the ref-state registry must stay bounded, evicting the oldest ids - /// while retaining recent ones (so post-clear `.hasRef()` keeps working for - /// a handle held for any realistic duration). + /// #6084: retired ids stay bounded, evicting the oldest while retaining + /// recent ones (so post-clear `.hasRef()` keeps working for a handle held + /// for any realistic duration). #[test] - fn insert_bounded_evicts_oldest_and_caps_size() { + fn retired_ids_evict_oldest_and_cap_size() { let mut s = TimerRefStates::default(); let cap = 4; for id in 1..=10i64 { - s.insert_bounded(id, id % 2 == 0, cap); + s.schedule(id, CallbackTimerKind::Timeout); + s.set_ref(id, id % 2 == 0, cap); + s.retire(id, cap); } assert_eq!(s.states.len(), cap); - assert_eq!(s.order.len(), cap); + assert_eq!(s.retired.len(), cap); for id in 1..=6i64 { - assert!(!s.states.contains_key(&id), "id {id} should be evicted"); + assert!(s.get(id).is_none(), "id {id} should be evicted"); } for id in 7..=10i64 { - assert_eq!(s.states.get(&id).copied(), Some(id % 2 == 0)); + assert_eq!(s.get(id).map(|st| st.has_ref), Some(id % 2 == 0)); + } + } + + /// #10447: a still-scheduled id is never an eviction candidate, however + /// many later timers come and go — its `unref()` and its kind survive. + #[test] + fn a_scheduled_id_survives_any_number_of_later_timers() { + let mut s = TimerRefStates::default(); + let cap = 16; + s.schedule(1, CallbackTimerKind::Timeout); + s.set_ref(1, false, cap); + s.schedule(2, CallbackTimerKind::Immediate); + scheduled_then_retired(&mut s, 3..=10_000, cap); + let keep = s.get(1).expect("scheduled id 1 was evicted"); + assert!(!keep.has_ref, "id 1's unref() was forgotten"); + assert!(matches!(keep.kind, Some(CallbackTimerKind::Timeout))); + assert!(matches!( + s.get(2).and_then(|st| st.kind), + Some(CallbackTimerKind::Immediate) + )); + assert_eq!(s.states.len(), cap + 2); + // Once it retires it is an ordinary eviction candidate again. + s.retire(1, cap); + s.retire(2, cap); + scheduled_then_retired(&mut s, 10_001..=10_000 + cap as i64, cap); + assert!(s.get(1).is_none() && s.get(2).is_none()); + assert_eq!(s.states.len(), cap); + } + + /// More live timers than the cap: all of them stay, and churn around them + /// still only keeps `cap` retired ids. + #[test] + fn live_timers_beyond_the_cap_are_all_kept() { + let mut s = TimerRefStates::default(); + let cap = 8; + for id in 1..=100i64 { + s.schedule(id, CallbackTimerKind::Timeout); + } + scheduled_then_retired(&mut s, 101..=1_000, cap); + assert!((1..=100i64).all(|id| s.get(id).is_some())); + assert_eq!(s.states.len(), 100 + cap); + assert_eq!(s.retired.len(), cap); + } + + /// #6084's leak must stay fixed: a million set+clear cycles at the real + /// cap leave the registry at the cap, not at a million entries. + #[test] + fn a_million_set_clear_cycles_stay_bounded() { + let mut s = TimerRefStates::default(); + let cap = TIMER_REF_STATES_CAP; + scheduled_then_retired(&mut s, 1..=1_000_000, cap); + assert_eq!(s.states.len(), cap); + assert_eq!(s.retired.len(), cap); + // `ref()`/`unref()` on ids the registry never held is bounded too. + for id in 2_000_000..2_000_000 + 3 * cap as i64 { + s.set_ref(id, false, cap); } + assert_eq!(s.states.len(), cap); + assert_eq!(s.retired.len(), cap); } #[test] - fn ref_unref_of_existing_id_does_not_grow_queue() { + fn ref_unref_and_repeated_retire_do_not_grow_the_queue() { let mut s = TimerRefStates::default(); let cap = 100; - s.insert_bounded(42, true, cap); - s.insert_bounded(42, false, cap); - s.insert_bounded(42, true, cap); - assert_eq!(s.order.len(), 1); + s.schedule(42, CallbackTimerKind::Timeout); + s.set_ref(42, false, cap); + s.set_ref(42, true, cap); + assert_eq!( + s.retired.len(), + 0, + "a scheduled id is not queued for eviction" + ); + s.retire(42, cap); + s.retire(42, cap); + s.set_ref(42, false, cap); + assert_eq!(s.retired.len(), 1); assert_eq!(s.states.len(), 1); - assert_eq!(s.states.get(&42).copied(), Some(true)); + assert_eq!(s.get(42).map(|st| st.has_ref), Some(false)); + s.retire(7, cap); + assert_eq!(s.states.len(), 1, "retiring an unknown id is a no-op"); + } + + /// End to end through the real queues: a live unref'd `setTimeout`, an + /// unref'd `setInterval` and a `setImmediate` keep their state across more + /// than `TIMER_REF_STATES_CAP` later set+clear cycles, and do not keep the + /// event loop alive. Before #10447 all three ids were evicted, so the + /// loop saw two ref'd timers and `is_known_timer_id` rejected all three. + #[test] + fn live_timers_keep_ref_state_across_timer_churn() { + use crate::timer::*; + let _serial = crate::gc::global_side_table_test_lock(); + test_clear_all_timer_scanner_roots(); + let timeout = js_set_timeout_callback(0, 50_000.0); + js_timer_unref(timeout); + let interval = setInterval(0, 50_000.0); + js_timer_unref(interval); + let immediate = js_set_immediate_callback(0); + let recent = js_set_timeout_callback(0, 50_000.0); + js_timer_unref(recent); + + for _ in 0..TIMER_REF_STATES_CAP + 1_000 { + clearTimeout(js_set_timeout_callback(0, 1_000.0)); + } + clearTimeout(recent); + for _ in 0..1_000 { + clearTimeout(js_set_timeout_callback(0, 1_000.0)); + } + + for id in [timeout, interval, immediate, recent] { + assert!( + is_known_timer_id(id), + "timer {id} dropped from the registry" + ); + } + assert_eq!(js_timer_has_ref(timeout), 0); + assert_eq!(js_timer_has_ref(interval), 0); + assert_eq!(js_timer_has_ref(immediate), 1); + assert_eq!( + js_timer_has_ref(recent), + 0, + "post-clear hasRef of a recent handle" + ); + assert!(matches!( + super::timer_handle_kind(timeout), + Some(CallbackTimerKind::Timeout) + )); + assert!(matches!( + super::timer_handle_kind(interval), + Some(CallbackTimerKind::Timeout) + )); + assert!(matches!( + super::timer_handle_kind(immediate), + Some(CallbackTimerKind::Immediate) + )); + assert_eq!( + js_interval_timer_has_pending(), + 0, + "unref'd interval kept the loop alive" + ); + + // Only the ref'd immediate keeps the loop alive; once it is gone, + // nothing does — and re-`ref()`ing the timeout re-arms it. + clearImmediate(immediate); + assert_eq!( + js_callback_timer_has_pending(), + 0, + "unref'd timeout kept the loop alive" + ); + js_timer_ref(timeout); + assert_eq!( + js_callback_timer_has_pending(), + 1, + "ref() after churn did not re-arm" + ); + + // The registry stayed bounded: every entry beyond the cap is scheduled. + let (len, scheduled) = super::test_ref_state_counts(); + assert!( + len <= TIMER_REF_STATES_CAP + scheduled, + "{len} entries, {scheduled} scheduled" + ); + + clearTimeout(timeout); + clearInterval(interval); } } @@ -82,8 +409,8 @@ mod tests { /// measured it as `pthread_mutex_lock` under `dispatch_primitive` on a pure /// class-hierarchy benchmark that schedules no timers at all. /// -/// Armed by `set_timer_ref_state`, which runs before any id becomes -/// observable, per `registry_latch`'s ordering rule. +/// Armed by `register_scheduled_timer` / `set_timer_ref_state`, which run +/// before any id becomes observable, per `registry_latch`'s ordering rule. pub(crate) static TIMER_IDS_NONEMPTY: crate::registry_latch::RegistryLatch = crate::registry_latch::RegistryLatch::new(); @@ -94,11 +421,11 @@ pub(crate) static TIMER_IDS_NONEMPTY: crate::registry_latch::RegistryLatch = /// this gate, any small handle (UI widget, drizzle, etc.) would accidentally /// route through timer dispatch. /// -/// Entries in `TIMER_REF_STATES` are inserted at schedule time and never -/// removed — clearing a timer marks it cleared in the queue but keeps the -/// id registered as "this was a timer" so post-clear `.hasRef()` / `+timer` -/// / `.unref()` still route through timer dispatch (Node keeps the -/// Timeout object alive after `clearTimeout` and methods still work). +/// A scheduled timer's id is always registered. After `clearTimeout` or +/// firing it stays registered — so post-clear `.hasRef()` / `+timer` / +/// `.unref()` still route through timer dispatch (Node keeps the Timeout +/// object alive and its methods still work) — until 65,536 later timers have +/// also retired. #[inline] pub fn is_known_timer_id(id: i64) -> bool { if id <= 0 || TIMER_IDS_NONEMPTY.is_idle() { @@ -109,12 +436,9 @@ pub fn is_known_timer_id(id: i64) -> bool { #[inline(never)] fn is_known_timer_id_slow(id: i64) -> bool { - super::TIMER_REF_STATES - .lock() - .unwrap() + lock_states() .as_ref() - .map(|s| s.states.contains_key(&id)) - .unwrap_or(false) + .is_some_and(|states| states.states.contains_key(&id)) } #[cfg(test)] diff --git a/crates/perry-runtime/src/timer/tests_inline.rs b/crates/perry-runtime/src/timer/tests_inline.rs index eb527440dd..f365981d55 100644 --- a/crates/perry-runtime/src/timer/tests_inline.rs +++ b/crates/perry-runtime/src/timer/tests_inline.rs @@ -56,6 +56,7 @@ pub(crate) fn test_seed_timer_scanner_roots( async_id: 0, trigger_async_id: 0, cleared: false, + _scheduled: ref_states::ScheduledTimerId::unregistered(), }); INTERVAL_TIMERS.lock().unwrap().push(IntervalTimer { // #6185: test scaffolding runs on the primary agent. @@ -69,6 +70,7 @@ pub(crate) fn test_seed_timer_scanner_roots( async_id: 0, trigger_async_id: 0, cleared: false, + _scheduled: ref_states::ScheduledTimerId::unregistered(), }); } @@ -184,6 +186,7 @@ mod expired_batch_order_tests { async_id: 0, trigger_async_id: 0, cleared: false, + _scheduled: crate::timer::ref_states::ScheduledTimerId::unregistered(), } } diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 0e3ed1f219..70d46c0e73 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -1117,12 +1117,6 @@ "verdict": "not_a_gc_pointer", "why": "In-flight job counter for perry/thread." }, - { - "file": "crates/perry-runtime/src/timer.rs", - "name": "TIMER_HANDLE_KINDS", - "verdict": "not_a_gc_pointer", - "why": "Maps scalar timer handle IDs to the CallbackTimerKind enum so clearTimeout/clearInterval can destroy the matching async_hooks resource. Neither the i64 keys nor the enum values contain a JS heap address." - }, { "file": "crates/perry-runtime/src/weakref/test_support.rs", "name": "DELIVERED", diff --git a/test-files/test_gap_10447_timer_ref_state_eviction.ts b/test-files/test_gap_10447_timer_ref_state_eviction.ts new file mode 100644 index 0000000000..a8b7f07c8c --- /dev/null +++ b/test-files/test_gap_10447_timer_ref_state_eviction.ts @@ -0,0 +1,77 @@ +// #10447: a SCHEDULED timer's ref state and handle kind must survive any +// number of later timers. The id -> ref-state registry is bounded (#6084), but +// it evicted the oldest 65,536+ ids whether or not they were still scheduled. +// After that many later timers a live `unref()`'d timer read as ref'd again — +// the process stayed alive until it fired and ran the callback the program had +// detached — and `.hasRef()` / `.ref()` / `.unref()` / `.constructor` / `+t` +// stopped resolving on the handle at all. +// +// Every "BUG" callback below belongs to an unref'd timer: once the last ref'd +// timer has fired nothing keeps the loop alive, so none of them may run (and +// the process exits promptly instead of waiting 1-1.5 s for them). + +const CHURN = 70000; // > 65,536 + +function churn(n: number): void { + for (let i = 0; i < n; i++) clearTimeout(setTimeout(() => {}, 1000)); +} + +function show(label: string, t: any): void { + const hasRef = typeof t.hasRef === "function" ? t.hasRef() : "missing"; + const ctor = t.constructor ? t.constructor.name : "missing"; + let line = `${label}: hasRef=${hasRef} ctor=${ctor}`; + // A Timeout coerces to its numeric id (an Immediate does not). + if (ctor !== "Immediate") line += ` primitive=${!Number.isNaN(+t)}`; + console.log(line); +} + +process.on("exit", () => console.log("exit")); + +// --- below-cap control: unaffected before and after the fix --- +const control = setTimeout(() => console.log("BUG: control unref'd timeout fired"), 1500); +control.unref(); +churn(1000); +show("control after 1000 timers", control); + +// --- subjects scheduled BEFORE more than 65,536 later timers --- +const unrefTimeout = setTimeout(() => console.log("BUG: unref'd timeout fired"), 1500); +unrefTimeout.unref(); + +const unrefInterval = setInterval(() => { + console.log("BUG: unref'd interval fired"); + clearInterval(unrefInterval); +}, 1000); +unrefInterval.unref(); + +const unrefLater = setTimeout(() => console.log("BUG: timeout unref'd after churn fired"), 1500); + +const reRef = setTimeout(() => console.log("re-ref'd timeout fired"), 1); +reRef.unref(); + +const immediate = setImmediate(() => {}); + +const refd = setTimeout(() => console.log("last ref'd timeout fired"), 20); + +churn(CHURN); +console.log(`churned ${CHURN} timers`); + +show("unref'd timeout", unrefTimeout); +show("unref'd interval", unrefInterval); +show("immediate", immediate); +show("ref'd timeout", refd); + +// unref() / ref() on a live handle that has 70k later timers behind it. +show("timeout before unref()", unrefLater); +unrefLater.unref(); +show("timeout after unref()", unrefLater); +show("timeout before ref()", reRef); +reRef.ref(); +show("timeout after ref()", reRef); + +// post-clear state on a recent handle is kept (the bounded part of #6084) +const recent = setTimeout(() => console.log("BUG: cleared timeout fired"), 1); +recent.unref(); +clearTimeout(recent); +show("recent cleared timeout", recent); + +console.log("main done"); From 968209c604d2a06c3733c3e077320b9dc3be095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 13:20:02 +0000 Subject: [PATCH 08/11] docs(changelog): add fragment for #10538 --- changelog.d/10538-timer-ref-state-eviction.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 changelog.d/10538-timer-ref-state-eviction.md diff --git a/changelog.d/10538-timer-ref-state-eviction.md b/changelog.d/10538-timer-ref-state-eviction.md new file mode 100644 index 0000000000..06f1f5a848 --- /dev/null +++ b/changelog.d/10538-timer-ref-state-eviction.md @@ -0,0 +1,20 @@ +Fixed `timer.unref()` being forgotten after 65,536 later timers (#10447). + +**What broke.** #6084 bounded the id→ref-state registry at 65,536 entries. It evicted by insertion order and never checked whether an id was still scheduled. A live, long-delay `unref()`'d timer was evicted once 65,536 newer timers had been created. The lookup then read the missing id as ref'd, so the process stayed alive until that timer fired and ran a callback the program had detached. rate-limiter-flexible, which creates one timer per key, lingered 12–15 s after a 1M-operation run. The evicted id also dropped out of `is_known_timer_id`, so `.hasRef()`, `.ref()`, `.unref()`, `.constructor` and `+t` stopped resolving on the handle. The id→kind table (`Timeout`/`Immediate`) had the same cap. Once full, it scanned all 65,536 keys for the minimum on every `setTimeout`, so 200k `clearTimeout(setTimeout(f, 1000))` cost 126 billion instructions. + +**Fix.** `crates/perry-runtime/src/timer/ref_states.rs`: +- One registry now holds `has_ref`, `kind` and a `scheduled` flag per id. +- Scheduling returns a `ScheduledTimerId` token that lives in the queue entry itself (`CallbackTimer`, `IntervalTimer`, and the mock-timer entries). Dropping the entry retires the id. Every removal path drops the entry: firing, `clearTimeout`/`clearInterval`/`clearImmediate`, agent purge, and mock clear/reset. +- Only retired ids are eviction candidates, so the map holds at most live timers + 65,536 entries. +- Scheduling does one registry insert, and there is no per-insert scan. +- The whole-queue liveness scans (`ownership.rs`) take the registry lock once per scan instead of once per entry. +- The `timer.rs` filters test `allow_unref` before looking up `has_ref`. +- The registry reports a `timer.ref_states` row in `PERRY_GC_CENSUS`. +- The stale `TIMER_HANDLE_KINDS` entry is removed from `scripts/gc_runtime_root_holders.json`. + +**Validation.** +- The gap test `test_gap_10447_timer_ref_state_eviction` matches Node byte for byte. On the baseline, every handle loses its methods and four unref'd callbacks fire. +- New unit tests in `ref_states.rs` cover: scheduled ids surviving churn, live timers beyond the cap, 1M set+clear cycles staying at 65,536 entries, and an end-to-end churn through the real queues. +- Gap suite: 814/820 pass; the 6 failures are the baseline's known ones. +- Instructions: 200k set+clear −99.3 %, 100k chained `setImmediate` −95.1 %. Below-cap schedule/fire/clear workloads are 4–8 % faster, and `asyncpipe` is flat (+0.1 %). +- Registry size is 65,536 entries after both 1M and 3M set+clear cycles. From 75efdb275a89c8de9d5661fd9ff703ccfb7d07df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 11:56:48 +0000 Subject: [PATCH 09/11] fix(codegen): stop typing BigInt-capable bitwise results as int32 `&` `|` `^` `<<` `>>` compute a BigInt from two BigInt operands, but the compiler assumed every bitwise result is an int32 Number: - HIR typed `a & b` over unknown operands `Number` (lower_types.rs, value_types.rs), so `stable_local_type_proof` vouched for it. - The integer-local proofs (integer_locals.rs judge, int_valued_ta_locals.rs) admitted every bitwise write, so `const x = a & b` took an int32 slot and `ToInt32`'d the BigInt result to `0`; a `bigint`-typed binding reached a call as `fptosi` of its box. - `Number(a & b)` elided `js_number_coerce` for any bitwise operand (bigint_set.rs), returning the BigInt unchanged. Each site now requires an operand that provably is not a BigInt. The not-BigInt fixpoint is exposed as `NotBigIntFacts` and computed ahead of the integer-local proofs. With the int32 slot no longer covering unproven operands, those operators take the existing guarded numeric diamond (tag test, inline `ToInt32 ToInt32`, BigInt-aware helper on the cold arm). --- .../perry-codegen/src/collectors/hir_facts.rs | 98 +- .../src/collectors/int_valued_ta_locals.rs | 102 +- .../collectors/int_valued_ta_locals/tests.rs | 103 +- .../src/collectors/integer_locals.rs | 67 +- .../src/collectors/not_bigint_locals.rs | 108 +- .../src/collectors/spec_abi_sites.rs | 4 +- .../src/expr/bigint_bitwise_tests.rs | 199 ++++ crates/perry-codegen/src/expr/bigint_set.rs | 11 +- crates/perry-codegen/src/expr/binary.rs | 41 +- crates/perry-codegen/src/expr/mod.rs | 2 + crates/perry-hir/src/analysis/value_types.rs | 22 +- .../src/analysis/value_types_tests.rs | 55 + crates/perry-hir/src/lower_types.rs | 9 +- crates/perry-hir/src/types.rs | 11 + crates/perry-hir/tests/shape_inference.rs | 26 + .../test_gap_10418_bigint_bitwise_typing.ts | 1031 +++++++++++++++++ 16 files changed, 1800 insertions(+), 89 deletions(-) create mode 100644 crates/perry-codegen/src/expr/bigint_bitwise_tests.rs create mode 100644 test-files/test_gap_10418_bigint_bitwise_typing.ts diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 48b4d6b2b0..aa2ba7e257 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -509,6 +509,10 @@ pub(crate) fn collect_type_facts( // above all the counter in `for (let i = …) sum += buf[i]`, have to be // walked for or the hottest buffer shape loses its i32 representation. let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); + // #10418: computed ahead of the integer-local proofs, which may treat a + // bitwise result as an int32 only when it cannot be a BigInt. + let not_bigint = + super::not_bigint_locals::NotBigIntFacts::collect(stmts, params, binding_types); let mut integer_locals = super::integer_locals::collect_integer_locals_with_seeds( stmts, flat_const_ids, @@ -516,6 +520,7 @@ pub(crate) fn collect_type_facts( arg_dependent_clamp_fn_ids, &numeric_locals, spec_i32_params, + ¬_bigint, ); // Native-i32 residency for integer-valued locals whose init/writes include a // possibly-out-of-bounds INT typed-array element read or a numeric-array @@ -533,6 +538,7 @@ pub(crate) fn collect_type_facts( binding_types, spec_ta_lens, spec_number_array_params, + ¬_bigint, ); // `--opt-report` (#6952) / promotion census (#7106): the win column // for this analysis, recorded at the ONE site where a candidate @@ -594,8 +600,7 @@ pub(crate) fn collect_type_facts( // feed the stronger fact into its downstream consumers explicitly. This // is a consequence of the range proof, not an additional assumption. integer_locals.extend(loop_bounded_i32_locals.iter().copied()); - let not_bigint_locals = - super::not_bigint_locals::collect_not_bigint_locals(stmts, params, binding_types); + let not_bigint_locals = not_bigint.into_locals(); // #8105: locals that hold a JS Number by construction. Computed here, not // inside the `Ptr` pass, so the fact does not vanish under // `PERRY_PTR_SHAPE_LOCALS=0` — `is_numeric_expr` is not a repsel consumer. @@ -2798,4 +2803,93 @@ mod tests { "`x++` over a disqualified local must not stay integer" ); } + + /// #10418: `&` `|` `^` `<<` `>>` over two operands that may be BigInts + /// compute a BigInt, so their result must not make a local integer-valued + /// — the int32 slot read `const x = a & b` back as `0`. A literal operand, + /// a proven-Number local or an integer candidate (here a raw-i32 spec + /// parameter) still proves the Number result. + #[test] + fn bigint_capable_bitwise_init_is_not_an_integer_local() { + let any_param = |id: u32| perry_hir::Param { + id, + name: format!("p{id}"), + ty: Type::Any, + default: None, + decorators: vec![], + is_rest: false, + arguments_object: None, + }; + let any_const = |id: u32, init: Expr| Stmt::Let { + id, + name: format!("v{id}"), + ty: Type::Any, + mutable: false, + init: Some(init), + }; + let bin = |op: BinaryOp, left: Expr, right: Expr| Expr::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }; + const A: u32 = 10; + const B: u32 = 11; + let params = [any_param(A), any_param(B)]; + let stmts = vec![ + // const x = a & b; + any_const( + 1, + bin(BinaryOp::BitAnd, Expr::LocalGet(A), Expr::LocalGet(B)), + ), + // const y = a & 255; + any_const( + 2, + bin(BinaryOp::BitAnd, Expr::LocalGet(A), Expr::Integer(255)), + ), + // const z = b << y; + any_const(3, bin(BinaryOp::Shl, Expr::LocalGet(B), Expr::LocalGet(2))), + // const w = x ^ b; + any_const( + 4, + bin(BinaryOp::BitXor, Expr::LocalGet(1), Expr::LocalGet(B)), + ), + ]; + let not_bigint = super::super::not_bigint_locals::NotBigIntFacts::collect( + &stmts, + ¶ms, + &HashMap::new(), + ); + let empty = HashSet::new(); + let ints = super::super::integer_locals::collect_integer_locals_with_seeds( + &stmts, + &empty, + &empty, + &empty, + &empty, + &empty, + ¬_bigint, + ); + assert!(!ints.contains(&1), "`a & b` may be a BigInt: {ints:?}"); + assert!(ints.contains(&2), "`a & 255` is a Number: {ints:?}"); + assert!( + ints.contains(&3), + "`b << y` has a proven-Number operand: {ints:?}" + ); + assert!(!ints.contains(&4), "`x ^ b` may be a BigInt: {ints:?}"); + + // A raw-i32 spec parameter is an integer candidate, so it proves the + // Number result of every bitwise operator it is an operand of. + let seeds: HashSet = [A].into_iter().collect(); + let ints = super::super::integer_locals::collect_integer_locals_with_seeds( + &stmts, + &empty, + &empty, + &empty, + &empty, + &seeds, + ¬_bigint, + ); + assert!(ints.contains(&1), "an i32 operand proves `a & b`: {ints:?}"); + assert!(ints.contains(&4), "`x ^ b` inherits `x`'s proof: {ints:?}"); + } } diff --git a/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs b/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs index d4b5e4dee8..bef9d7da7e 100644 --- a/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs +++ b/crates/perry-codegen/src/collectors/int_valued_ta_locals.rs @@ -39,7 +39,8 @@ //! NOT `Uint32`, NOT the float / bigint kinds), or a numeric-array read //! backed by the specialized entry's runtime guard. An erasable source //! `number[]` annotation alone is never evidence. Other admitted writes are: -//! a bitwise op (`& | ^ << >> >>>`), `~`, `Math.imul`, an i32 literal, +//! a bitwise op (`& | ^ << >> >>>`) or `~` whose result is not a BigInt +//! (#10418), `Math.imul`, an i32 literal, //! `undefined` (the hoisted-`var` seed — `ToInt32(undefined) == 0`, the //! slot's seed value), or a `Uint8ArrayGet`/`BufferIndexGet`. NOT `*` //! (a single product can exceed 2^53 and round). Additive `+`/`-` is @@ -88,6 +89,8 @@ use std::collections::{HashMap, HashSet}; use perry_hir::types::Type as HirType; use perry_hir::{BinaryOp, Expr, Param, Stmt, UnaryOp}; +use super::not_bigint_locals::NotBigIntFacts; + /// `PERRY_INT_VALUED_LOCALS` gate. Enabled by default; `=0`/`off`/`false` /// disables the analysis (returns an empty set), reverting the affected locals /// to the f64 representation. Mirrors the sibling codegen fast-path env gates. @@ -222,6 +225,46 @@ fn write_is_i32_producing_safe( } } +/// Rule (1) with the BigInt case excluded (#10418). `&` `|` `^` `<<` `>>` and +/// `~` compute a BigInt from BigInt operands, so such a write is an exact i32 +/// only when `not_bigint` proves its result a Number or an operand is one — a +/// `pool` member or another rule-(1) value. Without this, `let x = u8[0]; x = +/// a & b` truncated the BigInt `a & b` into the i32 slot. +fn write_is_i32_number_safe( + e: &Expr, + types: &HashMap, + guarded_number_array_params: &HashSet, + numeric_locals: &HashSet, + pool: &HashSet, + not_bigint: &NotBigIntFacts, +) -> bool { + if !write_is_i32_producing_safe(e, types, guarded_number_array_params, numeric_locals) { + return false; + } + let operand_is_number = |operand: &Expr| { + matches!(operand, Expr::LocalGet(id) if pool.contains(id)) + || write_is_i32_number_safe( + operand, + types, + guarded_number_array_params, + numeric_locals, + pool, + not_bigint, + ) + }; + match e { + Expr::Binary { left, right, .. } => { + not_bigint.bitwise_result_is_number(e) + || operand_is_number(left) + || operand_is_number(right) + } + Expr::Unary { operand, .. } => { + not_bigint.bitwise_result_is_number(e) || operand_is_number(operand) + } + _ => true, + } +} + /// True when the JavaScript value produced by a write is itself an exact i32, /// not merely a value whose `ToInt32` image is safe to retain. Once such a /// write dominates a later read, non-coercing observations are safe too: the @@ -302,7 +345,11 @@ fn additive_write_admissible( ta_lens: &HashMap, pool: &HashSet, numeric_locals: &HashSet, + not_bigint: &NotBigIntFacts, ) -> bool { + let admissible = |sub: &Expr| { + additive_write_admissible(sub, types, ta_lens, pool, numeric_locals, not_bigint) + }; match e { Expr::Integer(n) => super::i32_locals::integer_literal_fits_i32(*n), Expr::LocalGet(id) => pool.contains(id), @@ -315,16 +362,18 @@ fn additive_write_admissible( (Some(len), Some((lo, hi))) if lo >= 0 && hi < *len )) } - // Exact ToInt32/ToUint32 producers regardless of operand shape. - Expr::Binary { op, .. } if is_bitwise_binop(*op) => true, + // Exact ToInt32/ToUint32 producers once the result is not a BigInt + // (#10418): proven by `not_bigint` or by an admissible operand. + Expr::Binary { op, left, right } if is_bitwise_binop(*op) => { + not_bigint.bitwise_result_is_number(e) || admissible(left) || admissible(right) + } Expr::Unary { op: UnaryOp::BitNot, - .. - } => true, + operand, + } => not_bigint.bitwise_result_is_number(e) || admissible(operand), Expr::MathImul(_, _) => true, Expr::Binary { op, left, right } if matches!(op, BinaryOp::Add | BinaryOp::Sub) => { - additive_write_admissible(left, types, ta_lens, pool, numeric_locals) - && additive_write_admissible(right, types, ta_lens, pool, numeric_locals) + admissible(left) && admissible(right) } _ => false, } @@ -659,6 +708,8 @@ pub fn collect_int_valued_ta_locals( binding_types: &HashMap, extra_ta_lens: &HashMap, guarded_number_array_params: &HashSet, + // #10418: which bitwise writes are Numbers rather than possible BigInts. + not_bigint: &NotBigIntFacts, ) -> HashSet { // Declared-type map (params + let bindings), used to classify typed-array // receivers. Params are included so `lr: Int32Array` resolves. @@ -736,16 +787,23 @@ pub fn collect_int_valued_ta_locals( let snapshot = pool.clone(); pool.retain(|id| { facts.writes[id].iter().all(|(w, in_loop)| { - write_is_i32_producing_safe(w, &types, guarded_number_array_params, &numeric_locals) - || ((!in_loop || loop_reseeded.contains(id)) - && !additive_invalid.contains(id) - && additive_write_admissible( - w, - &types, - &ta_lens, - &snapshot, - &numeric_locals, - )) + write_is_i32_number_safe( + w, + &types, + guarded_number_array_params, + &numeric_locals, + &snapshot, + not_bigint, + ) || ((!in_loop || loop_reseeded.contains(id)) + && !additive_invalid.contains(id) + && additive_write_admissible( + w, + &types, + &ta_lens, + &snapshot, + &numeric_locals, + not_bigint, + )) }) }); if pool.len() == before { @@ -787,6 +845,7 @@ pub fn collect_int_valued_ta_locals( pool: &candidates, exact_after_root_normalization: &exact_after_root_normalization, numeric_locals: &numeric_locals, + not_bigint, }; let mut exact_i32 = HashSet::new(); observe_stmts( @@ -810,11 +869,13 @@ pub fn collect_int_valued_ta_locals( let snapshot = candidates.clone(); candidates.retain(|id| { facts.writes[id].iter().all(|(w, in_loop)| { - write_is_i32_producing_safe( + write_is_i32_number_safe( w, &types, guarded_number_array_params, &numeric_locals, + &snapshot, + not_bigint, ) || (!in_loop && !additive_invalid.contains(id) && additive_write_admissible( @@ -823,6 +884,7 @@ pub fn collect_int_valued_ta_locals( &ta_lens, &snapshot, &numeric_locals, + not_bigint, )) }) }); @@ -845,6 +907,8 @@ struct AdditiveCtx<'a> { exact_after_root_normalization: &'a HashSet, /// #7700: locals whose declared type says they hold a number. numeric_locals: &'a HashSet, + /// #10418: see `additive_write_admissible`. + not_bigint: &'a NotBigIntFacts, } // --------------------------------------------------------------------------- @@ -1300,6 +1364,7 @@ fn observe_stmts( additive.ta_lens, additive.pool, additive.numeric_locals, + additive.not_bigint, ) { observe_additive_rhs(e, cands, types, additive, exact_i32, disq); @@ -1539,6 +1604,7 @@ fn observe( additive.ta_lens, additive.pool, additive.numeric_locals, + additive.not_bigint, ) { observe_additive_rhs(value, cands, types, additive, exact_i32, disq); diff --git a/crates/perry-codegen/src/collectors/int_valued_ta_locals/tests.rs b/crates/perry-codegen/src/collectors/int_valued_ta_locals/tests.rs index c3504b46ef..557c877e4f 100644 --- a/crates/perry-codegen/src/collectors/int_valued_ta_locals/tests.rs +++ b/crates/perry-codegen/src/collectors/int_valued_ta_locals/tests.rs @@ -79,8 +79,21 @@ fn any_param(id: u32) -> Param { } } +/// `collect_int_valued_ta_locals` with the non-BigInt facts its production +/// caller derives from the same body, params and binding types. +fn collect( + stmts: &[Stmt], + params: &[Param], + binding_types: &HashMap, + ta_lens: &HashMap, + guarded: &HashSet, +) -> HashSet { + let not_bigint = NotBigIntFacts::collect(stmts, params, binding_types); + collect_int_valued_ta_locals(stmts, params, binding_types, ta_lens, guarded, ¬_bigint) +} + fn run(stmts: &[Stmt], params: &[Param]) -> HashSet { - collect_int_valued_ta_locals( + collect( stmts, params, &HashMap::new(), @@ -284,8 +297,7 @@ fn guarded_number_array_seed_is_native_after_bitwise_normalization() { }), ]; let guarded = HashSet::from([0]); - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); + let got = collect(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); assert!( got.contains(&5), "guarded number-array accumulator missing: {got:?}" @@ -307,8 +319,7 @@ fn guarded_number_array_bare_read_before_normalization_is_rejected() { set(5, xor(Expr::LocalGet(5), Expr::Integer(7))), ]; let guarded = HashSet::from([0]); - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); + let got = collect(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); assert!( !got.contains(&5), "a pre-normalization bare read must remain observable: {got:?}" @@ -328,8 +339,7 @@ fn conditional_bitwise_normalization_does_not_dominate_later_read() { Stmt::Return(Some(Expr::LocalGet(5))), ]; let guarded = HashSet::from([0]); - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); + let got = collect(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); assert!( !got.contains(&5), "one-branch normalization must not license a later bare read: {got:?}" @@ -351,8 +361,7 @@ fn unreachable_nested_normalization_does_not_license_later_read() { Stmt::Return(Some(Expr::LocalGet(5))), ]; let guarded = HashSet::from([0]); - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); + let got = collect(&stmts, ¶ms, &HashMap::new(), &HashMap::new(), &guarded); assert!( !got.contains(&5), "a nested normalization bypassed by break must not dominate: {got:?}" @@ -409,7 +418,7 @@ fn non_int_kind_typed_array_read_not_seeded() { let_stmt(5, HirType::Any, Some(idx_get(0, Expr::LocalGet(1)))), set(5, xor(Expr::LocalGet(5), Expr::Integer(3))), ]; - let got = collect_int_valued_ta_locals( + let got = collect( &stmts, ¶ms, &binding_types, @@ -458,8 +467,7 @@ fn wrap_i32_additive_chain_with_proven_operands_is_admitted() { ), set(8, xor(Expr::LocalGet(8), Expr::LocalGet(9))), ]; - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); + let got = collect(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); assert!( got.contains(&9), "wrap-i32 additive accumulator wrongly excluded: {got:?}" @@ -488,8 +496,7 @@ fn wrap_i32_additive_inside_loop_is_rejected() { )], }, ]; - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); + let got = collect(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); assert!( !got.contains(&9), "loop-carried additive chain wrongly admitted: {got:?}" @@ -515,8 +522,7 @@ fn wrap_i32_candidate_as_bare_index_is_rejected() { ), let_stmt(10, HirType::Any, Some(idx_get(0, Expr::LocalGet(9)))), ]; - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); + let got = collect(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); assert!( !got.contains(&9), "bare-index wrap-i32 candidate wrongly admitted: {got:?}" @@ -549,8 +555,7 @@ fn wrap_i32_additive_operand_with_possibly_undefined_value_is_rejected() { set(10, xor(Expr::LocalGet(10), Expr::Integer(1))), set(9, xor(Expr::LocalGet(9), Expr::Integer(1))), ]; - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); + let got = collect(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); assert!( !got.contains(&10), "additive over possibly-undefined operand wrongly admitted: {got:?}" @@ -587,8 +592,7 @@ fn wrap_i32_additive_operand_reset_in_bounds_is_admitted() { ), set(9, xor(Expr::LocalGet(9), Expr::Integer(1))), ]; - let got = - collect_int_valued_ta_locals(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); + let got = collect(&stmts, ¶ms, &HashMap::new(), &lens, &HashSet::new()); assert!( got.contains(&9), "in-bounds-reset additive accumulator wrongly rejected: {got:?}" @@ -613,7 +617,7 @@ fn wrap_i32_additive_without_length_proof_is_rejected() { ), set(9, xor(Expr::LocalGet(9), Expr::Integer(1))), ]; - let got = collect_int_valued_ta_locals( + let got = collect( &stmts, ¶ms, &HashMap::new(), @@ -625,3 +629,60 @@ fn wrap_i32_additive_without_length_proof_is_rejected() { "length-unproven additive operand wrongly admitted: {got:?}" ); } + +/// #10418: `a & b` over two operands that may be BigInts computes a BigInt, so +/// it is not an exact i32 write — the slot would truncate it to `0`. One +/// provably-Number operand (a literal, or another candidate) restores it. +#[test] +fn bigint_capable_bitwise_write_is_not_an_i32_write() { + // arr:Int32Array (0), a (1), b (2) untyped + // let x = arr[0]; x = a & b; arr[1] = x; + let params = [int32_array_param(0), any_param(1), any_param(2)]; + let store = |id: u32| { + Stmt::Expr(Expr::IndexSet { + object: Box::new(Expr::LocalGet(0)), + index: Box::new(Expr::Integer(1)), + value: Box::new(Expr::LocalGet(id)), + }) + }; + let bigint_capable = vec![ + let_stmt(9, HirType::Any, Some(idx_get(0, Expr::Integer(0)))), + set( + 9, + bin(BinaryOp::BitAnd, Expr::LocalGet(1), Expr::LocalGet(2)), + ), + store(9), + ]; + let got = run(&bigint_capable, ¶ms); + assert!( + !got.contains(&9), + "a possibly-BigInt `a & b` write must not admit an i32 slot: {got:?}" + ); + + // `a & 0xff` is a Number (a BigInt `a` throws), so the slot stays. + let masked = vec![ + let_stmt(9, HirType::Any, Some(idx_get(0, Expr::Integer(0)))), + set( + 9, + bin(BinaryOp::BitAnd, Expr::LocalGet(1), Expr::Integer(0xff)), + ), + store(9), + ]; + let got = run(&masked, ¶ms); + assert!( + got.contains(&9), + "literal-masked write lost its slot: {got:?}" + ); + + // `x ^ a` where `x` is itself a candidate: the candidate operand proves it. + let accumulator = vec![ + let_stmt(9, HirType::Any, Some(idx_get(0, Expr::Integer(0)))), + set(9, xor(Expr::LocalGet(9), Expr::LocalGet(1))), + store(9), + ]; + let got = run(&accumulator, ¶ms); + assert!( + got.contains(&9), + "candidate-operand write lost its slot: {got:?}" + ); +} diff --git a/crates/perry-codegen/src/collectors/integer_locals.rs b/crates/perry-codegen/src/collectors/integer_locals.rs index 534da30b55..ab31e24a33 100644 --- a/crates/perry-codegen/src/collectors/integer_locals.rs +++ b/crates/perry-codegen/src/collectors/integer_locals.rs @@ -27,12 +27,14 @@ //! unconditionally. //! //! `int32_producing_deps` is deliberately stricter than the admission-side -//! `is_int32_producing_expr` in two places where the latter is optimistic: -//! `Expr::Update` requires the updated local to itself be a candidate, and a +//! `is_int32_producing_expr` in three places where the latter is optimistic: +//! `Expr::Update` requires the updated local to itself be a candidate, a //! call to an *argument-dependent* clamp function (`clamp3`-shaped functions //! return one of their arguments verbatim) requires every argument to be -//! int-producing. Anything admission accepted that judgment rejects is simply -//! pruned. +//! int-producing, and a BigInt-capable bitwise operator (`&` `|` `^` `<<` +//! `>>`) requires an operand that is provably not a BigInt (#10418 — over two +//! BigInts it computes a BigInt, which an i32 slot reads back as `0`). +//! Anything admission accepted that judgment rejects is simply pruned. //! //! Scoping notes: `clamp_fn_ids` are *function* ids (module-global, no //! per-function contamination). `flat_const_ids` are module-init local ids of @@ -369,6 +371,10 @@ fn collect_int_ta_load_let_ids( } } +/// The integer-local proof with no spec seeds, and non-BigInt facts derived +/// from the body alone. Production reaches `collect_integer_locals_with_seeds` +/// through `collect_type_facts`, which has the params and binding types. +#[cfg(test)] pub fn collect_integer_locals( stmts: &[perry_hir::Stmt], flat_const_ids: &HashSet, @@ -386,6 +392,7 @@ pub fn collect_integer_locals( arg_dependent_clamp_fn_ids, numeric_locals, &HashSet::new(), + &super::not_bigint_locals::NotBigIntFacts::collect(stmts, &[], &HashMap::new()), ) } @@ -400,6 +407,8 @@ pub(crate) fn collect_integer_locals_with_seeds( arg_dependent_clamp_fn_ids: &HashSet, numeric_locals: &HashSet, seed_locals: &HashSet, + // #10418: which bitwise results are Numbers rather than possible BigInts. + not_bigint: &super::not_bigint_locals::NotBigIntFacts, ) -> HashSet { let mut candidates: HashSet = seed_locals.clone(); @@ -475,6 +484,7 @@ pub(crate) fn collect_integer_locals_with_seeds( clamp_fn_ids, arg_dependent_clamp_fn_ids, numeric_locals, + not_bigint, int_ta_views: &int_ta_views, dependents: HashMap::new(), disqualified: HashSet::new(), @@ -527,6 +537,8 @@ struct ProvenanceJudge<'a> { /// #7700: locals whose declared type says they hold a number, so a /// `u8[k]` keyed on one is a byte read. numeric_locals: &'a HashSet, + /// #10418: the non-BigInt proof a BigInt-capable bitwise obligation needs. + not_bigint: &'a super::not_bigint_locals::NotBigIntFacts, /// Const int-typed-array views (`id → length`) whose in-window element /// loads are integers by construction — obligations whose rhs is such a /// load pass without deps. @@ -556,6 +568,7 @@ impl ProvenanceJudge<'_> { self.clamp_fn_ids, self.arg_dependent_clamp_fn_ids, self.numeric_locals, + self.not_bigint, &mut deps, ) { for dep in deps { @@ -697,6 +710,10 @@ impl ProvenanceJudge<'_> { /// int-producing, and the argument deps are recorded. `clampU8`-shaped /// and `returns_integer` functions coerce internally (`| 0` / bitwise on /// every value-returning path) and stay argument-independent. +/// - `&` `|` `^` `<<` `>>` (#10418): int-producing only when their result +/// cannot be a BigInt — an operand `not_bigint` proves, or an operand that +/// is itself int-producing (whose deps are then recorded). Admission +/// accepts every bitwise expression; `a & b` over two BigInts is a BigInt. #[allow(clippy::too_many_arguments)] fn int32_producing_deps( e: &perry_hir::Expr, @@ -706,6 +723,7 @@ fn int32_producing_deps( clamp_fn_ids: &HashSet, arg_dependent_clamp_fn_ids: &HashSet, numeric_locals: &HashSet, + not_bigint: &super::not_bigint_locals::NotBigIntFacts, deps: &mut HashSet, ) -> bool { let recurse = |sub: &Expr, deps: &mut HashSet| { @@ -717,6 +735,7 @@ fn int32_producing_deps( clamp_fn_ids, arg_dependent_clamp_fn_ids, numeric_locals, + not_bigint, deps, ) }; @@ -754,15 +773,33 @@ fn int32_producing_deps( false } } - Expr::Binary { op, .. } => matches!( - op, - BinaryOp::BitAnd - | BinaryOp::BitOr - | BinaryOp::BitXor - | BinaryOp::Shl - | BinaryOp::Shr - | BinaryOp::UShr - ), + Expr::Binary { op, left, right } + if matches!( + op, + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + ) => + { + if not_bigint.bitwise_result_is_number(e) { + return true; + } + // An int-producing operand is a Number too; keep only the deps of + // the side that carried the proof. + [left, right].into_iter().any(|side| { + let mut side_deps = HashSet::new(); + let proven = recurse(side, &mut side_deps); + if proven { + deps.extend(side_deps); + } + proven + }) + } + Expr::Binary { + op: BinaryOp::UShr, .. + } => true, Expr::LocalGet(id) if candidates.contains(id) => { deps.insert(*id); true @@ -1019,7 +1056,9 @@ pub fn collect_flat_row_aliases( /// - `(expr) | 0` and `(expr) >>> 0`: the JS ToInt32 / ToUint32 idiom — /// always yields a 32-bit integer regardless of the inner expression. /// - Pure bitwise ops (`&`, `|`, `^`, `<<`, `>>`, `>>>`): per JS spec -/// these coerce both operands to int32 and return int32. +/// these coerce Number operands to int32 and return int32. Optimistic: +/// `&` `|` `^` `<<` `>>` over two BigInts compute a BigInt, which the +/// disqualification judgment rejects (#10418). /// - `Expr::Update`: `++` / `--` on an integer-stable local (admission /// doesn't verify the target; the disqualification judgment does). /// - (issue #49) `LocalGet(id)` when `id` is itself in `known_int_locals` — diff --git a/crates/perry-codegen/src/collectors/not_bigint_locals.rs b/crates/perry-codegen/src/collectors/not_bigint_locals.rs index 3578e0df01..a0958cbe81 100644 --- a/crates/perry-codegen/src/collectors/not_bigint_locals.rs +++ b/crates/perry-codegen/src/collectors/not_bigint_locals.rs @@ -27,7 +27,7 @@ use std::collections::{HashMap, HashSet}; use perry_hir::types::Type as HirType; -use perry_hir::{Expr, Param, Stmt, UnaryOp}; +use perry_hir::{BinaryOp, Expr, Param, Stmt, UnaryOp}; use crate::type_analysis::is_numeric_typed_array_class; @@ -37,16 +37,93 @@ pub fn collect_not_bigint_locals( params: &[Param], binding_types: &HashMap, ) -> HashSet { - // Declared-type map (params + let bindings). Used to judge a `LocalGet` - // leaf whose id is not one of the analyzed (written) candidates. - let mut types: HashMap = binding_types.clone(); - for p in params { - types.entry(p.id).or_insert_with(|| p.ty.clone()); + NotBigIntFacts::collect(stmts, params, binding_types).locals +} + +/// The non-BigInt proof in a form the `FnCtx`-free collectors can query +/// (#10418): the fixpoint's local set plus the leaf facts its judgment reads. +/// +/// `&` `|` `^` `<<` `>>` and `~` compute a BigInt from BigInt operands, so the +/// integer-local proofs may treat one of them as an int32 producer only when +/// [`NotBigIntFacts::bitwise_result_is_number`] holds. Before, they admitted +/// every bitwise expression, and `const x = a & b` over two `BigInt(…)` values +/// took an int32 slot that `ToInt32`'d the BigInt result to `0`. +/// +/// `Default` is the empty fact set — literal operands still prove a Number. +#[derive(Default)] +pub(crate) struct NotBigIntFacts { + types: HashMap, + numeric_locals: HashSet, + locals: HashSet, +} + +impl NotBigIntFacts { + pub(crate) fn collect( + stmts: &[Stmt], + params: &[Param], + binding_types: &HashMap, + ) -> Self { + // Declared-type map (params + let bindings). Used to judge a `LocalGet` + // leaf whose id is not one of the analyzed (written) candidates. + let mut types: HashMap = binding_types.clone(); + for p in params { + types.entry(p.id).or_insert_with(|| p.ty.clone()); + } + // #7700: locals holding a number, so `u8[k]` keyed on one is a byte + // read (which is never a BigInt) rather than a property read (which + // can be). + let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); + let locals = not_bigint_fixpoint(stmts, &types, &numeric_locals); + Self { + types, + numeric_locals, + locals, + } + } + + /// Locals whose value is provably never a BigInt. + pub(crate) fn into_locals(self) -> HashSet { + self.locals } - // #7700: locals holding a number, so `u8[k]` keyed on one is a byte read - // (which is never a BigInt) rather than a property read (which can be). - let numeric_locals = super::collect_numeric_typed_locals(stmts, params, binding_types); + /// Can evaluating `e` never produce a BigInt? + pub(crate) fn expr_is_not_bigint(&self, e: &Expr) -> bool { + expr_not_bigint(e, &self.types, &self.locals, &self.numeric_locals) + } + + /// Is `e` a bitwise operator whose completed result is always a Number? + /// `>>>` has no BigInt form. The other binary operators and `~` produce + /// a BigInt for BigInt operands and throw for a mixed pair, so they are a + /// Number once an operand provably is not a BigInt. Anything else: `false`. + pub(crate) fn bitwise_result_is_number(&self, e: &Expr) -> bool { + match e { + Expr::Binary { + op: BinaryOp::UShr, .. + } => true, + Expr::Binary { + op: + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr, + left, + right, + } => self.expr_is_not_bigint(left) || self.expr_is_not_bigint(right), + Expr::Unary { + op: UnaryOp::BitNot, + operand, + } => self.expr_is_not_bigint(operand), + _ => false, + } + } +} + +fn not_bigint_fixpoint( + stmts: &[Stmt], + types: &HashMap, + numeric_locals: &HashSet, +) -> HashSet { // Every write (Let init + `LocalSet` rhs) per candidate local. Descends // into closure bodies so a `LocalSet` to an ENCLOSING local inside a // closure is captured (LocalIds are unique per function, so the write is @@ -73,7 +150,7 @@ pub fn collect_not_bigint_locals( ws.iter().all(|rhs| match rhs { // A `let x;` binding is `undefined` — a non-BigInt. None => true, - Some(rhs) => expr_not_bigint(rhs, &types, ¬_bigint, &numeric_locals), + Some(rhs) => expr_not_bigint(rhs, types, ¬_bigint, numeric_locals), }) }) .unwrap_or(true); @@ -190,16 +267,7 @@ fn expr_not_bigint( /// `Named` (an object could `ToPrimitive` to a BigInt), and unions are /// deliberately excluded. fn type_is_not_bigint(t: Option<&HirType>) -> bool { - matches!( - t, - Some( - HirType::Number - | HirType::Int32 - | HirType::Boolean - | HirType::String - | HirType::StringLiteral(_) - ) - ) + t.is_some_and(HirType::is_non_bigint_primitive) } /// True when `object` indexes a numeric typed array (`Int32Array` etc.) or a diff --git a/crates/perry-codegen/src/collectors/spec_abi_sites.rs b/crates/perry-codegen/src/collectors/spec_abi_sites.rs index 6c7ba36411..0eac0a3a7a 100644 --- a/crates/perry-codegen/src/collectors/spec_abi_sites.rs +++ b/crates/perry-codegen/src/collectors/spec_abi_sites.rs @@ -583,12 +583,14 @@ fn judge_sites_in_body( let binding_types = HashMap::new(); let numeric_locals = super::collect_numeric_typed_locals(stmts, params, &binding_types); let empty = HashSet::new(); - let integer_locals = super::integer_locals::collect_integer_locals( + let integer_locals = super::integer_locals::collect_integer_locals_with_seeds( stmts, &empty, &empty, &empty, &numeric_locals, + &empty, + &super::not_bigint_locals::NotBigIntFacts::collect(stmts, params, &binding_types), ); let mut ready: HashSet = HashSet::new(); for s in stmts { diff --git a/crates/perry-codegen/src/expr/bigint_bitwise_tests.rs b/crates/perry-codegen/src/expr/bigint_bitwise_tests.rs new file mode 100644 index 0000000000..e5bfa3cba5 --- /dev/null +++ b/crates/perry-codegen/src/expr/bigint_bitwise_tests.rs @@ -0,0 +1,199 @@ +//! #10418: `&` `|` `^` `<<` `>>` over operands that may be BigInts compute a +//! BigInt. Codegen used to assume every bitwise result is an int32 Number: +//! `const x = a & b` took an int32 slot (the BigInt read back as `0`) and +//! `Number(a & b)` elided its coercion (the BigInt passed through). Each +//! assertion is paired with the Number shape that must keep its fast path. + +use crate::{compile_module, CompileOptions}; +use perry_hir::types::Type; +use perry_hir::{BinaryOp, Expr, Function, Module, Param, Stmt}; + +const A: u32 = 1; +const B: u32 = 2; +const X: u32 = 3; + +fn param(id: u32, ty: Type) -> Param { + Param { + id, + name: format!("p{id}"), + ty, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + } +} + +fn bin(op: BinaryOp, left: Expr, right: Expr) -> Expr { + Expr::Binary { + op, + left: Box::new(left), + right: Box::new(right), + } +} + +/// `function f(a, b) { }`, called once from module init, as IR. +fn function_ir(name: &str, params: Vec, body: Vec) -> String { + let mut module = Module::new(name); + module.init.push(Stmt::Expr(Expr::Call { + callee: Box::new(Expr::FuncRef(1)), + args: params.iter().map(|_| Expr::Undefined).collect(), + type_args: Vec::new(), + byte_offset: 0, + })); + module.functions.push(Function { + id: 1, + name: "f".to_string(), + type_params: Vec::new(), + params, + return_type: Type::Any, + body, + is_async: false, + is_generator: false, + is_strict: true, + is_exported: true, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + let opts = CompileOptions { + emit_ir_only: true, + output_type: "executable".to_string(), + ..Default::default() + }; + let ir = String::from_utf8(compile_module(&module, opts).expect("module compiles")) + .expect("LLVM IR is UTF-8"); + let start = ir + .match_indices("define ") + .find(|(index, _)| { + let line_end = ir[*index..].find('\n').map_or(ir.len(), |o| index + o); + ir[*index..line_end].contains("__f(") + }) + .map(|(index, _)| index) + .unwrap_or_else(|| panic!("missing function f:\n{ir}")); + let end = ir[start..].find("\n}").map_or(ir.len(), |o| start + o); + ir[start..end].to_string() +} + +/// `const x = ; return x;` +fn const_then_return(init: Expr, ty: Type) -> Vec { + vec![ + Stmt::Let { + id: X, + name: "x".to_string(), + ty, + mutable: false, + init: Some(init), + }, + Stmt::Return(Some(Expr::LocalGet(X))), + ] +} + +#[test] +fn possibly_bigint_bitwise_const_takes_no_int32_slot() { + for op in [ + BinaryOp::BitAnd, + BinaryOp::BitOr, + BinaryOp::BitXor, + BinaryOp::Shl, + BinaryOp::Shr, + ] { + let untyped = function_ir( + "bigint_bitwise_untyped", + vec![param(A, Type::Any), param(B, Type::Any)], + const_then_return(bin(op, Expr::LocalGet(A), Expr::LocalGet(B)), Type::Any), + ); + assert!( + !untyped.contains("alloca i32"), + "{op:?} over untyped operands may be a BigInt, so `x` needs a boxed slot:\n{untyped}" + ); + + let typed = function_ir( + "bigint_bitwise_typed", + vec![param(A, Type::BigInt), param(B, Type::BigInt)], + const_then_return(bin(op, Expr::LocalGet(A), Expr::LocalGet(B)), Type::BigInt), + ); + assert!( + !typed.contains("alloca i32"), + "{op:?} over `bigint` operands is a BigInt:\n{typed}" + ); + + let masked = function_ir( + "number_bitwise_masked", + vec![param(A, Type::Any), param(B, Type::Any)], + const_then_return(bin(op, Expr::LocalGet(A), Expr::Integer(3)), Type::Number), + ); + assert!( + masked.contains("alloca i32"), + "{op:?} with a Number operand is an int32 and keeps its slot:\n{masked}" + ); + } +} + +#[test] +fn number_of_possibly_bigint_bitwise_result_keeps_its_coercion() { + let number_of = |right: Expr| { + function_ir( + "number_of_bitwise", + vec![param(A, Type::Any), param(B, Type::Any)], + vec![Stmt::Return(Some(Expr::NumberCoerce(Box::new(bin( + BinaryOp::BitAnd, + Expr::LocalGet(A), + right, + )))))], + ) + }; + let unknown = number_of(Expr::LocalGet(B)); + assert!( + unknown.contains("call double @js_number_coerce("), + "`Number(a & b)` must convert a BigInt result:\n{unknown}" + ); + let masked = number_of(Expr::Integer(255)); + assert!( + !masked.contains("call double @js_number_coerce("), + "`Number(a & 255)` is already a Number:\n{masked}" + ); +} + +/// With the int32 slot gone, an unproven operand keeps the Number case inline +/// behind a tag test; a BigInt (or any non-Number) takes the helper arm. +#[test] +fn unproven_bitwise_operands_take_a_guarded_int32_arm() { + for (op, helper, native) in [ + (BinaryOp::BitAnd, "js_dynamic_bitand", "and i32"), + (BinaryOp::BitOr, "js_dynamic_bitor", "or i32"), + (BinaryOp::BitXor, "js_dynamic_bitxor", "xor i32"), + (BinaryOp::Shl, "js_dynamic_shl", "shl i32"), + (BinaryOp::Shr, "js_dynamic_shr", "ashr i32"), + (BinaryOp::UShr, "js_dynamic_ushr", "lshr i32"), + ] { + let ir = function_ir( + "guarded_bitwise", + vec![param(A, Type::Any), param(B, Type::Any)], + vec![Stmt::Return(Some(bin( + op, + Expr::LocalGet(A), + Expr::LocalGet(B), + )))], + ); + assert!( + ir.contains("guarded_arith.numeric"), + "{op:?} over unproven operands should test for Numbers:\n{ir}" + ); + assert!( + ir.contains(&format!("call double @{helper}(")), + "{op:?} must keep the BigInt-aware helper for non-Numbers:\n{ir}" + ); + assert!( + ir.contains(native), + "{op:?} should compute Numbers inline:\n{ir}" + ); + if matches!(op, BinaryOp::Shl | BinaryOp::Shr | BinaryOp::UShr) { + assert!( + ir.contains(", 31\n"), + "{op:?} must mask its shift count to 5 bits:\n{ir}" + ); + } + } +} diff --git a/crates/perry-codegen/src/expr/bigint_set.rs b/crates/perry-codegen/src/expr/bigint_set.rs index bc6f2460fa..7cddc74221 100644 --- a/crates/perry-codegen/src/expr/bigint_set.rs +++ b/crates/perry-codegen/src/expr/bigint_set.rs @@ -44,12 +44,19 @@ fn number_coerce_operand_is_already_primitive_number(ctx: &FnCtx<'_>, operand: & number_coerce_operand_is_already_primitive_number(ctx, left) && number_coerce_operand_is_already_primitive_number(ctx, right) } + // `>>>` has no BigInt form. The other bitwise operators compute a + // BigInt from two BigInt operands, so `Number(n & M)` may only be + // elided once an operand is proven non-BigInt (#10418 — eliding it + // returned the BigInt `n & M` unchanged). + BinaryOp::UShr => true, BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Shl - | BinaryOp::Shr - | BinaryOp::UShr => true, + | BinaryOp::Shr => { + crate::type_analysis::is_provably_not_bigint(ctx, left) + || crate::type_analysis::is_provably_not_bigint(ctx, right) + } BinaryOp::Pow => false, }, _ => false, diff --git a/crates/perry-codegen/src/expr/binary.rs b/crates/perry-codegen/src/expr/binary.rs index ebeac06c0f..ca1a5d34be 100644 --- a/crates/perry-codegen/src/expr/binary.rs +++ b/crates/perry-codegen/src/expr/binary.rs @@ -256,6 +256,10 @@ fn lower_guarded_numeric_add(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result /// not a Number, so `5n - 3n` and `1n - 1` both take the cold arm and keep /// the helper's exact semantics (a BigInt result and a TypeError /// respectively). +/// +/// The bitwise operators take the same guard (#10418): their numeric arm is +/// `ToInt32 ToInt32`, which `toint32_wrap` computes for every Number +/// (NaN and ±Infinity included), with the shift count masked to 5 bits. fn lower_guarded_numeric_arith( ctx: &mut FnCtx<'_>, op: BinaryOp, @@ -284,7 +288,31 @@ fn lower_guarded_numeric_arith( let native = |ctx: &mut FnCtx<'_>, l: &str, r: &str| match op { BinaryOp::Sub => ctx.block().fsub(l, r), BinaryOp::Mul => ctx.block().fmul(l, r), - _ => ctx.block().fdiv(l, r), + BinaryOp::Div => ctx.block().fdiv(l, r), + _ => { + let blk = ctx.block(); + let li = blk.toint32_wrap(l); + let ri = blk.toint32_wrap(r); + let v = match op { + BinaryOp::BitAnd => blk.and(I32, &li, &ri), + BinaryOp::BitOr => blk.or(I32, &li, &ri), + BinaryOp::BitXor => blk.xor(I32, &li, &ri), + BinaryOp::Shl => { + let shift = blk.and(I32, &ri, "31"); + blk.shl(I32, &li, &shift) + } + BinaryOp::Shr => { + let shift = blk.and(I32, &ri, "31"); + blk.ashr(I32, &li, &shift) + } + _ => { + let shift = blk.and(I32, &ri, "31"); + let v = blk.lshr(I32, &li, &shift); + return blk.uitofp(I32, &v, DOUBLE); + } + }; + blk.sitofp(I32, &v, DOUBLE) + } }; // Every leaf already vouched for: no diamond to emit. let Some(all_num) = cond else { @@ -322,7 +350,7 @@ fn lower_guarded_numeric_arith( } /// `PERRY_GUARDED_ARITH=0` restores the unconditional dynamic helper for -/// `-`, `*` and `/`. +/// `-`, `*`, `/` and the bitwise operators. fn guarded_arith_enabled() -> bool { !matches!( std::env::var("PERRY_GUARDED_ARITH").as_deref(), @@ -1205,8 +1233,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // operation while `s += v` did not. The guard needs no // proof: a BigInt is not a Number, so it fails the test // and the cold arm runs this same helper. + // + // #10418: so do the bitwise operators. An unproven + // operand used to reach their int32 path anyway, + // through an int32 local slot that also truncated a + // BigInt result; the tag test keeps the Number case + // inline without that hole. if guarded_arith_enabled() - && matches!(op, BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div) + && (matches!(op, BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div) + || (is_bitwise_op(*op) && inline_nonbigint_bitwise_enabled())) { return lower_guarded_numeric_arith(ctx, *op, left, right, fname); } diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 778f93e792..e6ada1e9a2 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -3008,6 +3008,8 @@ mod string_length; pub(crate) mod string_window; pub(crate) mod suffix_cursor; +#[cfg(test)] +mod bigint_bitwise_tests; mod ptr_numarray_access; mod ta_param_f64_read; mod u8_buffer_read; diff --git a/crates/perry-hir/src/analysis/value_types.rs b/crates/perry-hir/src/analysis/value_types.rs index cb56b187b3..26ef9ac324 100644 --- a/crates/perry-hir/src/analysis/value_types.rs +++ b/crates/perry-hir/src/analysis/value_types.rs @@ -1679,12 +1679,22 @@ fn infer_binary_type( Type::Any } } - BinaryOp::BitAnd - | BinaryOp::BitOr - | BinaryOp::BitXor - | BinaryOp::Shl - | BinaryOp::Shr - | BinaryOp::UShr => Type::Number, + // `>>>` has no BigInt form (it throws), so it is always a Number. + BinaryOp::UShr => Type::Number, + // The other bitwise operators compute a BigInt for two BigInt operands + // and throw for a mixed pair, so the result is a Number only once an + // operand provably is not a BigInt (#10418). + BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::Shl | BinaryOp::Shr => { + let left_ty = infer_expr_type(left, env); + let right_ty = infer_expr_type(right, env); + if matches!(left_ty, Type::BigInt) && matches!(right_ty, Type::BigInt) { + Type::BigInt + } else if left_ty.is_non_bigint_primitive() || right_ty.is_non_bigint_primitive() { + Type::Number + } else { + Type::Any + } + } } } diff --git a/crates/perry-hir/src/analysis/value_types_tests.rs b/crates/perry-hir/src/analysis/value_types_tests.rs index 215b8b5a07..a73012829d 100644 --- a/crates/perry-hir/src/analysis/value_types_tests.rs +++ b/crates/perry-hir/src/analysis/value_types_tests.rs @@ -1837,3 +1837,58 @@ fn resolves_this_and_super_in_class_context() { Type::Number ); } + +/// #10418: `&` `|` `^` `<<` `>>` compute a BigInt for two BigInt operands, so +/// the result is a Number only when an operand provably is not a BigInt. +/// Typing `a & b` over unknown operands as Number gave the binding an int32 +/// slot that read a BigInt result back as `0`. +#[test] +fn bitwise_result_is_number_only_with_a_non_bigint_operand() { + let env = empty_env(); + let binary = |op, left, right| Expr::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }; + let unknown = || Expr::LocalGet(1); + for op in [ + BinaryOp::BitAnd, + BinaryOp::BitOr, + BinaryOp::BitXor, + BinaryOp::Shl, + BinaryOp::Shr, + ] { + assert_eq!( + infer_expr_type(&binary(op, unknown(), unknown()), &env), + Type::Any, + "{op:?} over unknown operands may be a BigInt" + ); + assert_eq!( + infer_expr_type(&binary(op, unknown(), Expr::Integer(255)), &env), + Type::Number, + "{op:?} with a Number operand is a Number or throws" + ); + assert_eq!( + infer_expr_type(&binary(op, Expr::Bool(true), unknown()), &env), + Type::Number, + "{op:?} with a Boolean operand is a Number or throws" + ); + assert_eq!( + infer_expr_type( + &binary( + op, + Expr::BigInt("7".to_string()), + Expr::BigInt("3".to_string()) + ), + &env + ), + Type::BigInt, + "{op:?} over BigInt operands is a BigInt" + ); + } + // `>>>` has no BigInt form. + assert_eq!( + infer_expr_type(&binary(BinaryOp::UShr, unknown(), unknown()), &env), + Type::Number + ); +} diff --git a/crates/perry-hir/src/lower_types.rs b/crates/perry-hir/src/lower_types.rs index 0e44e0d3f0..0c9ea5caf5 100644 --- a/crates/perry-hir/src/lower_types.rs +++ b/crates/perry-hir/src/lower_types.rs @@ -432,14 +432,19 @@ fn infer_type_from_expr_inner(expr: &ast::Expr, ctx: &LoweringContext) -> Type { } // Bitwise operators preserve BigInt when either side is - // inferred as BigInt; otherwise they produce Number. + // inferred as BigInt. They produce a Number only when an + // operand is provably not a BigInt: `a & b` over two unknown + // operands is a BigInt for BigInt inputs (#10418 — typing it + // Number gave `const x = a & b` an int32 slot that read `0`). BitAnd | BitOr | BitXor | LShift | RShift => { let left = infer_type_from_expr(&bin.left, ctx); let right = infer_type_from_expr(&bin.right, ctx); if bigint_result_type_from_operand_types(&left, &right) { Type::BigInt - } else { + } else if left.is_non_bigint_primitive() || right.is_non_bigint_primitive() { Type::Number + } else { + Type::Any } } ZeroFillRShift => Type::Number, diff --git a/crates/perry-hir/src/types.rs b/crates/perry-hir/src/types.rs index 287a95cb36..1466c4028f 100644 --- a/crates/perry-hir/src/types.rs +++ b/crates/perry-hir/src/types.rs @@ -146,6 +146,17 @@ impl Type { matches!(self, Type::String | Type::StringLiteral(_)) } + /// Check if this type is a primitive whose `ToNumeric` can never be a + /// BigInt. One such operand makes `&` `|` `^` `<<` `>>` produce a Number + /// (a BigInt on the other side throws); `Any`, objects (whose + /// `valueOf` may return a BigInt) and unions are not proof (#10418). + pub fn is_non_bigint_primitive(&self) -> bool { + matches!( + self, + Type::Number | Type::Int32 | Type::Boolean | Type::String | Type::StringLiteral(_) + ) + } + /// Check if this type is definitely not represented as a JS number. /// /// `Any`/`Unknown`/type variables return false because they might still be diff --git a/crates/perry-hir/tests/shape_inference.rs b/crates/perry-hir/tests/shape_inference.rs index 5160efa779..038a3d3ed0 100644 --- a/crates/perry-hir/tests/shape_inference.rs +++ b/crates/perry-hir/tests/shape_inference.rs @@ -278,6 +278,32 @@ fn function_with_mixed_returns_bails_to_any() { assert_eq!(find_fn(&module, "mixed").return_type, Type::Any); } +/// #10418: `a & b` over two operands that may be BigInts is a BigInt for +/// BigInt inputs, so neither a binding initialized from it nor a function +/// returning it may be typed `Number` — that type reached codegen as an int32 +/// slot that read the BigInt result back as `0`. One Number operand keeps it. +#[test] +fn bitwise_over_possible_bigints_is_not_inferred_number() { + let module = lower_src( + r#" + const A = BigInt(1003); + const B = BigInt(5); + const both = A & B; + const masked = A & 0xff; + const unsigned = A >>> 0; + function pair(a, b) { return a ^ b; } + function mask(a) { return a << 3; } + function typed(a: bigint, b: bigint) { return a >> b; } + "#, + ); + assert_ne!(find_local_type(&module, "both"), &Type::Number); + assert_eq!(find_local_type(&module, "masked"), &Type::Number); + assert_eq!(find_local_type(&module, "unsigned"), &Type::Number); + assert_eq!(find_fn(&module, "pair").return_type, Type::Any); + assert_eq!(find_fn(&module, "mask").return_type, Type::Number); + assert_eq!(find_fn(&module, "typed").return_type, Type::BigInt); +} + #[test] fn function_with_no_returns_infers_void() { let module = lower_src( diff --git a/test-files/test_gap_10418_bigint_bitwise_typing.ts b/test-files/test_gap_10418_bigint_bitwise_typing.ts new file mode 100644 index 0000000000..bdefc19730 --- /dev/null +++ b/test-files/test_gap_10418_bigint_bitwise_typing.ts @@ -0,0 +1,1031 @@ +// #10418: a binary bitwise operator (`&` `|` `^` `<<` `>>`) over BigInt operands +// the compiler cannot prove statically (untyped params, `BigInt(...)` +// initializers, property/element reads, arithmetic results) was assumed to +// produce an int32 Number. `const x = a & b` took an i32 slot (read back as +// `0`), `Number(a & b)` was elided (the BigInt passed through), a +// `bigint`-typed result reached a call as `fptosi` of its box, and +// `(_2n << k) * _2n` threw "Cannot mix BigInt and other types". This is +// @noble/hashes' `fromBig` (`Number((n >> _32n) & U32_MASK64) | 0` with +// `U32_MASK64 = BigInt(2 ** 32 - 1)`) and @noble/curves' `_2n << (c1 - _1n - _1n)`. +// +// Matrix: operator x operand shape x consumer, one function per operator and +// shape, one local per consumer. Row order: typeof x, String(x), x * 2n, +// x === expected, f(x), Number(x), `let` String(x), then the same consumers +// applied to the expression itself. `+ - * ** %` and `~` are controls; the +// Number section at the end pins the int32 semantics the fast path keeps. + +const _0n = BigInt(0); +const _1n = BigInt(1); +const _2n = BigInt(2); +const A_LIT = 1003n; +const B_LIT = 5n; +const A_BIG = BigInt(1003); +const B_BIG = BigInt(5); +const OBJ_LIT = { a: 1003n, b: 5n }; +const OBJ_BIG = { a: BigInt(1003), b: BigInt(5) }; +const ARR_LIT = [1003n, 5n]; +const ARR_BIG = [BigInt(1003), BigInt(5)]; + +function show(v) { + return typeof v + ":" + String(v); +} + +function row(...values: unknown[]): string { + return values.map((v) => (typeof v === "string" ? v : show(v))).join(" "); +} + +function and_lit(): string { + const x1 = 1003n & 5n, x2 = 1003n & 5n, x3 = 1003n & 5n, + x4 = 1003n & 5n, x5 = 1003n & 5n, x6 = 1003n & 5n; + let x7 = 1003n & 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (1003n & 5n), String(1003n & 5n), (1003n & 5n) * _2n, (1003n & 5n) === 1n, + show(1003n & 5n), Number(1003n & 5n)); +} +function and_constLit(): string { + const a = 1003n, b = 5n; + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_letLit(): string { + let a = 1003n, b = 5n; + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_constBig(): string { + const a = BigInt(1003), b = BigInt(5); + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_letBig(): string { + let a = BigInt(1003), b = BigInt(5); + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_typedParam(a: bigint, b: bigint): string { + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_untypedParam(a, b): string { + const x1 = a & b, x2 = a & b, x3 = a & b, x4 = a & b, x5 = a & b, x6 = a & b; + let x7 = a & b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & b), String(a & b), (a & b) * _2n, (a & b) === 1n, show(a & b), Number(a & b)); +} +function and_moduleLit(): string { + const x1 = A_LIT & B_LIT, x2 = A_LIT & B_LIT, x3 = A_LIT & B_LIT, + x4 = A_LIT & B_LIT, x5 = A_LIT & B_LIT, x6 = A_LIT & B_LIT; + let x7 = A_LIT & B_LIT; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (A_LIT & B_LIT), String(A_LIT & B_LIT), (A_LIT & B_LIT) * _2n, (A_LIT & B_LIT) === 1n, + show(A_LIT & B_LIT), Number(A_LIT & B_LIT)); +} +function and_moduleBig(): string { + const x1 = A_BIG & B_BIG, x2 = A_BIG & B_BIG, x3 = A_BIG & B_BIG, + x4 = A_BIG & B_BIG, x5 = A_BIG & B_BIG, x6 = A_BIG & B_BIG; + let x7 = A_BIG & B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (A_BIG & B_BIG), String(A_BIG & B_BIG), (A_BIG & B_BIG) * _2n, (A_BIG & B_BIG) === 1n, + show(A_BIG & B_BIG), Number(A_BIG & B_BIG)); +} +function and_propertyLit(o): string { + const x1 = o.a & o.b, x2 = o.a & o.b, x3 = o.a & o.b, + x4 = o.a & o.b, x5 = o.a & o.b, x6 = o.a & o.b; + let x7 = o.a & o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (o.a & o.b), String(o.a & o.b), (o.a & o.b) * _2n, (o.a & o.b) === 1n, show(o.a & o.b), + Number(o.a & o.b)); +} +function and_propertyBig(o): string { + const x1 = o.a & o.b, x2 = o.a & o.b, x3 = o.a & o.b, + x4 = o.a & o.b, x5 = o.a & o.b, x6 = o.a & o.b; + let x7 = o.a & o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (o.a & o.b), String(o.a & o.b), (o.a & o.b) * _2n, (o.a & o.b) === 1n, show(o.a & o.b), + Number(o.a & o.b)); +} +function and_elementLit(v): string { + const x1 = v[0] & v[1], x2 = v[0] & v[1], x3 = v[0] & v[1], + x4 = v[0] & v[1], x5 = v[0] & v[1], x6 = v[0] & v[1]; + let x7 = v[0] & v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (v[0] & v[1]), String(v[0] & v[1]), (v[0] & v[1]) * _2n, (v[0] & v[1]) === 1n, + show(v[0] & v[1]), Number(v[0] & v[1])); +} +function and_elementBig(v): string { + const x1 = v[0] & v[1], x2 = v[0] & v[1], x3 = v[0] & v[1], + x4 = v[0] & v[1], x5 = v[0] & v[1], x6 = v[0] & v[1]; + let x7 = v[0] & v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (v[0] & v[1]), String(v[0] & v[1]), (v[0] & v[1]) * _2n, (v[0] & v[1]) === 1n, + show(v[0] & v[1]), Number(v[0] & v[1])); +} +function and_arith(p, q): string { + const x1 = (p + _0n) & (q + _0n), x2 = (p + _0n) & (q + _0n), x3 = (p + _0n) & (q + _0n), + x4 = (p + _0n) & (q + _0n), x5 = (p + _0n) & (q + _0n), x6 = (p + _0n) & (q + _0n); + let x7 = (p + _0n) & (q + _0n); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof ((p + _0n) & (q + _0n)), String((p + _0n) & (q + _0n)), ((p + _0n) & (q + _0n)) * _2n, + ((p + _0n) & (q + _0n)) === 1n, show((p + _0n) & (q + _0n)), Number((p + _0n) & (q + _0n))); +} +function and_inlineBig(): string { + const x1 = BigInt(1003) & BigInt(5), x2 = BigInt(1003) & BigInt(5), x3 = BigInt(1003) & BigInt(5), + x4 = BigInt(1003) & BigInt(5), x5 = BigInt(1003) & BigInt(5), x6 = BigInt(1003) & BigInt(5); + let x7 = BigInt(1003) & BigInt(5); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (BigInt(1003) & BigInt(5)), String(BigInt(1003) & BigInt(5)), + (BigInt(1003) & BigInt(5)) * _2n, (BigInt(1003) & BigInt(5)) === 1n, + show(BigInt(1003) & BigInt(5)), Number(BigInt(1003) & BigInt(5))); +} +function and_paramAndLit(a): string { + const x1 = a & 5n, x2 = a & 5n, x3 = a & 5n, x4 = a & 5n, x5 = a & 5n, x6 = a & 5n; + let x7 = a & 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1n, show(x5), Number(x6), String(x7), + typeof (a & 5n), String(a & 5n), (a & 5n) * _2n, (a & 5n) === 1n, show(a & 5n), + Number(a & 5n)); +} +function or_lit(): string { + const x1 = 1003n | 5n, x2 = 1003n | 5n, x3 = 1003n | 5n, + x4 = 1003n | 5n, x5 = 1003n | 5n, x6 = 1003n | 5n; + let x7 = 1003n | 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (1003n | 5n), String(1003n | 5n), (1003n | 5n) * _2n, (1003n | 5n) === 1007n, + show(1003n | 5n), Number(1003n | 5n)); +} +function or_constLit(): string { + const a = 1003n, b = 5n; + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_letLit(): string { + let a = 1003n, b = 5n; + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_constBig(): string { + const a = BigInt(1003), b = BigInt(5); + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_letBig(): string { + let a = BigInt(1003), b = BigInt(5); + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_typedParam(a: bigint, b: bigint): string { + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_untypedParam(a, b): string { + const x1 = a | b, x2 = a | b, x3 = a | b, x4 = a | b, x5 = a | b, x6 = a | b; + let x7 = a | b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | b), String(a | b), (a | b) * _2n, (a | b) === 1007n, show(a | b), Number(a | b)); +} +function or_moduleLit(): string { + const x1 = A_LIT | B_LIT, x2 = A_LIT | B_LIT, x3 = A_LIT | B_LIT, + x4 = A_LIT | B_LIT, x5 = A_LIT | B_LIT, x6 = A_LIT | B_LIT; + let x7 = A_LIT | B_LIT; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (A_LIT | B_LIT), String(A_LIT | B_LIT), (A_LIT | B_LIT) * _2n, + (A_LIT | B_LIT) === 1007n, show(A_LIT | B_LIT), Number(A_LIT | B_LIT)); +} +function or_moduleBig(): string { + const x1 = A_BIG | B_BIG, x2 = A_BIG | B_BIG, x3 = A_BIG | B_BIG, + x4 = A_BIG | B_BIG, x5 = A_BIG | B_BIG, x6 = A_BIG | B_BIG; + let x7 = A_BIG | B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (A_BIG | B_BIG), String(A_BIG | B_BIG), (A_BIG | B_BIG) * _2n, + (A_BIG | B_BIG) === 1007n, show(A_BIG | B_BIG), Number(A_BIG | B_BIG)); +} +function or_propertyLit(o): string { + const x1 = o.a | o.b, x2 = o.a | o.b, x3 = o.a | o.b, + x4 = o.a | o.b, x5 = o.a | o.b, x6 = o.a | o.b; + let x7 = o.a | o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (o.a | o.b), String(o.a | o.b), (o.a | o.b) * _2n, (o.a | o.b) === 1007n, + show(o.a | o.b), Number(o.a | o.b)); +} +function or_propertyBig(o): string { + const x1 = o.a | o.b, x2 = o.a | o.b, x3 = o.a | o.b, + x4 = o.a | o.b, x5 = o.a | o.b, x6 = o.a | o.b; + let x7 = o.a | o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (o.a | o.b), String(o.a | o.b), (o.a | o.b) * _2n, (o.a | o.b) === 1007n, + show(o.a | o.b), Number(o.a | o.b)); +} +function or_elementLit(v): string { + const x1 = v[0] | v[1], x2 = v[0] | v[1], x3 = v[0] | v[1], + x4 = v[0] | v[1], x5 = v[0] | v[1], x6 = v[0] | v[1]; + let x7 = v[0] | v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (v[0] | v[1]), String(v[0] | v[1]), (v[0] | v[1]) * _2n, (v[0] | v[1]) === 1007n, + show(v[0] | v[1]), Number(v[0] | v[1])); +} +function or_elementBig(v): string { + const x1 = v[0] | v[1], x2 = v[0] | v[1], x3 = v[0] | v[1], + x4 = v[0] | v[1], x5 = v[0] | v[1], x6 = v[0] | v[1]; + let x7 = v[0] | v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (v[0] | v[1]), String(v[0] | v[1]), (v[0] | v[1]) * _2n, (v[0] | v[1]) === 1007n, + show(v[0] | v[1]), Number(v[0] | v[1])); +} +function or_arith(p, q): string { + const x1 = (p + _0n) | (q + _0n), x2 = (p + _0n) | (q + _0n), x3 = (p + _0n) | (q + _0n), + x4 = (p + _0n) | (q + _0n), x5 = (p + _0n) | (q + _0n), x6 = (p + _0n) | (q + _0n); + let x7 = (p + _0n) | (q + _0n); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof ((p + _0n) | (q + _0n)), String((p + _0n) | (q + _0n)), ((p + _0n) | (q + _0n)) * _2n, + ((p + _0n) | (q + _0n)) === 1007n, show((p + _0n) | (q + _0n)), Number((p + _0n) | (q + _0n))); +} +function or_inlineBig(): string { + const x1 = BigInt(1003) | BigInt(5), x2 = BigInt(1003) | BigInt(5), x3 = BigInt(1003) | BigInt(5), + x4 = BigInt(1003) | BigInt(5), x5 = BigInt(1003) | BigInt(5), x6 = BigInt(1003) | BigInt(5); + let x7 = BigInt(1003) | BigInt(5); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (BigInt(1003) | BigInt(5)), String(BigInt(1003) | BigInt(5)), + (BigInt(1003) | BigInt(5)) * _2n, (BigInt(1003) | BigInt(5)) === 1007n, + show(BigInt(1003) | BigInt(5)), Number(BigInt(1003) | BigInt(5))); +} +function or_paramAndLit(a): string { + const x1 = a | 5n, x2 = a | 5n, x3 = a | 5n, x4 = a | 5n, x5 = a | 5n, x6 = a | 5n; + let x7 = a | 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1007n, show(x5), Number(x6), String(x7), + typeof (a | 5n), String(a | 5n), (a | 5n) * _2n, (a | 5n) === 1007n, show(a | 5n), + Number(a | 5n)); +} +function xor_lit(): string { + const x1 = 1003n ^ 5n, x2 = 1003n ^ 5n, x3 = 1003n ^ 5n, + x4 = 1003n ^ 5n, x5 = 1003n ^ 5n, x6 = 1003n ^ 5n; + let x7 = 1003n ^ 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (1003n ^ 5n), String(1003n ^ 5n), (1003n ^ 5n) * _2n, (1003n ^ 5n) === 1006n, + show(1003n ^ 5n), Number(1003n ^ 5n)); +} +function xor_constLit(): string { + const a = 1003n, b = 5n; + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_letLit(): string { + let a = 1003n, b = 5n; + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_constBig(): string { + const a = BigInt(1003), b = BigInt(5); + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_letBig(): string { + let a = BigInt(1003), b = BigInt(5); + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_typedParam(a: bigint, b: bigint): string { + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_untypedParam(a, b): string { + const x1 = a ^ b, x2 = a ^ b, x3 = a ^ b, x4 = a ^ b, x5 = a ^ b, x6 = a ^ b; + let x7 = a ^ b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ b), String(a ^ b), (a ^ b) * _2n, (a ^ b) === 1006n, show(a ^ b), Number(a ^ b)); +} +function xor_moduleLit(): string { + const x1 = A_LIT ^ B_LIT, x2 = A_LIT ^ B_LIT, x3 = A_LIT ^ B_LIT, + x4 = A_LIT ^ B_LIT, x5 = A_LIT ^ B_LIT, x6 = A_LIT ^ B_LIT; + let x7 = A_LIT ^ B_LIT; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (A_LIT ^ B_LIT), String(A_LIT ^ B_LIT), (A_LIT ^ B_LIT) * _2n, + (A_LIT ^ B_LIT) === 1006n, show(A_LIT ^ B_LIT), Number(A_LIT ^ B_LIT)); +} +function xor_moduleBig(): string { + const x1 = A_BIG ^ B_BIG, x2 = A_BIG ^ B_BIG, x3 = A_BIG ^ B_BIG, + x4 = A_BIG ^ B_BIG, x5 = A_BIG ^ B_BIG, x6 = A_BIG ^ B_BIG; + let x7 = A_BIG ^ B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (A_BIG ^ B_BIG), String(A_BIG ^ B_BIG), (A_BIG ^ B_BIG) * _2n, + (A_BIG ^ B_BIG) === 1006n, show(A_BIG ^ B_BIG), Number(A_BIG ^ B_BIG)); +} +function xor_propertyLit(o): string { + const x1 = o.a ^ o.b, x2 = o.a ^ o.b, x3 = o.a ^ o.b, + x4 = o.a ^ o.b, x5 = o.a ^ o.b, x6 = o.a ^ o.b; + let x7 = o.a ^ o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (o.a ^ o.b), String(o.a ^ o.b), (o.a ^ o.b) * _2n, (o.a ^ o.b) === 1006n, + show(o.a ^ o.b), Number(o.a ^ o.b)); +} +function xor_propertyBig(o): string { + const x1 = o.a ^ o.b, x2 = o.a ^ o.b, x3 = o.a ^ o.b, + x4 = o.a ^ o.b, x5 = o.a ^ o.b, x6 = o.a ^ o.b; + let x7 = o.a ^ o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (o.a ^ o.b), String(o.a ^ o.b), (o.a ^ o.b) * _2n, (o.a ^ o.b) === 1006n, + show(o.a ^ o.b), Number(o.a ^ o.b)); +} +function xor_elementLit(v): string { + const x1 = v[0] ^ v[1], x2 = v[0] ^ v[1], x3 = v[0] ^ v[1], + x4 = v[0] ^ v[1], x5 = v[0] ^ v[1], x6 = v[0] ^ v[1]; + let x7 = v[0] ^ v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (v[0] ^ v[1]), String(v[0] ^ v[1]), (v[0] ^ v[1]) * _2n, (v[0] ^ v[1]) === 1006n, + show(v[0] ^ v[1]), Number(v[0] ^ v[1])); +} +function xor_elementBig(v): string { + const x1 = v[0] ^ v[1], x2 = v[0] ^ v[1], x3 = v[0] ^ v[1], + x4 = v[0] ^ v[1], x5 = v[0] ^ v[1], x6 = v[0] ^ v[1]; + let x7 = v[0] ^ v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (v[0] ^ v[1]), String(v[0] ^ v[1]), (v[0] ^ v[1]) * _2n, (v[0] ^ v[1]) === 1006n, + show(v[0] ^ v[1]), Number(v[0] ^ v[1])); +} +function xor_arith(p, q): string { + const x1 = (p + _0n) ^ (q + _0n), x2 = (p + _0n) ^ (q + _0n), x3 = (p + _0n) ^ (q + _0n), + x4 = (p + _0n) ^ (q + _0n), x5 = (p + _0n) ^ (q + _0n), x6 = (p + _0n) ^ (q + _0n); + let x7 = (p + _0n) ^ (q + _0n); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof ((p + _0n) ^ (q + _0n)), String((p + _0n) ^ (q + _0n)), ((p + _0n) ^ (q + _0n)) * _2n, + ((p + _0n) ^ (q + _0n)) === 1006n, show((p + _0n) ^ (q + _0n)), Number((p + _0n) ^ (q + _0n))); +} +function xor_inlineBig(): string { + const x1 = BigInt(1003) ^ BigInt(5), x2 = BigInt(1003) ^ BigInt(5), x3 = BigInt(1003) ^ BigInt(5), + x4 = BigInt(1003) ^ BigInt(5), x5 = BigInt(1003) ^ BigInt(5), x6 = BigInt(1003) ^ BigInt(5); + let x7 = BigInt(1003) ^ BigInt(5); + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (BigInt(1003) ^ BigInt(5)), String(BigInt(1003) ^ BigInt(5)), + (BigInt(1003) ^ BigInt(5)) * _2n, (BigInt(1003) ^ BigInt(5)) === 1006n, + show(BigInt(1003) ^ BigInt(5)), Number(BigInt(1003) ^ BigInt(5))); +} +function xor_paramAndLit(a): string { + const x1 = a ^ 5n, x2 = a ^ 5n, x3 = a ^ 5n, x4 = a ^ 5n, x5 = a ^ 5n, x6 = a ^ 5n; + let x7 = a ^ 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1006n, show(x5), Number(x6), String(x7), + typeof (a ^ 5n), String(a ^ 5n), (a ^ 5n) * _2n, (a ^ 5n) === 1006n, show(a ^ 5n), + Number(a ^ 5n)); +} +function shl_lit(): string { + const x1 = 1003n << 5n, x2 = 1003n << 5n, x3 = 1003n << 5n, + x4 = 1003n << 5n, x5 = 1003n << 5n, x6 = 1003n << 5n; + let x7 = 1003n << 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (1003n << 5n), String(1003n << 5n), (1003n << 5n) * _2n, (1003n << 5n) === 32096n, + show(1003n << 5n), Number(1003n << 5n)); +} +function shl_constLit(): string { + const a = 1003n, b = 5n; + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_letLit(): string { + let a = 1003n, b = 5n; + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_constBig(): string { + const a = BigInt(1003), b = BigInt(5); + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_letBig(): string { + let a = BigInt(1003), b = BigInt(5); + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_typedParam(a: bigint, b: bigint): string { + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_untypedParam(a, b): string { + const x1 = a << b, x2 = a << b, x3 = a << b, x4 = a << b, x5 = a << b, x6 = a << b; + let x7 = a << b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << b), String(a << b), (a << b) * _2n, (a << b) === 32096n, show(a << b), + Number(a << b)); +} +function shl_moduleLit(): string { + const x1 = A_LIT << B_LIT, x2 = A_LIT << B_LIT, x3 = A_LIT << B_LIT, + x4 = A_LIT << B_LIT, x5 = A_LIT << B_LIT, x6 = A_LIT << B_LIT; + let x7 = A_LIT << B_LIT; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (A_LIT << B_LIT), String(A_LIT << B_LIT), (A_LIT << B_LIT) * _2n, + (A_LIT << B_LIT) === 32096n, show(A_LIT << B_LIT), Number(A_LIT << B_LIT)); +} +function shl_moduleBig(): string { + const x1 = A_BIG << B_BIG, x2 = A_BIG << B_BIG, x3 = A_BIG << B_BIG, + x4 = A_BIG << B_BIG, x5 = A_BIG << B_BIG, x6 = A_BIG << B_BIG; + let x7 = A_BIG << B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (A_BIG << B_BIG), String(A_BIG << B_BIG), (A_BIG << B_BIG) * _2n, + (A_BIG << B_BIG) === 32096n, show(A_BIG << B_BIG), Number(A_BIG << B_BIG)); +} +function shl_propertyLit(o): string { + const x1 = o.a << o.b, x2 = o.a << o.b, x3 = o.a << o.b, + x4 = o.a << o.b, x5 = o.a << o.b, x6 = o.a << o.b; + let x7 = o.a << o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (o.a << o.b), String(o.a << o.b), (o.a << o.b) * _2n, (o.a << o.b) === 32096n, + show(o.a << o.b), Number(o.a << o.b)); +} +function shl_propertyBig(o): string { + const x1 = o.a << o.b, x2 = o.a << o.b, x3 = o.a << o.b, + x4 = o.a << o.b, x5 = o.a << o.b, x6 = o.a << o.b; + let x7 = o.a << o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (o.a << o.b), String(o.a << o.b), (o.a << o.b) * _2n, (o.a << o.b) === 32096n, + show(o.a << o.b), Number(o.a << o.b)); +} +function shl_elementLit(v): string { + const x1 = v[0] << v[1], x2 = v[0] << v[1], x3 = v[0] << v[1], + x4 = v[0] << v[1], x5 = v[0] << v[1], x6 = v[0] << v[1]; + let x7 = v[0] << v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (v[0] << v[1]), String(v[0] << v[1]), (v[0] << v[1]) * _2n, (v[0] << v[1]) === 32096n, + show(v[0] << v[1]), Number(v[0] << v[1])); +} +function shl_elementBig(v): string { + const x1 = v[0] << v[1], x2 = v[0] << v[1], x3 = v[0] << v[1], + x4 = v[0] << v[1], x5 = v[0] << v[1], x6 = v[0] << v[1]; + let x7 = v[0] << v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (v[0] << v[1]), String(v[0] << v[1]), (v[0] << v[1]) * _2n, (v[0] << v[1]) === 32096n, + show(v[0] << v[1]), Number(v[0] << v[1])); +} +function shl_arith(p, q): string { + const x1 = (p + _0n) << (q + _0n), x2 = (p + _0n) << (q + _0n), x3 = (p + _0n) << (q + _0n), + x4 = (p + _0n) << (q + _0n), x5 = (p + _0n) << (q + _0n), x6 = (p + _0n) << (q + _0n); + let x7 = (p + _0n) << (q + _0n); + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof ((p + _0n) << (q + _0n)), String((p + _0n) << (q + _0n)), + ((p + _0n) << (q + _0n)) * _2n, ((p + _0n) << (q + _0n)) === 32096n, + show((p + _0n) << (q + _0n)), Number((p + _0n) << (q + _0n))); +} +function shl_inlineBig(): string { + const x1 = BigInt(1003) << BigInt(5), x2 = BigInt(1003) << BigInt(5), x3 = BigInt(1003) << BigInt(5), + x4 = BigInt(1003) << BigInt(5), x5 = BigInt(1003) << BigInt(5), x6 = BigInt(1003) << BigInt(5); + let x7 = BigInt(1003) << BigInt(5); + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (BigInt(1003) << BigInt(5)), String(BigInt(1003) << BigInt(5)), + (BigInt(1003) << BigInt(5)) * _2n, (BigInt(1003) << BigInt(5)) === 32096n, + show(BigInt(1003) << BigInt(5)), Number(BigInt(1003) << BigInt(5))); +} +function shl_paramAndLit(a): string { + const x1 = a << 5n, x2 = a << 5n, x3 = a << 5n, x4 = a << 5n, x5 = a << 5n, x6 = a << 5n; + let x7 = a << 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 32096n, show(x5), Number(x6), String(x7), + typeof (a << 5n), String(a << 5n), (a << 5n) * _2n, (a << 5n) === 32096n, show(a << 5n), + Number(a << 5n)); +} +function shr_lit(): string { + const x1 = 1003n >> 5n, x2 = 1003n >> 5n, x3 = 1003n >> 5n, + x4 = 1003n >> 5n, x5 = 1003n >> 5n, x6 = 1003n >> 5n; + let x7 = 1003n >> 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (1003n >> 5n), String(1003n >> 5n), (1003n >> 5n) * _2n, (1003n >> 5n) === 31n, + show(1003n >> 5n), Number(1003n >> 5n)); +} +function shr_constLit(): string { + const a = 1003n, b = 5n; + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_letLit(): string { + let a = 1003n, b = 5n; + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_constBig(): string { + const a = BigInt(1003), b = BigInt(5); + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_letBig(): string { + let a = BigInt(1003), b = BigInt(5); + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_typedParam(a: bigint, b: bigint): string { + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_untypedParam(a, b): string { + const x1 = a >> b, x2 = a >> b, x3 = a >> b, x4 = a >> b, x5 = a >> b, x6 = a >> b; + let x7 = a >> b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> b), String(a >> b), (a >> b) * _2n, (a >> b) === 31n, show(a >> b), + Number(a >> b)); +} +function shr_moduleLit(): string { + const x1 = A_LIT >> B_LIT, x2 = A_LIT >> B_LIT, x3 = A_LIT >> B_LIT, + x4 = A_LIT >> B_LIT, x5 = A_LIT >> B_LIT, x6 = A_LIT >> B_LIT; + let x7 = A_LIT >> B_LIT; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (A_LIT >> B_LIT), String(A_LIT >> B_LIT), (A_LIT >> B_LIT) * _2n, + (A_LIT >> B_LIT) === 31n, show(A_LIT >> B_LIT), Number(A_LIT >> B_LIT)); +} +function shr_moduleBig(): string { + const x1 = A_BIG >> B_BIG, x2 = A_BIG >> B_BIG, x3 = A_BIG >> B_BIG, + x4 = A_BIG >> B_BIG, x5 = A_BIG >> B_BIG, x6 = A_BIG >> B_BIG; + let x7 = A_BIG >> B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (A_BIG >> B_BIG), String(A_BIG >> B_BIG), (A_BIG >> B_BIG) * _2n, + (A_BIG >> B_BIG) === 31n, show(A_BIG >> B_BIG), Number(A_BIG >> B_BIG)); +} +function shr_propertyLit(o): string { + const x1 = o.a >> o.b, x2 = o.a >> o.b, x3 = o.a >> o.b, + x4 = o.a >> o.b, x5 = o.a >> o.b, x6 = o.a >> o.b; + let x7 = o.a >> o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (o.a >> o.b), String(o.a >> o.b), (o.a >> o.b) * _2n, (o.a >> o.b) === 31n, + show(o.a >> o.b), Number(o.a >> o.b)); +} +function shr_propertyBig(o): string { + const x1 = o.a >> o.b, x2 = o.a >> o.b, x3 = o.a >> o.b, + x4 = o.a >> o.b, x5 = o.a >> o.b, x6 = o.a >> o.b; + let x7 = o.a >> o.b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (o.a >> o.b), String(o.a >> o.b), (o.a >> o.b) * _2n, (o.a >> o.b) === 31n, + show(o.a >> o.b), Number(o.a >> o.b)); +} +function shr_elementLit(v): string { + const x1 = v[0] >> v[1], x2 = v[0] >> v[1], x3 = v[0] >> v[1], + x4 = v[0] >> v[1], x5 = v[0] >> v[1], x6 = v[0] >> v[1]; + let x7 = v[0] >> v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (v[0] >> v[1]), String(v[0] >> v[1]), (v[0] >> v[1]) * _2n, (v[0] >> v[1]) === 31n, + show(v[0] >> v[1]), Number(v[0] >> v[1])); +} +function shr_elementBig(v): string { + const x1 = v[0] >> v[1], x2 = v[0] >> v[1], x3 = v[0] >> v[1], + x4 = v[0] >> v[1], x5 = v[0] >> v[1], x6 = v[0] >> v[1]; + let x7 = v[0] >> v[1]; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (v[0] >> v[1]), String(v[0] >> v[1]), (v[0] >> v[1]) * _2n, (v[0] >> v[1]) === 31n, + show(v[0] >> v[1]), Number(v[0] >> v[1])); +} +function shr_arith(p, q): string { + const x1 = (p + _0n) >> (q + _0n), x2 = (p + _0n) >> (q + _0n), x3 = (p + _0n) >> (q + _0n), + x4 = (p + _0n) >> (q + _0n), x5 = (p + _0n) >> (q + _0n), x6 = (p + _0n) >> (q + _0n); + let x7 = (p + _0n) >> (q + _0n); + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof ((p + _0n) >> (q + _0n)), String((p + _0n) >> (q + _0n)), + ((p + _0n) >> (q + _0n)) * _2n, ((p + _0n) >> (q + _0n)) === 31n, show((p + _0n) >> (q + _0n)), + Number((p + _0n) >> (q + _0n))); +} +function shr_inlineBig(): string { + const x1 = BigInt(1003) >> BigInt(5), x2 = BigInt(1003) >> BigInt(5), x3 = BigInt(1003) >> BigInt(5), + x4 = BigInt(1003) >> BigInt(5), x5 = BigInt(1003) >> BigInt(5), x6 = BigInt(1003) >> BigInt(5); + let x7 = BigInt(1003) >> BigInt(5); + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (BigInt(1003) >> BigInt(5)), String(BigInt(1003) >> BigInt(5)), + (BigInt(1003) >> BigInt(5)) * _2n, (BigInt(1003) >> BigInt(5)) === 31n, + show(BigInt(1003) >> BigInt(5)), Number(BigInt(1003) >> BigInt(5))); +} +function shr_paramAndLit(a): string { + const x1 = a >> 5n, x2 = a >> 5n, x3 = a >> 5n, x4 = a >> 5n, x5 = a >> 5n, x6 = a >> 5n; + let x7 = a >> 5n; + return row(typeof x1, String(x2), x3 * _2n, x4 === 31n, show(x5), Number(x6), String(x7), + typeof (a >> 5n), String(a >> 5n), (a >> 5n) * _2n, (a >> 5n) === 31n, show(a >> 5n), + Number(a >> 5n)); +} +function add_typedParam(a: bigint, b: bigint): string { + const x1 = a + b, x2 = a + b, x3 = a + b, x4 = a + b, x5 = a + b, x6 = a + b; + let x7 = a + b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1008n, show(x5), Number(x6), String(x7), + typeof (a + b), String(a + b), (a + b) * _2n, (a + b) === 1008n, show(a + b), Number(a + b)); +} +function add_untypedParam(a, b): string { + const x1 = a + b, x2 = a + b, x3 = a + b, x4 = a + b, x5 = a + b, x6 = a + b; + let x7 = a + b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1008n, show(x5), Number(x6), String(x7), + typeof (a + b), String(a + b), (a + b) * _2n, (a + b) === 1008n, show(a + b), Number(a + b)); +} +function add_moduleBig(): string { + const x1 = A_BIG + B_BIG, x2 = A_BIG + B_BIG, x3 = A_BIG + B_BIG, + x4 = A_BIG + B_BIG, x5 = A_BIG + B_BIG, x6 = A_BIG + B_BIG; + let x7 = A_BIG + B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1008n, show(x5), Number(x6), String(x7), + typeof (A_BIG + B_BIG), String(A_BIG + B_BIG), (A_BIG + B_BIG) * _2n, + (A_BIG + B_BIG) === 1008n, show(A_BIG + B_BIG), Number(A_BIG + B_BIG)); +} +function sub_typedParam(a: bigint, b: bigint): string { + const x1 = a - b, x2 = a - b, x3 = a - b, x4 = a - b, x5 = a - b, x6 = a - b; + let x7 = a - b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 998n, show(x5), Number(x6), String(x7), + typeof (a - b), String(a - b), (a - b) * _2n, (a - b) === 998n, show(a - b), Number(a - b)); +} +function sub_untypedParam(a, b): string { + const x1 = a - b, x2 = a - b, x3 = a - b, x4 = a - b, x5 = a - b, x6 = a - b; + let x7 = a - b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 998n, show(x5), Number(x6), String(x7), + typeof (a - b), String(a - b), (a - b) * _2n, (a - b) === 998n, show(a - b), Number(a - b)); +} +function sub_moduleBig(): string { + const x1 = A_BIG - B_BIG, x2 = A_BIG - B_BIG, x3 = A_BIG - B_BIG, + x4 = A_BIG - B_BIG, x5 = A_BIG - B_BIG, x6 = A_BIG - B_BIG; + let x7 = A_BIG - B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 998n, show(x5), Number(x6), String(x7), + typeof (A_BIG - B_BIG), String(A_BIG - B_BIG), (A_BIG - B_BIG) * _2n, (A_BIG - B_BIG) === 998n, + show(A_BIG - B_BIG), Number(A_BIG - B_BIG)); +} +function mul_typedParam(a: bigint, b: bigint): string { + const x1 = a * b, x2 = a * b, x3 = a * b, x4 = a * b, x5 = a * b, x6 = a * b; + let x7 = a * b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 5015n, show(x5), Number(x6), String(x7), + typeof (a * b), String(a * b), (a * b) * _2n, (a * b) === 5015n, show(a * b), Number(a * b)); +} +function mul_untypedParam(a, b): string { + const x1 = a * b, x2 = a * b, x3 = a * b, x4 = a * b, x5 = a * b, x6 = a * b; + let x7 = a * b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 5015n, show(x5), Number(x6), String(x7), + typeof (a * b), String(a * b), (a * b) * _2n, (a * b) === 5015n, show(a * b), Number(a * b)); +} +function mul_moduleBig(): string { + const x1 = A_BIG * B_BIG, x2 = A_BIG * B_BIG, x3 = A_BIG * B_BIG, + x4 = A_BIG * B_BIG, x5 = A_BIG * B_BIG, x6 = A_BIG * B_BIG; + let x7 = A_BIG * B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 5015n, show(x5), Number(x6), String(x7), + typeof (A_BIG * B_BIG), String(A_BIG * B_BIG), (A_BIG * B_BIG) * _2n, + (A_BIG * B_BIG) === 5015n, show(A_BIG * B_BIG), Number(A_BIG * B_BIG)); +} +function pow_typedParam(a: bigint, b: bigint): string { + const x1 = a ** b, x2 = a ** b, x3 = a ** b, x4 = a ** b, x5 = a ** b, x6 = a ** b; + let x7 = a ** b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1015090270405243n, show(x5), Number(x6), + String(x7), typeof (a ** b), String(a ** b), (a ** b) * _2n, (a ** b) === 1015090270405243n, + show(a ** b), Number(a ** b)); +} +function pow_untypedParam(a, b): string { + const x1 = a ** b, x2 = a ** b, x3 = a ** b, x4 = a ** b, x5 = a ** b, x6 = a ** b; + let x7 = a ** b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1015090270405243n, show(x5), Number(x6), + String(x7), typeof (a ** b), String(a ** b), (a ** b) * _2n, (a ** b) === 1015090270405243n, + show(a ** b), Number(a ** b)); +} +function pow_moduleBig(): string { + const x1 = A_BIG ** B_BIG, x2 = A_BIG ** B_BIG, x3 = A_BIG ** B_BIG, + x4 = A_BIG ** B_BIG, x5 = A_BIG ** B_BIG, x6 = A_BIG ** B_BIG; + let x7 = A_BIG ** B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 1015090270405243n, show(x5), Number(x6), + String(x7), typeof (A_BIG ** B_BIG), String(A_BIG ** B_BIG), (A_BIG ** B_BIG) * _2n, + (A_BIG ** B_BIG) === 1015090270405243n, show(A_BIG ** B_BIG), Number(A_BIG ** B_BIG)); +} +function mod_typedParam(a: bigint, b: bigint): string { + const x1 = a % b, x2 = a % b, x3 = a % b, x4 = a % b, x5 = a % b, x6 = a % b; + let x7 = a % b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 3n, show(x5), Number(x6), String(x7), + typeof (a % b), String(a % b), (a % b) * _2n, (a % b) === 3n, show(a % b), Number(a % b)); +} +function mod_untypedParam(a, b): string { + const x1 = a % b, x2 = a % b, x3 = a % b, x4 = a % b, x5 = a % b, x6 = a % b; + let x7 = a % b; + return row(typeof x1, String(x2), x3 * _2n, x4 === 3n, show(x5), Number(x6), String(x7), + typeof (a % b), String(a % b), (a % b) * _2n, (a % b) === 3n, show(a % b), Number(a % b)); +} +function mod_moduleBig(): string { + const x1 = A_BIG % B_BIG, x2 = A_BIG % B_BIG, x3 = A_BIG % B_BIG, + x4 = A_BIG % B_BIG, x5 = A_BIG % B_BIG, x6 = A_BIG % B_BIG; + let x7 = A_BIG % B_BIG; + return row(typeof x1, String(x2), x3 * _2n, x4 === 3n, show(x5), Number(x6), String(x7), + typeof (A_BIG % B_BIG), String(A_BIG % B_BIG), (A_BIG % B_BIG) * _2n, (A_BIG % B_BIG) === 3n, + show(A_BIG % B_BIG), Number(A_BIG % B_BIG)); +} +function and_closure(a, b): string { + return show((() => a & b)()); +} +function or_closure(a, b): string { + return show((() => a | b)()); +} +function xor_closure(a, b): string { + return show((() => a ^ b)()); +} +function shl_closure(a, b): string { + return show((() => a << b)()); +} +function shr_closure(a, b): string { + return show((() => a >> b)()); +} +function bitnot_untypedParam(a): string { + const x1 = ~a, x2 = ~a, x3 = ~a; + return row(show(x1), x2 === -1004n, Number(x3), Number(~a), (~a) * _2n); +} +function bitnot_typedParam(a: bigint): string { + const x1 = ~a, x2 = ~a, x3 = ~a; + return row(show(x1), x2 === -1004n, Number(x3), Number(~a), (~a) * _2n); +} +function bitnot_moduleBig(): string { + const x1 = ~A_BIG, x2 = ~A_BIG, x3 = ~A_BIG; + return row(show(x1), x2 === -1004n, Number(x3), Number(~A_BIG), (~A_BIG) * _2n); +} + +const cases: Array<[string, () => string]> = [ + ["and_lit", () => and_lit()], + ["and_constLit", () => and_constLit()], + ["and_letLit", () => and_letLit()], + ["and_constBig", () => and_constBig()], + ["and_letBig", () => and_letBig()], + ["and_typedParam", () => and_typedParam(BigInt(1003), BigInt(5))], + ["and_untypedParam", () => and_untypedParam(BigInt(1003), BigInt(5))], + ["and_moduleLit", () => and_moduleLit()], + ["and_moduleBig", () => and_moduleBig()], + ["and_propertyLit", () => and_propertyLit(OBJ_LIT)], + ["and_propertyBig", () => and_propertyBig(OBJ_BIG)], + ["and_elementLit", () => and_elementLit(ARR_LIT)], + ["and_elementBig", () => and_elementBig(ARR_BIG)], + ["and_arith", () => and_arith(BigInt(1003), BigInt(5))], + ["and_inlineBig", () => and_inlineBig()], + ["and_paramAndLit", () => and_paramAndLit(BigInt(1003))], + ["or_lit", () => or_lit()], + ["or_constLit", () => or_constLit()], + ["or_letLit", () => or_letLit()], + ["or_constBig", () => or_constBig()], + ["or_letBig", () => or_letBig()], + ["or_typedParam", () => or_typedParam(BigInt(1003), BigInt(5))], + ["or_untypedParam", () => or_untypedParam(BigInt(1003), BigInt(5))], + ["or_moduleLit", () => or_moduleLit()], + ["or_moduleBig", () => or_moduleBig()], + ["or_propertyLit", () => or_propertyLit(OBJ_LIT)], + ["or_propertyBig", () => or_propertyBig(OBJ_BIG)], + ["or_elementLit", () => or_elementLit(ARR_LIT)], + ["or_elementBig", () => or_elementBig(ARR_BIG)], + ["or_arith", () => or_arith(BigInt(1003), BigInt(5))], + ["or_inlineBig", () => or_inlineBig()], + ["or_paramAndLit", () => or_paramAndLit(BigInt(1003))], + ["xor_lit", () => xor_lit()], + ["xor_constLit", () => xor_constLit()], + ["xor_letLit", () => xor_letLit()], + ["xor_constBig", () => xor_constBig()], + ["xor_letBig", () => xor_letBig()], + ["xor_typedParam", () => xor_typedParam(BigInt(1003), BigInt(5))], + ["xor_untypedParam", () => xor_untypedParam(BigInt(1003), BigInt(5))], + ["xor_moduleLit", () => xor_moduleLit()], + ["xor_moduleBig", () => xor_moduleBig()], + ["xor_propertyLit", () => xor_propertyLit(OBJ_LIT)], + ["xor_propertyBig", () => xor_propertyBig(OBJ_BIG)], + ["xor_elementLit", () => xor_elementLit(ARR_LIT)], + ["xor_elementBig", () => xor_elementBig(ARR_BIG)], + ["xor_arith", () => xor_arith(BigInt(1003), BigInt(5))], + ["xor_inlineBig", () => xor_inlineBig()], + ["xor_paramAndLit", () => xor_paramAndLit(BigInt(1003))], + ["shl_lit", () => shl_lit()], + ["shl_constLit", () => shl_constLit()], + ["shl_letLit", () => shl_letLit()], + ["shl_constBig", () => shl_constBig()], + ["shl_letBig", () => shl_letBig()], + ["shl_typedParam", () => shl_typedParam(BigInt(1003), BigInt(5))], + ["shl_untypedParam", () => shl_untypedParam(BigInt(1003), BigInt(5))], + ["shl_moduleLit", () => shl_moduleLit()], + ["shl_moduleBig", () => shl_moduleBig()], + ["shl_propertyLit", () => shl_propertyLit(OBJ_LIT)], + ["shl_propertyBig", () => shl_propertyBig(OBJ_BIG)], + ["shl_elementLit", () => shl_elementLit(ARR_LIT)], + ["shl_elementBig", () => shl_elementBig(ARR_BIG)], + ["shl_arith", () => shl_arith(BigInt(1003), BigInt(5))], + ["shl_inlineBig", () => shl_inlineBig()], + ["shl_paramAndLit", () => shl_paramAndLit(BigInt(1003))], + ["shr_lit", () => shr_lit()], + ["shr_constLit", () => shr_constLit()], + ["shr_letLit", () => shr_letLit()], + ["shr_constBig", () => shr_constBig()], + ["shr_letBig", () => shr_letBig()], + ["shr_typedParam", () => shr_typedParam(BigInt(1003), BigInt(5))], + ["shr_untypedParam", () => shr_untypedParam(BigInt(1003), BigInt(5))], + ["shr_moduleLit", () => shr_moduleLit()], + ["shr_moduleBig", () => shr_moduleBig()], + ["shr_propertyLit", () => shr_propertyLit(OBJ_LIT)], + ["shr_propertyBig", () => shr_propertyBig(OBJ_BIG)], + ["shr_elementLit", () => shr_elementLit(ARR_LIT)], + ["shr_elementBig", () => shr_elementBig(ARR_BIG)], + ["shr_arith", () => shr_arith(BigInt(1003), BigInt(5))], + ["shr_inlineBig", () => shr_inlineBig()], + ["shr_paramAndLit", () => shr_paramAndLit(BigInt(1003))], + ["add_typedParam", () => add_typedParam(BigInt(1003), BigInt(5))], + ["add_untypedParam", () => add_untypedParam(BigInt(1003), BigInt(5))], + ["add_moduleBig", () => add_moduleBig()], + ["sub_typedParam", () => sub_typedParam(BigInt(1003), BigInt(5))], + ["sub_untypedParam", () => sub_untypedParam(BigInt(1003), BigInt(5))], + ["sub_moduleBig", () => sub_moduleBig()], + ["mul_typedParam", () => mul_typedParam(BigInt(1003), BigInt(5))], + ["mul_untypedParam", () => mul_untypedParam(BigInt(1003), BigInt(5))], + ["mul_moduleBig", () => mul_moduleBig()], + ["pow_typedParam", () => pow_typedParam(BigInt(1003), BigInt(5))], + ["pow_untypedParam", () => pow_untypedParam(BigInt(1003), BigInt(5))], + ["pow_moduleBig", () => pow_moduleBig()], + ["mod_typedParam", () => mod_typedParam(BigInt(1003), BigInt(5))], + ["mod_untypedParam", () => mod_untypedParam(BigInt(1003), BigInt(5))], + ["mod_moduleBig", () => mod_moduleBig()], + ["and_closure", () => and_closure(BigInt(1003), BigInt(5))], + ["or_closure", () => or_closure(BigInt(1003), BigInt(5))], + ["xor_closure", () => xor_closure(BigInt(1003), BigInt(5))], + ["shl_closure", () => shl_closure(BigInt(1003), BigInt(5))], + ["shr_closure", () => shr_closure(BigInt(1003), BigInt(5))], + ["bitnot_untypedParam", () => bitnot_untypedParam(BigInt(1003))], + ["bitnot_typedParam", () => bitnot_typedParam(BigInt(1003))], + ["bitnot_moduleBig", () => bitnot_moduleBig()], +]; +for (const [name, run] of cases) { + try { + console.log(name, run()); + } catch (e) { + console.log(name, "threw", (e as Error).constructor.name, (e as Error).message); + } +} + +// Each section runs on its own so one failure cannot hide the next. +function section(name: string, run: () => void): void { + try { + run(); + } catch (e) { + console.log(name, "threw", (e as Error).constructor.name, (e as Error).message); + } +} + +// Module-scope bindings take the module-global path, not a function local. +const MOD_AND = A_BIG & B_BIG; +const MOD_XOR = A_BIG ^ B_BIG; +let MOD_SHL = A_BIG << B_BIG; +console.log("module scope", show(MOD_AND), show(MOD_XOR), show(MOD_SHL), Number(A_BIG >> B_BIG)); + +// Compound assignment was already correct; keep it that way. +function compound(a, b): string { + let x = a; + x &= b; + let y = a; + y <<= b; + let z = a; + z ^= b; + return show(x) + " " + show(y) + " " + show(z); +} +section("compound", () => console.log("compound", compound(BigInt(1003), BigInt(5)))); + +// `Number(n & M)` with a non-literal mask — @noble/curves `curve.js`. +const M32 = BigInt(2 ** 32 - 1); +function numberOfMask(n): number { + return Number(n & M32); +} +function numberOfMaskTyped(n: bigint): number { + return Number(n & M32); +} +section("Number(n & M)", () => { + console.log( + "Number(n & M)", + typeof numberOfMask(BigInt("0x1234567890abcdef")), + numberOfMask(BigInt("0x1234567890abcdef")), + numberOfMaskTyped(BigInt("0xfedcba9876543210")), + ); +}); + +// @noble/curves weierstrass.js: `_2n << (c1 - _1n - _1n)` then `* _2n`. +function sqrtRatioPrelude(q): string { + let l = _0n; + for (let o = q - _1n; o % _2n === _0n; o /= _2n) l += _1n; + const c1 = l; + const _2n_pow_c1_1 = _2n << (c1 - _1n - _1n); + const _2n_pow_c1 = _2n_pow_c1_1 * _2n; + const c2 = (q - _1n) / _2n_pow_c1; + return [c1, _2n_pow_c1_1, _2n_pow_c1, c2].map(String).join(" "); +} +section("sqrtRatio", () => { + console.log("sqrtRatio", sqrtRatioPrelude(BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"))); +}); + +// @noble/curves: `Number(q.y & _1n)` on a point-like object. +function yParity(q): number { + return Number(q.y & _1n); +} +section("parity", () => console.log("parity", yParity({ y: BigInt(12345) }), yParity({ y: BigInt(12344) }))); + +// @noble/hashes `_u64.ts`: fromBig / split / toBig round trip. +const U32_MASK64 = BigInt(2 ** 32 - 1); +const _32n = BigInt(32); +function fromBig(n: bigint, le = false): { h: number; l: number } { + if (le) return { h: Number(n & U32_MASK64), l: Number((n >> _32n) & U32_MASK64) }; + return { h: Number((n >> _32n) & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 }; +} +function split(lst: bigint[], le = false): Uint32Array[] { + const len = lst.length; + const Ah = new Uint32Array(len); + const Al = new Uint32Array(len); + for (let i = 0; i < len; i++) { + const { h, l } = fromBig(lst[i], le); + [Ah[i], Al[i]] = [h, l]; + } + return [Ah, Al]; +} +const toBig = (h: number, l: number): bigint => (BigInt(h >>> 0) << _32n) | BigInt(l >>> 0); +section("u64", () => { + const K512 = [ + "0x428a2f98d728ae22", "0x7137449123ef65cd", "0xb5c0fbcfec4d3b2f", "0xe9b5dba58189dbbc", + "0x3956c25bf348b538", "0x59f111f1b605d019", "0x923f82a4af194f9b", "0xab1c5ed5da6d8118", + "0xffffffffffffffff", "0x0000000000000000", "0x8000000000000001", + ].map((n) => BigInt(n)); + const [Kh, Kl] = split(K512); + const single = fromBig(BigInt("0x428a2f98d728ae22")); + console.log("fromBig", single.h, single.l); + let roundTrip = true; + for (let i = 0; i < K512.length; i++) { + const back = toBig(Kh[i], Kl[i]); + if (back !== K512[i]) roundTrip = false; + console.log("split", i, Kh[i], Kl[i], back.toString(16)); + } + const le = fromBig(BigInt("0x0123456789abcdef"), true); + console.log("fromBig le", le.h, le.l, "roundTrip", roundTrip); +}); + +// Mixed BigInt/Number operands must still throw. +function mixed(a, b): string { + try { + const x = a & b; + return "no throw " + show(x); + } catch (e) { + return (e as Error).constructor.name; + } +} +section("mixed", () => console.log("mixed", mixed(BigInt(3), 1), mixed(1, BigInt(3)), mixed(BigInt(3), BigInt(1)))); + +// Number controls: the int32 fast path must keep ToInt32 semantics. +function numUntyped(a, b): string { + const x = a & b; + const y = a << b; + const z = a ^ b; + return [x, y, z, Number(a | b), typeof (a >> b)].join(" "); +} +function numTyped(a: number, b: number): string { + const x = a & b; + const y = a << b; + const z = (a ^ b) >>> 0; + const w = a >> 1; + return [x, y, z, w, Number(a | b)].join(" "); +} +section("number", () => { + console.log("number untyped", numUntyped(1003, 5), numUntyped(-1, 31), numUntyped(0x7fffffff, 1)); + console.log("number typed", numTyped(1003, 5), numTyped(-5, 30), numTyped(4294967295, 3)); + let h = 0x811c9dc5 | 0; + for (let i = 0; i < 1000; i++) { + h ^= i & 0xff; + h = Math.imul(h, 16777619); + const t = (h << 5) | (h >>> 27); + h = (h + t) | 0; + } + console.log("number hash", h, h >>> 0); +}); From 764dc56a86f8da61e4eb413bc797799e53893a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 13:21:06 +0000 Subject: [PATCH 10/11] docs(changelog): fragment for #10540 --- changelog.d/10540-bigint-bitwise-typing.md | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 changelog.d/10540-bigint-bitwise-typing.md diff --git a/changelog.d/10540-bigint-bitwise-typing.md b/changelog.d/10540-bigint-bitwise-typing.md new file mode 100644 index 0000000000..5f88a73028 --- /dev/null +++ b/changelog.d/10540-bigint-bitwise-typing.md @@ -0,0 +1,25 @@ +fix(codegen): BigInt `&` `|` `^` `<<` `>>` no longer typed as int32 (#10418). + +These operators compute a BigInt when both operands are BigInts, but the compiler assumed every bitwise result is an int32 Number. For BigInt operands it could not prove statically (untyped params, `BigInt(...)` initializers, property/element reads, arithmetic results): +- `const x = a & b` read back as `0`. +- `Number(a & b)` returned the BigInt unchanged. +- A `bigint`-typed result reached a call as `fptosi` of its box. +- `(_2n << k) * _2n` threw "Cannot mix BigInt and other types". + +This broke @noble/hashes' `fromBig` (`Number((n >> _32n) & U32_MASK64) | 0`): sha384/sha512/blake2b/blake2s/argon2 gave wrong digests and sha3/keccak threw at module load. Every @noble/curves-based package inherited the failure. + +Root cause, three layers: +- HIR typing (`lower_types.rs`, `analysis/value_types.rs`) typed the operators `Number` over unknown operands. +- The integer-local disqualification judge (`collectors/integer_locals.rs`) and `collectors/int_valued_ta_locals.rs` accepted every bitwise write as int-producing, so the binding took an int32 slot that `ToInt32`'d the BigInt result. +- `Number()` elision (`expr/bigint_set.rs`) treated any bitwise operand as already a Number. + +Fix: +- A bitwise result is a Number only when an operand provably is not a BigInt (`Type::is_non_bigint_primitive`). The not-BigInt fixpoint is exposed as `NotBigIntFacts` and computed ahead of the integer-local proofs. +- Bitwise ops whose operands are unproven now take the existing guarded numeric diamond (`lower_guarded_numeric_arith`: tag test, inline `ToInt32 ToInt32` with a masked shift count, BigInt-aware helper on the cold arm). The Number case stays inline, and proven-Number operands keep the unguarded path. + +Validation: +- `test-files/test_gap_10418_bigint_bitwise_typing.ts` covers operator × 16 operand shapes × 13 consumers, controls for `+ - * ** % ~`, `Number(n & M)`, and a noble `fromBig`/`split`/`toBig` round trip. Baseline: 97 lines differ from Node. Now: identical. +- New perry-hir and perry-codegen unit tests. +- @noble/hashes 2.2.0 sha256/sha512/sha3_256/keccak_256/blake2b/blake2s/blake3/sha384/hmac/argon2 digests match Node. +- Gap suite: no new failures. +- Instructions (no auto-optimize): SHA-256 compress −1.2 %, untyped integer-hash helpers −56 %, `bench_bitwise` unchanged. From 16f4bf4417d646ab36d2565549be9a8b10506f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 17 Sep 2026 15:35:23 +0200 Subject: [PATCH 11/11] chore: release merge train 214 as v0.5.1592 --- CLAUDE.md | 2 +- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 83 insertions(+), 83 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a3a2dccc99..b616a33981 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.1591 +**Current Version:** 0.5.1592 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 33f676ed1b..480657380e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5623,7 +5623,7 @@ checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" [[package]] name = "perry" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "base64 0.22.1", @@ -5687,7 +5687,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-dispatch", "serde", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "cc", "libc", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "aho-corasick", "anyhow", @@ -5721,7 +5721,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-hir", @@ -5729,7 +5729,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-hir", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-dispatch", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-hir", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "base64 0.22.1", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-hir", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "async-trait", "clap", @@ -5798,14 +5798,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "serde", "serde_json", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1591" +version = "0.5.1592" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "clap", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "block2", "objc2", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "argon2", "perry-ffi", @@ -5858,7 +5858,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "reqwest", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "bcrypt", "perry-ffi", @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "rusqlite", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "scraper", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "perry-runtime", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "chrono", "cron", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "chrono", "perry-ffi", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "rust_decimal", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "serde_json", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5941,7 +5941,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "perry-runtime", @@ -5949,14 +5949,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "bytes", "http-body-util", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "bytes", "lazy_static", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "bytes", @@ -6018,7 +6018,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "lazy_static", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "lru", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "chrono", "perry-ffi", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "bson", "futures-util", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "chrono", "perry-ffi", @@ -6080,7 +6080,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "nanoid", "perry-ffi", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "bytes", "perry-ffi", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "lettre", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "notify", "perry-ffi", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "printpdf", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "sqlx", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "perry-runtime", @@ -6171,7 +6171,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "governor", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "fast_image_resize", "image", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "lazy_static", "perry-ffi", @@ -6199,7 +6199,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-ffi", @@ -6219,7 +6219,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "perry-runtime", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "uuid", @@ -6236,7 +6236,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-ffi", "perry-validation", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "futures-util", "lazy_static", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "brotli", "flate2", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-api-manifest", @@ -6298,11 +6298,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1591" +version = "0.5.1592" [[package]] name = "perry-parser" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "perry-diagnostics", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perex", "regex", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "ahash", "base64 0.22.1", @@ -6381,14 +6381,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6477,21 +6477,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "dirs", "perry-ffi", @@ -6501,7 +6501,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "jni", @@ -6516,7 +6516,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "rand 0.10.2", "serde", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "block2", @@ -6583,7 +6583,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1591" +version = "0.5.1592" [[package]] name = "perry-ui-test" @@ -6594,11 +6594,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1591" +version = "0.5.1592" [[package]] name = "perry-ui-tvos" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "block2", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "block2", @@ -6632,7 +6632,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "block2", "libc", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "libc", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "base64 0.22.1", "libc", @@ -6678,7 +6678,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "anyhow", "base64 0.22.1", @@ -6693,7 +6693,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "idna", "regex", @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1591" +version = "0.5.1592" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index b14b82b1b6..b4f5891706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1591" +version = "0.5.1592" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"