@@ -140,90 +140,132 @@ impl<'a> Walker<'a> {
140140 }
141141
142142 fn try_recover ( & mut self , stmts : & mut ArenaVec < ' a , Statement < ' a > > ) {
143- let mut hit: Option < ( usize , Vec < Statement < ' a > > ) > = None ;
143+ // Each hit is the inclusive statement range `[start, end]` to replace with
144+ // the linearized `seq`. The for-form is a single statement (`start == end`);
145+ // the while-form also consumes its preceding hoisted-init statement(s).
146+ let mut hit: Option < ( usize , usize , Vec < Statement < ' a > > ) > = None ;
144147 for i in 0 ..stmts. len ( ) {
145- let Statement :: ForStatement ( f) = & stmts[ i] else {
146- continue ;
147- } ;
148- // The loop must never exit via its own test: `for (…; true; )` or
149- // `for (…; ; )`. A loop with a real test could fall through before the
150- // chain ends — not a static linear walk.
151- let test_is_always = match & f. test {
152- None => true ,
153- Some ( Expression :: BooleanLiteral ( b) ) => b. value ,
154- _ => false ,
155- } ;
156- if !test_is_always {
157- continue ;
158- }
159- // No `for`-update clause: the cursor is advanced solely by `i++` inside
160- // the discriminant. An update would be a second, unmodeled mutation.
161- if f. update . is_some ( ) {
162- continue ;
163- }
164- let Some ( init) = & f. init else { continue } ;
165- let Some ( ( arr_name, idx_name, keys) ) = parse_init ( init) else {
166- continue ;
167- } ;
168- // The order array must be provably immutable (never `push`/`[k]=`/etc.).
169- if self . mutable . contains ( & arr_name) {
170- continue ;
148+ if let Some ( seq) = self . try_for_form ( & stmts[ i] ) {
149+ hit = Some ( ( i, i, seq) ) ;
150+ break ;
171151 }
172- // Body: `switch (a[i++]) { … }`, optionally wrapped in a block and/or
173- // trailed by a `break`. `has_break_exit` is true when the loop body has a
174- // trailing `break` reachable when the switch matches nothing — i.e. the
175- // loop cleanly exits once the cursor runs off the end of the array.
176- let Some ( ( switch, has_break_exit) ) = body_switch ( & f. body , & arr_name, & idx_name) else {
177- continue ;
178- } ;
179- if switch. cases . iter ( ) . any ( |c| c. test . is_none ( ) ) {
180- continue ; // no `default` clause
181- }
182- let Some ( cases) = parse_cases ( switch, & arr_name, & idx_name) else {
183- continue ;
184- } ;
185- // `a` and `i` must be dispatch-only: referenced *nowhere* outside this
186- // `for` statement, program-wide. Compare each name's total occurrence
187- // count to its count inside the for-statement; equal ⇒ no escape. (The
188- // census counts binding ids too, so the param/var declaration of `a`/`i`
189- // outside the loop also counts as an escape and is correctly rejected —
190- // unless, as in the CFF shape, the name's only binding is the for-init
191- // assignment target, which lives inside the for-statement.)
192- let for_stmt = & stmts[ i] ;
193- let arr_local = count_name_in_stmt ( for_stmt, & arr_name) ;
194- let idx_local = count_name_in_stmt ( for_stmt, & idx_name) ;
195- if self . global_counts . get ( & arr_name) . copied ( ) . unwrap_or ( 0 ) != arr_local
196- || self . global_counts . get ( & idx_name) . copied ( ) . unwrap_or ( 0 ) != idx_local
197- {
198- continue ;
152+ if let Some ( ( start, seq) ) = self . try_while_form ( stmts, i) {
153+ hit = Some ( ( start, i, seq) ) ;
154+ break ;
199155 }
200- let Some ( seq) = simulate_sequence ( & keys, & cases, has_break_exit, self . ast . allocator )
201- else {
202- continue ;
203- } ;
204- hit = Some ( ( i, seq) ) ;
205- break ;
206156 }
207157
208- let Some ( ( i , seq) ) = hit else {
158+ let Some ( ( start , end , seq) ) = hit else {
209159 return ;
210160 } ;
211- // Splice: replace the `for` dispatcher (statement i) with the linear
161+ // Splice: replace the dispatcher range `[start, end]` with the linear
212162 // sequence. Everything else stays in order.
213163 let old = std:: mem:: replace ( stmts, self . ast . vec ( ) ) ;
214164 let mut seq = Some ( seq) ;
215165 for ( k, s) in old. into_iter ( ) . enumerate ( ) {
216- if k == i {
166+ if k == start {
217167 for st in seq. take ( ) . unwrap ( ) {
218168 stmts. push ( st) ;
219169 }
220- continue ;
170+ }
171+ if ( start..=end) . contains ( & k) {
172+ continue ; // drop the originals in the dispatcher range
221173 }
222174 stmts. push ( s) ;
223175 }
224176 self . changed = true ;
225177 }
226178
179+ /// `for (a = <const arr>, i = 0; true; ) switch (a[i++]) { … }` — the cursor
180+ /// setup lives in the for-init. Returns the linearized sequence, or `None`.
181+ fn try_for_form ( & self , stmt : & Statement < ' a > ) -> Option < Vec < Statement < ' a > > > {
182+ let Statement :: ForStatement ( f) = stmt else {
183+ return None ;
184+ } ;
185+ // The loop must never exit via its own test: `for (…; true; )` /
186+ // `for (…; ; )`. A real test could fall through before the chain ends.
187+ let test_is_always = match & f. test {
188+ None => true ,
189+ Some ( Expression :: BooleanLiteral ( b) ) => b. value ,
190+ _ => false ,
191+ } ;
192+ if !test_is_always {
193+ return None ;
194+ }
195+ // No `for`-update clause: the cursor advances solely via `i++` in the
196+ // discriminant. An update would be a second, unmodeled mutation.
197+ if f. update . is_some ( ) {
198+ return None ;
199+ }
200+ let ( arr_name, idx_name, keys) = parse_init ( f. init . as_ref ( ) ?) ?;
201+ let ( switch, has_break_exit) = body_switch ( & f. body , & arr_name, & idx_name) ?;
202+ // Escape gate over the single `for` statement (see `finish`).
203+ self . finish ( & arr_name, & idx_name, & keys, switch, has_break_exit, & [ stmt] )
204+ }
205+
206+ /// `var a = <const arr>, i = 0; while (true) { switch (a[i++]) { … } [break;] }`
207+ /// — the equivalent dispatcher whose cursor setup is hoisted to statement(s)
208+ /// preceding the loop (the obfuscator emits this, and lowering a `for(index)`
209+ /// wrapper leaves it). Returns `(start_index, sequence)`: the index of the
210+ /// first init statement consumed, and the linearized body.
211+ fn try_while_form (
212+ & self ,
213+ stmts : & [ Statement < ' a > ] ,
214+ i : usize ,
215+ ) -> Option < ( usize , Vec < Statement < ' a > > ) > {
216+ let Statement :: WhileStatement ( w) = & stmts[ i] else {
217+ return None ;
218+ } ;
219+ // `while (true)` / `while (1)` — never exits via the test.
220+ let always = matches ! ( & w. test, Expression :: BooleanLiteral ( b) if b. value)
221+ || int_literal ( & w. test ) . is_some_and ( |n| n != 0 ) ;
222+ if !always {
223+ return None ;
224+ }
225+ let ( start, pairs) = gather_hoisted_init ( stmts, i) ?;
226+ let ( arr_name, idx_name, keys) = pairs_to_init ( pairs) ?;
227+ let ( switch, has_break_exit) = body_switch ( & w. body , & arr_name, & idx_name) ?;
228+ // Escape gate spans the init statement(s) plus the loop — `a`/`i` must be
229+ // referenced nowhere else (see `finish`). The init's binding ids are not
230+ // counted (the census skips declarations), so a clean dispatcher's only
231+ // counted references are the single `a[i++]` read and `i++`.
232+ let escape: Vec < & Statement < ' a > > = stmts[ start..=i] . iter ( ) . collect ( ) ;
233+ let seq = self . finish ( & arr_name, & idx_name, & keys, switch, has_break_exit, & escape) ?;
234+ Some ( ( start, seq) )
235+ }
236+
237+ /// Shared gating + linearization for both dispatcher forms. `escape_stmts` is
238+ /// the set of statements the cursor `a`/`i` is allowed to appear in; if either
239+ /// name's program-wide reference count exceeds its count there, the cursor
240+ /// escapes and we decline.
241+ fn finish (
242+ & self ,
243+ arr_name : & str ,
244+ idx_name : & str ,
245+ keys : & [ Key ] ,
246+ switch : & SwitchStatement < ' a > ,
247+ has_break_exit : bool ,
248+ escape_stmts : & [ & Statement < ' a > ] ,
249+ ) -> Option < Vec < Statement < ' a > > > {
250+ // The order array must be provably immutable (never `push`/`[k]=`/etc.).
251+ if self . mutable . contains ( arr_name) {
252+ return None ;
253+ }
254+ if switch. cases . iter ( ) . any ( |c| c. test . is_none ( ) ) {
255+ return None ; // no `default` clause
256+ }
257+ let cases = parse_cases ( switch, arr_name, idx_name) ?;
258+ let count_in = |name : & str | -> usize {
259+ escape_stmts. iter ( ) . map ( |s| count_name_in_stmt ( s, name) ) . sum ( )
260+ } ;
261+ if self . global_counts . get ( arr_name) . copied ( ) . unwrap_or ( 0 ) != count_in ( arr_name)
262+ || self . global_counts . get ( idx_name) . copied ( ) . unwrap_or ( 0 ) != count_in ( idx_name)
263+ {
264+ return None ;
265+ }
266+ simulate_sequence ( keys, & cases, has_break_exit, self . ast . allocator )
267+ }
268+
227269 /// Descend into every statement list reachable from `stmt`, so nested
228270 /// dispatchers (inside functions, `try` blocks, branches, …) are recovered.
229271 /// Mirrors `unflatten_cfg`'s recursion.
@@ -327,45 +369,17 @@ enum Key {
327369fn parse_init ( init : & ForStatementInit < ' _ > ) -> Option < ( String , String , Vec < Key > ) > {
328370 // Collect the (target-name, init-expr) pairs in source order.
329371 let pairs: Vec < ( String , & Expression < ' _ > ) > = match init {
330- ForStatementInit :: VariableDeclaration ( vd) => {
331- let mut out = Vec :: with_capacity ( vd. declarations . len ( ) ) ;
332- for d in & vd. declarations {
333- let BindingPattern :: BindingIdentifier ( id) = & d. id else {
334- return None ;
335- } ;
336- out. push ( ( id. name . to_string ( ) , d. init . as_ref ( ) ?) ) ;
337- }
338- out
339- }
372+ ForStatementInit :: VariableDeclaration ( vd) => decl_pairs ( vd) ?,
340373 // An expression init: a single assignment or a comma sequence of them.
341- _ => {
342- let expr = init. as_expression ( ) ?;
343- let mut exprs: Vec < & Expression < ' _ > > = Vec :: new ( ) ;
344- match expr {
345- Expression :: SequenceExpression ( seq) => {
346- for e in & seq. expressions {
347- exprs. push ( e) ;
348- }
349- }
350- other => exprs. push ( other) ,
351- }
352- let mut out = Vec :: with_capacity ( exprs. len ( ) ) ;
353- for e in exprs {
354- let Expression :: AssignmentExpression ( a) = e else {
355- return None ;
356- } ;
357- if a. operator != AssignmentOperator :: Assign {
358- return None ;
359- }
360- let AssignmentTarget :: AssignmentTargetIdentifier ( id) = & a. left else {
361- return None ;
362- } ;
363- out. push ( ( id. name . to_string ( ) , & a. right ) ) ;
364- }
365- out
366- }
374+ _ => assign_pairs ( init. as_expression ( ) ?) ?,
367375 } ;
376+ pairs_to_init ( pairs)
377+ }
368378
379+ /// Reduce the ordered `(name, init-expr)` pairs to `(array, cursor, keys)`. The
380+ /// shared tail of every init form: exactly two bindings — a constant key array,
381+ /// then a cursor initialized to `0`.
382+ fn pairs_to_init ( pairs : Vec < ( String , & Expression < ' _ > ) > ) -> Option < ( String , String , Vec < Key > ) > {
369383 if pairs. len ( ) != 2 {
370384 return None ;
371385 }
@@ -382,6 +396,90 @@ fn parse_init(init: &ForStatementInit<'_>) -> Option<(String, String, Vec<Key>)>
382396 Some ( ( arr_name. clone ( ) , idx_name. clone ( ) , keys) )
383397}
384398
399+ /// `var a = …, i = …` → its `(name, init)` pairs. Every declarator must be a
400+ /// plain identifier with an initializer.
401+ fn decl_pairs < ' b , ' a > (
402+ vd : & ' b oxc_ast:: ast:: VariableDeclaration < ' a > ,
403+ ) -> Option < Vec < ( String , & ' b Expression < ' a > ) > > {
404+ let mut out = Vec :: with_capacity ( vd. declarations . len ( ) ) ;
405+ for d in & vd. declarations {
406+ let BindingPattern :: BindingIdentifier ( id) = & d. id else {
407+ return None ;
408+ } ;
409+ out. push ( ( id. name . to_string ( ) , d. init . as_ref ( ) ?) ) ;
410+ }
411+ Some ( out)
412+ }
413+
414+ /// `a = …` or `a = …, i = …` (a comma sequence of simple assignments) → its
415+ /// `(name, init)` pairs.
416+ fn assign_pairs < ' b , ' a > ( expr : & ' b Expression < ' a > ) -> Option < Vec < ( String , & ' b Expression < ' a > ) > > {
417+ let mut exprs: Vec < & Expression < ' _ > > = Vec :: new ( ) ;
418+ match expr {
419+ Expression :: SequenceExpression ( seq) => {
420+ for e in & seq. expressions {
421+ exprs. push ( e) ;
422+ }
423+ }
424+ other => exprs. push ( other) ,
425+ }
426+ let mut out = Vec :: with_capacity ( exprs. len ( ) ) ;
427+ for e in exprs {
428+ let Expression :: AssignmentExpression ( a) = e else {
429+ return None ;
430+ } ;
431+ if a. operator != AssignmentOperator :: Assign {
432+ return None ;
433+ }
434+ let AssignmentTarget :: AssignmentTargetIdentifier ( id) = & a. left else {
435+ return None ;
436+ } ;
437+ out. push ( ( id. name . to_string ( ) , & a. right ) ) ;
438+ }
439+ Some ( out)
440+ }
441+
442+ /// Interpret a *statement* as part of a hoisted dispatcher init — the while-form
443+ /// `var a = …, i = 0; while (true) …` where the cursor setup precedes the loop
444+ /// instead of living in a `for`-init. Accepts a `var` declaration or an
445+ /// assignment/comma-sequence expression statement.
446+ fn stmt_pairs < ' b , ' a > ( stmt : & ' b Statement < ' a > ) -> Option < Vec < ( String , & ' b Expression < ' a > ) > > {
447+ match stmt {
448+ Statement :: VariableDeclaration ( vd) => decl_pairs ( vd) ,
449+ Statement :: ExpressionStatement ( es) => assign_pairs ( & es. expression ) ,
450+ _ => None ,
451+ }
452+ }
453+
454+ /// Collect the cursor-init `(name, init)` pairs from the statement(s) immediately
455+ /// preceding the while-loop at `i`, returning the first consumed index. Accepts
456+ /// either one statement that binds both (`var a = …, i = 0;`) or two adjacent
457+ /// statements that bind one each (`var a = …; var i = 0;`). The init must sit
458+ /// directly before the loop so the spliced range stays contiguous.
459+ fn gather_hoisted_init < ' b , ' a > (
460+ stmts : & ' b [ Statement < ' a > ] ,
461+ i : usize ,
462+ ) -> Option < ( usize , Vec < ( String , & ' b Expression < ' a > ) > ) > {
463+ if i == 0 {
464+ return None ;
465+ }
466+ if let Some ( pairs) = stmt_pairs ( & stmts[ i - 1 ] ) {
467+ if pairs. len ( ) == 2 {
468+ return Some ( ( i - 1 , pairs) ) ;
469+ }
470+ if pairs. len ( ) == 1 && i >= 2 {
471+ if let Some ( prev) = stmt_pairs ( & stmts[ i - 2 ] ) {
472+ if prev. len ( ) == 1 {
473+ let mut combined = prev;
474+ combined. extend ( pairs) ;
475+ return Some ( ( i - 2 , combined) ) ;
476+ }
477+ }
478+ }
479+ }
480+ None
481+ }
482+
385483/// Resolve a provably constant array of dispatch keys from the array initializer:
386484/// * `"a|b|c".split("|")` (any single-char/string separator) → split the literal;
387485/// * `["a", "b", 3]` → a literal array of string/number elements.
0 commit comments