Skip to content
Merged
14 changes: 9 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,15 @@ The compiler is designed for fast feedback loops and scales to large codebases:

### Stacked pull requests

When a PR depends on another unmerged PR, create a native GitHub stack with
`gh stack` rather than only targeting the preceding feature branch. Keep the
branches linear and in the same repository, and list branches or PRs from
bottom to top. For existing PRs, use `gh stack link BOTTOM_PR [NEXT_PR...]`,
then verify that GitHub reports stack metadata and runs CI for every PR.
When a PR depends on another unmerged PR, make a native GitHub stack. Keep the
branches linear and in the same repository, then run `gh stack link BOTTOM
[NEXT...]`, listing the stack bottom to top. Each argument is a branch name or
a PR number: the command pushes each branch, reuses the PR that already exists
for it, and opens one where there is none, so branches alone are enough. Open
the PRs yourself first if you want to write their titles and descriptions.

Linking sets each PR's base to the branch below it, leaving only the bottom PR
on `master`. CI runs on every PR in the stack.

### Code Quality

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
- Split `lambda.ml` into the IR and its traversals, static exits and path translation, so the module defining `Lambda.t` no longer reaches into `Env` or `Path`. https://github.com/rescript-lang/rescript/pull/8618
- Record a record field's `@as` rename on the declaration instead of re-reading the attribute, so every place that needs the runtime name reads one field. https://github.com/rescript-lang/rescript/pull/8619
- Record a variant constructor's `@as` tag on the declaration instead of re-interpreting its attributes, keeping the source spelling for printing. https://github.com/rescript-lang/rescript/pull/8619
- Optimization passes now return the term they were given when they change nothing, rather than rebuilding an identical one. https://github.com/rescript-lang/rescript/pull/8620
- Merge the duplicate Lam intermediate representation into Lambda, removing the conversion layer and obsolete supporting infrastructure. Lambda is now a single private, normalized representation, with generated JavaScript remaining semantically unchanged. https://github.com/rescript-lang/rescript/pull/8608
- Add genType and source map controls and output to the developer playground. https://github.com/rescript-lang/rescript/pull/8448
- Rework the object-type representation end to end: object rows are plain field chains carrying a per-field mutability state (no phantom setter members), object literals are typed directly and property access and assignment are first-class AST and Lambda nodes shared between the Lambda and JS pipelines, and dead class-system remnants (the field-presence lattice, the class-abbreviation memo on object types, method-send typing) are removed. https://github.com/rescript-lang/rescript/pull/8597
Expand Down
33 changes: 28 additions & 5 deletions compiler/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,37 @@ Typedtree translation in `compiler/ml/translcore.ml` and
`compiler/ml/translmod.ml` produces the `Lambda` representation defined in
`compiler/ml/lambda.mli`.

[`lam_convert.ml`](lam_convert.ml)
: Collects the modules a compilation unit depends on, read off the Lambda
term.

`lam_pass_*.ml` and the other `lam_*.ml` modules
: Analyze and transform Lambda. [`lam_compile_main.ml`](lam_compile_main.ml)
coordinates the backend pass sequence; read it before inserting or
reordering a pass.

The sequence is hand-unrolled rather than iterated to a fixed point. Only
`simplify_alias` reads the statistics, and a fresh `collect_info` runs
immediately before each of its three rounds. `simplify_lets` and `sroa`
compute what they need themselves. Each `-debug-ir` dump is named after the
pass whose output it holds.

| # | pass | statistics |
|---|---|---|
| 1 | `collapse_var_aliases` | |
| 2 | `deep_flatten` | |
| 3 | `simplify_exits` | |
| 4 | `simplify_alias` | reads a snapshot taken just before |
| 5 | `deep_flatten` | |
| 6 | `simplify_alias` | reads a snapshot taken just before |
| 7 | `deep_flatten` | |
| 8 | `simplify_exits` | |
| 9 | `simplify_alias` | reads a snapshot taken just before |
| 10 | `simplify_lets` | own occurrence count |
| 11 | `sroa` | own field-use classification |
| 12 | `simplify_exits` | |
| 13 | `guard_raises` | |

A snapshot is fresh when its pass starts, but `simplify_alias` also mutates
the table as it rewrites, so entries can describe an earlier version of the
term by the time the pass finishes.

[`lam_compile.ml`](lam_compile.ml)
: Lowers Lambda to JavaScript IR. Primitive-specific and FFI lowering is split
into `lam_compile_primitive.ml`, `lam_compile_external_call.ml`, and related
Expand All @@ -35,7 +57,8 @@ Typedtree translation in `compiler/ml/translcore.ml` and
## Changing a representation

`Lambda.t` is private: every term is built through the constructors in
[`../ml/lambda.mli`](../ml/lambda.mli), six of which normalize as they build.
[`../ml/lambda.mli`](../ml/lambda.mli), seven of which normalize as they
build: `apply`, `prim`, `switch`, `stringswitch`, `if_`, `seq` and `not_`.
A constructor may replace a node with an equivalent one, but may not move code
between branches - that is what a pass is for. When adding or changing a
constructor, search every producer, traversal, optimizer, printer, serializer,
Expand Down
30 changes: 13 additions & 17 deletions compiler/core/lam_compile_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -290,48 +290,44 @@ let compile (output_prefix : string) export_idents hoisted (lam : Lambda.t) =
Lam_compile_env.reset ()
in
let may_required_modules = required_modules lam in
let lam = d "initial" lam in
let lam =
Lam_pass_collapse_var_aliases.collapse ~exports:export_ident_sets lam
in

let lam = d "initial" lam in
let lam = d "collapse_var_aliases" lam in
let lam = Lam_pass_deep_flatten.deep_flatten lam in
let lam = d "flatten0" lam in
let lam = d "deep_flatten 1" lam in
let meta : Lam_stats.t = Lam_stats.make ~export_idents ~export_ident_sets in
let lam =
let lam =
lam |> d "flatten1" |> Lam_pass_exits.simplify_exits |> d "simplify_exits"
lam |> Lam_pass_exits.simplify_exits |> d "simplify_exits 1"
|> (fun lam ->
Lam_pass_collect.collect_info meta lam;
if debug_ir then
Ext_log.dwarn ~__POS__ "Before simplify_alias: %a@." Lam_stats.print
meta;
lam)
|> Lam_pass_remove_alias.simplify_alias meta
|> d "simplify_alias" |> Lam_pass_deep_flatten.deep_flatten
|> d "flatten2"
|> d "simplify_alias 1" |> Lam_pass_deep_flatten.deep_flatten
|> d "deep_flatten 2"
in
(* Inling happens*)

(* Inlining happens *)
let () = Lam_pass_collect.collect_info meta lam in
let lam = Lam_pass_remove_alias.simplify_alias meta lam in
let lam = d "simplify_alias 2" lam in
let lam = Lam_pass_deep_flatten.deep_flatten lam in
let lam = d "deep_flatten 3" lam in
let lam = lam |> Lam_pass_exits.simplify_exits in
let () = Lam_pass_collect.collect_info meta lam in

lam |> d "simplify_alias_before"
lam |> d "simplify_exits 2"
|> Lam_pass_remove_alias.simplify_alias meta
|> d "before-simplify_lets"
|> d "simplify_alias 3"
(* we should investigate a better way to put different passes : )*)
|> Lam_pass_lets_dce.simplify_lets
|> d "simplify_lets" |> Lam_pass_sroa.simplify |> d "sroa"
|> d "before-simplify-exits"
(* |> (fun lam -> Lam_pass_collect.collect_info meta lam
; Lam_pass_remove_alias.simplify_alias meta lam) *)
(* |> Lam_group_pass.scc_pass
|> d "scc" *)
|> Lam_pass_exits.simplify_exits
|> Lam_pass_guard_raises.guard_raises |> d "simplify_lets"
|> Lam_pass_exits.simplify_exits |> d "simplify_exits 3"
|> Lam_pass_guard_raises.guard_raises |> d "guard_raises"
|> fun lam ->
if debug_ir then
Ext_log.dwarn ~__POS__ "Before coercion: %a@." Lam_stats.print meta;
Expand Down
144 changes: 77 additions & 67 deletions compiler/core/lam_exit_count.ml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ let count_exit (exits : collection) i = Hash_int.find_default exits i 0
let incr_exit (exits : collection) i =
Hash_int.add_or_update exits i 1 ~update:succ

(* Whether [Lam_pass_exits] could rewrite anything here. It names the two
nodes that pass touches, so a new case there that rewrites something else
has to be added here too, or the pass will silently stop firing. *)
let rec has_static_exit (lam : Lambda.t) =
match lam with
| Lstaticraise _ | Lstaticcatch _ -> true
| _ -> Lambda_traverse.shallow_exists has_static_exit lam

(**
This funcition counts how each [exit] is used, it will affect how the following optimizations performed.

Expand All @@ -48,70 +56,72 @@ let incr_exit (exits : collection) i =
For Lswitch, if it is not exhuastive pattern match, default will be counted twice.
Since for pattern match, we will test whether it is an integer or block, both have default cases predicate: [sw_consts_full] vs nconsts
*)
let count_helper (lam : Lambda.t) : collection =
let exits : collection = Hash_int.create 17 in
let rec count (lam : Lambda.t) =
match lam with
| Lstaticraise (i, ls) ->
incr_exit exits i;
Ext_list.iter ls count
| Lstaticcatch (l1, (i, _), l2) ->
count l1;
if count_exit exits i > 0 then count l2
| Lstringswitch (l, sw, d) ->
count l;
Ext_list.iter_snd sw count;
Ext_option.iter d count
| Lglobal_module _ | Lvar _ | Lconst _ -> ()
| Lapply {ap_func; ap_args; _} ->
count ap_func;
Ext_list.iter ap_args count
| Lfunction {body} -> count body
| Llet (_, _, l1, l2) ->
count l2;
count l1
| Lletrec (bindings, body) ->
Ext_list.iter_snd bindings count;
count body
| Lprim {args; _} -> List.iter count args
| Lswitch (l, sw) ->
count_default sw;
count l;
Ext_list.iter_snd sw.sw_consts count;
Ext_list.iter_snd sw.sw_blocks count
| Ltrywith (l1, _v, l2) ->
count l1;
count l2
| Lifthenelse (l1, l2, l3) ->
count l1;
count l2;
count l3
| Lsequence (l1, l2) ->
count l1;
count l2
| Lbreak | Lcontinue -> ()
| Lwhile (l1, l2) ->
count l1;
count l2
| Lfor (_, l1, l2, _dir, l3) ->
count l1;
count l2;
count l3
| Lfor_of (_, l1, l2) ->
count l1;
count l2
| Lfor_await_of (_, l1, l2) ->
count l1;
count l2
| Lassign (_, l) -> count l
and count_default sw =
match sw.sw_failaction with
| None -> ()
| Some al ->
if (not sw.sw_consts_full) && not sw.sw_blocks_full then (
count al;
count al)
else count al
in
count lam;
exits
let count_helper (lam : Lambda.t) : collection option =
if not (has_static_exit lam) then None
else
let exits : collection = Hash_int.create 17 in
let rec count (lam : Lambda.t) =
match lam with
| Lstaticraise (i, ls) ->
incr_exit exits i;
Ext_list.iter ls count
| Lstaticcatch (l1, (i, _), l2) ->
count l1;
if count_exit exits i > 0 then count l2
| Lstringswitch (l, sw, d) ->
count l;
Ext_list.iter_snd sw count;
Ext_option.iter d count
| Lglobal_module _ | Lvar _ | Lconst _ -> ()
| Lapply {ap_func; ap_args; _} ->
count ap_func;
Ext_list.iter ap_args count
| Lfunction {body} -> count body
| Llet (_, _, l1, l2) ->
count l2;
count l1
| Lletrec (bindings, body) ->
Ext_list.iter_snd bindings count;
count body
| Lprim {args; _} -> List.iter count args
| Lswitch (l, sw) ->
count_default sw;
count l;
Ext_list.iter_snd sw.sw_consts count;
Ext_list.iter_snd sw.sw_blocks count
| Ltrywith (l1, _v, l2) ->
count l1;
count l2
| Lifthenelse (l1, l2, l3) ->
count l1;
count l2;
count l3
| Lsequence (l1, l2) ->
count l1;
count l2
| Lbreak | Lcontinue -> ()
| Lwhile (l1, l2) ->
count l1;
count l2
| Lfor (_, l1, l2, _dir, l3) ->
count l1;
count l2;
count l3
| Lfor_of (_, l1, l2) ->
count l1;
count l2
| Lfor_await_of (_, l1, l2) ->
count l1;
count l2
| Lassign (_, l) -> count l
and count_default sw =
match sw.sw_failaction with
| None -> ()
| Some al ->
if (not sw.sw_consts_full) && not sw.sw_blocks_full then (
count al;
count al)
else count al
in
count lam;
Some exits
3 changes: 2 additions & 1 deletion compiler/core/lam_exit_count.mli
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

type collection

val count_helper : Lambda.t -> collection
val count_helper : Lambda.t -> collection option
(** [None] when the term holds no static exit, so nothing needs rewriting. *)

val count_exit : collection -> int -> int
45 changes: 7 additions & 38 deletions compiler/core/lam_pass_collapse_var_aliases.ml
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,20 @@ let rec resolve tbl id =
| Some id' -> resolve tbl id'

let collapse ~exports (lam : Lambda.t) : Lambda.t =
let tbl = Hash_ident.create 64 in
let tbl = Hash_ident.create 16 in
let rec go (lam : Lambda.t) : Lambda.t =
match lam with
| Lvar x -> Lambda.var (resolve tbl x)
| Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> lam
| Lapply {ap_func; ap_args; ap_info; ap_transformed_jsx} ->
Lambda.apply (go ap_func) (Ext_list.map ap_args go) ap_info
~ap_transformed_jsx
| Lfunction {params; body; attr; loc} ->
Lambda.function_ ~loc ~attr ~params ~body:(go body)
| Lvar x ->
let x' = resolve tbl x in
if x' == x then lam else Lambda.var x'
| Llet (Alias, id, Lvar u, body) ->
let u = resolve tbl u in
Hash_ident.add tbl id u;
(* The binding is dropped unless the name is exported, in which case it
has to survive under its own name. *)
if Set_ident.mem exports id then
Lambda.let_ Alias id (Lambda.var u) (go body)
else go body
| Llet (kind, id, arg, body) -> Lambda.let_ kind id (go arg) (go body)
| Lletrec (bindings, body) ->
Lambda.letrec (Ext_list.map_snd bindings go) (go body)
| Lprim {primitive; args; loc} ->
Lambda.prim ~primitive ~args:(Ext_list.map args go) loc
| Lswitch (arg, sw) ->
Lambda.switch (go arg)
{
sw with
sw_consts = Ext_list.map_snd sw.sw_consts go;
sw_blocks = Ext_list.map_snd sw.sw_blocks go;
sw_failaction = Ext_option.map sw.sw_failaction go;
}
| Lstringswitch (arg, cases, default) ->
Lambda.stringswitch (go arg)
(Ext_list.map_snd cases go)
(Ext_option.map default go)
| Lstaticraise (i, args) -> Lambda.staticraise i (Ext_list.map args go)
| Lstaticcatch (body, ids, handler) ->
Lambda.staticcatch (go body) ids (go handler)
| Ltrywith (body, id, handler) -> Lambda.try_ (go body) id (go handler)
| Lifthenelse (b, t, e) -> Lambda.if_ (go b) (go t) (go e)
| Lsequence (a, b) -> Lambda.seq (go a) (go b)
| Lwhile (b, body) -> Lambda.while_ (go b) (go body)
| Lfor (id, lo, hi, dir, body) ->
Lambda.for_ id (go lo) (go hi) dir (go body)
| Lfor_of (id, iterable, body) -> Lambda.for_of id (go iterable) (go body)
| Lfor_await_of (id, iterable, body) ->
Lambda.for_await_of id (go iterable) (go body)
| Lassign (id, e) -> Lambda.assign id (go e)
| _ -> Lambda_traverse.shallow_map_sharing go lam
in
go lam
2 changes: 1 addition & 1 deletion compiler/core/lam_pass_count.ml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ let absorb_info (x : used_info) (y : used_info) =
so uses of outer bindings are marked as captured. The optimizer uses the
captured flag to restrict inlining without inflating the occurrence count. *)
let collect_occurs lam : occ_tbl =
let occ : occ_tbl = Hash_ident.create 83 in
let occ : occ_tbl = Hash_ident.create 16 in

(* Current use count of a variable. *)
let used v =
Expand Down
Loading
Loading