package js_of_ocaml-compiler

  1. Overview
  2. Docs
Compiler from OCaml bytecode to JavaScript

Install

dune-project
 Dependency

Authors

Maintainers

Sources

js_of_ocaml-6.4.1.tbz
sha256=e59bbffcaefaba3191620556514b7f53bb3249e3f881a070d72724234dffd819
sha512=bb470f316f9c81a3b2b8dedd0b0f18f8e06beb48dd70df1d9f846de90ffab439cab5ef7a93a8166af0998068b06097e8319bc0d9f4f23a7159b0de8b3b515746

doc/src/js_of_ocaml-compiler/js_variable_coalescing.ml.html

Source file js_variable_coalescing.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
(* Js_of_ocaml compiler
 * http://www.ocsigen.org/js_of_ocaml/
 * Copyright (C) 2026
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, with linking exception;
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *)

(*
   This pass merges JavaScript variables with disjoint lifespans into
   a single variable. This reduces both code size and runtime memory
   usage. In particular, V8's bytecode allocates one stack slot per
   local variable not captured by nested functions; for large toplevel
   functions, this can create huge stack frames.

   Algorithm Overview
   ------------------

   The algorithm is based on linear scan register allocation, adapted
   for variable coalescing rather than physical register assignment.
   See: "Linear Scan Register Allocation in the Context of SSA Form and
   Register Constraints" by Hanspeter Mössenböck and Michael Pfeiffer
   (https://dl.acm.org/doi/10.1145/543552.512558).

   1. Build a CFG for each function scope (statements become nodes,
      control flow creates edges).

   2. Compute live variable sets via backward dataflow analysis:
        LiveIn(n) = Use(n) ∪ (LiveOut(n) - Def(n))
        LiveOut(n) = ∪ LiveIn(succ) for all successors

   3. Compute live ranges (intervals) for each variable. We use 2x
      position granularity: CFG node i maps to positions 2i (before, for
      uses) and 2i+1 (after, for defs). Variables may have multiple
      disjoint intervals (holes in their liveness).

   4. Sort variables by their first live position.

   5. Process variables in order using linear scan:
      - Maintain active set (currently live variables) and inactive
        set (variables in a hole that will become live again).
      - For each variable, try to coalesce with an existing
        representative:
        a) Hint-based: If this variable is a copy target (x = y), try
           to reuse y's representative. This eliminates the copy when
           successful.
        b) Opportunistic: Reuse any dead or non-interfering inactive
           variable.
      - If no coalescing possible, the variable becomes its own
        representative.

   Scope Handling
   --------------

   Each function scope is processed independently. Variables captured by
   nested functions are currently excluded from coalescing. While they
   could theoretically be merged with preceding variables, they are
   typically allocated on the heap (e.g., in V8's context objects)
   rather than the stack. Thus, they do not contribute to stack frame
   size, but are likely slower to access.

   Block-scoped variables (let, const) are excluded from this analysis.
   Js_of_ocaml rarely uses them, except occasionally for captured
   variables, so there is little benefit in extending the implementation
   to support them. In pretty mode, only compiler-generated variables
   are coalesced to preserve user-defined names.

   We skip trivial scopes with 0-1 candidate variables.

   Copy Hints
   ----------

   When we see `var x = y` where y is a local variable, we record a
   "hint" that x should use y's representative if possible. Indeed, if
   x and y share the same variable, the assignment becomes a no-op
   that can be eliminated by later passes.

   Exception Handling
   ------------------

   Exception handlers (catch/finally) require special care. Since an
   exception can be thrown from any point in the try block, variables
   live at the handler entry must be considered live throughout the
   entire try block. We extend their live ranges accordingly.

   Implementation Details
   ----------------------

   To avoid quadratic behavior in pathological cases, the search for
   non-interfering inactive variables is limited to a constant number
   of candidates.
*)

open Stdlib
open Javascript
module Var = Code.Var

let times = Debug.find "times"

let debug = Debug.find "var-coalescing"

let stats = Debug.find "stats"

type pass_stats =
  { mutable candidates : int
  ; mutable hint_coalesced : int
  ; mutable opportunistic_coalesced : int
  ; mutable time_collect : float
  ; mutable time_cfg : float
  ; mutable time_solve : float
  ; mutable time_live_range : float
  ; mutable time_allocate : float
  ; mutable time_mark_captured : float
  ; mutable time_rename : float
  }

(* Mark variables used in the current function (not visiting nested
   functions). *)
let mark_captured_variables pass_stats captured_vars f =
  let t = Timer.make () in
  let visitor =
    object
      inherit Js_traverse.iter as super

      method! fun_decl _ = ()

      method! ident i =
        match i with
        | V v -> Var.Tbl.set captured_vars v true
        | _ -> ()

      method visit f = super#fun_decl f
    end
  in
  visitor#visit f;
  pass_stats.time_mark_captured <- pass_stats.time_mark_captured +. Timer.get t

(* Collect local variables in the current function *)
let collect_locals captured_vars params stmts =
  let locals = ref Var.Set.empty in
  let add i =
    match i with
    | V v -> if not (Var.Tbl.get captured_vars v) then locals := Var.Set.add v !locals
    | S _ -> ()
  in
  let add_list ids = List.iter ids ~f:add in
  let visitor =
    object
      inherit Js_traverse.iter as super

      method! fun_decl _ = () (* Do not descend into nested functions *)

      method! statement s =
        (match s with
        | Variable_statement (Var, decls) ->
            List.iter decls ~f:(fun d ->
                (* Don't coalesce functions, so that we can use
                   [function x () { ... }] instead of [var x = ...]. *)
                match d with
                | DeclIdent (_, Some (EFun _, _)) -> ()
                | _ -> add_list (bound_idents_of_variable_declaration d))
        | ForIn_statement (left, _, _)
        | ForOf_statement (left, _, _)
        | ForAwaitOf_statement (left, _, _) -> (
            match left with
            | Right (Var, binding) -> add_list (bound_idents_of_binding binding)
            | Left _ (* Expression *) | Right ((Let | Const | Using | AwaitUsing), _) ->
                (* Block-scoped variables (let/const) are not hoisted
                   to function scope, so we don't collect them here
                   for the whole-function liveness analysis that this
                   module performs for 'var' optimization. *)
                ())
        | For_statement (init, _, _, _) -> (
            match init with
            | Right (Var, decls) ->
                List.iter decls ~f:(fun d ->
                    add_list (bound_idents_of_variable_declaration d))
            | Left _ (* Expression or empty *)
            | Right ((Let | Const | Using | AwaitUsing), _) -> ())
        | _ -> ());
        super#statement s
    end
  in
  add_list (bound_idents_of_params params);
  visitor#statements stmts;
  (* In pretty mode, only coalesce compiler-generated variables to preserve
     user-defined variable names for readability. *)
  if Config.Flag.pretty () then Var.Set.filter Var.generated_name !locals else !locals

(* Liveness Analysis *)

(* Unique ID for nodes in the CFG *)

module Node = struct
  type t = int
end

module NodeSet = struct
  type t = BitSet.t

  type elt = int

  let iter f t = BitSet.iter ~f t

  let mem = BitSet.mem

  let add = BitSet.set

  let remove = BitSet.unset

  let copy = BitSet.copy
end

module NodeTbl = struct
  type 'a t = 'a array

  type key = int

  type size = int

  let get t k = t.(k)

  let set t k v = t.(k) <- v

  let make = Array.make
end

module G = Dgraph.Make_Imperative (Node) (NodeSet) (NodeTbl)

module Domain = struct
  type t = Var.Set.t

  let equal = Var.Set.equal

  let bot = Var.Set.empty
end

module Solver = G.Solver (Domain)

(* CFG Construction *)
type action =
  | Use of Var.Set.t
  | Def of Var.Set.t
  | DefUse of Var.Set.t * Var.Set.t
  | Nop

let defs_of_action = function
  | Def d | DefUse (d, _) -> d
  | Use _ | Nop -> Var.Set.empty

let uses_of_action = function
  | Use u | DefUse (_, u) -> u
  | Def _ | Nop -> Var.Set.empty

type node_id = int

type stmt_graph =
  { entry : node_id
  ; size : int
  ; actions : action array
  ; succs : node_id list array
  ; coalescing_hints : Var.t Var.Hashtbl.t
  ; try_blocks : node_id Int.Hashtbl.t (* end_id -> start_id *)
  }

(* Mutable builder used during CFG construction *)
type graph_builder =
  { mutable nodes : (node_id * action * node_id list) list
  ; hints : Var.t Var.Hashtbl.t
  ; tries : node_id Int.Hashtbl.t (* end_id -> start_id *)
  }

(* Context entry for break/continue statement targeting *)
type context_entry =
  { labels : Label.t list (* Labels for this entry; empty for unlabelled loops *)
  ; break : node_id
  ; continue : node_id option
  ; iter_or_switch : bool
        (* Whether unlabelled [break] can target this entry. True for
           iteration statements and [switch]; false for labelled
           non-iteration statements (e.g. labelled blocks), which are
           valid targets only for [break label]. *)
  }

let add_var candidates v s =
  match v with
  | V x -> if Var.Set.mem x candidates then Var.Set.add x s else s
  | S _ -> s

let pattern_defs candidates p =
  List.fold_left
    ~f:(fun s v -> add_var candidates v s)
    ~init:Var.Set.empty
    (bound_idents_of_pattern p)

let rec find_break label ctx =
  match ctx, label with
  | [], _ -> failwith "Break without loop"
  | { labels; break; _ } :: _, Some l when List.mem ~eq:Label.equal l labels -> break
  | { iter_or_switch = true; break; _ } :: _, None -> break
  | _ :: rest, _ -> find_break label rest

let rec find_continue label ctx =
  match ctx, label with
  | [], _ -> failwith "Continue without loop"
  | { labels; continue = Some target; _ } :: _, Some l
    when List.mem ~eq:Label.equal l labels -> target
  (* For unlabeled continue, match any actual loop *)
  | { continue = Some target; _ } :: _, None -> target
  | _ :: rest, _ -> find_continue label rest

(* Checks whether a statement handles its own break/continue context.
     For such statements, Labelled_statement delegates label handling. *)
let has_own_context stmt =
  match stmt with
  | While_statement _
  | Do_while_statement _
  | For_statement _
  | ForIn_statement _
  | ForOf_statement _
  | ForAwaitOf_statement _
  | Switch_statement _ -> true
  | Block _
  | Variable_statement _
  | Function_declaration _
  | Class_declaration _
  | Empty_statement
  | Expression_statement _
  | If_statement _
  | Continue_statement _
  | Break_statement _
  | Return_statement _
  | With_statement _
  | Throw_statement _
  | Try_statement _
  | Debugger_statement
  | Import _
  | Export _ -> false
  | Labelled_statement _ -> assert false

(* Visitor to build the graph *)
let build_cfg stmts candidates param_vars =
  let builder =
    { nodes = []; hints = Var.Hashtbl.create 16; tries = Int.Hashtbl.create 8 }
  in
  let next_id = ref 0 in
  let reserve_id () =
    let id = !next_id in
    incr next_id;
    id
  in
  let set_node id action succs = builder.nodes <- (id, action, succs) :: builder.nodes in
  let add_node action succs =
    let id = reserve_id () in
    set_node id action succs;
    id
  in
  (* Record copy relationships (x = y) as coalescing hints. When allocating
     registers, we prefer to assign x and y to the same variable if their
     live ranges don't interfere, enabling copy propagation. *)
  (* Reuse the visitor to avoid allocating a new object for every expression *)
  let expr_use =
    let visitor =
      object
        inherit Js_traverse.iter as super

        val mutable use = Var.Set.empty

        method collect e =
          use <- Var.Set.empty;
          super#expression e;
          use

        method! ident i =
          match i with
          | V v when Var.Set.mem v candidates -> use <- Var.Set.add v use
          | _ -> ()
      end
    in
    fun e -> visitor#collect e
  in
  let rec expr_use_def e =
    match e with
    | EBin (Eq, EVar v, e') ->
        let u, d = expr_use_def e' in
        u, add_var candidates v d
    | _ -> expr_use e, Var.Set.empty
  in
  let decl_use_def decl =
    let u, d =
      match decl with
      | DeclIdent (_, Some (e, _)) | DeclPattern (_, (e, _)) -> expr_use_def e
      | DeclIdent (_, None) -> Var.Set.empty, Var.Set.empty
    in
    match decl with
    | DeclIdent (x, Some _) -> u, add_var candidates x d
    | DeclIdent (V _, None) | DeclIdent (S _, _) -> u, d
    | DeclPattern (p, _) -> u, Var.Set.union (pattern_defs candidates p) d
  in
  let add_hint x y =
    match x, y with
    | V x, V y when Var.Set.mem x candidates && Var.Set.mem y candidates ->
        Var.Hashtbl.replace builder.hints x y
    | _ -> ()
  in
  (* The CFG is built backwards: we visit statements from the last one to the
     first one.
     - [exit] is the node id of the statement following the current one.
     - [context] contains the targets for break and continue statements.

     Statements that own a break/continue context (iteration statements and
     [switch]) are visited through [visit_owning_stmt], which takes a [labels]
     list to associate with the new context entry. All other statements go
     through [visit_stmt], which has no [labels] parameter — so we cannot
     accidentally drop labels for a statement that needs them.

     Both functions return the node id of the first statement of the visited
     block (which acts as the entry point for that block). *)
  let rec visit_stmt context exit stmt =
    match stmt with
    | Block stmts -> visit_stmts context exit stmts
    | Expression_statement e ->
        let u, d = expr_use_def e in
        let entry = add_node (DefUse (d, u)) [ exit ] in
        (match e with
        | EBin (Eq, EVar x, EVar y) -> add_hint x y
        | _ -> ());
        entry
    | If_statement (cond, (then_s, _), else_s) ->
        let else_entry =
          match else_s with
          | Some (s, _) -> visit_stmt context exit s
          | None -> exit
        in
        let then_entry = visit_stmt context exit then_s in
        let u_cond, _ = expr_use_def cond in
        add_node (Use u_cond) [ then_entry; else_entry ]
    | While_statement _
    | Do_while_statement _
    | Switch_statement _
    | ForIn_statement _
    | ForOf_statement _
    | ForAwaitOf_statement _
    | For_statement _ -> visit_owning_stmt [] context exit stmt
    | Break_statement label -> find_break label context
    | Continue_statement label -> find_continue label context
    | Variable_statement (_kind, decls) ->
        let entry, _ =
          List.fold_right decls ~init:(exit, exit) ~f:(fun decl (next, _) ->
              let u, d = decl_use_def decl in
              let node = add_node (DefUse (d, u)) [ next ] in
              (match decl with
              | DeclIdent (x, Some (EVar y, _)) -> add_hint x y
              | _ -> ());
              node, node)
        in
        entry
    | Return_statement (eopt, _) ->
        let u =
          match eopt with
          | Some e -> fst (expr_use_def e)
          | None -> Var.Set.empty
        in
        add_node (Use u) []
    | Throw_statement e ->
        let u, _ = expr_use_def e in
        add_node (Use u) []
    | Try_statement (body, catch, finally) ->
        let finally_entry =
          match finally with
          | Some block -> visit_stmts context exit block
          | None -> exit
        in
        let catch_entry =
          match catch with
          | Some (_, block) -> visit_stmts context finally_entry block
          | None -> finally_entry
        in
        let inner_body_entry = visit_stmts context finally_entry body in
        (* Wrap [body_entry] in a fresh [Nop] shim whose only predecessor
           is the [Try] node we are about to create. This guarantees that
           in the DFS used to compute the RPO layout, [body_entry] is
           numbered strictly after [catch_entry]/[finally_entry] (which
           are visited first by [list_rev_iter]).

           Without the shim, [body_entry] can coincide with a node
           reachable from [catch_entry] or [finally_entry] — e.g. when
           the body is empty. That would make the body end up inside
           catch's/finally's DFS subtree, violating
           [body_order < catch_order] in [compute_live_ranges]. *)
        let body_entry = add_node Nop [ inner_body_entry ] in
        (* Map handler entry -> try body entry. This is used during live range
           computation to extend the live range of variables that are live at the
           handler entry to cover the entire try body, since any statement in the
           try block might throw and jump to the handler. *)
        if Option.is_some catch
        then Int.Hashtbl.replace builder.tries catch_entry body_entry;
        if Option.is_some finally
        then Int.Hashtbl.replace builder.tries finally_entry body_entry;
        add_node Nop [ body_entry; catch_entry; finally_entry ]
    | Labelled_statement (label, (stmt, _)) ->
        (* Collect all labels for chained labelled statements *)
        let rec collect_labels acc s =
          match s with
          | Labelled_statement (l, (inner, _)) -> collect_labels (l :: acc) inner
          | _ -> List.rev acc, s
        in
        let all_labels, inner_stmt = collect_labels [ label ] stmt in
        if has_own_context inner_stmt
        then visit_owning_stmt all_labels context exit inner_stmt
        else
          let context =
            { labels = all_labels; break = exit; continue = None; iter_or_switch = false }
            :: context
          in
          visit_stmt context exit inner_stmt
    | Empty_statement | Debugger_statement -> exit
    | Function_declaration (_, _) | Class_declaration (_, _) -> exit
    | With_statement (e, (body, _)) ->
        let body_entry = visit_stmt context exit body in
        let u, _ = expr_use_def e in
        add_node (Use u) [ body_entry ]
    | Import (_, _) | Export (_, _) -> exit
  (* Visits a statement that owns its own break/continue context: iteration
     statements ([while]/[do-while]/[for]/[for-in]/[for-of]/[for-await-of])
     and [switch]. [labels] are the labels (possibly empty) under which this
     statement appears, and become part of the new context entry. *)
  and visit_owning_stmt labels context exit stmt =
    match stmt with
    | While_statement (cond, (body, _)) ->
        let loop_check = reserve_id () in
        let context =
          { labels; break = exit; continue = Some loop_check; iter_or_switch = true }
          :: context
        in
        let body_entry = visit_stmt context loop_check body in
        let u_cond, _ = expr_use_def cond in
        set_node loop_check (Use u_cond) [ body_entry; exit ];
        loop_check
    | Do_while_statement ((body, _), cond) ->
        let loop_check = reserve_id () in
        let u_cond, _ = expr_use_def cond in
        let context' =
          { labels; break = exit; continue = Some loop_check; iter_or_switch = true }
          :: context
        in
        let body_entry = visit_stmt context' loop_check body in
        set_node loop_check (Use u_cond) [ body_entry; exit ];
        body_entry
    | Switch_statement (cond, pre_cases, def_opt, post_cases) ->
        let context =
          { labels; break = exit; continue = None; iter_or_switch = true } :: context
        in
        let process_body (_, stmts) (next_body, entries) =
          let next_body = visit_stmts context next_body stmts in
          next_body, next_body :: entries
        in
        let next_body, post_bodies =
          List.fold_right ~f:process_body post_cases ~init:(exit, [])
        in
        let next_body, default_body =
          match def_opt with
          | Some stmts ->
              let entry = visit_stmts context next_body stmts in
              entry, entry
          | None -> next_body, exit
        in
        let _, pre_bodies =
          List.fold_right ~f:process_body pre_cases ~init:(next_body, [])
        in
        let process_case (e, _) body_entry next_case =
          let u, _ = expr_use_def e in
          add_node (Use u) [ body_entry; next_case ]
        in
        let next_case =
          List.fold_right2 ~f:process_case post_cases post_bodies ~init:default_body
        in
        let first_case =
          List.fold_right2 ~f:process_case pre_cases pre_bodies ~init:next_case
        in
        let u_cond, _ = expr_use_def cond in
        add_node (Use u_cond) [ first_case ]
    | ForIn_statement (left, right, (body, _))
    | ForOf_statement (left, right, (body, _))
    | ForAwaitOf_statement (left, right, (body, _)) ->
        let loop_check = reserve_id () in
        let context =
          { labels; break = exit; continue = Some loop_check; iter_or_switch = true }
          :: context
        in
        let body_entry = visit_stmt context loop_check body in
        let body_start =
          match left with
          | Left (EVar (V v)) ->
              if Var.Set.mem v candidates
              then add_node (Def (Var.Set.singleton v)) [ body_entry ]
              else body_entry
          | Right (Var, BindingIdent (V v)) ->
              add_node (Def (Var.Set.singleton v)) [ body_entry ]
          | Left e ->
              let u, _ = expr_use_def e in
              add_node (Use u) [ body_entry ]
          | Right (Var, BindingPattern p) ->
              add_node (Def (pattern_defs candidates p)) [ body_entry ]
          | Right (Var, BindingIdent (S _)) | Right ((Let | Const | Using | AwaitUsing), _)
            -> body_entry
        in
        set_node loop_check Nop [ body_start; exit ];
        let u_right, _ = expr_use_def right in
        add_node (Use u_right) [ loop_check ]
    | For_statement (init, cond, update, (body, _)) -> (
        let loop_check = reserve_id () in
        let update_node =
          match update with
          | Some u ->
              let use, def = expr_use_def u in
              add_node (DefUse (def, use)) [ loop_check ]
          | None -> loop_check
        in
        let context =
          { labels; break = exit; continue = Some update_node; iter_or_switch = true }
          :: context
        in
        let body_entry = visit_stmt context update_node body in
        (* Condition check *)
        (match cond with
        | Some c ->
            let u_cond, _ = expr_use_def c in
            set_node loop_check (Use u_cond) [ body_entry; exit ]
        | None -> set_node loop_check Nop [ body_entry ]);
        (* Handle init *)
        match init with
        | Left (Some e) ->
            let u, d = expr_use_def e in
            add_node (DefUse (d, u)) [ loop_check ]
        | Right (Var, decls) ->
            List.fold_right decls ~init:loop_check ~f:(fun decl next ->
                let u, d = decl_use_def decl in
                add_node (DefUse (d, u)) [ next ])
        | Right ((Let | Const | Using | AwaitUsing), _) | Left None -> loop_check)
    | Block _
    | Expression_statement _
    | If_statement _
    | Break_statement _
    | Continue_statement _
    | Variable_statement _
    | Return_statement _
    | Throw_statement _
    | Try_statement _
    | Labelled_statement _
    | Empty_statement
    | Debugger_statement
    | Function_declaration _
    | Class_declaration _
    | With_statement _
    | Import _
    | Export _ -> assert false
  and visit_stmts context exit stmts =
    List.fold_left (List.rev stmts) ~init:exit ~f:(fun next (s, _) ->
        visit_stmt context next s)
  in
  let end_node = add_node Nop [] in
  let start_node = visit_stmts [] end_node stmts in
  let entry =
    if Var.Set.is_empty param_vars
    then start_node
    else add_node (Def param_vars) [ start_node ]
  in
  (* Convert to arrays *)
  let size = !next_id in
  let actions = Array.make size Nop in
  let succs = Array.make size [] in
  List.iter builder.nodes ~f:(fun (id, action, s) ->
      actions.(id) <- action;
      succs.(id) <- s);
  { entry
  ; size
  ; actions
  ; succs
  ; coalescing_hints = builder.hints
  ; try_blocks = builder.tries
  }

(* Fixpoint Computation *)
let compute_liveness pass_stats stmts candidates param_vars =
  let t_cfg = Timer.make () in
  let g = build_cfg stmts candidates param_vars in
  pass_stats.time_cfg <- pass_stats.time_cfg +. Timer.get t_cfg;
  let t_solve = Timer.make () in
  let domain_set = BitSet.create' g.size in
  for i = 0 to g.size - 1 do
    BitSet.set domain_set i
  done;
  let preds = Array.make (Array.length g.succs) [] in
  Array.iteri
    ~f:(fun i l -> List.iter ~f:(fun j -> preds.(j) <- i :: preds.(j)) l)
    g.succs;
  let inv_graph =
    { G.domain = domain_set; iter_children = (fun f i -> List.iter ~f preds.(i)) }
  in
  let transfer_func state node_id =
    let live_in_succs =
      List.fold_left g.succs.(node_id) ~init:Var.Set.empty ~f:(fun acc id ->
          let v = state.(id) in
          Var.Set.union acc v)
    in
    let def = defs_of_action g.actions.(node_id) in
    let use = uses_of_action g.actions.(node_id) in
    (* LiveIn = Use U (LiveOut - Def) *)
    Var.Set.union use (Var.Set.diff live_in_succs def)
  in
  let live_in_map = Solver.f g.size inv_graph transfer_func in
  pass_stats.time_solve <- pass_stats.time_solve +. Timer.get t_solve;
  g, live_in_map

(* Live Range representation: sorted list of disjoint (start, end) intervals.
   We use 2x granularity for positions: each CFG node n at linear position i
   maps to positions 2*i (before the node, for uses) and 2*i+1 (after the node,
   for defs). This allows distinguishing between a variable that is live-in
   before a definition versus live-out after it at the same CFG node. *)
module Live_range = struct
  type interval =
    { start_pos : int
    ; end_pos : int
    }

  type t =
    { id : Var.t (* The variable associated with this range *)
    ; mutable ranges : interval list (* sorted by start_pos *)
    ; mutable assigned : bool
    }

  let create v = { id = v; ranges = []; assigned = false }

  let print_ranges f r =
    Format.fprintf
      f
      "@[%a@]"
      (Format.pp_print_list
         ~pp_sep:(fun f () -> Format.fprintf f ",@,")
         (fun f r -> Format.fprintf f "%d-%d" r.start_pos r.end_pos))
      r

  let print f t = print_ranges f t.ranges

  let add_range t start_pos end_pos =
    let rec loop s e acc = function
      | [] -> List.rev ({ start_pos = s; end_pos = e } :: acc)
      | r :: rest ->
          if e < r.start_pos - 1
          then List.rev_append acc ({ start_pos = s; end_pos = e } :: r :: rest)
          else if s > r.end_pos + 1
          then loop s e (r :: acc) rest
          else
            (* Overlap or adjacent, merge *)
            let new_start = min s r.start_pos in
            let new_end = max e r.end_pos in
            loop new_start new_end acc rest
    in
    t.ranges <-
      (match t.ranges with
      | [] -> [ { start_pos; end_pos } ]
      | r :: _ when r.start_pos > end_pos + 1 -> { start_pos; end_pos } :: t.ranges
      | _ -> loop start_pos end_pos [] t.ranges)

  let add_ranges t other_ranges =
    match t.ranges, other_ranges with
    | [], l | l, [] -> t.ranges <- l
    | l1, l2 ->
        let rec loop acc l1 l2 =
          match l1, l2 with
          | [], l | l, [] -> List.rev_append acc l
          | h1 :: t1, h2 :: t2 ->
              if h1.start_pos < h2.start_pos then step acc h1 t1 l2 else step acc h2 t2 l1
        and step acc current rest other =
          match acc with
          | prev :: acc_rest when prev.end_pos + 1 >= current.start_pos ->
              let merged = { prev with end_pos = max prev.end_pos current.end_pos } in
              loop (merged :: acc_rest) rest other
          | _ -> loop (current :: acc) rest other
        in
        t.ranges <- loop [] l1 l2

  let get_start_pos t =
    match t.ranges with
    | [] -> max_int
    | r :: _ -> r.start_pos

  let get_first_hole t =
    match t.ranges with
    | [] -> 0
    | r :: _ -> r.end_pos + 1

  (* This function consumes ranges before the current position *)
  let rec advance t position =
    match t.ranges with
    | [] -> `Dead
    | r :: rem ->
        if r.end_pos < position
        then (
          t.ranges <- rem;
          advance t position)
        else if r.start_pos > position
        then `Inactive
        else `Active

  let intersects t1 t2 =
    let rec loop l1 l2 =
      match l1, l2 with
      | [], _ | _, [] -> false
      | r1 :: rest1, r2 :: rest2 ->
          if r1.end_pos < r2.start_pos
          then loop rest1 l2
          else if r2.end_pos < r1.start_pos
          then loop l1 rest2
          else true
    in
    loop t1.ranges t2.ranges
end

let compute_live_ranges pass_stats g live_in_map candidates param_vars =
  let t = Timer.make () in
  (* Linearize the CFG (Reverse Post Order) *)
  let visited = Array.make g.size false in
  let layout = Array.make g.size 0 in
  let i = ref g.size in
  let rec list_rev_iter ~f l =
    match l with
    | [] -> ()
    | x :: r ->
        list_rev_iter ~f r;
        f x
  in
  let rec dfs n =
    if not visited.(n)
    then (
      visited.(n) <- true;
      let succs = g.succs.(n) in
      (* Natural order (important for exception handlers):
         if->else, try->catch->finally *)
      list_rev_iter succs ~f:dfs;
      decr i;
      layout.(!i) <- n)
  in
  dfs g.entry;
  let num_reachable = g.size - !i in
  let layout =
    (* Some nodes may be unreachable *)
    if !i = 0 then layout else Array.sub layout ~pos:!i ~len:num_reachable
  in

  (* Map Node -> Linear Index *)
  let node_order = Array.make g.size (-1) in
  Array.iteri ~f:(fun i n -> node_order.(n) <- i) layout;

  let ranges = Var.Hashtbl.create (Var.Set.cardinal candidates) in
  Var.Set.iter (fun v -> Var.Hashtbl.add ranges v (Live_range.create v)) candidates;

  (* Active ranges map: Var -> end_pos of current active range *)
  let active_ranges = Var.Hashtbl.create 64 in

  let commit_range v start_pos end_pos =
    let r = Var.Hashtbl.find ranges v in
    Live_range.add_range r start_pos end_pos
  in

  for order = num_reachable - 1 downto 0 do
    let node_id = layout.(order) in
    let start_idx = 2 * order in
    let end_idx = (2 * order) + 1 in
    let succs = g.succs.(node_id) in
    (* 2x granularity:
       - 2*order: state before the node (uses)
       - 2*order+1: state after the node (defs) *)
    (* Optimization: if the only successor is the next node in linear order
       (fallthrough), we can skip recomputing live_out since it equals the
       live_in of the next iteration. This avoids closing and reopening ranges
       for the common case of sequential statements. *)
    let is_fallthrough =
      match succs with
      | [ s ] -> order < num_reachable - 1 && layout.(order + 1) = s
      | _ -> false
    in
    if not is_fallthrough
    then (
      (* Calculate live_out from successors *)
      let live_out =
        List.fold_left succs ~init:Var.Set.empty ~f:(fun acc sid ->
            Var.Set.union acc live_in_map.(sid))
      in
      (* Process variables that are currently active but not in live_out. *)
      let to_remove = ref [] in
      Var.Hashtbl.iter
        (fun v high ->
          if not (Var.Set.mem v live_out)
          then (
            (* Range ends after this node *)
            commit_range v (end_idx + 1) high;
            to_remove := v :: !to_remove))
        active_ranges;
      List.iter ~f:(Var.Hashtbl.remove active_ranges) !to_remove;
      (* Process variables in live_out but are not active. *)
      Var.Set.iter
        (fun v ->
          if not (Var.Hashtbl.mem active_ranges v)
          then Var.Hashtbl.add active_ranges v end_idx)
        live_out);
    (* 2. Process Defs *)
    let defs = defs_of_action g.actions.(node_id) in
    Var.Set.iter
      (fun v ->
        match Var.Hashtbl.find_opt active_ranges v with
        | Some high ->
            commit_range v end_idx high;
            Var.Hashtbl.remove active_ranges v
        | None ->
            (* Defined but not live out: dead assignment. *)
            commit_range v end_idx end_idx)
      defs;
    (* 3. Process Uses *)
    let uses = uses_of_action g.actions.(node_id) in
    Var.Set.iter
      (fun v ->
        if not (Var.Hashtbl.mem active_ranges v)
        then
          (* Becomes live at use. *)
          Var.Hashtbl.add active_ranges v start_idx)
      uses;
    (* Try-catch/finally liveness extension: when we reach a catch or finally
       handler entry, any variable that is live at this point must have been
       live throughout the entire try body. This is because any statement in
       the try block might throw an exception and jump directly to the handler,
       so the variable's value at any point in the try body might be observed.
       We extend all active live ranges back to the start of the try body. *)
    match Int.Hashtbl.find_opt g.try_blocks node_id with
    | None -> ()
    | Some start_id ->
        let start_order = node_order.(start_id) in
        assert (start_order < order);
        let start_idx = 2 * start_order in
        Var.Hashtbl.iter (fun v high -> commit_range v start_idx high) active_ranges;
        Var.Hashtbl.clear active_ranges
  done;
  (* Close all remaining active ranges at 0 *)
  Var.Hashtbl.iter (fun v high -> commit_range v 0 high) active_ranges;
  (* Mark parameters as live at very start
     (they must have distinct names). *)
  Var.Set.iter (fun v -> commit_range v 0 0) param_vars;
  pass_stats.time_live_range <- pass_stats.time_live_range +. Timer.get t;
  ranges

module Active_pqueue = Pqueue.Make (struct
  type t = Live_range.t

  let compare r r' = compare (Live_range.get_first_hole r) (Live_range.get_first_hole r')
end)

module Inactive_pqueue = Pqueue.Make (struct
  type t = int * Var.t

  let compare (p, _) (p', _) = compare (p : int) p'
end)

(* Linear scan register allocation. Assigns each variable to a representative
   variable (possibly itself) such that variables with the same representative
   have disjoint live ranges and can share the same JavaScript variable name.

   The algorithm processes variables in order of their start position, maintaining:
   - active: variables currently live (sorted by next hole position)
   - inactive: variables in a "hole" in their live range (temporarily not live)
   - free_pool: variables whose live ranges have ended completely

   For each new variable, we try to find a representative in this order:
   1. Hint-based: if this variable is the target of a copy (x = y), try to reuse
      y's representative to enable copy propagation
   2. Free pool: reuse a completely dead variable
   3. Inactive: reuse a variable currently in a hole if ranges don't interfere
   4. Self: use the variable itself (no coalescing) *)
let allocate_registers pass_stats subst ranges hints =
  let hint_count = ref 0 in
  let opportunistic_count = ref 0 in
  let t = Timer.make () in
  (* Sort by Start Position *)
  let sorted_intervals =
    let intervals = Var.Hashtbl.fold (fun _ r acc -> r :: acc) ranges [] in
    List.sort
      ~cmp:(fun a b ->
        Int.compare (Live_range.get_start_pos a) (Live_range.get_start_pos b))
      intervals
  in
  (* List of variables that overlap with the current location *)
  let active = ref Active_pqueue.empty in
  (* List of variables that have started but are currently in a hole *)
  let inactive_queue = ref Inactive_pqueue.empty in
  let inactive = Var.Hashtbl.create 128 in
  (* List of variables which are no longer live *)
  let free_pool = ref [] in
  let rec update_active_queue position =
    match Active_pqueue.find_min !active with
    | exception Not_found -> ()
    | r -> (
        if Live_range.get_first_hole r <= position
        then
          let active' = Active_pqueue.remove_min !active in
          match Live_range.advance r position with
          | `Dead ->
              free_pool := r :: !free_pool;
              active := active';
              update_active_queue position
          | `Inactive ->
              inactive_queue :=
                Inactive_pqueue.add (Live_range.get_start_pos r, r.id) !inactive_queue;
              Var.Hashtbl.replace inactive r.id r;
              active := active';
              update_active_queue position
          | `Active ->
              active := Active_pqueue.add r active';
              update_active_queue position)
  in
  let rec update_inactive_queue position =
    match Inactive_pqueue.find_min !inactive_queue with
    | exception Not_found -> ()
    | p, v -> (
        if p <= position
        then
          let inactive' = Inactive_pqueue.remove_min !inactive_queue in
          match Var.Hashtbl.find_opt inactive v with
          | None ->
              (* Was actually already removed from the queue *)
              inactive_queue := inactive';
              update_inactive_queue position
          | Some r -> (
              match Live_range.advance r position with
              | `Dead ->
                  free_pool := r :: !free_pool;
                  inactive_queue := inactive';
                  Var.Hashtbl.remove inactive v;
                  update_inactive_queue position
              | `Inactive ->
                  inactive_queue :=
                    Inactive_pqueue.add (Live_range.get_start_pos r, r.id) inactive';
                  update_inactive_queue position
              | `Active ->
                  active := Active_pqueue.add r !active;
                  inactive_queue := inactive';
                  Var.Hashtbl.remove inactive v;
                  update_inactive_queue position))
  in
  let rec get_free () =
    match !free_pool with
    | [] -> None
    | r :: rs ->
        free_pool := rs;
        if r.assigned then get_free () else Some r
  in
  List.iter
    ~f:(fun current ->
      let position = Live_range.get_start_pos current in
      (* Update queues *)
      update_active_queue position;
      update_inactive_queue position;
      (* Try hint-based coalescing first. If current variable is the target of
         a copy (current = src), we try to reuse src's representative. This is
         beneficial because if they share the same variable, the copy becomes
         a no-op. We can only do this if the representative is dead, inactive
         with non-interfering ranges, or not yet started. *)
      let hint_repr =
        match Var.Hashtbl.find_opt hints current.Live_range.id with
        | None -> None
        | Some src -> (
            match Var.Tbl.get subst src with
            | None -> None
            | Some var -> (
                let r = Var.Hashtbl.find ranges var in
                match Live_range.advance r position with
                | `Dead ->
                    (* Source is completely dead, safe to reuse *)
                    r.assigned <- true;
                    Some r
                | `Inactive ->
                    (* Source is in a hole; check if remaining ranges interfere *)
                    let conflicts = Live_range.intersects r current in
                    if conflicts
                    then None
                    else (
                      Var.Hashtbl.remove inactive r.id;
                      Some r)
                | `Active ->
                    (* Source is still live, cannot coalesce *)
                    None))
      in
      let repr =
        match hint_repr with
        | Some r ->
            incr hint_count;
            r
        | None -> (
            (* Try to find a valid reuse from inactive.
               We prioritize the inactive list over the free list to implement a
               "Best Fit" strategy. Reusing a variable from the inactive list
               fills a specific "hole" in a lifetime, which is a more constrained
               resource. By using it first, we save the "universally compatible"
               free variables for later intervals that might not fit into
               available holes.

               We iterate using the priority queue [inactive_queue] to check
               variables with the smallest holes first, further optimizing the
               fit. *)
            let candidate =
              let rec loop q count =
                if count >= 50 || Inactive_pqueue.is_empty q
                then None
                else
                  let _, v = Inactive_pqueue.find_min q in
                  let q' = Inactive_pqueue.remove_min q in
                  match Var.Hashtbl.find_opt inactive v with
                  | None -> loop q' count (* Stale, don't count *)
                  | Some iv ->
                      if not (Live_range.intersects iv current)
                      then Some iv
                      else loop q' (count + 1)
              in
              loop !inactive_queue 0
            in
            match candidate with
            | Some r ->
                incr opportunistic_count;
                Var.Hashtbl.remove inactive r.id;
                r
            | None -> (
                match get_free () with
                | Some r ->
                    incr opportunistic_count;
                    r
                | None -> current))
      in
      if not (Var.equal current.id repr.id)
      then (
        (* Remove the names since they can be confusing. *)
        Var.forget_generated_name current.id;
        Var.forget_generated_name repr.id;
        (* Merge current into repr *)
        Live_range.add_ranges repr current.Live_range.ranges);
      Var.Tbl.set subst current.id (Some repr.id);
      active := Active_pqueue.add repr !active)
    sorted_intervals;

  pass_stats.time_allocate <- pass_stats.time_allocate +. Timer.get t;

  !hint_count, !opportunistic_count

(* Per-scope optimization *)
let optimize_scope pass_stats captured_vars subst params stmts =
  let t_collect = Timer.make () in
  let candidates = collect_locals captured_vars params stmts in
  pass_stats.time_collect <- pass_stats.time_collect +. Timer.get t_collect;
  let num_candidates = Var.Set.cardinal candidates in
  pass_stats.candidates <- pass_stats.candidates + num_candidates;
  (* Early exit: no benefit from coalescing with 0-1 candidates *)
  if num_candidates <= 1
  then ()
  else
    let param_vars =
      List.fold_left
        ~f:(fun vars v -> add_var candidates v vars)
        ~init:Var.Set.empty
        (bound_idents_of_params params)
    in
    let g, live_in = compute_liveness pass_stats stmts candidates param_vars in
    if debug ()
    then
      Format.eprintf
        "  candidates: %d, params: %d, stmts: %d, nodes: %d@."
        num_candidates
        (Var.Set.cardinal param_vars)
        (List.length stmts)
        g.size;
    let intervals = compute_live_ranges pass_stats g live_in candidates param_vars in
    if debug ()
    then
      Var.Hashtbl.iter
        (fun _ r ->
          Format.eprintf
            "@[<2>%a:@ %a@]@."
            Code.Var.print
            r.Live_range.id
            Live_range.print
            r)
        intervals;
    if debug ()
    then
      Format.eprintf
        "  hints: %d, intervals computed for %d vars@."
        (Var.Hashtbl.length g.coalescing_hints)
        (Var.Hashtbl.length intervals);
    let hint_count, opportunistic_count =
      allocate_registers pass_stats subst intervals g.coalescing_hints
    in
    pass_stats.hint_coalesced <- pass_stats.hint_coalesced + hint_count;
    pass_stats.opportunistic_coalesced <-
      pass_stats.opportunistic_coalesced + opportunistic_count;
    if debug ()
    then
      Format.eprintf
        "Scope liveness: %d hint + %d opportunistic = %d coalesced@."
        hint_count
        opportunistic_count
        (hint_count + opportunistic_count)

let rename subst program =
  let rename =
    object
      inherit Js_traverse.map

      method! ident i =
        match i with
        | S _ -> i
        | V v -> (
            match Var.Tbl.get subst v with
            | None -> i
            | Some v' -> V v')
    end
  in
  rename#program program

let f program =
  let t = Timer.make () in
  let pass_stats =
    { candidates = 0
    ; hint_coalesced = 0
    ; opportunistic_coalesced = 0
    ; time_collect = 0.
    ; time_cfg = 0.
    ; time_solve = 0.
    ; time_live_range = 0.
    ; time_allocate = 0.
    ; time_mark_captured = 0.
    ; time_rename = 0.
    }
  in
  let captured_vars = Var.Tbl.make () false in
  let subst = Var.Tbl.make () None in
  let visitor =
    object
      inherit Js_traverse.iter as super

      method! fun_decl f =
        (* Optimize inner functions first *)
        super#fun_decl f;
        let _, params, body, _ = f in
        optimize_scope pass_stats captured_vars subst params body;
        mark_captured_variables pass_stats captured_vars f
    end
  in
  visitor#program program;
  (* Also optimize top-level statements *)
  let empty_params = { list = []; rest = None } in
  optimize_scope pass_stats captured_vars subst empty_params program;
  let t_rename = Timer.make () in
  let program = rename subst program in
  pass_stats.time_rename <- Timer.get t_rename;
  if times ()
  then
    Format.eprintf
      "    liveness analysis: %a (collect: %.2f, cfg: %.2f, solve: %.2f, live_range: \
       %.2f, allocate: %.2f, mark_captured: %.2f, rename: %.2f)@."
      Timer.print
      t
      pass_stats.time_collect
      pass_stats.time_cfg
      pass_stats.time_solve
      pass_stats.time_live_range
      pass_stats.time_allocate
      pass_stats.time_mark_captured
      pass_stats.time_rename;
  if stats ()
  then
    Format.eprintf
      "Stats - variable coalescing: %d candidates, %d coalesced (%d hint, %d \
       opportunistic)@."
      pass_stats.candidates
      (pass_stats.hint_coalesced + pass_stats.opportunistic_coalesced)
      pass_stats.hint_coalesced
      pass_stats.opportunistic_coalesced;
  program