Skip to content

Commit 82f0e96

Browse files
proggeramlugRalph KüpperRalph Küpper
authored
fix(lint): unbreak the 2000-line file-size gate on main (#7830)
* fix(lint): unbreak the 2000-line file-size gate on main Two files crossed the cap in the 2026-08-11 batch and `lint` has been red on `main` ever since: crates/perry-hir/src/lower/pre_scan.rs 1985 -> 2011 (#7828) crates/perry-runtime/src/gc/layout.rs 1982 -> 2023 (#7809, #7812) Both are pure code moves, no logic change: * `pre_scan_weakref_locals` and its doc comment move to `lower/pre_scan/weakref_locals.rs` (1653 lines left behind). It is the cohesive unit — one top-level pre-scan with its own five local sets — and it is where #7828 added the lines. * `LayoutSlotMask` (the enum and its whole `impl`) moves to `gc/layout/slot_mask.rs` (1807 lines left behind). Its visibility widens from `pub(super)` to `pub(in crate::gc)` because the type is now one module deeper and `layout_tables.rs` / `hot_tls.rs` still name it; the reachable set is unchanged. `scripts/check_file_size.sh` passes. * docs: changelog fragment for #7830 * docs: condense #7830 changelog entry --------- Co-authored-by: Ralph Küpper <ralph3@skelpo.com> Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 1ee158d commit 82f0e96

5 files changed

Lines changed: 610 additions & 581 deletions

File tree

changelog.d/7830-file-size-gate.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**The 2,000-line file-size lint gate passes again.** The GC layout slot-mask
2+
implementation and weak-reference local pre-scan now live in dedicated modules,
3+
without behavior changes.

crates/perry-hir/src/lower/pre_scan.rs

Lines changed: 3 additions & 362 deletions
Original file line numberDiff line numberDiff line change
@@ -10,368 +10,9 @@ use swc_ecma_ast as ast;
1010
use super::*;
1111
use crate::ir::*;
1212

13-
/// `let/const x = new FinalizationRegistry(...)` bindings into the lowering
14-
/// context. This is used by `obj.method()` lowering to recognise these instances
15-
/// without requiring type inference (Perry's existing var-decl type inference
16-
/// doesn't extend to WeakRef/FinalizationRegistry).
17-
pub(crate) fn pre_scan_weakref_locals(ast_module: &ast::Module, ctx: &mut LoweringContext) {
18-
fn classify_new(new_expr: &ast::NewExpr, shadowed: &HashSet<String>) -> Option<&'static str> {
19-
if let ast::Expr::Ident(ident) = new_expr.callee.as_ref() {
20-
let name = ident.sym.as_ref();
21-
// #6233: a user declaration of this name (`class Proxy {}`,
22-
// `function WeakRef() {}`, an import, …) shadows the global, so
23-
// `new <name>()` constructs the USER binding — don't track the
24-
// result as a weak/proxy intrinsic instance. Name-keyed and
25-
// scope-blind, matching this scan's own granularity.
26-
if shadowed.contains(name) {
27-
return None;
28-
}
29-
match name {
30-
"WeakRef" => Some("WeakRef"),
31-
"FinalizationRegistry" => Some("FinalizationRegistry"),
32-
"WeakMap" => Some("WeakMap"),
33-
"WeakSet" => Some("WeakSet"),
34-
"Proxy" => Some("Proxy"),
35-
_ => None,
36-
}
37-
} else {
38-
None
39-
}
40-
}
41-
fn unwrap_init(mut e: &ast::Expr) -> &ast::Expr {
42-
loop {
43-
match e {
44-
ast::Expr::TsAs(ts_as) => e = &ts_as.expr,
45-
ast::Expr::TsTypeAssertion(ta) => e = &ta.expr,
46-
ast::Expr::TsNonNull(nn) => e = &nn.expr,
47-
ast::Expr::TsConstAssertion(ca) => e = &ca.expr,
48-
ast::Expr::Paren(p) => e = &p.expr,
49-
_ => break,
50-
}
51-
}
52-
e
53-
}
54-
// Names bound (anywhere in the module) to a `new <X>` whose `<X>` is NOT a
55-
// tracked weak type — i.e. an ordinary class/constructor. The weak-locals
56-
// sets are keyed by BARE NAME with no scope discrimination, so a name reused
57-
// across functions (extremely common in minified bundles, e.g. a one-letter
58-
// `z`) for both `new WeakMap()` in one function and `new SomeCache()` in
59-
// another would route the second function's `z.get/set` to the WeakMap
60-
// intrinsic — throwing `Invalid value used as weak map key` when the
61-
// non-weak cache is legitimately keyed by a string. Collect such ambiguous
62-
// names and subtract them from all weak-locals sets after the walk so the
63-
// call falls back to ordinary dynamic method dispatch (correct for both a
64-
// real WeakMap and the other constructor).
65-
fn record_var(
66-
decl: &ast::VarDeclarator,
67-
ctx: &mut LoweringContext,
68-
poison: &mut HashSet<String>,
69-
shadowed: &HashSet<String>,
70-
) {
71-
if let (ast::Pat::Ident(ident), Some(init)) = (&decl.name, decl.init.as_ref()) {
72-
let init_unwrapped = unwrap_init(init);
73-
if let ast::Expr::New(new_expr) = init_unwrapped {
74-
let name = ident.id.sym.to_string();
75-
match classify_new(new_expr, shadowed) {
76-
Some("WeakRef") => {
77-
ctx.weakref_locals.insert(name);
78-
}
79-
Some("FinalizationRegistry") => {
80-
ctx.finreg_locals.insert(name);
81-
}
82-
Some("WeakMap") => {
83-
ctx.weakmap_locals.insert(name);
84-
}
85-
Some("WeakSet") => {
86-
ctx.weakset_locals.insert(name);
87-
}
88-
Some("Proxy") => {
89-
ctx.proxy_locals.insert(name);
90-
}
91-
// `new <OtherClass>()` — this name is also used for a
92-
// non-weak instance somewhere; mark it ambiguous.
93-
None => {
94-
poison.insert(name);
95-
}
96-
_ => {}
97-
}
98-
} else if matches!(init_unwrapped, ast::Expr::Call(_) | ast::Expr::Await(_)) {
99-
// #7775: a name bound to a CALL result is just as ambiguous as
100-
// one bound to `new <OtherClass>()`, and it was not poisoned at
101-
// all. `const a = build(10)` in one function and
102-
// `const a = new Proxy(raw, {})` in another made the FIRST
103-
// function's `a.length` lower to `js_proxy_get` on a plain
104-
// array — `undefined`, so the read loop after it ran zero
105-
// iterations. The proxy function never even had to be called.
106-
//
107-
// Deliberately narrow: only call/await initializers, which are
108-
// the opaque ones. A literal, a member read or an identifier
109-
// copy stays unpoisoned, so the common `const p = new Proxy(…)`
110-
// in a module that also does `const p = { … }` elsewhere is
111-
// untouched by this arm.
112-
poison.insert(ident.id.sym.to_string());
113-
} else if let ast::Expr::Member(member) = init_unwrapped {
114-
// #1750: `const w = path.win32` / `const p = path.posix`.
115-
// Record the alias so `w.normalize(...)` later dispatches like
116-
// `path.win32.normalize(...)`. The root ident is stored
117-
// unresolved; the `path` check is deferred to call lowering.
118-
if let (ast::Expr::Ident(root), ast::MemberProp::Ident(sub_prop)) =
119-
(member.obj.as_ref(), &member.prop)
120-
{
121-
let sub = sub_prop.sym.as_ref();
122-
if sub == "win32" || sub == "posix" {
123-
ctx.register_subns_path_alias(
124-
ident.id.sym.to_string(),
125-
root.sym.to_string(),
126-
sub.to_string(),
127-
);
128-
}
129-
}
130-
// #3144: `const m = [].map` / `const s = "".slice` /
131-
// `const f = Array.prototype.filter` — track the local so a
132-
// later `m.call(arr, ...)` / `m.apply(arr, [...])` rewrites to a
133-
// direct call. Uses the same receiver rule as the existing
134-
// `.call`/`.apply` builtin-prototype rewrite.
135-
if let Some(method) =
136-
crate::lower::expr_call::intrinsics::as_builtin_proto_method_ref(
137-
ctx,
138-
init_unwrapped,
139-
)
140-
{
141-
ctx.builtin_proto_method_locals
142-
.insert(ident.id.sym.to_string(), method);
143-
}
144-
}
145-
}
146-
}
147-
fn walk_stmt(
148-
stmt: &ast::Stmt,
149-
ctx: &mut LoweringContext,
150-
poison: &mut HashSet<String>,
151-
shadowed: &HashSet<String>,
152-
) {
153-
match stmt {
154-
ast::Stmt::Decl(ast::Decl::Var(var_decl)) => {
155-
for decl in &var_decl.decls {
156-
record_var(decl, ctx, poison, shadowed);
157-
}
158-
}
159-
ast::Stmt::Decl(ast::Decl::Using(using_decl)) => {
160-
for decl in &using_decl.decls {
161-
record_var(decl, ctx, poison, shadowed);
162-
}
163-
}
164-
// Function declarations — descend into the body so `const
165-
// ref = new WeakRef(x)` inside a function is still tracked
166-
// and `ref.deref()` lowers to `Expr::WeakRefDeref` instead
167-
// of falling through to the generic method dispatch.
168-
ast::Stmt::Decl(ast::Decl::Fn(fn_decl)) => {
169-
if let Some(body) = &fn_decl.function.body {
170-
for s in &body.stmts {
171-
walk_stmt(s, ctx, poison, shadowed);
172-
}
173-
}
174-
}
175-
ast::Stmt::Block(block) => {
176-
for s in &block.stmts {
177-
walk_stmt(s, ctx, poison, shadowed);
178-
}
179-
}
180-
ast::Stmt::If(if_stmt) => {
181-
walk_stmt(&if_stmt.cons, ctx, poison, shadowed);
182-
if let Some(alt) = &if_stmt.alt {
183-
walk_stmt(alt, ctx, poison, shadowed);
184-
}
185-
}
186-
ast::Stmt::While(w) => walk_stmt(&w.body, ctx, poison, shadowed),
187-
ast::Stmt::DoWhile(w) => walk_stmt(&w.body, ctx, poison, shadowed),
188-
ast::Stmt::For(f) => {
189-
if let Some(ast::VarDeclOrExpr::VarDecl(vd)) = &f.init {
190-
for decl in &vd.decls {
191-
record_var(decl, ctx, poison, shadowed);
192-
}
193-
}
194-
walk_stmt(&f.body, ctx, poison, shadowed);
195-
}
196-
ast::Stmt::ForIn(f) => walk_stmt(&f.body, ctx, poison, shadowed),
197-
ast::Stmt::ForOf(f) => walk_stmt(&f.body, ctx, poison, shadowed),
198-
ast::Stmt::Try(t) => {
199-
for s in &t.block.stmts {
200-
walk_stmt(s, ctx, poison, shadowed);
201-
}
202-
if let Some(catch) = &t.handler {
203-
for s in &catch.body.stmts {
204-
walk_stmt(s, ctx, poison, shadowed);
205-
}
206-
}
207-
if let Some(finalizer) = &t.finalizer {
208-
for s in &finalizer.stmts {
209-
walk_stmt(s, ctx, poison, shadowed);
210-
}
211-
}
212-
}
213-
ast::Stmt::Switch(s) => {
214-
for case in &s.cases {
215-
for s in &case.cons {
216-
walk_stmt(s, ctx, poison, shadowed);
217-
}
218-
}
219-
}
220-
_ => {}
221-
}
222-
}
223-
// #6233: collect user declarations that shadow one of the tracked
224-
// constructor names — `class Proxy {}` / `function WeakRef() {}` /
225-
// `const WeakMap = …` / `import { WeakSet } from …`. This scan is
226-
// name-keyed with no scope discrimination (see the poison-set comment
227-
// below), so a shadow anywhere in the module suppresses tracking
228-
// module-wide and the affected locals fall back to ordinary dispatch.
229-
fn record_shadow(name: &str, shadowed: &mut HashSet<String>) {
230-
if matches!(
231-
name,
232-
"WeakRef" | "FinalizationRegistry" | "WeakMap" | "WeakSet" | "Proxy"
233-
) {
234-
shadowed.insert(name.to_string());
235-
}
236-
}
237-
fn collect_shadowing_decls(stmt: &ast::Stmt, shadowed: &mut HashSet<String>) {
238-
match stmt {
239-
ast::Stmt::Decl(ast::Decl::Class(cd)) => record_shadow(cd.ident.sym.as_ref(), shadowed),
240-
ast::Stmt::Decl(ast::Decl::Fn(fd)) => {
241-
record_shadow(fd.ident.sym.as_ref(), shadowed);
242-
if let Some(body) = &fd.function.body {
243-
for s in &body.stmts {
244-
collect_shadowing_decls(s, shadowed);
245-
}
246-
}
247-
}
248-
ast::Stmt::Decl(ast::Decl::Var(vd)) => {
249-
for d in &vd.decls {
250-
if let ast::Pat::Ident(ident) = &d.name {
251-
// `const Proxy = …` — the declared NAME shadows; a
252-
// `new Proxy()` bound to an ordinary local is handled
253-
// by classify_new/poison, not here.
254-
record_shadow(ident.id.sym.as_ref(), shadowed);
255-
}
256-
}
257-
}
258-
ast::Stmt::Block(block) => {
259-
for s in &block.stmts {
260-
collect_shadowing_decls(s, shadowed);
261-
}
262-
}
263-
ast::Stmt::If(if_stmt) => {
264-
collect_shadowing_decls(&if_stmt.cons, shadowed);
265-
if let Some(alt) = &if_stmt.alt {
266-
collect_shadowing_decls(alt, shadowed);
267-
}
268-
}
269-
ast::Stmt::While(w) => collect_shadowing_decls(&w.body, shadowed),
270-
ast::Stmt::DoWhile(w) => collect_shadowing_decls(&w.body, shadowed),
271-
ast::Stmt::For(f) => collect_shadowing_decls(&f.body, shadowed),
272-
ast::Stmt::ForIn(f) => collect_shadowing_decls(&f.body, shadowed),
273-
ast::Stmt::ForOf(f) => collect_shadowing_decls(&f.body, shadowed),
274-
ast::Stmt::Try(t) => {
275-
for s in &t.block.stmts {
276-
collect_shadowing_decls(s, shadowed);
277-
}
278-
if let Some(catch) = &t.handler {
279-
for s in &catch.body.stmts {
280-
collect_shadowing_decls(s, shadowed);
281-
}
282-
}
283-
if let Some(finalizer) = &t.finalizer {
284-
for s in &finalizer.stmts {
285-
collect_shadowing_decls(s, shadowed);
286-
}
287-
}
288-
}
289-
ast::Stmt::Switch(s) => {
290-
for case in &s.cases {
291-
for s in &case.cons {
292-
collect_shadowing_decls(s, shadowed);
293-
}
294-
}
295-
}
296-
_ => {}
297-
}
298-
}
299-
let mut shadowed: HashSet<String> = HashSet::new();
300-
for item in &ast_module.body {
301-
match item {
302-
ast::ModuleItem::Stmt(stmt) => collect_shadowing_decls(stmt, &mut shadowed),
303-
ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => {
304-
match &export_decl.decl {
305-
ast::Decl::Class(cd) => record_shadow(cd.ident.sym.as_ref(), &mut shadowed),
306-
ast::Decl::Fn(fd) => record_shadow(fd.ident.sym.as_ref(), &mut shadowed),
307-
ast::Decl::Var(vd) => {
308-
for d in &vd.decls {
309-
if let ast::Pat::Ident(ident) = &d.name {
310-
record_shadow(ident.id.sym.as_ref(), &mut shadowed);
311-
}
312-
}
313-
}
314-
_ => {}
315-
}
316-
}
317-
ast::ModuleItem::ModuleDecl(ast::ModuleDecl::Import(import_decl)) => {
318-
for spec in &import_decl.specifiers {
319-
let local = match spec {
320-
ast::ImportSpecifier::Named(named) => named.local.sym.as_ref(),
321-
ast::ImportSpecifier::Default(default) => default.local.sym.as_ref(),
322-
ast::ImportSpecifier::Namespace(ns) => ns.local.sym.as_ref(),
323-
};
324-
record_shadow(local, &mut shadowed);
325-
}
326-
}
327-
_ => {}
328-
}
329-
}
330-
let mut poison: HashSet<String> = HashSet::new();
331-
for item in &ast_module.body {
332-
match item {
333-
ast::ModuleItem::Stmt(stmt) => walk_stmt(stmt, ctx, &mut poison, &shadowed),
334-
ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => {
335-
if let ast::Decl::Var(var_decl) = &export_decl.decl {
336-
for decl in &var_decl.decls {
337-
record_var(decl, ctx, &mut poison, &shadowed);
338-
}
339-
}
340-
}
341-
_ => {}
342-
}
343-
}
344-
// A name reused for both `new WeakMap()`/`new WeakSet()` and a non-weak
345-
// constructor is ambiguous: the bare-name weak-locals set can't tell the
346-
// two bindings apart, so routing `.set/.get/.has/.delete`/`.add` to the
347-
// weak intrinsic would be wrong for the non-weak instance (e.g. a
348-
// string-keyed cache → `Invalid value used as weak map key`). Drop such
349-
// ambiguous names so their method calls fall back to ordinary dynamic
350-
// dispatch. This is correct for a real WeakMap/WeakSet too: the runtime's
351-
// `WeakMap/WeakSet.prototype` thunks (collection_proto_thunks) re-validate
352-
// the receiver's class id and re-enter `js_weakmap_set` etc.
353-
//
354-
// Restricted to weakmap/weakset only: WeakRef.deref / FinalizationRegistry
355-
// .register / Proxy use distinct method names that don't collide with the
356-
// cache `.get/.set/.add` family, and (unlike WeakMap/WeakSet) have no
357-
// runtime method-dispatch fallback — they rely on the codegen fast path —
358-
// so dropping them could regress a genuine instance with no upside.
359-
for name in &poison {
360-
ctx.weakmap_locals.remove(name);
361-
ctx.weakset_locals.remove(name);
362-
// #7775: `proxy_locals` was left in — the note above weighed a lost
363-
// codegen fast path against "no upside", because it only considered
364-
// METHOD dispatch (`.deref()`, `.register()`), where a poisoned name
365-
// costs speed. A PROPERTY read is the other half and the objection does
366-
// not hold there: an ambiguous name routed a NON-proxy receiver's
367-
// `a.length` to `js_proxy_get`, which answers `undefined`. That is a
368-
// wrong answer, not a slow one, and it is what a poisoned name buys
369-
// back. A genuine proxy keeps working through the ordinary dynamic
370-
// property path (asserted in
371-
// `test-files/test_gap_proxy_local_name_collision_7775.ts`).
372-
ctx.proxy_locals.remove(name);
373-
}
374-
}
13+
mod weakref_locals;
14+
15+
pub(crate) use weakref_locals::pre_scan_weakref_locals;
37516

37617
/// Pre-scan top-level function declarations for the standard TypeScript
37718
/// mixin pattern:

0 commit comments

Comments
 (0)