Skip to content

Commit c10223e

Browse files
authored
Reject await in a function that an async function returns directly (#9169)
Why: The built-in PPX let a function in the body of an async function inherit its async context, so `await` in a plain arrow returned by an async function passed the check, and the JavaScript had `await` in a function that is not async (#9168). The inheritance comes from the curried parsetree, where `async (x, y) => ...` was nested functions; in the n-ary parsetree a function in a function's body is a function of its own, so each function is now in an async context exactly when it is async. Diagnostic added (await in a returned function): Example: let f = async x => y => await y + x Before (js): a SyntaxError when the module loads async function f(x) { return y => await y + x | 0; } After (diagnostic): A.res:1:25-31 Await on expression not in an async context Review: Does a working program stop compiling? Only one whose `await` in such a function was removed by the optimizer, as in an unused definition or a branch under a constant `false`; the same code in any other plain function is already rejected. Programs that still compile give the same JavaScript, since only the await check reads the async context. Signed-off-by: mununki <woonki.moon@gmail.com>
1 parent cc827cd commit c10223e

6 files changed

Lines changed: 91 additions & 23 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Reject `await` in a function that an async function returns directly, as in `async x => y => await y`, which produced JavaScript with `await` outside an async function.

‎compiler/frontend/bs_builtin_ppx.ml‎

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ let local_module_type_decl ~await_context
113113
(Mtd.mk ~loc {txt = name; loc} ~typ:(Mty.typeof_ ~loc me)));
114114
])
115115

116-
let expr_mapper ~async_context ~in_function_def (self : mapper)
117-
(e : Parsetree.expression) =
118-
let old_in_function_def = !in_function_def in
119-
in_function_def := false;
116+
let expr_mapper ~async_context (self : mapper) (e : Parsetree.expression) =
120117
match e.pexp_desc with
121118
(* Its output should not be rewritten anymore *)
122119
| Pexp_regexp {pattern; flags} ->
@@ -168,8 +165,10 @@ let expr_mapper ~async_context ~in_function_def (self : mapper)
168165
| Pexp_fun {newtypes; params; body; async} -> (
169166
match Ast_attributes.process_attributes_rev e.pexp_attributes with
170167
| Nothing, _ ->
171-
(* Handle @async x => y => ... is in async context *)
172-
async_context := (old_in_function_def && !async_context) || async;
168+
(* A [Pexp_fun] holds all parameters of one function, so a [Pexp_fun]
169+
in its body is another function: in [async x => y => await y], the
170+
inner function is not async and compiles to a plain arrow. *)
171+
async_context := async;
173172
let attrs = self.attributes self e.pexp_attributes in
174173
let params =
175174
Ext_list.map params (fun (p : Parsetree.fun_param) ->
@@ -179,13 +178,7 @@ let expr_mapper ~async_context ~in_function_def (self : mapper)
179178
p_pat = self.pat self p.p_pat;
180179
})
181180
in
182-
let saved_in_function_def = !in_function_def in
183-
in_function_def := true;
184-
(* Keep reporting the body as part of a function definition so nested
185-
functions propagate async context exactly like the legacy curried
186-
mapper did (GH #7974). *)
187181
let body = self.expr self body in
188-
in_function_def := saved_in_function_def;
189182
let newtypes =
190183
Ext_list.map newtypes (fun (name, nt_attrs) ->
191184
(name, self.attributes self nt_attrs))
@@ -432,10 +425,9 @@ let expr_mapper ~async_context ~in_function_def (self : mapper)
432425
| Some (_, _, Some _) | None -> default_expr_mapper self e)
433426
| _ -> default_expr_mapper self e
434427

435-
let expr_mapper ~async_context ~in_function_def (self : mapper)
436-
(e : Parsetree.expression) =
428+
let expr_mapper ~async_context (self : mapper) (e : Parsetree.expression) =
437429
let async_saved = !async_context in
438-
let result = expr_mapper ~async_context ~in_function_def self e in
430+
let result = expr_mapper ~async_context self e in
439431
async_context := async_saved;
440432
let check_await () =
441433
if !async_context = false then
@@ -690,7 +682,6 @@ let structure_item_mapper (self : mapper) (str : Parsetree.structure_item) :
690682

691683
type mapper_context = {
692684
async_context: bool ref;
693-
in_function_def: bool ref;
694685
await_context: (string, string) Hashtbl.t ref;
695686
}
696687

@@ -699,11 +690,7 @@ type mapper_context = {
699690
function body must not change whether an await is accepted here. *)
700691
let mapper_context_key =
701692
Request_local.new_key (fun () ->
702-
{
703-
async_context = ref true;
704-
in_function_def = ref false;
705-
await_context = ref (Hashtbl.create 10);
706-
})
693+
{async_context = ref true; await_context = ref (Hashtbl.create 10)})
707694

708695
let mapper_context () = Domain.DLS.get mapper_context_key
709696

@@ -897,8 +884,7 @@ let mapper : mapper =
897884
expr =
898885
(fun self expression ->
899886
let context = mapper_context () in
900-
expr_mapper ~async_context:context.async_context
901-
~in_function_def:context.in_function_def self expression);
887+
expr_mapper ~async_context:context.async_context self expression);
902888
pat = pat_mapper;
903889
typ = typ_mapper;
904890
signature_item = signature_item_mapper;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/await_in_returned_function.res:3:28-36
4+
5+
1 │ let a = async () => 3
6+
2 │
7+
3 │ let foo = async x => y => (await a()) + x + y
8+
4 │
9+
10+
Await on expression not in an async context
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
let a = async () => 3
2+
3+
let foo = async x => y => (await a()) + x + y
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Generated by ReScript, PLEASE EDIT WITH CARE
2+
3+
import * as Mocha from "mocha";
4+
import * as Test_utils from "./test_utils.mjs";
5+
6+
async function addAsync(x) {
7+
return async y => await y + x | 0;
8+
}
9+
10+
async function addPlain(x) {
11+
return y => y + x | 0;
12+
}
13+
14+
async function addThree(x) {
15+
return y => (async z => (await y + await z | 0) + x | 0);
16+
}
17+
18+
Mocha.describe("Async_returned_function_test", () => {
19+
Mocha.test("async function returning an async function", async () => {
20+
let add = await addAsync(1);
21+
return Test_utils.eq("File \"async_returned_function_test.res\", line 18, characters 7-14", 3, await add(Promise.resolve(2)));
22+
});
23+
Mocha.test("async function returning a plain function", async () => {
24+
let add = await addPlain(1);
25+
return Test_utils.eq("File \"async_returned_function_test.res\", line 23, characters 7-14", 3, add(2));
26+
});
27+
Mocha.test("plain function between two async functions", async () => {
28+
let add = await addThree(1);
29+
return Test_utils.eq("File \"async_returned_function_test.res\", line 28, characters 7-14", 6, await add(Promise.resolve(2))(Promise.resolve(3)));
30+
});
31+
});
32+
33+
export {
34+
addAsync,
35+
addPlain,
36+
addThree,
37+
}
38+
/* Not a pure module */
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
open Mocha
2+
open Test_utils
3+
4+
@module("mocha")
5+
external testAsync: (string, unit => promise<unit>) => unit = "test"
6+
7+
// Each arrow is a function of its own: the inner arrows below must compile to
8+
// JS functions that are async exactly when they are async in the source.
9+
let addAsync = async x => async y => (await y) + x
10+
11+
let addPlain = async x => y => y + x
12+
13+
let addThree = async x => y => async z => (await y) + await z + x
14+
15+
describe(__MODULE__, () => {
16+
testAsync("async function returning an async function", async () => {
17+
let add = await addAsync(1)
18+
eq(__LOC__, 3, await add(Promise.resolve(2)))
19+
})
20+
21+
testAsync("async function returning a plain function", async () => {
22+
let add = await addPlain(1)
23+
eq(__LOC__, 3, add(2))
24+
})
25+
26+
testAsync("plain function between two async functions", async () => {
27+
let add = await addThree(1)
28+
eq(__LOC__, 6, await add(Promise.resolve(2))(Promise.resolve(3)))
29+
})
30+
})

0 commit comments

Comments
 (0)