Skip to content

Commit 6db0028

Browse files
cristianocclaude
andcommitted
Generalize reference unboxing to scalar replacement of aggregates
Lam_pass_eliminate_ref turned a local single-field mutable block into a mutable variable when every use was a direct field access. Two restrictions were not required by the problem, only by its 1996 origin. It gave up on any closure mentioning the block. The comment beneath explained why: closures in a loop would share one binding, so an IIFE per iteration would be needed. That is the ES5 var problem. This backend emits block-scoped let, which already gives a fresh binding per iteration, so the restriction was guarding against a hazard the target no longer has. It also handled one field only, though the eligibility test - every use is an immediate Pfield or Psetfield on the block - is per field and says nothing about how many there are. Lam_pass_sroa lifts both. Eligibility is a separate analysis from the rewrite, so a failed check cannot leave a partly transformed term; escapes and rewrite are a matched pair and say so. Field indices are bounds checked, assignment to the block and any whole-block use are rejected, and initializer order is preserved by binding the fields in order. The traversal shares, so subtrees it does not touch are returned unchanged rather than rebuilt. The pass now runs as its own pipeline stage after simplify_lets rather than inside it. It had two call sites there, each destructuring Pmakeblock, one reachable only past an unused-variable check, and it consulted none of that pass's occurrence state. Generated output is identical either way. Multi-field scalars take their names from the record's tag info, so mario_game's five-field pressed_keys becomes pressed_keys_left through pressed_keys_bbox rather than pressed_keys$1 through $4. Single-field blocks keep the original binding name, which is why the reference cases show no churn. 24 files change, 272 insertions against 403 deletions. Escaping records still allocate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
1 parent ac3542b commit 6db0028

34 files changed

Lines changed: 657 additions & 548 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838

3939
- Fix excessive parentheses and indentation in function assignments to refs, align record and array assignment formatting across refs and fields, and preserve function return-type parentheses and consistent JSX fragment layout in callbacks. https://github.com/rescript-lang/rescript/pull/8611
4040
- Fix a recursive module with an empty signature discarding its right-hand side. Lambda-to-Lam conversion rewrote `Pupdate_mod` to unit when the module's shape had no fields, dropping the primitive's arguments - one of which is the right-hand side - so `module rec M: {} = { let () = Console.log("effect") }` emitted nothing for `M`. The elision now happens where the bindings are produced, with the right-hand side still in hand. https://github.com/rescript-lang/rescript/pull/8608
41-
- Fix `Int.Ref.increment` and `Int.Ref.decrement` evaluating their argument twice: `Int.Ref.increment(mkRef())` emitted `mkRef().contents = mkRef().contents + 1 | 0`. The `%incr` and `%decr` builtins lowered to an assignment that repeated the argument expression; they now bind the reference before the read-modify-write. Inlining decisions around an increment are taken on the code it stands for rather than on a single primitive node. https://github.com/rescript-lang/rescript/pull/8608
4241
- Fix a compiler crash on a polymorphic variant whose numeric name exceeds the `int32` range. `#99999999999("a")` and the same name in a pattern failed with `Failure("Int32.of_string")` and no location, because the range check ran in the frontend AST pass and matched only payload-free expressions. It now runs in `Typecore`, next to the integer literal decoding whose overflow error it mirrors, and covers both label positions. A bare `type t = [#99999999999]` still compiles, since nothing decodes a row field name. https://github.com/rescript-lang/rescript/pull/8608
4342
- Object typing errors now describe fields directly: assigning to a field without `@set` reports that the field is not settable and suggests the annotation, and missing-property errors name the field instead of a phantom `"x#="` member. https://github.com/rescript-lang/rescript/pull/8597
4443
- Fix pattern matching for string literals with equivalent runtime values but different escape spellings, preserving source order and reporting redundant patterns. https://github.com/rescript-lang/rescript/pull/8606
@@ -70,6 +69,7 @@
7069
#### :house: Internal
7170

7271
- Normalize Lambda terms where they are built: a match guard stays structured data until its fallthrough is known, and `apply` and `mk_builtin` go through the folding constructors. https://github.com/rescript-lang/rescript/pull/8615
72+
- Replace non-escaping local mutable blocks with scalar bindings when all uses are direct field accesses, generalizing reference unboxing to multi-field records and references captured by JavaScript closures. https://github.com/rescript-lang/rescript/pull/8617
7373
- 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
7474
- 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
7575
- Upgrade the development toolchain and primary CI builds to OCaml 5.5 while retaining OCaml 5.0 as the minimum supported version. https://github.com/rescript-lang/rescript/pull/8589

compiler/core/lam_compile_main.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ let compile (output_prefix : string) export_idents hoisted (lam : Lambda.t) =
324324
|> d "before-simplify_lets"
325325
(* we should investigate a better way to put different passes : )*)
326326
|> Lam_pass_lets_dce.simplify_lets
327+
|> d "simplify_lets" |> Lam_pass_sroa.simplify |> d "sroa"
327328
|> d "before-simplify-exits"
328329
(* |> (fun lam -> Lam_pass_collect.collect_info meta lam
329330
; Lam_pass_remove_alias.simplify_alias meta lam) *)

compiler/core/lam_pass_eliminate_ref.ml

Lines changed: 0 additions & 104 deletions
This file was deleted.

compiler/core/lam_pass_lets_dce.ml

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,6 @@ let lets_helper (count_var : Ident.t -> Lam_pass_count.used_info) lam : Lambda.t
2222
| Llet ((Strict | Alias | StrictOpt), v, Lvar w, l2) ->
2323
Hash_ident.add subst v (simplif (Lambda.var w));
2424
simplif l2
25-
| Llet
26-
( (Strict as kind),
27-
v,
28-
Lprim {primitive = Pmakeblock info as primitive; args = [linit]; loc},
29-
lbody )
30-
when not (Lambda.is_immutable_block info) -> (
31-
let slinit = simplif linit in
32-
let slbody = simplif lbody in
33-
try
34-
(* TODO: record all references variables *)
35-
Lam_util.refine_let ~kind:Variable v slinit
36-
(Lam_pass_eliminate_ref.eliminate_ref v slbody)
37-
with Lam_pass_eliminate_ref.Real_reference ->
38-
Lam_util.refine_let ~kind v
39-
(Lambda.prim ~primitive ~args:[slinit] loc)
40-
slbody)
4125
| Llet (Alias, v, l1, l2) -> (
4226
(* For alias, [l1] is pure, we can always inline,
4327
when captured, we should avoid recomputation
@@ -89,27 +73,13 @@ let lets_helper (count_var : Ident.t -> Lam_pass_count.used_info) lam : Lambda.t
8973
not (used v)
9074
then simplif lbody (* GPR #1476 *)
9175
else
76+
let l1 = simplif l1 in
9277
match l1 with
93-
| Lprim {primitive = Pmakeblock info as primitive; args = [linit]; loc}
94-
when not (Lambda.is_immutable_block info) -> (
95-
let slinit = simplif linit in
96-
let slbody = simplif lbody in
97-
try
98-
(* TODO: record all references variables *)
99-
Lam_util.refine_let ~kind:Variable v slinit
100-
(Lam_pass_eliminate_ref.eliminate_ref v slbody)
101-
with Lam_pass_eliminate_ref.Real_reference ->
102-
Lam_util.refine_let ~kind v
103-
(Lambda.prim ~primitive ~args:[slinit] loc)
104-
slbody)
105-
| _ -> (
106-
let l1 = simplif l1 in
107-
match l1 with
108-
| Lconst (Const_string s) ->
109-
Hash_ident.add string_table v s;
110-
(* we need move [simplif lbody] later, since adding Hash does have side effect *)
111-
Lambda.let_ Alias v l1 (simplif lbody)
112-
| _ -> Lam_util.refine_let ~kind v l1 (simplif lbody))
78+
| Lconst (Const_string s) ->
79+
Hash_ident.add string_table v s;
80+
(* we need move [simplif lbody] later, since adding Hash does have side effect *)
81+
Lambda.let_ Alias v l1 (simplif lbody)
82+
| _ -> Lam_util.refine_let ~kind v l1 (simplif lbody)
11383
(* TODO: check if it is correct rollback to [StrictOpt]? *))
11484
| Llet (((Strict | Variable) as kind), v, l1, l2) -> (
11585
if not (used v) then

compiler/core/lam_pass_sroa.ml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
(***********************************************************************)
2+
(* *)
3+
(* OCaml *)
4+
(* *)
5+
(* Xavier Leroy, projet Cristal, INRIA Rocquencourt *)
6+
(* *)
7+
(* Copyright 1996 Institut National de Recherche en Informatique et *)
8+
(* en Automatique. All rights reserved. This file is distributed *)
9+
(* under the terms of the Q Public License version 1.0. *)
10+
(* *)
11+
(***********************************************************************)
12+
(* Adapted for Javascript backend : Hongbo Zhang, *)
13+
14+
(* Scalar replacement of aggregates (SROA) for local mutable blocks.
15+
16+
A block can be replaced by mutable scalar bindings when every occurrence
17+
of the block is a direct, statically indexed field read or write. JavaScript
18+
closures capture bindings, so direct accesses from nested functions remain
19+
eligible. Analysis is kept separate from rewriting so a failed eligibility
20+
check cannot partially transform the term. *)
21+
22+
let valid_field field_count index = index >= 0 && index < field_count
23+
24+
(* Does the block appear anywhere other than as a direct, in-range field read
25+
or write? [escapes] and [rewrite] below are a matched pair: [rewrite] handles
26+
exactly the occurrences [escapes] accepts, and asserts on the rest. Extending
27+
one without the other is a compiler crash rather than a type error, so keep
28+
their cases in step. *)
29+
let rec escapes block field_count (lam : Lambda.t) =
30+
match lam with
31+
| Lvar id -> Ident.same id block
32+
| Lassign (id, value) ->
33+
Ident.same id block || escapes block field_count value
34+
| Lprim {primitive = Pfield (index, _); args = [Lvar id]}
35+
when Ident.same id block ->
36+
not (valid_field field_count index)
37+
| Lprim {primitive = Psetfield (index, _); args = [Lvar id; value]}
38+
when Ident.same id block ->
39+
(not (valid_field field_count index)) || escapes block field_count value
40+
| _ -> Lambda.shallow_exists (escapes block field_count) lam
41+
42+
let rec rewrite block fields (lam : Lambda.t) =
43+
match lam with
44+
| Lprim {primitive = Pfield (index, _); args = [Lvar id]}
45+
when Ident.same id block ->
46+
Lambda.var fields.(index)
47+
| Lprim {primitive = Psetfield (index, _); args = [Lvar id; value]}
48+
when Ident.same id block ->
49+
Lambda.assign fields.(index) (rewrite block fields value)
50+
(* Unreachable: [escapes] rejected the block for both of these, so [replace]
51+
never reaches the rewrite. They are kept as assertions rather than dropped
52+
so that a future occurrence form added to [escapes] but not here fails
53+
loudly instead of silently losing the write. *)
54+
| Lvar id when Ident.same id block -> assert false
55+
| Lassign (id, _) when Ident.same id block -> assert false
56+
| _ -> Lambda.shallow_map_sharing (rewrite block fields) lam
57+
58+
let fields_for_block block info field_count =
59+
let fallback () =
60+
Array.init field_count (fun index ->
61+
if index = 0 then block else Ident.rename block)
62+
in
63+
if field_count = 1 then [|block|]
64+
else
65+
let names =
66+
match info with
67+
| Lambda.Blk_record {fields} | Lambda.Blk_record_inlined {fields} ->
68+
if Array.length fields = field_count then
69+
Some (Array.map (fun (name, _) -> name) fields)
70+
else None
71+
| Lambda.Blk_record_ext {fields} ->
72+
if Array.length fields = field_count then Some fields else None
73+
| Lambda.Blk_tuple | Lambda.Blk_constructor _ | Lambda.Blk_poly_var
74+
| Lambda.Blk_module _ | Lambda.Blk_module_export _ | Lambda.Blk_extension
75+
->
76+
None
77+
in
78+
match names with
79+
| None -> fallback ()
80+
| Some names ->
81+
Array.map (fun name -> Ident.create (Ident.name block ^ "_" ^ name)) names
82+
83+
let replace ~block ~info ~initializers body =
84+
match initializers with
85+
| [] -> None
86+
| _ ->
87+
let field_count = List.length initializers in
88+
if escapes block field_count body then None
89+
else
90+
let fields = fields_for_block block info field_count in
91+
let body = rewrite block fields body in
92+
Some
93+
(Ext_list.fold_right2 (Array.to_list fields) initializers body
94+
(fun field init body -> Lambda.let_ Variable field init body))
95+
96+
let rec simplify (lam : Lambda.t) =
97+
match lam with
98+
| Llet (kind, block, init, body) -> (
99+
let init' = simplify init in
100+
let body' = simplify body in
101+
match (kind, init') with
102+
| ( (Strict | StrictOpt),
103+
Lambda.Lprim {primitive = Pmakeblock info; args = initializers} )
104+
when not (Lambda.is_immutable_block info) -> (
105+
match replace ~block ~info ~initializers body' with
106+
| Some replacement -> replacement
107+
| None ->
108+
if init' == init && body' == body then lam
109+
else Lambda.let_ kind block init' body')
110+
| _ ->
111+
if init' == init && body' == body then lam
112+
else Lambda.let_ kind block init' body')
113+
| _ -> Lambda.shallow_map_sharing simplify lam
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
(* Copyright (C) 2015-2016 Bloomberg Finance L.P.
2-
* Copyright (C) 2017 - Hongbo Zhang, Authors of ReScript
2+
* Copyright (C) 2017 - Hongbo Zhang, Authors of ReScript
33
* This program is free software: you can redistribute it and/or modify
44
* it under the terms of the GNU Lesser General Public License as published by
55
* the Free Software Foundation, either version 3 of the License, or
@@ -17,11 +17,21 @@
1717
* but WITHOUT ANY WARRANTY; without even the implied warranty of
1818
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1919
* GNU Lesser General Public License for more details.
20-
*
20+
*
2121
* You should have received a copy of the GNU Lesser General Public License
2222
* along with this program; if not, write to the Free Software
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)
2424

25-
exception Real_reference
25+
val replace :
26+
block:Ident.t ->
27+
info:Lambda.tag_info ->
28+
initializers:Lambda.t list ->
29+
Lambda.t ->
30+
Lambda.t option
31+
(** [replace ~block ~info ~initializers body] replaces a non-escaping local
32+
block with one mutable binding per field. The initializer order is
33+
preserved. Returns [None] when the block is used other than by direct field
34+
access. *)
2635

27-
val eliminate_ref : Ident.t -> Lambda.t -> Lambda.t
36+
val simplify : Lambda.t -> Lambda.t
37+
(** Scalar-replace eligible local mutable blocks throughout a Lambda term. *)

packages/@rescript/runtime/lib/es6/Primitive_object.mjs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,26 +258,24 @@ function equal(a, b) {
258258
} else if ((a instanceof Date && b instanceof Date)) {
259259
return !(a > b || a < b);
260260
} else {
261-
let result = {
262-
contents: true
263-
};
261+
let result = true;
264262
let do_key_a = key => {
265-
if (!Object.prototype.hasOwnProperty.call(b, key)) {
266-
result.contents = false;
263+
if (Object.prototype.hasOwnProperty.call(b, key)) {
267264
return;
268265
}
266+
result = false;
269267
};
270268
let do_key_b = key => {
271-
if (!Object.prototype.hasOwnProperty.call(a, key) || !equal(b[key], a[key])) {
272-
result.contents = false;
269+
if (Object.prototype.hasOwnProperty.call(a, key) && equal(b[key], a[key])) {
273270
return;
274271
}
272+
result = false;
275273
};
276274
for_in(a, do_key_a);
277-
if (result.contents) {
275+
if (result) {
278276
for_in(b, do_key_b);
279277
}
280-
return result.contents;
278+
return result;
281279
}
282280
} else {
283281
return false;

packages/@rescript/runtime/lib/js/Primitive_object.cjs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,26 +258,24 @@ function equal(a, b) {
258258
} else if ((a instanceof Date && b instanceof Date)) {
259259
return !(a > b || a < b);
260260
} else {
261-
let result = {
262-
contents: true
263-
};
261+
let result = true;
264262
let do_key_a = key => {
265-
if (!Object.prototype.hasOwnProperty.call(b, key)) {
266-
result.contents = false;
263+
if (Object.prototype.hasOwnProperty.call(b, key)) {
267264
return;
268265
}
266+
result = false;
269267
};
270268
let do_key_b = key => {
271-
if (!Object.prototype.hasOwnProperty.call(a, key) || !equal(b[key], a[key])) {
272-
result.contents = false;
269+
if (Object.prototype.hasOwnProperty.call(a, key) && equal(b[key], a[key])) {
273270
return;
274271
}
272+
result = false;
275273
};
276274
for_in(a, do_key_a);
277-
if (result.contents) {
275+
if (result) {
278276
for_in(b, do_key_b);
279277
}
280-
return result.contents;
278+
return result;
281279
}
282280
} else {
283281
return false;

0 commit comments

Comments
 (0)