From 6a76aa0eaafe41125de9daeeb1f36afb2b1a1bd8 Mon Sep 17 00:00:00 2001 From: Ralph Date: Tue, 16 Jun 2026 22:22:42 -0700 Subject: [PATCH 1/2] refactor(transform/hir): de-duplicate HIR-walking helpers (#5293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse copy-pasted HIR/IR walkers whose copies must each be hand-updated when a new Expr variant is added — a missed copy is a silent miscompile, not a build error (the same bug-class as #5143). - compute_max_local_id / compute_max_func_id: delete the copies in finally_inline, async_to_generator (incl. compute_max_func_id_module), state_desugar, and unroll; route all callers through the canonical, already-`pub`, exhaustive-walker-backed implementations in generator::id_scan. First the canonical scanners are made a true superset of every copy: they now also scan switch `case.test` exprs and module-global initializers (a higher max id is always safe — fresh ids just start higher; a missed id collides). - class_computed_member_registration_expr: drop the 3 byte-identical copies in lower/{lower_expr,module_decl,stmt}.rs and re-export the canonical lower_decl::class_computed one through lower_decl::mod. - collect_assigned_locals_expr: replace the ~1120-line hand-rolled per-variant descent (ending in a `_ => {}` catch-all that silently skipped newly-added variants) with the exhaustive walk_expr_children for child descent, keeping only the 7 assignment-recording arms and the intentional don't-recurse-into-closures arm custom. Pure refactor, no behavior change. Verified: perry-hir (158) and perry-transform (34) lib tests pass in release; an end-to-end smoke program covering switch case-tests, module-global closures, async, generators, finally, computed class members, and unrolled loops matches `node --experimental-strip-types` byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-hir/src/analysis.rs | 1080 +---------------- crates/perry-hir/src/lower/lower_expr.rs | 32 - crates/perry-hir/src/lower/module_decl.rs | 32 - crates/perry-hir/src/lower/stmt.rs | 32 - crates/perry-hir/src/lower_decl/mod.rs | 1 + .../perry-transform/src/async_to_generator.rs | 321 +---- crates/perry-transform/src/finally_inline.rs | 155 +-- .../perry-transform/src/generator/id_scan.rs | 26 + crates/perry-transform/src/state_desugar.rs | 280 +---- crates/perry-transform/src/unroll/mod.rs | 343 +----- 10 files changed, 63 insertions(+), 2239 deletions(-) diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 4e53c495b0..4b196dd52a 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -328,202 +328,24 @@ pub(crate) fn collect_assigned_locals_stmt(stmt: &Stmt, assigned: &mut Vec) { + // #5293: only these arms RECORD an assignment (or intentionally stop + // descent). Every other variant delegates child descent to the exhaustive + // `walk_expr_children` below. The previous hand-rolled version enumerated + // ~100 variants and ended in a `_ => {}` catch-all, so a newly-added Expr + // variant nesting a `LocalSet`/`Update` silently escaped assignment + // tracking (the same bug-class as #5143). The walker is exhaustively + // matched on Expr, so a forgotten variant is a compile error there. match expr { Expr::LocalSet(id, value) => { // This is an assignment to a local variable assigned.push(*id); collect_assigned_locals_expr(value, assigned); - } - Expr::Binary { left, right, .. } - | Expr::Compare { left, right, .. } - | Expr::Logical { left, right, .. } => { - collect_assigned_locals_expr(left, assigned); - collect_assigned_locals_expr(right, assigned); - } - Expr::Unary { operand, .. } => { - collect_assigned_locals_expr(operand, assigned); - } - Expr::Call { callee, args, .. } => { - collect_assigned_locals_expr(callee, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::PropertyGet { object, .. } => { - collect_assigned_locals_expr(object, assigned); - } - Expr::PropertySet { object, value, .. } => { - collect_assigned_locals_expr(object, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::PropertyUpdate { object, .. } => { - collect_assigned_locals_expr(object, assigned); - } - Expr::IndexGet { object, index } => { - collect_assigned_locals_expr(object, assigned); - collect_assigned_locals_expr(index, assigned); - } - Expr::IndexSet { - object, - index, - value, - } => { - collect_assigned_locals_expr(object, assigned); - collect_assigned_locals_expr(index, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::IndexUpdate { object, index, .. } => { - collect_assigned_locals_expr(object, assigned); - collect_assigned_locals_expr(index, assigned); - } - Expr::Array(elements) => { - for elem in elements { - collect_assigned_locals_expr(elem, assigned); - } - } - Expr::ArraySpread(elements) => { - for elem in elements { - match elem { - ArrayElement::Expr(e) => collect_assigned_locals_expr(e, assigned), - ArrayElement::Spread(e) => collect_assigned_locals_expr(e, assigned), - ArrayElement::Hole => {} - } - } - } - Expr::Conditional { - condition, - then_expr, - else_expr, - } => { - collect_assigned_locals_expr(condition, assigned); - collect_assigned_locals_expr(then_expr, assigned); - collect_assigned_locals_expr(else_expr, assigned); - } - Expr::New { args, .. } => { - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::Closure { .. } => { - // Don't recurse into nested closures - assignments there are local to that closure - } - Expr::Await(inner) => { - collect_assigned_locals_expr(inner, assigned); - } - Expr::Sequence(exprs) => { - for e in exprs { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::SuperCall(args) => { - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::SuperMethodCall { args, .. } => { - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::ObjectSuperPropertyGet { - home, - key, - receiver, - } => { - collect_assigned_locals_expr(home, assigned); - collect_assigned_locals_expr(key, assigned); - collect_assigned_locals_expr(receiver, assigned); - } - Expr::SuperPropertySet { key, value, .. } => { - collect_assigned_locals_expr(key, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::ObjectSuperPropertySet { - home, - key, - value, - receiver, - } => { - collect_assigned_locals_expr(home, assigned); - collect_assigned_locals_expr(key, assigned); - collect_assigned_locals_expr(value, assigned); - collect_assigned_locals_expr(receiver, assigned); - } - Expr::ObjectSuperMethodCall { - home, - key, - receiver, - args, - } => { - collect_assigned_locals_expr(home, assigned); - collect_assigned_locals_expr(key, assigned); - collect_assigned_locals_expr(receiver, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } + return; } Expr::Update { id, .. } => { // Update is an assignment assigned.push(*id); - } - // File system operations - Expr::FsReadFileSync(path) => { - collect_assigned_locals_expr(path, assigned); - } - Expr::FsWriteFileSync(path, content) => { - collect_assigned_locals_expr(path, assigned); - collect_assigned_locals_expr(content, assigned); - } - Expr::FsExistsSync(path) - | Expr::FsMkdirSync(path) - | Expr::FsUnlinkSync(path) - | Expr::FsReadFileBinary(path) - | Expr::FsRmRecursive(path) => { - collect_assigned_locals_expr(path, assigned); - } - Expr::FsAppendFileSync(path, content) => { - collect_assigned_locals_expr(path, assigned); - collect_assigned_locals_expr(content, assigned); - } - Expr::ChildProcessSpawnBackground { - command, - args, - log_file, - env_json, - } => { - collect_assigned_locals_expr(command, assigned); - if let Some(a) = args { - collect_assigned_locals_expr(a, assigned); - } - collect_assigned_locals_expr(log_file, assigned); - if let Some(e) = env_json { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::ChildProcessGetProcessStatus(h) | Expr::ChildProcessKillProcess(h) => { - collect_assigned_locals_expr(h, assigned); - } - // Path operations - Expr::PathJoin(a, b) - | Expr::PathMatchesGlob(a, b) - | Expr::PathResolveJoin(a, b) - | Expr::PathWin32Join(a, b) => { - collect_assigned_locals_expr(a, assigned); - collect_assigned_locals_expr(b, assigned); - } - Expr::PathDirname(path) - | Expr::PathBasename(path) - | Expr::PathExtname(path) - | Expr::PathResolve(path) - | Expr::PathIsAbsolute(path) - | Expr::PathToNamespacedPath(path) - | Expr::FileURLToPath(path) => { - collect_assigned_locals_expr(path, assigned); - } - Expr::PathWin32 { args, .. } => { - for e in args { - collect_assigned_locals_expr(e, assigned); - } + return; } // Array methods - push/unshift may reassign the array pointer Expr::ArrayPush { array_id, value } @@ -534,20 +356,7 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec { assigned.push(*array_id); // These may reallocate the array collect_assigned_locals_expr(value, assigned); - } - Expr::ArrayPop(_array_id) | Expr::ArrayShift(_array_id) => { - // These modify the array but don't reallocate - } - Expr::ArrayIndexOf { array, value, .. } | Expr::ArrayIncludes { array, value, .. } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::ArraySlice { array, start, end } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(start, assigned); - if let Some(e) = end { - collect_assigned_locals_expr(e, assigned); - } + return; } Expr::ArraySplice { array_id, @@ -563,71 +372,14 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(callback, assigned); - } - Expr::ArraySort { array, comparator } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(comparator, assigned); - } - Expr::ArrayReduce { - array, - callback, - initial, - } - | Expr::ArrayReduceRight { - array, - callback, - initial, - } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(callback, assigned); - if let Some(init) = initial { - collect_assigned_locals_expr(init, assigned); - } - } - Expr::ArrayToReversed { array } => { - collect_assigned_locals_expr(array, assigned); - } - Expr::ArrayToSorted { array, comparator } => { - collect_assigned_locals_expr(array, assigned); - if let Some(cmp) = comparator { - collect_assigned_locals_expr(cmp, assigned); - } - } - Expr::ArrayToSpliced { - array, - start, - delete_count, - items, - } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(start, assigned); - collect_assigned_locals_expr(delete_count, assigned); - for item in items { - collect_assigned_locals_expr(item, assigned); - } - } - Expr::ArrayWith { - array, - index, - value, - } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(index, assigned); - collect_assigned_locals_expr(value, assigned); + return; } Expr::ArrayReverseValue { receiver } => { if let Expr::LocalGet(id) = receiver.as_ref() { assigned.push(*id); } collect_assigned_locals_expr(receiver, assigned); + return; } Expr::ArrayCopyWithin { array_id, @@ -641,815 +393,21 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec { - collect_assigned_locals_expr(receiver, assigned); - collect_assigned_locals_expr(target, assigned); - collect_assigned_locals_expr(start, assigned); - if let Some(e) = end { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::ArrayEntries(array) | Expr::ArrayKeys(array) | Expr::ArrayValues(array) => { - collect_assigned_locals_expr(array, assigned); - } - Expr::ArrayJoin { array, separator } => { - collect_assigned_locals_expr(array, assigned); - if let Some(sep) = separator { - collect_assigned_locals_expr(sep, assigned); - } - } - Expr::ArrayFlat { array } => { - collect_assigned_locals_expr(array, assigned); - } - // Native module calls - Expr::NativeMethodCall { object, args, .. } => { - if let Some(obj) = object { - collect_assigned_locals_expr(obj, assigned); - } - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - // Static member access - Expr::StaticFieldGet { .. } => {} - Expr::StaticFieldSet { value, .. } => { - collect_assigned_locals_expr(value, assigned); - } - Expr::StaticMethodCall { args, .. } => { - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - // String methods - Expr::StringSplit(string, delimiter) => { - collect_assigned_locals_expr(string, assigned); - collect_assigned_locals_expr(delimiter, assigned); - } - Expr::StringFromCharCode(code) | Expr::StringFromCharCodeSpread(code) => { - collect_assigned_locals_expr(code, assigned); - } - // Map operations - Expr::MapNew => {} - Expr::MapNewFromArray(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - Expr::MapSet { map, key, value } => { - collect_assigned_locals_expr(map, assigned); - collect_assigned_locals_expr(key, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::MapGet { map, key } | Expr::MapHas { map, key } | Expr::MapDelete { map, key } => { - collect_assigned_locals_expr(map, assigned); - collect_assigned_locals_expr(key, assigned); - } - Expr::MapSize(map) - | Expr::MapClear(map) - | Expr::MapEntries(map) - | Expr::MapKeys(map) - | Expr::MapValues(map) => { - collect_assigned_locals_expr(map, assigned); - } - // Set operations - Expr::SetNew => {} - Expr::SetNewFromArray(expr) => { - collect_assigned_locals_expr(expr, assigned); + return; } Expr::SetAdd { set_id, value } => { assigned.push(*set_id); // Set is modified by add collect_assigned_locals_expr(value, assigned); + return; } - Expr::SetHas { set, value } | Expr::SetDelete { set, value } => { - collect_assigned_locals_expr(set, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::SetSize(set) | Expr::SetClear(set) | Expr::SetValues(set) => { - collect_assigned_locals_expr(set, assigned); - } - // JSON operations - Expr::JsonParse(expr) - | Expr::JsonStringify(expr) - | Expr::JsonRawJson(expr) - | Expr::JsonIsRawJson(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - // Math operations - Expr::MathFloor(expr) - | Expr::MathCeil(expr) - | Expr::MathRound(expr) - | Expr::MathTrunc(expr) - | Expr::MathSign(expr) - | Expr::MathAbs(expr) - | Expr::MathSqrt(expr) - | Expr::MathLog(expr) - | Expr::MathLog2(expr) - | Expr::MathLog10(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - Expr::MathPow(base, exp) | Expr::MathImul(base, exp) => { - collect_assigned_locals_expr(base, assigned); - collect_assigned_locals_expr(exp, assigned); - } - Expr::MathMin(args) | Expr::MathMax(args) => { - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::MathMinSpread(expr) | Expr::MathMaxSpread(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - Expr::MathRandom => {} - // Crypto operations - Expr::CryptoRandomBytes(expr) | Expr::CryptoSha256(expr) | Expr::CryptoMd5(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - Expr::CryptoRandomUUID => {} - Expr::CryptoRandomUUIDv7 => {} - // OS operations (no assignments) - Expr::OsPlatform - | Expr::OsArch - | Expr::OsHostname - | Expr::OsHomedir - | Expr::OsTmpdir - | Expr::OsTotalmem - | Expr::OsFreemem - | Expr::OsUptime - | Expr::OsType - | Expr::OsRelease - | Expr::OsCpus - | Expr::OsNetworkInterfaces - | Expr::OsUserInfo - | Expr::OsUserInfoBuffer - | Expr::OsEOL - | Expr::OsDevNull - | Expr::OsAvailableParallelism - | Expr::OsEndianness - | Expr::OsLoadavg - | Expr::OsMachine - | Expr::OsVersion => {} - // Buffer operations - Expr::BufferFrom { data, encoding } => { - collect_assigned_locals_expr(data, assigned); - if let Some(enc) = encoding { - collect_assigned_locals_expr(enc, assigned); - } - } - Expr::BufferFromArrayBuffer { - data, - byte_offset, - length, - } => { - collect_assigned_locals_expr(data, assigned); - collect_assigned_locals_expr(byte_offset, assigned); - if let Some(len) = length { - collect_assigned_locals_expr(len, assigned); - } - } - Expr::BufferAlloc { - size, - fill, - encoding, - } => { - collect_assigned_locals_expr(size, assigned); - if let Some(f) = fill { - collect_assigned_locals_expr(f, assigned); - } - if let Some(e) = encoding { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::BufferAllocUnsafe(expr) - | Expr::BufferConcat(expr) - | Expr::BufferIsBuffer(expr) - | Expr::BufferIsEncoding(expr) - | Expr::BufferLength(expr) => { - collect_assigned_locals_expr(expr, assigned); - } - Expr::BufferConcatWithLength { list, total_length } => { - collect_assigned_locals_expr(list, assigned); - collect_assigned_locals_expr(total_length, assigned); - } - Expr::BufferByteLength { data, encoding } => { - collect_assigned_locals_expr(data, assigned); - if let Some(enc) = encoding { - collect_assigned_locals_expr(enc, assigned); - } - } - Expr::BufferToString { buffer, encoding } => { - collect_assigned_locals_expr(buffer, assigned); - if let Some(enc) = encoding { - collect_assigned_locals_expr(enc, assigned); - } - } - Expr::BufferFill { buffer, value } => { - collect_assigned_locals_expr(buffer, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::BufferSlice { buffer, start, end } => { - collect_assigned_locals_expr(buffer, assigned); - if let Some(s) = start { - collect_assigned_locals_expr(s, assigned); - } - if let Some(e) = end { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::BufferCopy { - source, - target, - target_start, - source_start, - source_end, - } => { - collect_assigned_locals_expr(source, assigned); - collect_assigned_locals_expr(target, assigned); - if let Some(ts) = target_start { - collect_assigned_locals_expr(ts, assigned); - } - if let Some(ss) = source_start { - collect_assigned_locals_expr(ss, assigned); - } - if let Some(se) = source_end { - collect_assigned_locals_expr(se, assigned); - } - } - Expr::BufferWrite { - buffer, - string, - offset, - encoding, - } => { - collect_assigned_locals_expr(buffer, assigned); - collect_assigned_locals_expr(string, assigned); - if let Some(o) = offset { - collect_assigned_locals_expr(o, assigned); - } - if let Some(e) = encoding { - collect_assigned_locals_expr(e, assigned); - } - } - Expr::BufferEquals { buffer, other } => { - collect_assigned_locals_expr(buffer, assigned); - collect_assigned_locals_expr(other, assigned); - } - Expr::BufferIndexGet { buffer, index } => { - collect_assigned_locals_expr(buffer, assigned); - collect_assigned_locals_expr(index, assigned); - } - Expr::BufferIndexSet { - buffer, - index, - value, - } => { - collect_assigned_locals_expr(buffer, assigned); - collect_assigned_locals_expr(index, assigned); - collect_assigned_locals_expr(value, assigned); - } - // Child Process operations - Expr::ChildProcessExecSync { command, options } => { - collect_assigned_locals_expr(command, assigned); - if let Some(opts) = options { - collect_assigned_locals_expr(opts, assigned); - } - } - Expr::ChildProcessSpawnSync { - command, - args, - options, - } - | Expr::ChildProcessSpawn { - command, - args, - options, - } => { - collect_assigned_locals_expr(command, assigned); - if let Some(a) = args { - collect_assigned_locals_expr(a, assigned); - } - if let Some(opts) = options { - collect_assigned_locals_expr(opts, assigned); - } - } - Expr::ChildProcessFork { - module, - args, - options, - } => { - collect_assigned_locals_expr(module, assigned); - if let Some(a) = args { - collect_assigned_locals_expr(a, assigned); - } - if let Some(opts) = options { - collect_assigned_locals_expr(opts, assigned); - } - } - Expr::ChildProcessExec { - command, - options, - callback, - } => { - collect_assigned_locals_expr(command, assigned); - if let Some(opts) = options { - collect_assigned_locals_expr(opts, assigned); - } - if let Some(cb) = callback { - collect_assigned_locals_expr(cb, assigned); - } - } - // Net operations - Expr::NetCreateServer { - options, - connection_listener, - } => { - if let Some(opts) = options { - collect_assigned_locals_expr(opts, assigned); - } - if let Some(cl) = connection_listener { - collect_assigned_locals_expr(cl, assigned); - } - } - Expr::NetCreateConnection { - port, - host, - connect_listener, - } - | Expr::NetConnect { - port, - host, - connect_listener, - } => { - collect_assigned_locals_expr(port, assigned); - if let Some(h) = host { - collect_assigned_locals_expr(h, assigned); - } - if let Some(cl) = connect_listener { - collect_assigned_locals_expr(cl, assigned); - } - } - // Date operations - Expr::DateNow => {} - Expr::DateNew(args) => { - for a in args { - collect_assigned_locals_expr(a, assigned); - } - } - Expr::DateGetTime(date) - | Expr::DateToISOString(date) - | Expr::DateGetFullYear(date) - | Expr::DateGetMonth(date) - | Expr::DateGetDate(date) - | Expr::DateGetDay(date) - | Expr::DateGetHours(date) - | Expr::DateGetMinutes(date) - | Expr::DateGetSeconds(date) - | Expr::DateGetMilliseconds(date) => { - collect_assigned_locals_expr(date, assigned); - } - // URL operations - Expr::UrlNew { url, base } => { - collect_assigned_locals_expr(url, assigned); - if let Some(base_expr) = base { - collect_assigned_locals_expr(base_expr, assigned); - } - } - Expr::UrlPatternNew { input, base } => { - collect_assigned_locals_expr(input, assigned); - if let Some(base_expr) = base { - collect_assigned_locals_expr(base_expr, assigned); - } - } - Expr::UrlGetHref(url) - | Expr::UrlGetPathname(url) - | Expr::UrlGetProtocol(url) - | Expr::UrlGetHost(url) - | Expr::UrlGetHostname(url) - | Expr::UrlGetPort(url) - | Expr::UrlGetSearch(url) - | Expr::UrlGetHash(url) - | Expr::UrlGetOrigin(url) - | Expr::UrlGetSearchParams(url) - | Expr::UrlParse(url) - | Expr::UrlInstanceToString(url) - | Expr::UrlInstanceToJSON(url) => { - collect_assigned_locals_expr(url, assigned); - } - Expr::UrlCanParse(url) => { - collect_assigned_locals_expr(url, assigned); - } - Expr::UrlCanParseWithBase { input, base } => { - collect_assigned_locals_expr(input, assigned); - collect_assigned_locals_expr(base, assigned); - } - Expr::UrlParseWithBase { input, base } => { - collect_assigned_locals_expr(input, assigned); - collect_assigned_locals_expr(base, assigned); - } - // URLSearchParams operations - Expr::UrlSearchParamsNew(init) => { - if let Some(init_expr) = init { - collect_assigned_locals_expr(init_expr, assigned); - } - } - Expr::UrlSearchParamsMissingArgs { params, args, .. } => { - collect_assigned_locals_expr(params, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::UrlSearchParamsGet { params, name } - | Expr::UrlSearchParamsGetAll { params, name } => { - collect_assigned_locals_expr(params, assigned); - collect_assigned_locals_expr(name, assigned); - } - Expr::UrlSearchParamsHas { - params, - name, - value, - } - | Expr::UrlSearchParamsDelete { - params, - name, - value, - } => { - collect_assigned_locals_expr(params, assigned); - collect_assigned_locals_expr(name, assigned); - if let Some(v) = value { - collect_assigned_locals_expr(v, assigned); - } - } - Expr::UrlSearchParamsSet { - params, - name, - value, - } - | Expr::UrlSearchParamsAppend { - params, - name, - value, - } => { - collect_assigned_locals_expr(params, assigned); - collect_assigned_locals_expr(name, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::UrlSearchParamsForEach { - params, - callback, - this_arg, - } => { - collect_assigned_locals_expr(params, assigned); - collect_assigned_locals_expr(callback, assigned); - if let Some(this_arg) = this_arg { - collect_assigned_locals_expr(this_arg, assigned); - } - } - Expr::UrlSearchParamsToString(params) - | Expr::UrlSearchParamsEntries(params) - | Expr::UrlSearchParamsKeys(params) - | Expr::UrlSearchParamsValues(params) - | Expr::UrlSearchParamsSort(params) => { - collect_assigned_locals_expr(params, assigned); - } - Expr::GlobalSet(_, value) => { - collect_assigned_locals_expr(value, assigned); - } - // Terminal expressions that don't have children or don't assign - Expr::LocalGet(_) - | Expr::GlobalGet(_) - | Expr::FuncRef(_) - | Expr::ExternFuncRef { .. } - | Expr::PodLayoutSizeOf { .. } - | Expr::PodLayoutAlignOf { .. } - | Expr::PodLayoutOffsetOf { .. } - | Expr::NewTarget - | Expr::ClassRef(_) - | Expr::Number(_) - | Expr::Integer(_) - | Expr::Bool(_) - | Expr::String(_) - | Expr::BigInt(_) - | Expr::Object(_) - | Expr::TypeOf(_) - | Expr::InstanceOf { .. } - | Expr::EnumMember { .. } - | Expr::This - | Expr::Null - | Expr::Undefined - | Expr::EnvGet(_) - | Expr::ProcessUptime - | Expr::ProcessCwd - | Expr::ProcessMemoryUsage - | Expr::ProcessEnv - | Expr::GlobalThisExpr - | Expr::NativeModuleRef(_) - | Expr::RegExp { .. } => {} - Expr::ObjectKeys(obj) - | Expr::ForInKeys(obj) - | Expr::ObjectValues(obj) - | Expr::ObjectEntries(obj) => { - collect_assigned_locals_expr(obj, assigned); - } - Expr::ObjectGroupBy { items, key_fn } | Expr::MapGroupBy { items, key_fn } => { - collect_assigned_locals_expr(items, assigned); - collect_assigned_locals_expr(key_fn, assigned); - } - Expr::ArrayIsArray(value) - | Expr::ArrayFrom(value) - | Expr::ArrayFromArrayLikeHoley(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::ArrayFromMapped { - iterable, - map_fn, - this_arg, - } => { - collect_assigned_locals_expr(iterable, assigned); - collect_assigned_locals_expr(map_fn, assigned); - if let Some(t) = this_arg { - collect_assigned_locals_expr(t, assigned); - } - } - Expr::RegExpTest { regex, string } => { - collect_assigned_locals_expr(regex, assigned); - collect_assigned_locals_expr(string, assigned); - } - Expr::RegExpDynamic { pattern, flags } => { - collect_assigned_locals_expr(pattern, assigned); - if let Some(f) = flags { - collect_assigned_locals_expr(f, assigned); - } - } - Expr::StringMatch { string, regex } => { - collect_assigned_locals_expr(string, assigned); - collect_assigned_locals_expr(regex, assigned); - } - Expr::StringReplace { - string, - pattern, - replacement, - } => { - collect_assigned_locals_expr(string, assigned); - collect_assigned_locals_expr(pattern, assigned); - collect_assigned_locals_expr(replacement, assigned); - } - Expr::ParseInt { string, radix } => { - collect_assigned_locals_expr(string, assigned); - if let Some(r) = radix { - collect_assigned_locals_expr(r, assigned); - } - } - Expr::ParseFloat(string) => { - collect_assigned_locals_expr(string, assigned); - } - Expr::NumberCoerce(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::BigIntCoerce(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::StringCoerce(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::ObjectCoerce(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::BooleanCoerce(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::IsNaN(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::IsUndefinedOrBareNan(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::IsFinite(value) => { - collect_assigned_locals_expr(value, assigned); - } - Expr::StaticPluginResolve(value) => { - collect_assigned_locals_expr(value, assigned); - } - // JS runtime expressions - Expr::JsLoadModule { .. } => {} - Expr::JsGetExport { module_handle, .. } => { - collect_assigned_locals_expr(module_handle, assigned); - } - Expr::JsCallFunction { - module_handle, - args, - .. - } => { - collect_assigned_locals_expr(module_handle, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::JsCallMethod { object, args, .. } => { - collect_assigned_locals_expr(object, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - // #853: an earlier arm in this match (lines 682-695) already - // covers every Expr::Os* variant. The duplicate arm here was - // dead — removed. - // Delete operator - Expr::Delete(inner) => { - collect_assigned_locals_expr(inner, assigned); - } - // Error operations - Expr::ErrorNew(msg) => { - if let Some(m) = msg { - collect_assigned_locals_expr(m, assigned); - } - } - Expr::ErrorMessage(err) => { - collect_assigned_locals_expr(err, assigned); - } - Expr::ErrorNewWithCause { message, cause: b } - | Expr::ErrorNewWithOptions { - message, - options: b, - .. - } => { - collect_assigned_locals_expr(message, assigned); - collect_assigned_locals_expr(b, assigned); - } - Expr::TypeErrorNew(m) - | Expr::RangeErrorNew(m) - | Expr::ReferenceErrorNew(m) - | Expr::SyntaxErrorNew(m) => { - collect_assigned_locals_expr(m, assigned); - } - Expr::AggregateErrorNew { - errors, - message, - options, - } => { - collect_assigned_locals_expr(errors, assigned); - collect_assigned_locals_expr(message, assigned); - options - .iter() - .for_each(|o| collect_assigned_locals_expr(o, assigned)); - } - // Uint8Array operations - Expr::Uint8ArrayNew(size) => { - if let Some(s) = size { - collect_assigned_locals_expr(s, assigned); - } - } - Expr::Uint8ArrayFrom(data) | Expr::Uint8ArrayLength(data) => { - collect_assigned_locals_expr(data, assigned); - } - Expr::Uint8ArrayGet { array, index } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(index, assigned); - } - Expr::Uint8ArraySet { - array, - index, - value, - } => { - collect_assigned_locals_expr(array, assigned); - collect_assigned_locals_expr(index, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::TypedArrayNew { arg, .. } => { - if let Some(a) = arg { - collect_assigned_locals_expr(a, assigned); - } - } - Expr::NativeArenaAlloc(byte_length) | Expr::NativeArenaDispose(byte_length) => { - collect_assigned_locals_expr(byte_length, assigned); - } - Expr::NativeArenaView { - owner, - byte_offset, - length, - .. - } => { - collect_assigned_locals_expr(owner, assigned); - collect_assigned_locals_expr(byte_offset, assigned); - collect_assigned_locals_expr(length, assigned); - } - Expr::NativePodView { - owner, - byte_offset, - count, - .. - } => { - collect_assigned_locals_expr(owner, assigned); - collect_assigned_locals_expr(byte_offset, assigned); - collect_assigned_locals_expr(count, assigned); - } - Expr::NativeMemoryFillU32 { view, value } => { - collect_assigned_locals_expr(view, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::NativeMemoryCopy { dst, src } => { - collect_assigned_locals_expr(dst, assigned); - collect_assigned_locals_expr(src, assigned); - } - // Dynamic env access - Expr::EnvGetDynamic(key) => { - collect_assigned_locals_expr(key, assigned); - } - // JS runtime expressions with sub-expressions - Expr::JsGetProperty { object, .. } => { - collect_assigned_locals_expr(object, assigned); - } - Expr::JsSetProperty { object, value, .. } => { - collect_assigned_locals_expr(object, assigned); - collect_assigned_locals_expr(value, assigned); - } - Expr::JsNew { - module_handle, - args, - .. - } => { - collect_assigned_locals_expr(module_handle, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::JsNewFromHandle { constructor, args } => { - collect_assigned_locals_expr(constructor, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - Expr::JsCreateCallback { closure, .. } => { - collect_assigned_locals_expr(closure, assigned); - } - // Spread call expressions - Expr::CallSpread { callee, args, .. } => { - collect_assigned_locals_expr(callee, assigned); - for arg in args { - match arg { - CallArg::Expr(e) | CallArg::Spread(e) => { - collect_assigned_locals_expr(e, assigned) - } - } - } - } - // Void operator - Expr::Void(inner) => { - collect_assigned_locals_expr(inner, assigned); - } - // Yield expression - Expr::Yield { value, .. } => { - if let Some(v) = value { - collect_assigned_locals_expr(v, assigned); - } - } - // Dynamic new expression - Expr::NewDynamic { callee, args, .. } => { - collect_assigned_locals_expr(callee, assigned); - for arg in args { - collect_assigned_locals_expr(arg, assigned); - } - } - // Object rest destructuring - Expr::ObjectRest { object, .. } => { - collect_assigned_locals_expr(object, assigned); - } - // Fetch with options - Expr::FetchWithOptions { - url, - method, - body, - headers, - headers_dynamic, - } => { - collect_assigned_locals_expr(url, assigned); - collect_assigned_locals_expr(method, assigned); - collect_assigned_locals_expr(body, assigned); - for (_, v) in headers { - collect_assigned_locals_expr(v, assigned); - } - if let Some(hd) = headers_dynamic { - collect_assigned_locals_expr(hd, assigned); - } - } - Expr::FetchGetWithAuth { url, auth_header } => { - collect_assigned_locals_expr(url, assigned); - collect_assigned_locals_expr(auth_header, assigned); - } - Expr::FetchPostWithAuth { - url, - auth_header, - body, - } => { - collect_assigned_locals_expr(url, assigned); - collect_assigned_locals_expr(auth_header, assigned); - collect_assigned_locals_expr(body, assigned); + Expr::Closure { .. } => { + // Don't recurse into nested closures - assignments there are local + // to that closure. + return; } - // Catch-all for any other terminal expressions _ => {} } + walk_expr_children(expr, &mut |child| collect_assigned_locals_expr(child, assigned)); } /// Rewrite all `Expr::This` references inside a block of statements to diff --git a/crates/perry-hir/src/lower/lower_expr.rs b/crates/perry-hir/src/lower/lower_expr.rs index 89da302994..5b971f5f14 100644 --- a/crates/perry-hir/src/lower/lower_expr.rs +++ b/crates/perry-hir/src/lower/lower_expr.rs @@ -40,38 +40,6 @@ use crate::lower_types::extract_ts_type_with_ctx; /// crash, and they now get a clean "nested too deeply" diagnostic instead. pub(crate) const MAX_EXPR_LOWER_DEPTH: u32 = 2000; -fn class_computed_member_registration_expr(class_name: &str, member: &ClassComputedMember) -> Expr { - match member.kind { - ClassComputedMemberKind::Method => Expr::RegisterClassComputedMethod { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - method_name: member.function.name.clone(), - is_static: member.is_static, - param_count: member.function.params.len() as u32, - has_rest: member - .function - .params - .last() - .map(|p| p.is_rest) - .unwrap_or(false), - }, - ClassComputedMemberKind::Getter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: Some(member.function.name.clone()), - setter_name: None, - is_static: member.is_static, - }, - ClassComputedMemberKind::Setter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: None, - setter_name: Some(member.function.name.clone()), - is_static: member.is_static, - }, - } -} - pub(crate) fn throw_reference_error_expr(helper_name: &str) -> Expr { Expr::Call { callee: Box::new(Expr::ExternFuncRef { diff --git a/crates/perry-hir/src/lower/module_decl.rs b/crates/perry-hir/src/lower/module_decl.rs index 186e829cc9..445579f980 100644 --- a/crates/perry-hir/src/lower/module_decl.rs +++ b/crates/perry-hir/src/lower/module_decl.rs @@ -14,38 +14,6 @@ use swc_ecma_ast as ast; use super::*; use crate::ir::*; -fn class_computed_member_registration_expr(class_name: &str, member: &ClassComputedMember) -> Expr { - match member.kind { - ClassComputedMemberKind::Method => Expr::RegisterClassComputedMethod { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - method_name: member.function.name.clone(), - is_static: member.is_static, - param_count: member.function.params.len() as u32, - has_rest: member - .function - .params - .last() - .map(|p| p.is_rest) - .unwrap_or(false), - }, - ClassComputedMemberKind::Getter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: Some(member.function.name.clone()), - setter_name: None, - is_static: member.is_static, - }, - ClassComputedMemberKind::Setter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: None, - setter_name: Some(member.function.name.clone()), - is_static: member.is_static, - }, - } -} - fn is_cjs_style_native_default_import(module_name: &str) -> bool { matches!( module_name, diff --git a/crates/perry-hir/src/lower/stmt.rs b/crates/perry-hir/src/lower/stmt.rs index 49ff32bf6a..8b207346b7 100644 --- a/crates/perry-hir/src/lower/stmt.rs +++ b/crates/perry-hir/src/lower/stmt.rs @@ -14,38 +14,6 @@ use swc_ecma_ast as ast; use super::*; use crate::ir::*; -fn class_computed_member_registration_expr(class_name: &str, member: &ClassComputedMember) -> Expr { - match member.kind { - ClassComputedMemberKind::Method => Expr::RegisterClassComputedMethod { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - method_name: member.function.name.clone(), - is_static: member.is_static, - param_count: member.function.params.len() as u32, - has_rest: member - .function - .params - .last() - .map(|p| p.is_rest) - .unwrap_or(false), - }, - ClassComputedMemberKind::Getter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: Some(member.function.name.clone()), - setter_name: None, - is_static: member.is_static, - }, - ClassComputedMemberKind::Setter => Expr::RegisterClassComputedAccessor { - class_name: class_name.to_string(), - key_expr: Box::new(member.key_expr.clone()), - getter_name: None, - setter_name: Some(member.function.name.clone()), - is_static: member.is_static, - }, - } -} - fn emit_class_expression_value_binding( ctx: &mut LoweringContext, module: &mut Module, diff --git a/crates/perry-hir/src/lower_decl/mod.rs b/crates/perry-hir/src/lower_decl/mod.rs index 37fd01d8fe..6de57d6589 100644 --- a/crates/perry-hir/src/lower_decl/mod.rs +++ b/crates/perry-hir/src/lower_decl/mod.rs @@ -35,6 +35,7 @@ pub(crate) use block::{ }; pub(crate) use body_stmt::{find_native_return_in_stmts, lower_body_stmt}; pub(crate) use class_captures::synthesize_class_captures; +pub(crate) use class_computed::class_computed_member_registration_expr; pub(crate) use class_decl::{lower_class_decl, lower_class_from_ast}; pub(crate) use class_members::{ lower_class_method, lower_class_method_with_name, lower_class_prop, lower_constructor, diff --git a/crates/perry-transform/src/async_to_generator.rs b/crates/perry-transform/src/async_to_generator.rs index f9ae366afb..d20a573a61 100644 --- a/crates/perry-transform/src/async_to_generator.rs +++ b/crates/perry-transform/src/async_to_generator.rs @@ -74,6 +74,10 @@ use perry_hir::ir::*; use perry_types::{LocalId, Type}; use std::collections::HashSet; +// #5293: the max-LocalId / max-FuncId scans were copy-pasted here; route through +// the canonical exhaustive-walker implementations in `generator::id_scan`. +use crate::generator::{compute_max_func_id, compute_max_local_id}; + /// Run the pre-pass on every async function in the module. pub fn transform_async_to_generator(module: &mut Module) { // Conservative module-level scope: skip the rewrite ENTIRELY if the @@ -146,7 +150,7 @@ pub fn transform_async_to_generator(module: &mut Module) { collect_async_step_closures(module); if !module.async_step_closures.is_empty() { - let mut next_func_id: perry_types::FuncId = compute_max_func_id_module(module) + 1; + let mut next_func_id: perry_types::FuncId = compute_max_func_id(module) + 1; // Walk the HIR, rewriting matched async closures in-place. The // walker descends into nested closures so chains like // `async () => { items.map(async x => await f(x)) }` are @@ -211,147 +215,6 @@ pub fn transform_async_to_generator(module: &mut Module) { } } -fn compute_max_func_id_module(module: &Module) -> perry_types::FuncId { - let mut m: perry_types::FuncId = 0; - for func in &module.functions { - m = m.max(func.id); - } - // Scan all closures' func_ids in the HIR so we don't collide with an - // existing closure id when synthesizing transform-internal func_ids. - let mut max_closure_id: perry_types::FuncId = 0; - for func in &module.functions { - scan_stmts_for_max_closure_id(&func.body, &mut max_closure_id); - } - for stmt in &module.init { - scan_stmt_for_max_closure_id(stmt, &mut max_closure_id); - } - for global in &module.globals { - if let Some(init) = &global.init { - scan_expr_for_max_closure_id(init, &mut max_closure_id); - } - } - for class in &module.classes { - for mb in &class.methods { - m = m.max(mb.id); - scan_stmts_for_max_closure_id(&mb.body, &mut max_closure_id); - } - for mb in &class.static_methods { - m = m.max(mb.id); - scan_stmts_for_max_closure_id(&mb.body, &mut max_closure_id); - } - if let Some(ctor) = &class.constructor { - m = m.max(ctor.id); - scan_stmts_for_max_closure_id(&ctor.body, &mut max_closure_id); - } - for g in &class.getters { - m = m.max(g.1.id); - scan_stmts_for_max_closure_id(&g.1.body, &mut max_closure_id); - } - for s in &class.setters { - m = m.max(s.1.id); - scan_stmts_for_max_closure_id(&s.1.body, &mut max_closure_id); - } - for member in &class.computed_members { - m = m.max(member.function.id); - scan_stmts_for_max_closure_id(&member.function.body, &mut max_closure_id); - scan_expr_for_max_closure_id(&member.key_expr, &mut max_closure_id); - } - // Issue #5143: scan class field initializers — they hold closures - // whose func_ids must not be reused by the async/for-of desugar. - for field in class.fields.iter().chain(class.static_fields.iter()) { - if let Some(init) = &field.init { - scan_expr_for_max_closure_id(init, &mut max_closure_id); - } - if let Some(key_expr) = &field.key_expr { - scan_expr_for_max_closure_id(key_expr, &mut max_closure_id); - } - } - if let Some(extends_expr) = &class.extends_expr { - scan_expr_for_max_closure_id(extends_expr, &mut max_closure_id); - } - } - m.max(max_closure_id) -} - -fn scan_stmts_for_max_closure_id(stmts: &[Stmt], m: &mut perry_types::FuncId) { - for s in stmts { - scan_stmt_for_max_closure_id(s, m); - } -} - -fn scan_stmt_for_max_closure_id(stmt: &Stmt, m: &mut perry_types::FuncId) { - match stmt { - Stmt::Let { init: Some(e), .. } => scan_expr_for_max_closure_id(e, m), - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr_for_max_closure_id(e, m), - Stmt::Return(Some(e)) => scan_expr_for_max_closure_id(e, m), - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr_for_max_closure_id(condition, m); - scan_stmts_for_max_closure_id(then_branch, m); - if let Some(eb) = else_branch { - scan_stmts_for_max_closure_id(eb, m); - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr_for_max_closure_id(condition, m); - scan_stmts_for_max_closure_id(body, m); - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(i) = init { - scan_stmt_for_max_closure_id(i, m); - } - if let Some(c) = condition { - scan_expr_for_max_closure_id(c, m); - } - if let Some(u) = update { - scan_expr_for_max_closure_id(u, m); - } - scan_stmts_for_max_closure_id(body, m); - } - Stmt::Try { - body, - catch, - finally, - } => { - scan_stmts_for_max_closure_id(body, m); - if let Some(c) = catch { - scan_stmts_for_max_closure_id(&c.body, m); - } - if let Some(f) = finally { - scan_stmts_for_max_closure_id(f, m); - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr_for_max_closure_id(discriminant, m); - for case in cases { - scan_stmts_for_max_closure_id(&case.body, m); - } - } - Stmt::Labeled { body, .. } => scan_stmt_for_max_closure_id(body, m), - _ => {} - } -} - -fn scan_expr_for_max_closure_id(expr: &Expr, m: &mut perry_types::FuncId) { - if let Expr::Closure { func_id, body, .. } = expr { - *m = (*m).max(*func_id); - scan_stmts_for_max_closure_id(body, m); - return; - } - perry_hir::walker::walk_expr_children(expr, &mut |e| scan_expr_for_max_closure_id(e, m)); -} - fn rewrite_async_closures_in_stmts( stmts: &mut Vec, work: &std::collections::HashSet, @@ -619,180 +482,6 @@ fn expr_has_capturing_closure(expr: &Expr) -> bool { found } -/// Compute the max LocalId already used in the module so we can allocate -/// fresh ids for hoisted awaits without colliding. Mirrors -/// `generator::compute_max_local_id` but inlined here to avoid a -/// pub-visibility bump on the generator helper. -fn compute_max_local_id(module: &Module) -> LocalId { - let mut max_id: LocalId = 0; - for func in &module.functions { - for param in &func.params { - max_id = max_id.max(param.id); - } - scan_stmts(&func.body, &mut max_id); - } - for stmt in &module.init { - scan_stmt(stmt, &mut max_id); - } - for global in &module.globals { - max_id = max_id.max(global.id); - } - // Also scan class member bodies — they share the LocalId namespace. - // The v0.5.323 issue #212 fix allocates method-local "fresh ids" via - // ctx.fresh_local() for the per-method rebinds of captured outer - // locals (`let X = this.__perry_cap_`). Those ids are NOT in - // module.functions, but they DO live in the same global LocalId - // space my pre-pass allocates fresh ids from. Without this scan, my - // pre-pass's allocations for the async-step driver collide with - // class-method rebind ids — at codegen, the colliding LocalGet for - // the async-step's `__iter` returns the captured-by-class-method - // box pointer instead of the iter object, surfacing as the same - // `[PERRY WARN] js_box_set: null box pointer` chain that the - // forEach-inner-closure-captures-outer-array pattern produces. - for class in &module.classes { - for method in &class.methods { - for param in &method.params { - max_id = max_id.max(param.id); - } - scan_stmts(&method.body, &mut max_id); - } - for static_method in &class.static_methods { - for param in &static_method.params { - max_id = max_id.max(param.id); - } - scan_stmts(&static_method.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - for param in &ctor.params { - max_id = max_id.max(param.id); - } - scan_stmts(&ctor.body, &mut max_id); - } - for getter in &class.getters { - for param in &getter.1.params { - max_id = max_id.max(param.id); - } - scan_stmts(&getter.1.body, &mut max_id); - } - for setter in &class.setters { - for param in &setter.1.params { - max_id = max_id.max(param.id); - } - scan_stmts(&setter.1.body, &mut max_id); - } - } - max_id -} - -fn scan_stmts(stmts: &[Stmt], m: &mut LocalId) { - for s in stmts { - scan_stmt(s, m); - } -} - -fn scan_stmt(stmt: &Stmt, m: &mut LocalId) { - match stmt { - Stmt::Let { id, init, .. } => { - *m = (*m).max(*id); - if let Some(e) = init { - scan_expr(e, m); - } - } - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr(e, m), - Stmt::Return(e) => { - if let Some(e) = e { - scan_expr(e, m); - } - } - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr(condition, m); - scan_stmts(then_branch, m); - if let Some(eb) = else_branch { - scan_stmts(eb, m); - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr(condition, m); - scan_stmts(body, m); - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(i) = init { - scan_stmt(i, m); - } - if let Some(c) = condition { - scan_expr(c, m); - } - if let Some(u) = update { - scan_expr(u, m); - } - scan_stmts(body, m); - } - Stmt::Try { - body, - catch, - finally, - } => { - scan_stmts(body, m); - if let Some(c) = catch { - if let Some((id, _)) = c.param { - *m = (*m).max(id); - } - scan_stmts(&c.body, m); - } - if let Some(f) = finally { - scan_stmts(f, m); - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr(discriminant, m); - for case in cases { - scan_stmts(&case.body, m); - } - } - Stmt::Labeled { body, .. } => scan_stmt(body, m), - _ => {} - } -} - -fn scan_expr(expr: &Expr, m: &mut LocalId) { - if let Expr::LocalGet(id) | Expr::LocalSet(id, _) = expr { - *m = (*m).max(*id); - } - if let Expr::Closure { - params, - captures, - mutable_captures, - body, - .. - } = expr - { - for p in params { - *m = (*m).max(p.id); - } - for c in captures { - *m = (*m).max(*c); - } - for c in mutable_captures { - *m = (*m).max(*c); - } - scan_stmts(body, m); - return; - } - perry_hir::walker::walk_expr_children(expr, &mut |e| scan_expr(e, m)); -} - fn alloc_local(next_id: &mut LocalId) -> LocalId { let id = *next_id; *next_id += 1; diff --git a/crates/perry-transform/src/finally_inline.rs b/crates/perry-transform/src/finally_inline.rs index 65f1377d2e..d4caf023b7 100644 --- a/crates/perry-transform/src/finally_inline.rs +++ b/crates/perry-transform/src/finally_inline.rs @@ -77,6 +77,10 @@ use std::collections::HashMap; use perry_hir::{Expr, Module, Stmt}; use perry_types::{LocalId, Type}; +// #5293: the max-LocalId scan was copy-pasted here; route through the canonical +// (exhaustive-walker-backed) implementation in `generator::id_scan` instead. +use crate::generator::compute_max_local_id; + /// One open try-with-finally on the lowering stack. The cloned `body` /// gets inlined ahead of any abrupt completion (return/break/continue) /// that would escape this try frame. `loop_depth_at_push` is the value @@ -159,157 +163,6 @@ pub fn inline_finally_into_returns(module: &mut Module) { } } -/// Walk every Function / Method / Constructor / Getter / Setter body in -/// the module to find the largest LocalId currently in use, so we can -/// allocate fresh ids without colliding. Mirrors the equivalent helper in -/// `async_to_generator.rs` (kept private there) — duplicated here to avoid -/// a pub-visibility bump on a transform-internal helper. -fn compute_max_local_id(module: &Module) -> LocalId { - let mut max_id: LocalId = 0; - for func in &module.functions { - for p in &func.params { - max_id = max_id.max(p.id); - } - scan_max(&func.body, &mut max_id); - } - for class in &module.classes { - for method in &class.methods { - for p in &method.params { - max_id = max_id.max(p.id); - } - scan_max(&method.body, &mut max_id); - } - for static_method in &class.static_methods { - for p in &static_method.params { - max_id = max_id.max(p.id); - } - scan_max(&static_method.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - for p in &ctor.params { - max_id = max_id.max(p.id); - } - scan_max(&ctor.body, &mut max_id); - } - } - for stmt in &module.init { - scan_max(std::slice::from_ref(stmt), &mut max_id); - } - for global in &module.globals { - max_id = max_id.max(global.id); - } - max_id -} - -fn scan_max(stmts: &[Stmt], max_id: &mut LocalId) { - use perry_hir::walker::walk_expr_children; - fn scan_e(e: &Expr, m: &mut LocalId) { - match e { - Expr::LocalGet(id) => *m = (*m).max(*id), - Expr::LocalSet(id, v) => { - *m = (*m).max(*id); - scan_e(v, m); - } - Expr::Closure { - params, - captures, - mutable_captures, - body, - .. - } => { - for p in params { - *m = (*m).max(p.id); - } - for c in captures { - *m = (*m).max(*c); - } - for c in mutable_captures { - *m = (*m).max(*c); - } - scan_max(body, m); - } - _ => walk_expr_children(e, &mut |c| scan_e(c, m)), - } - } - fn scan_s(s: &Stmt, m: &mut LocalId) { - match s { - Stmt::Let { id, init, .. } => { - *m = (*m).max(*id); - if let Some(e) = init { - scan_e(e, m); - } - } - Stmt::Expr(e) | Stmt::Throw(e) => scan_e(e, m), - Stmt::Return(Some(e)) => scan_e(e, m), - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_e(condition, m); - scan_max(then_branch, m); - if let Some(eb) = else_branch { - scan_max(eb, m); - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_e(condition, m); - scan_max(body, m); - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(i) = init { - scan_s(i, m); - } - if let Some(c) = condition { - scan_e(c, m); - } - if let Some(u) = update { - scan_e(u, m); - } - scan_max(body, m); - } - Stmt::Try { - body, - catch, - finally, - } => { - scan_max(body, m); - if let Some(c) = catch { - if let Some((id, _)) = c.param { - *m = (*m).max(id); - } - scan_max(&c.body, m); - } - if let Some(f) = finally { - scan_max(f, m); - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_e(discriminant, m); - for case in cases { - if let Some(t) = &case.test { - scan_e(t, m); - } - scan_max(&case.body, m); - } - } - Stmt::Labeled { body, .. } => scan_s(body, m), - _ => {} - } - } - for s in stmts { - scan_s(s, max_id); - } -} - /// Process a statement list with the given stack of enclosing finally /// bodies (innermost last). Each abrupt-completion stmt encountered /// (`Stmt::Return`, `Stmt::Break`, `Stmt::Continue`, diff --git a/crates/perry-transform/src/generator/id_scan.rs b/crates/perry-transform/src/generator/id_scan.rs index 526a46b055..b552c8b879 100644 --- a/crates/perry-transform/src/generator/id_scan.rs +++ b/crates/perry-transform/src/generator/id_scan.rs @@ -16,6 +16,13 @@ pub fn compute_max_local_id(module: &Module) -> LocalId { } for global in &module.globals { max_id = max_id.max(global.id); + // #5293: a module-global initializer (`const f = (x) => ...`) holds a + // closure whose params/body live in this LocalId namespace. The + // de-duplicated copies that routed through here did not all scan it, + // so fold it in — a too-high max is always safe, a missed id collides. + if let Some(init) = &global.init { + scan_expr_for_max_local(init, &mut max_id); + } } // Also scan class member bodies — they share the LocalId namespace. // The v0.5.323 issue #212 fix allocates method-local rebind ids per @@ -155,6 +162,12 @@ pub fn scan_stmt_for_max_local(stmt: &Stmt, max_id: &mut LocalId) { } => { scan_expr_for_max_local(discriminant, max_id); for case in cases { + // #5293: `case (() => x)():` / `case foo[i]:` test exprs carry + // their own LocalIds (and nested closures). The merged copies + // scanned these; the canonical must too. + if let Some(test) = &case.test { + scan_expr_for_max_local(test, max_id); + } scan_stmts_for_max_local(&case.body, max_id); } } @@ -238,6 +251,14 @@ pub fn compute_max_func_id(module: &Module) -> FuncId { for stmt in &module.init { scan_stmt_for_max_func(stmt, &mut max_id); } + // #5293: module-global initializers (`const f = () => ...`) hold closures + // whose FuncIds live in this namespace. `async_to_generator`'s merged copy + // scanned global inits; fold it in so the canonical is a true superset. + for global in &module.globals { + if let Some(init) = &global.init { + scan_expr_for_max_func(init, &mut max_id); + } + } // Issue #154: class member bodies share the FuncId namespace — their // nested closures (executors, callbacks, dispose-method bodies) // carry FuncIds that the iterator state-machine transform must not @@ -358,6 +379,11 @@ pub fn scan_stmt_for_max_func(stmt: &Stmt, max_id: &mut FuncId) { } => { scan_expr_for_max_func(discriminant, max_id); for case in cases { + // #5293: a closure in a switch case test (`case (() => 1)():`) + // carries a FuncId the merged copies scanned; mirror them here. + if let Some(test) = &case.test { + scan_expr_for_max_func(test, max_id); + } scan_stmts_for_max_func(&case.body, max_id); } } diff --git a/crates/perry-transform/src/state_desugar.rs b/crates/perry-transform/src/state_desugar.rs index 0b883868d3..6a85810ebc 100644 --- a/crates/perry-transform/src/state_desugar.rs +++ b/crates/perry-transform/src/state_desugar.rs @@ -60,6 +60,10 @@ use perry_hir::{Expr, Module, Param, Stmt}; use perry_types::{FuncId, LocalId, Type}; use std::collections::{HashMap, HashSet}; +// #5293: the max-LocalId / max-FuncId scans were copy-pasted here; route through +// the canonical exhaustive-walker implementations in `generator::id_scan`. +use crate::generator::{compute_max_func_id, compute_max_local_id}; + /// Counters threaded through the rewrite for fresh `LocalId` / `FuncId` /// allocation. The NavStack lowering needs both: each call site spawns a /// closure (one fresh `FuncId`) holding `1 + N` fresh local bindings (host @@ -167,282 +171,6 @@ fn collect_module_string_consts(init: &[Stmt]) -> HashMap { env } -/// Walk the entire module to find the highest `LocalId` already in use. -/// Mirrors `async_to_generator::compute_max_local_id` shape (param scan -/// + stmt scan + class member scan) so allocations don't collide with -/// `ctx.fresh_local()` ids inside class methods or with later transforms -/// that allocate from the same global namespace. -fn compute_max_local_id(module: &Module) -> LocalId { - let mut max_id: LocalId = 0; - let walk_stmts = |stmts: &[Stmt], max_id: &mut LocalId| { - for stmt in stmts { - scan_stmt_local_ids(stmt, max_id); - } - }; - for func in &module.functions { - for p in &func.params { - max_id = max_id.max(p.id); - } - walk_stmts(&func.body, &mut max_id); - } - walk_stmts(&module.init, &mut max_id); - for global in &module.globals { - max_id = max_id.max(global.id); - } - for class in &module.classes { - for method in &class.methods { - for p in &method.params { - max_id = max_id.max(p.id); - } - walk_stmts(&method.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - for p in &ctor.params { - max_id = max_id.max(p.id); - } - walk_stmts(&ctor.body, &mut max_id); - } - } - max_id -} - -fn compute_max_func_id(module: &Module) -> FuncId { - let mut max_id: FuncId = 0; - for func in &module.functions { - max_id = max_id.max(func.id); - } - let walk_stmts = |stmts: &[Stmt], max_id: &mut FuncId| { - for stmt in stmts { - scan_stmt_func_ids(stmt, max_id); - } - }; - walk_stmts(&module.init, &mut max_id); - for func in &module.functions { - walk_stmts(&func.body, &mut max_id); - } - for class in &module.classes { - for method in &class.methods { - max_id = max_id.max(method.id); - walk_stmts(&method.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - max_id = max_id.max(ctor.id); - walk_stmts(&ctor.body, &mut max_id); - } - } - max_id -} - -fn scan_stmt_local_ids(stmt: &Stmt, max_id: &mut LocalId) { - match stmt { - Stmt::Let { id, init, .. } => { - *max_id = (*max_id).max(*id); - if let Some(e) = init { - scan_expr_local_ids(e, max_id); - } - } - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr_local_ids(e, max_id), - Stmt::Return(Some(e)) => scan_expr_local_ids(e, max_id), - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr_local_ids(condition, max_id); - for s in then_branch { - scan_stmt_local_ids(s, max_id); - } - if let Some(eb) = else_branch { - for s in eb { - scan_stmt_local_ids(s, max_id); - } - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr_local_ids(condition, max_id); - for s in body { - scan_stmt_local_ids(s, max_id); - } - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(i) = init { - scan_stmt_local_ids(i, max_id); - } - if let Some(c) = condition { - scan_expr_local_ids(c, max_id); - } - if let Some(u) = update { - scan_expr_local_ids(u, max_id); - } - for s in body { - scan_stmt_local_ids(s, max_id); - } - } - Stmt::Try { - body, - catch, - finally, - .. - } => { - for s in body { - scan_stmt_local_ids(s, max_id); - } - if let Some(c) = catch { - if let Some((id, _)) = c.param { - *max_id = (*max_id).max(id); - } - for s in &c.body { - scan_stmt_local_ids(s, max_id); - } - } - if let Some(f) = finally { - for s in f { - scan_stmt_local_ids(s, max_id); - } - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr_local_ids(discriminant, max_id); - for case in cases { - if let Some(t) = &case.test { - scan_expr_local_ids(t, max_id); - } - for s in &case.body { - scan_stmt_local_ids(s, max_id); - } - } - } - Stmt::Labeled { body, .. } => scan_stmt_local_ids(body, max_id), - _ => {} - } -} - -fn scan_expr_local_ids(e: &Expr, max_id: &mut LocalId) { - match e { - Expr::LocalGet(id) | Expr::LocalSet(id, _) => { - *max_id = (*max_id).max(*id); - } - _ => {} - } - use perry_hir::walker::walk_expr_children; - walk_expr_children(e, &mut |child| scan_expr_local_ids(child, max_id)); - if let Expr::Closure { params, body, .. } = e { - for p in params { - *max_id = (*max_id).max(p.id); - } - for s in body { - scan_stmt_local_ids(s, max_id); - } - } -} - -fn scan_stmt_func_ids(stmt: &Stmt, max_id: &mut FuncId) { - match stmt { - Stmt::Let { init: Some(e), .. } => scan_expr_func_ids(e, max_id), - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr_func_ids(e, max_id), - Stmt::Return(Some(e)) => scan_expr_func_ids(e, max_id), - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr_func_ids(condition, max_id); - for s in then_branch { - scan_stmt_func_ids(s, max_id); - } - if let Some(eb) = else_branch { - for s in eb { - scan_stmt_func_ids(s, max_id); - } - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr_func_ids(condition, max_id); - for s in body { - scan_stmt_func_ids(s, max_id); - } - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(i) = init { - scan_stmt_func_ids(i, max_id); - } - if let Some(c) = condition { - scan_expr_func_ids(c, max_id); - } - if let Some(u) = update { - scan_expr_func_ids(u, max_id); - } - for s in body { - scan_stmt_func_ids(s, max_id); - } - } - Stmt::Try { - body, - catch, - finally, - .. - } => { - for s in body { - scan_stmt_func_ids(s, max_id); - } - if let Some(c) = catch { - for s in &c.body { - scan_stmt_func_ids(s, max_id); - } - } - if let Some(f) = finally { - for s in f { - scan_stmt_func_ids(s, max_id); - } - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr_func_ids(discriminant, max_id); - for case in cases { - if let Some(t) = &case.test { - scan_expr_func_ids(t, max_id); - } - for s in &case.body { - scan_stmt_func_ids(s, max_id); - } - } - } - Stmt::Labeled { body, .. } => scan_stmt_func_ids(body, max_id), - _ => {} - } -} - -fn scan_expr_func_ids(e: &Expr, max_id: &mut FuncId) { - match e { - Expr::FuncRef(id) => *max_id = (*max_id).max(*id), - Expr::Closure { func_id, body, .. } => { - *max_id = (*max_id).max(*func_id); - for s in body { - scan_stmt_func_ids(s, max_id); - } - } - _ => {} - } - use perry_hir::walker::walk_expr_children; - walk_expr_children(e, &mut |child| scan_expr_func_ids(child, max_id)); -} - /// Walk the entire module and return the set of `LocalId`s passed as the /// first positional argument to a handle-based state API on `perry/ui`. /// These bindings can't be safely keyed-rewritten because the runtime FFI diff --git a/crates/perry-transform/src/unroll/mod.rs b/crates/perry-transform/src/unroll/mod.rs index 883a6b329d..bc58e498a4 100644 --- a/crates/perry-transform/src/unroll/mod.rs +++ b/crates/perry-transform/src/unroll/mod.rs @@ -76,6 +76,10 @@ use std::collections::HashMap; mod escape_analysis; use escape_analysis::compute_loop_escaping_ids; +// #5293: the max-LocalId / max-FuncId scans were copy-pasted here; route through +// the canonical exhaustive-walker implementations in `generator::id_scan`. +use crate::generator::{compute_max_func_id, compute_max_local_id}; + /// Maximum trip count we'll fully unroll. 8 covers the canonical /// image-kernel shapes (3×3, 5×5, 7×7) without blowing up code size. const MAX_TRIP_COUNT: i64 = 8; @@ -659,345 +663,6 @@ fn substitute_localget_with_int(expr: &mut Expr, iv_id: LocalId, value: i64) { }); } -/// Walk a top-level scan of every `LocalId` reachable from `module` and -/// return the highest. Mirrors the helper in `generator.rs` / -/// `async_to_generator.rs`; duplicated here to avoid leaking a public -/// dependency between transform passes (the convention in this crate is -/// each pass inlines its own scan). Used to seed `next_local_id` so the -/// fresh ids handed out by `refresh_local_ids` can never collide with an -/// id already in use elsewhere in the module — every later transform -/// (async_to_generator, transform_generators, codegen) assumes globally -/// unique LocalIds. -fn compute_max_local_id(module: &Module) -> LocalId { - let mut max_id: LocalId = 0; - for func in &module.functions { - for p in &func.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&func.body, &mut max_id); - } - scan_stmts_for_max_local(&module.init, &mut max_id); - for global in &module.globals { - if global.id > max_id { - max_id = global.id; - } - } - for class in &module.classes { - for method in &class.methods { - for p in &method.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&method.body, &mut max_id); - } - for sm in &class.static_methods { - for p in &sm.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&sm.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - for p in &ctor.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&ctor.body, &mut max_id); - } - for (_n, g) in &class.getters { - for p in &g.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&g.body, &mut max_id); - } - for (_n, s) in &class.setters { - for p in &s.params { - if p.id > max_id { - max_id = p.id; - } - } - scan_stmts_for_max_local(&s.body, &mut max_id); - } - } - max_id -} - -fn scan_stmts_for_max_local(stmts: &[Stmt], max_id: &mut LocalId) { - for s in stmts { - scan_stmt_for_max_local(s, max_id); - } -} - -fn scan_stmt_for_max_local(stmt: &Stmt, max_id: &mut LocalId) { - match stmt { - Stmt::Let { id, init, .. } => { - if *id > *max_id { - *max_id = *id; - } - if let Some(e) = init { - scan_expr_for_max_local(e, max_id); - } - } - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr_for_max_local(e, max_id), - Stmt::Return(opt) => { - if let Some(e) = opt { - scan_expr_for_max_local(e, max_id); - } - } - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr_for_max_local(condition, max_id); - scan_stmts_for_max_local(then_branch, max_id); - if let Some(eb) = else_branch { - scan_stmts_for_max_local(eb, max_id); - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr_for_max_local(condition, max_id); - scan_stmts_for_max_local(body, max_id); - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(s) = init { - scan_stmt_for_max_local(s, max_id); - } - if let Some(c) = condition { - scan_expr_for_max_local(c, max_id); - } - if let Some(u) = update { - scan_expr_for_max_local(u, max_id); - } - scan_stmts_for_max_local(body, max_id); - } - Stmt::Try { - body, - catch, - finally, - } => { - scan_stmts_for_max_local(body, max_id); - if let Some(c) = catch { - if let Some((id, _)) = &c.param { - if *id > *max_id { - *max_id = *id; - } - } - scan_stmts_for_max_local(&c.body, max_id); - } - if let Some(f) = finally { - scan_stmts_for_max_local(f, max_id); - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr_for_max_local(discriminant, max_id); - for c in cases { - if let Some(t) = &c.test { - scan_expr_for_max_local(t, max_id); - } - scan_stmts_for_max_local(&c.body, max_id); - } - } - Stmt::Labeled { body, .. } => scan_stmt_for_max_local(body, max_id), - Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(ids) => { - for id in ids { - if *id > *max_id { - *max_id = *id; - } - } - } - } -} - -fn scan_expr_for_max_local(expr: &Expr, max_id: &mut LocalId) { - fn bump(max_id: &mut LocalId, id: LocalId) { - if id > *max_id { - *max_id = id; - } - } - match expr { - Expr::LocalGet(id) | Expr::Update { id, .. } => bump(max_id, *id), - Expr::LocalSet(id, _) => bump(max_id, *id), - Expr::ArrayPush { array_id, .. } - | Expr::ArrayPushSpread { array_id, .. } - | Expr::ArrayUnshift { array_id, .. } - | Expr::ArraySplice { array_id, .. } - | Expr::ArrayCopyWithin { array_id, .. } => bump(max_id, *array_id), - Expr::ArrayPop(id) | Expr::ArrayShift(id) => bump(max_id, *id), - Expr::SetAdd { set_id, .. } => bump(max_id, *set_id), - Expr::Closure { - params, - body, - captures, - mutable_captures, - .. - } => { - for p in params { - bump(max_id, p.id); - } - for c in captures { - bump(max_id, *c); - } - for c in mutable_captures { - bump(max_id, *c); - } - scan_stmts_for_max_local(body, max_id); - } - _ => {} - } - walk_expr_children(expr, &mut |child| scan_expr_for_max_local(child, max_id)); -} - -/// Mirrors `compute_max_local_id` but for `FuncId`. Used to seed -/// `next_func_id` so the fresh ids handed out to cloned `Expr::Closure`s -/// in `refresh_in_expr` can never collide with a FuncId already in use -/// elsewhere (top-level `Function::id`, other closures, generator state- -/// machine helpers, etc.). Codegen keys compiled functions by FuncId, so -/// any collision would collapse two different bodies into one. -fn compute_max_func_id(module: &Module) -> FuncId { - let mut max_id: FuncId = 0; - for func in &module.functions { - if func.id > max_id { - max_id = func.id; - } - scan_stmts_for_max_func(&func.body, &mut max_id); - } - scan_stmts_for_max_func(&module.init, &mut max_id); - for class in &module.classes { - for m in &class.methods { - scan_stmts_for_max_func(&m.body, &mut max_id); - } - for sm in &class.static_methods { - scan_stmts_for_max_func(&sm.body, &mut max_id); - } - if let Some(ctor) = &class.constructor { - scan_stmts_for_max_func(&ctor.body, &mut max_id); - } - for (_n, g) in &class.getters { - scan_stmts_for_max_func(&g.body, &mut max_id); - } - for (_n, s) in &class.setters { - scan_stmts_for_max_func(&s.body, &mut max_id); - } - } - max_id -} - -fn scan_stmts_for_max_func(stmts: &[Stmt], max_id: &mut FuncId) { - for s in stmts { - scan_stmt_for_max_func(s, max_id); - } -} - -fn scan_stmt_for_max_func(stmt: &Stmt, max_id: &mut FuncId) { - match stmt { - Stmt::Let { init, .. } => { - if let Some(e) = init { - scan_expr_for_max_func(e, max_id); - } - } - Stmt::Expr(e) | Stmt::Throw(e) => scan_expr_for_max_func(e, max_id), - Stmt::Return(opt) => { - if let Some(e) = opt { - scan_expr_for_max_func(e, max_id); - } - } - Stmt::If { - condition, - then_branch, - else_branch, - } => { - scan_expr_for_max_func(condition, max_id); - scan_stmts_for_max_func(then_branch, max_id); - if let Some(eb) = else_branch { - scan_stmts_for_max_func(eb, max_id); - } - } - Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { - scan_expr_for_max_func(condition, max_id); - scan_stmts_for_max_func(body, max_id); - } - Stmt::For { - init, - condition, - update, - body, - } => { - if let Some(s) = init { - scan_stmt_for_max_func(s, max_id); - } - if let Some(c) = condition { - scan_expr_for_max_func(c, max_id); - } - if let Some(u) = update { - scan_expr_for_max_func(u, max_id); - } - scan_stmts_for_max_func(body, max_id); - } - Stmt::Try { - body, - catch, - finally, - } => { - scan_stmts_for_max_func(body, max_id); - if let Some(c) = catch { - scan_stmts_for_max_func(&c.body, max_id); - } - if let Some(f) = finally { - scan_stmts_for_max_func(f, max_id); - } - } - Stmt::Switch { - discriminant, - cases, - } => { - scan_expr_for_max_func(discriminant, max_id); - for c in cases { - if let Some(t) = &c.test { - scan_expr_for_max_func(t, max_id); - } - scan_stmts_for_max_func(&c.body, max_id); - } - } - Stmt::Labeled { body, .. } => scan_stmt_for_max_func(body, max_id), - Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} - Stmt::PreallocateBoxes(_) => {} - } -} - -fn scan_expr_for_max_func(expr: &Expr, max_id: &mut FuncId) { - fn bump(max_id: &mut FuncId, id: FuncId) { - if id > *max_id { - *max_id = id; - } - } - match expr { - Expr::FuncRef(id) => bump(max_id, *id), - Expr::Closure { func_id, body, .. } => { - bump(max_id, *func_id); - scan_stmts_for_max_func(body, max_id); - } - _ => {} - } - walk_expr_children(expr, &mut |child| scan_expr_for_max_func(child, max_id)); -} - /// Per-iteration LocalId remap pass for `try_unroll_for`. /// /// Walks `stmts` and assigns a fresh `LocalId` (drawn from `next_id`) to From 1c7ba04320a0807bdbcd74459ad626f57f00c742 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 07:34:19 +0000 Subject: [PATCH 2/2] style(hir): rustfmt collect_assigned_locals_expr walk closure cargo fmt wants the walk_expr_children closure body wrapped in a block since the single-line form exceeds the width limit. Fixes the lint CI failure on this branch; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0161bea2ibAsgFkKE2w7F3Ei --- crates/perry-hir/src/analysis.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 4b196dd52a..ec29133ae5 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -407,7 +407,9 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec {} } - walk_expr_children(expr, &mut |child| collect_assigned_locals_expr(child, assigned)); + walk_expr_children(expr, &mut |child| { + collect_assigned_locals_expr(child, assigned) + }); } /// Rewrite all `Expr::This` references inside a block of statements to