Skip to content

Commit 85c954c

Browse files
cristianocclaude
andcommitted
Read back the sharing changes for clarity
Reviewing the sharing series on clarity rather than allocation found three places where chasing the property made the code worse, all of them mine. simplify_alias's string switch had become a when-guard containing a match, which ran the same lookup twice and left an arm the guard makes unreachable. It now finds the constant once and branches on that. regroups_binding mirrors flatten's cases one for one, including why a null conversion of a variable is left alone while any other one is split. Nothing said the two have to stay in step, or that drifting costs the flattening silently, because the binding then takes the fast path and never reaches flatten at all. Two passes bound `as original` for a value already in scope as `lam`, giving one idiom two spellings across seven passes. The traversal every pass delegates to had no test. Breaking the sharing in its Lapply and Lswitch arms leaves every fixture in the repository byte for byte identical and no test failing, while all seven passes quietly lose the property. The new test checks each constructor twice: that an identity map hands the node back, and that a replacing map does not, since a node whose children were never visited would pass the first by doing nothing. Signed-off-by: Cristiano Calcagno <ccrisccris@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
1 parent 78f2bbf commit 85c954c

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

compiler/core/lam_pass_deep_flatten.ml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ let rec rhs_is_beta_residue (lam : Lambda.t) =
131131
(* [flatten] restructures a binding only when it hoists something out of the
132132
right hand side, splits a null conversion, or eliminates a tuple. Every
133133
other binding comes back as the same binding, so it can be rebuilt in place
134-
and shared instead of taken apart and reassembled. *)
134+
and shared instead of taken apart and reassembled.
135+
136+
This mirrors [flatten]'s own cases one for one, including why a null
137+
conversion of a variable is left alone while any other one is split, so a
138+
case added there that restructures has to be added here too. Drifting apart
139+
costs the flattening, silently: the binding takes the fast path and is never
140+
handed to [flatten] at all. *)
135141
let regroups_binding (str : Lambda.let_kind) (id : Ident.t) (arg : Lambda.t) =
136142
if rhs_is_beta_residue arg then false
137143
else
@@ -223,7 +229,7 @@ let deep_flatten (lam : Lambda.t) : Lambda.t =
223229
| Llet _ ->
224230
let res, groups = flatten [] lam in
225231
lambda_of_groups res ~rev_bindings:groups
226-
| Lletrec (bind_args, body) as original -> (
232+
| Lletrec (bind_args, body) -> (
227233
(* Attention: don't mess up with internal {let rec} *)
228234
(* Keep the mapped list so a group from which nothing can be extracted
229235
remains physically shared when neither its bindings nor body change. *)
@@ -252,7 +258,7 @@ let deep_flatten (lam : Lambda.t) : Lambda.t =
252258
let rev_wrap, recursive_bindings = extract [] groups in
253259
let body' = aux body in
254260
match rev_wrap with
255-
| [] when groups == bind_args && body' == body -> original
261+
| [] when groups == bind_args && body' == body -> lam
256262
| [] -> Lambda.letrec groups body'
257263
| _ ->
258264
lambda_of_groups

compiler/core/lam_pass_exits.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lambda.t) :
156156
Lambda.t =
157157
let rec simplif (lam : Lambda.t) =
158158
match lam with
159-
| Lstaticcatch (l1, (i, xs), l2) as original -> (
159+
| Lstaticcatch (l1, (i, xs), l2) -> (
160160
let i_occur = query i in
161161
match (i_occur, l2) with
162162
| 0, _ -> simplif l1
@@ -184,7 +184,7 @@ let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lambda.t) :
184184
simplif l1)
185185
else
186186
let l1' = simplif l1 in
187-
if l1' == l1 && l2' == l2 then original
187+
if l1' == l1 && l2' == l2 then lam
188188
else Lambda.staticcatch l1' (i, xs) l2')
189189
| Lstaticraise (i, []) -> (
190190
match Hash_int.find_opt subst i with

compiler/core/lam_pass_remove_alias.ml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,22 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lambda.t) : Lambda.t =
236236
(* *\) *)
237237
(* when Ext_list.same_length params args -> *)
238238
(* simpl (Lam_beta_reduce.propogate_beta_reduce meta params body args) *)
239-
| Lstringswitch (Lvar s, sw, d)
240-
when match Hash_ident.find_opt meta.ident_tbl s with
241-
| Some (Constant _) -> true
242-
| Some _ | None -> false -> (
243-
(* The scrutinee is a known constant, so switch on it directly. *)
244-
match Hash_ident.find_opt meta.ident_tbl s with
245-
| Some (Constant c) ->
239+
| Lstringswitch (l, sw, d) -> (
240+
let known_constant =
241+
match l with
242+
| Lvar s -> (
243+
match Hash_ident.find_opt meta.ident_tbl s with
244+
| Some (Constant c) -> Some c
245+
| Some _ | None -> None)
246+
| _ -> None
247+
in
248+
match known_constant with
249+
| Some c ->
250+
(* Switch on the constant the scrutinee is bound to. *)
246251
Lambda.stringswitch (Lambda.const c)
247252
(Ext_list.map_snd sw simpl)
248253
(Ext_option.map d simpl)
249-
| Some _ | None -> Lambda_traverse.shallow_map_sharing simpl lam)
254+
| None -> Lambda_traverse.shallow_map_sharing simpl lam)
250255
| _ -> Lambda_traverse.shallow_map_sharing simpl lam
251256
in
252257
simpl lam
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
open OUnit
2+
3+
let loc = Location.none
4+
let x = Ident.create "x"
5+
let y = Ident.create "y"
6+
let debugger = Lambda.prim ~primitive:Pdebugger ~args:[] loc
7+
let var = Lambda.var x
8+
9+
(* One node per constructor that has children. A leaf shares trivially, so it
10+
would pass either check below without exercising anything. *)
11+
let nodes : (string * Lambda.t) list =
12+
[
13+
("apply", Lambda.apply var [var] {ap_loc = loc; ap_inlined = Default_inline});
14+
( "function",
15+
Lambda.function_ ~loc ~attr:Lambda.default_function_attribute ~params:[x]
16+
~body:debugger );
17+
("let", Lambda.let_ Strict y debugger var);
18+
("letrec", Lambda.letrec [(y, debugger)] var);
19+
("prim", Lambda.prim ~primitive:Pdebugger ~args:[var] loc);
20+
( "switch",
21+
Lambda.switch var
22+
{
23+
sw_consts_full = false;
24+
sw_consts = [(Switch_int 0, debugger)];
25+
sw_blocks_full = false;
26+
sw_blocks = [];
27+
sw_failaction = Some debugger;
28+
sw_dispatch = Switch_direct;
29+
} );
30+
("stringswitch", Lambda.stringswitch var [("a", debugger)] (Some debugger));
31+
("staticraise", Lambda.staticraise 1 [var]);
32+
( "staticcatch",
33+
Lambda.staticcatch (Lambda.staticraise 1 []) (1, []) debugger );
34+
("trywith", Lambda.try_ debugger y var);
35+
("ifthenelse", Lambda.if_ var debugger debugger);
36+
("sequence", Lambda.seq debugger var);
37+
("while", Lambda.while_ var debugger);
38+
("for", Lambda.for_ y var var Upto debugger);
39+
("for_of", Lambda.for_of y var debugger);
40+
("for_await_of", Lambda.for_await_of y var debugger);
41+
("assign", Lambda.assign x debugger);
42+
]
43+
44+
(* Every optimization pass routes its "nothing to do here" case through
45+
[shallow_map_sharing], so an arm of it that stops sharing silently costs the
46+
property in all of them. That is invisible to generated output: breaking the
47+
[Lapply] and [Lswitch] arms leaves every fixture in the repository byte for
48+
byte identical. Add a node above when adding a Lambda constructor. *)
49+
let suites =
50+
__FILE__
51+
>::: [
52+
( "an unchanged child is not rebuilt" >:: fun _ ->
53+
List.iter
54+
(fun (name, node) ->
55+
assert_bool
56+
(name ^ " should be handed back when nothing changed")
57+
(Lambda_traverse.shallow_map_sharing (fun lam -> lam) node
58+
== node))
59+
nodes );
60+
( "a changed child is rebuilt" >:: fun _ ->
61+
(* Without this, a node whose children were never visited would pass
62+
the check above by doing nothing at all. *)
63+
List.iter
64+
(fun (name, node) ->
65+
assert_bool
66+
(name ^ " should be rebuilt when a child changed")
67+
(Lambda_traverse.shallow_map_sharing
68+
(fun _ -> Lambda.const Lambda.const_unit)
69+
node
70+
!= node))
71+
nodes );
72+
]

tests/ounit_tests/ounit_tests_main.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ let suites =
2020
Ounit_util_tests.suites;
2121
Ounit_rec_check_tests.suites;
2222
Ounit_lambda_constant_tests.suites;
23+
Ounit_lambda_traverse_tests.suites;
2324
Ounit_deep_flatten_tests.suites;
2425
Ounit_exits_tests.suites;
2526
Ounit_sroa_tests.suites;

0 commit comments

Comments
 (0)