Skip to content

Commit df927f0

Browse files
authored
flambda-backend: Add a test for an interaction between omitted mli and overeager heap allocation of argument (#1816)
* Add tests These tests show how the defaulting of mode variables affect whether objects can be stack allocated in the presence/absence of an mli. * Add documentation
1 parent 92ddf14 commit df927f0

File tree

7 files changed

+247
-1
lines changed

7 files changed

+247
-1
lines changed

jane/doc/local-reference.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ function types, see below) then inference will resolve them according to what
7979
appears in the `.mli`. If there is no `.mli` file, then inference will always
8080
choose `global` for anything that can be accessed from another file.
8181

82+
Local annotations (or the lack thereof) in the mli don't affect inference
83+
within the ml. In the below example, the `~foo` parameter is inferred to
84+
be local internally to `A`, so `foo:(Some x)` can be constructed locally.
85+
86+
```ocaml
87+
(* in a.mli *)
88+
val f1 : foo:local_ int option -> unit
89+
val f2 : int -> unit
90+
91+
(* in a.ml *)
92+
let f1 ~foo:_ = ()
93+
let f2 x = f1 ~foo:(Some x) (* [Some x] is stack allocated *)
94+
```
95+
96+
<!-- See Note [Inference affects allocation in mli-less files] in [ocaml/testsuite/tests/typing-local/alloc_arg_with_mli.ml]
97+
in the flambda-backend Git repo. The ensuing paragraph is related to that
98+
note; we can remove this comment when the note is resolved.
99+
-->
100+
However, a missing mli *does* affect inference within the ml. As a conservative rule of thumb,
101+
function arguments in an mli-less file will be heap-allocated unless the function parameter or argument
102+
is annotated with `local_`. This is due to an implementation detail of the type-checker and
103+
is not fundamental, but for now, it's yet another reason to prefer writing mlis.
104+
105+
```ocaml
106+
(* in a.ml; a.mli is missing *)
107+
let f1 ~foo:_ = ()
108+
let f2 x = f1 ~foo:(Some x) (* [Some x] is heap allocated *)
109+
```
110+
82111
## Regions
83112

84113
Every local allocation takes places inside a _region_, which is a block of code

ocamltest/ocaml_actions.ml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ let compile_module compiler module_ log env =
291291
let is_c = is_c_file module_with_filetype in
292292
let c_headers_flags =
293293
if is_c then Ocaml_flags.c_includes else "" in
294+
let compile_only_flag_opt =
295+
if Environments.lookup_as_bool Ocaml_variables.compile_only env = Some false
296+
then ""
297+
else " -c "
298+
in
294299
let commandline =
295300
[
296301
compiler#name;
@@ -301,7 +306,8 @@ let compile_module compiler module_ log env =
301306
libraries compiler#target env;
302307
backend_default_flags env compiler#target;
303308
backend_flags env compiler#target;
304-
"-c " ^ module_;
309+
compile_only_flag_opt;
310+
module_;
305311
] in
306312
let exit_status =
307313
Actions_helpers.run_cmd
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
(* TEST
2+
* stack-allocation
3+
** native
4+
*)
5+
6+
(* Check whether functions that *could* take their argument
7+
locally allow their callers to locally construct the argument.
8+
9+
Among other things, this checks how mode variables are defaulted in the
10+
presence of an mli.
11+
12+
See the [..._no_mli.ml] version of this test for how mode variables
13+
are defaulted in the absence of an mli.
14+
*)
15+
16+
let check_if_zero_alloc =
17+
let measure_words f =
18+
let before = Gc.allocated_bytes () in
19+
ignore (f () : _);
20+
let after = Gc.allocated_bytes () in
21+
int_of_float (after -. before) / (Sys.word_size / 8)
22+
in
23+
fun[@inline never] ~name ~f x ->
24+
let words = measure_words (fun () -> f x) - measure_words ignore in
25+
Printf.printf "%s: %d words%s\n" name words
26+
(if words = 0 then "" else " (allocates!)")
27+
28+
external escape : 'a -> 'a = "%identity"
29+
30+
type t = { a : int; b : int }
31+
32+
module _ : sig end = struct
33+
let[@inline never] take_unrestricted { a; b } = a + b
34+
35+
let () =
36+
check_if_zero_alloc ~name:"take unrestricted of global (not exposed)" 0 ~f:(fun x ->
37+
take_unrestricted { a = x; b = x } [@nontail])
38+
end
39+
40+
module _ : sig end = struct
41+
let[@inline never] take_unrestricted { a; b } = a + b
42+
43+
let () =
44+
check_if_zero_alloc ~name:"take unrestricted of local (not exposed)" 0 ~f:(fun x ->
45+
take_unrestricted (local_ { a = x; b = x }) [@nontail])
46+
end
47+
48+
module _ : sig end = struct
49+
let[@inline never] take_local (local_ { a; b }) = a + b
50+
51+
let () =
52+
check_if_zero_alloc ~name:"take local of local (not exposed)" 0 ~f:(fun x ->
53+
take_local (local_ { a = x; b = x }) [@nontail])
54+
end
55+
56+
module _ : sig end = struct
57+
let[@inline never] take_local (local_ { a; b }) = a + b
58+
59+
let () =
60+
check_if_zero_alloc ~name:"take local of global (not exposed)" 0 ~f:(fun x ->
61+
take_local { a = x; b = x } [@nontail])
62+
end
63+
64+
module _ : sig end = struct
65+
let[@inline never] take_global ({ a; b } as t) = ignore (escape t); a + b
66+
67+
let () =
68+
check_if_zero_alloc
69+
~name:"take global of global (not exposed; expected to allocate)"
70+
0
71+
~f:(fun x -> take_global { a = x; b = x } [@nontail])
72+
end
73+
74+
module M1 = struct
75+
let[@inline never] take_unrestricted { a; b } = a + b
76+
77+
(* Note [Inference affects allocation in mli-less files]
78+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79+
80+
At the moment, this allocates in the without-mli case, but not for a
81+
fundamental reason: when the type signature is inferred, the inferred
82+
types and thus mode variables are reused. Instead, we could create new
83+
copies of types with fresh mode variables related by submoding.
84+
*)
85+
let () =
86+
check_if_zero_alloc ~name:"take unrestricted of global (exposed)" 0 ~f:(fun x ->
87+
take_unrestricted { a = x; b = x } [@nontail])
88+
end
89+
90+
module M2 = struct
91+
let[@inline never] take_unrestricted { a; b } = a + b
92+
93+
let () =
94+
check_if_zero_alloc ~name:"take unrestricted of local (exposed)" 0 ~f:(fun x ->
95+
take_unrestricted (local_ { a = x; b = x }) [@nontail])
96+
end
97+
98+
module M3 = struct
99+
let[@inline never] take_local (local_ { a; b }) = a + b
100+
101+
let () =
102+
check_if_zero_alloc ~name:"take local of local (exposed)" 0 ~f:(fun x ->
103+
take_local (local_ { a = x; b = x }) [@nontail])
104+
end
105+
106+
module M4 = struct
107+
let[@inline never] take_local (local_ { a; b }) = a + b
108+
109+
let () =
110+
check_if_zero_alloc ~name:"take local of global (exposed)" 0 ~f:(fun x ->
111+
take_local { a = x; b = x } [@nontail])
112+
end
113+
114+
module M5 = struct
115+
let[@inline never] take_global ({ a; b } as t) = ignore (escape t); a + b
116+
117+
let () =
118+
check_if_zero_alloc
119+
~name:"take global of global (exposed; expected to allocate)"
120+
0
121+
~f:(fun x -> take_global { a = x; b = x } [@nontail])
122+
end
123+
124+
module M6 = struct
125+
let[@inline never] take_local__global_in_mli (local_ { a; b }) = a + b
126+
127+
let () =
128+
check_if_zero_alloc ~name:"take local of local (exposed)" 0 ~f:(fun x ->
129+
take_local__global_in_mli (local_ { a = x; b = x }) [@nontail])
130+
end
131+
132+
module M7 = struct
133+
let[@inline never] take_local__global_in_mli (local_ { a; b }) = a + b
134+
135+
let () =
136+
check_if_zero_alloc ~name:"take local of global (exposed)" 0 ~f:(fun x ->
137+
take_local__global_in_mli { a = x; b = x } [@nontail])
138+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type t = { a : int; b : int }
2+
3+
module M1 : sig
4+
val take_unrestricted : t -> int
5+
end
6+
7+
module M2 : sig
8+
val take_unrestricted : t -> int
9+
end
10+
11+
module M3 : sig
12+
val take_local : local_ t -> int
13+
end
14+
15+
module M4 : sig
16+
val take_local : local_ t -> int
17+
end
18+
19+
module M5 : sig
20+
val take_global : t -> int
21+
end
22+
23+
module M6 : sig
24+
val take_local__global_in_mli : t -> int
25+
end
26+
27+
module M7 : sig
28+
val take_local__global_in_mli : t -> int
29+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
take unrestricted of global (not exposed): 0 words
2+
take unrestricted of local (not exposed): 0 words
3+
take local of local (not exposed): 0 words
4+
take local of global (not exposed): 0 words
5+
take global of global (not exposed; expected to allocate): 3 words (allocates!)
6+
take unrestricted of global (exposed): 0 words
7+
take unrestricted of local (exposed): 0 words
8+
take local of local (exposed): 0 words
9+
take local of global (exposed): 0 words
10+
take global of global (exposed; expected to allocate): 3 words (allocates!)
11+
take local of local (exposed): 0 words
12+
take local of global (exposed): 0 words
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
(* TEST
2+
3+
readonly_files = "alloc_arg_with_mli.ml"
4+
5+
* stack-allocation
6+
** native
7+
compile_only = "false"
8+
flags = "-o ${test_build_directory}/alloc_arg_without_mli.opt"
9+
module = "alloc_arg_with_mli.ml"
10+
*)
11+
12+
(* Check whether functions that *could* take their argument
13+
locally allow their callers to locally construct the argument.
14+
15+
Among other things, this checks how mode variables are defaulted in the
16+
absence of an mli.
17+
18+
See the [..._with_mli.ml] version of this test for how mode variables
19+
are defaulted in the presence of an mli.
20+
*)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
take unrestricted of global (not exposed): 0 words
2+
take unrestricted of local (not exposed): 0 words
3+
take local of local (not exposed): 0 words
4+
take local of global (not exposed): 0 words
5+
take global of global (not exposed; expected to allocate): 3 words (allocates!)
6+
take unrestricted of global (exposed): 3 words (allocates!)
7+
take unrestricted of local (exposed): 0 words
8+
take local of local (exposed): 0 words
9+
take local of global (exposed): 0 words
10+
take global of global (exposed; expected to allocate): 3 words (allocates!)
11+
take local of local (exposed): 0 words
12+
take local of global (exposed): 0 words

0 commit comments

Comments
 (0)