Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe promises #5709

Merged
merged 9 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
## :rocket: New Feature

- Add extra variants for output filename suffixes in `bsconfig.json`: `.bs.mjs` and `.bs.cjs` are allowed https://github.com/rescript-lang/rescript-compiler/pull/5631
- Safe promises: t-first Js.Promise2 bindings, and remove warning for nested promises https://github.com/rescript-lang/rescript-compiler/pull/5709

#### :bug: Bug Fix

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ test-all: test test-gentype
lib: build
node scripts/install -force-lib-rebuild

artifacts: lib
./scripts/makeArtifactList.js

clean-gentype:
make -C jscomp/gentype_tests/typescript-react-example clean

Expand Down
4 changes: 0 additions & 4 deletions jscomp/ext/warnings.ml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ type t =
| Bs_integer_literal_overflow (* 107 *)
| Bs_uninterpreted_delimiters of string (* 108 *)
| Bs_toplevel_expression_unit (* 109 *)
| Bs_nested_promise of string (* 110 *)

(* If you remove a warning, leave a hole in the numbering. NEVER change
the numbers of existing warnings.
Expand Down Expand Up @@ -150,7 +149,6 @@ let number = function
| Bs_integer_literal_overflow -> 107
| Bs_uninterpreted_delimiters _ -> 108
| Bs_toplevel_expression_unit -> 109
| Bs_nested_promise _ -> 110

let last_warning_number = 110

Expand Down Expand Up @@ -494,8 +492,6 @@ let message = function
| Bs_uninterpreted_delimiters s -> "Uninterpreted delimiters " ^ s
| Bs_toplevel_expression_unit ->
"Toplevel expression is expected to have unit type."
| Bs_nested_promise s ->
"Expression uses nested promise type " ^ s ^ " which is unsafe."

let sub_locs = function
| Deprecated (_, def, use) ->
Expand Down
1 change: 0 additions & 1 deletion jscomp/ext/warnings.mli
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ type t =
| Bs_integer_literal_overflow (* 107 *)
| Bs_uninterpreted_delimiters of string (* 108 *)
| Bs_toplevel_expression_unit (* 109 *)
| Bs_nested_promise of string (* 110 *)

val parse_options : bool -> string -> unit

Expand Down
188 changes: 95 additions & 93 deletions jscomp/main/builtin_cmi_datasets.ml

Large diffs are not rendered by default.

58 changes: 30 additions & 28 deletions jscomp/main/builtin_cmj_datasets.ml

Large diffs are not rendered by default.

35 changes: 0 additions & 35 deletions jscomp/ml/typecore.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1865,45 +1865,10 @@ and type_expect ?in_function ?recarg env sexp ty_expected =
type_expect_ ?in_function ?recarg env sexp ty_expected
)
in
checkTypeInvariant exp;
Cmt_format.set_saved_types
(Cmt_format.Partial_expression exp :: previous_saved_types);
exp

(* NOTE: the type invariant check should have no side effects and be efficient *)
and checkTypeInvariant exp : unit =
let rec extractPromise t =
match t.desc with
| Tconstr
(Pdot (Pdot (Pident { name = "Js" }, "Promise", _), "t", _), [ t1 ], _)
| Tconstr (Pident { name = "promise" }, [ t1 ], _) ->
(* Improvement: check for type aliases, if it can be done efficiently *)
Some t1
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> extractPromise t1
| _ -> None
in
(* Only traverse arguments of a type constructors and function types.
This should guarantee that the traversal finished quickly. *)
let rec findNestedPromise t =
match t.desc with
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> findNestedPromise t1
| Tconstr (_, ts, _) -> (
match extractPromise t with
| Some t1 -> (
match extractPromise t1 with
| Some _t2 ->
let nestedType = Format.asprintf "%a" Printtyp.type_expr t in
Location.prerr_warning exp.exp_loc
(Bs_nested_promise nestedType)
| None -> ts |> List.iter findNestedPromise)
| None -> ts |> List.iter findNestedPromise)
| Tarrow (_, t1, t2, _) ->
findNestedPromise t1;
findNestedPromise t2
| _ -> ()
in
findNestedPromise exp.exp_type

and type_expect_ ?in_function ?(recarg=Rejected) env sexp ty_expected =
let loc = sexp.pexp_loc in
(* Record the expression type before unifying it with the expected type *)
Expand Down
3 changes: 3 additions & 0 deletions jscomp/others/js.ml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ module Re = Js_re
module Promise = Js_promise
(** Provide bindings to JS Promise *)

module Promise2 = Js_promise2
(** Provide bindings to JS Promise *)

module Date = Js_date
(** Provide bindings for JS Date *)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: integrate orthogonal quality of life improvements from https://github.com/ryyppy/rescript-promise

TODO2: plan the whole Js.Foo2 story and how it should move to be the default instead. This cleanup could go into v11. For a separate issue.

Expand Down
18 changes: 18 additions & 0 deletions jscomp/others/js_promise2.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
include Js_promise

/** Type-safe t-first then */
let then: (promise<'a>, 'a => promise<'b>) => promise<'b> = %raw(`
function(p, cont) {
Promise.resolve(p).then(cont)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you wrap a promise with Promise.resolve?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because promises are flattened at runtime and where you expect to have, say promise<int> because you looked inside promise<promise<int>> what you actually get is an int. When that happens, the int is converted back to the promise<int> you were expecting, so you can call .then on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because promises are flattened at runtime and where you expect to have, say promise because you looked inside promise<promise> what you actually get is an int.

Once it became a promise it will stay a promise. So if we have promise<promise<int>> in runtime, it'll be flattened to promise<int> (not int). So we can call .then without additional Promise.resolve.

At least it works this way in JavaScript. Maybe we're talking about different things.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not easy to explain in few words. But you can check the example: https://github.com/rescript-lang/rescript-compiler/pull/5709/files#diff-dfcc91a2765ccd42f814d348224f3bc20b343386851e14fd99132000aa56fd78R13

It is flattened to promise<int>, but when you "look inside" i.e. await it, you get int. (Or you could look inside by doing another .then).

Another way to answer: in JS await is happy to receive a value such as a number, it does not need a promise. This was done intentionally. This change does the same for then: it makes then happy to receive a value such as a number.

Copy link
Member

@DZakh DZakh Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. But I'm not sure that the problem should be solved by Promise bindings. It's more of a types problem, not a runtime one.

For example, if xxx has the type promise<promise<promise<int>>>, the Promise2 will also stop working correctly. I think it's fine to provide some helper function to flatten promises eg Promise2.flatten, and leave everything to the user's responsibility.

This code covers an edge-case while going against the runtime-free Js module nature. It tries to keep the correct behavior. That's definitely good, but I'm not sure whether it's worth it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I suggest is:

let nestedPromise = async (xxx: promise<int>) => {
  let xx = await xxx
  Js.log2("Promise2.then", xx)
}

module Promise2 = {
  external flatten: promise<promise<'a>> => promise<'a> = "%identity"
}

let someFnThatReturnsNestedPromises: unit => promise<promise<promise<int>>> = () => %raw("Promise.resolve(123)")

someFnThatReturnsNestedPromises()
  ->Promise2.flatten
  ->Promise2.flatten
  ->nestedPromise
  ->ignore

This way, we solve the problem immediately as it's raised. No twisting of the promise type meaning. Also, the nestedPromise code became much cleaner.

I think there's some confusion on what the problem is.
The problem is that you can inadvertently write some code that uses nested promises and crash.
The nested promise type can easily be in the middle of an expression, and the user might never see it written down explicitly.
If you already know there are nested promises, then clearly there's no problem and you can flatten them explicitly.
The problem this is trying to solve is when you don't realise you're using nested promises.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider crashing a healthy thing when it happens during development. I think it's where we derail in our opinions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially when using Js module. Maybe it's for Belt to handle this problem. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, wanting those examples to crash is one possible opinion.
What is surprising, at least to me, is that the same example written only in terms of async/await does not crash.
This boils down to await 3 not crashing.

Copy link
Member

@DZakh DZakh Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, wanting those examples to crash is one possible opinion.

It's how it used to work https://github.com/rescript-lang/rescript-compiler/pull/5709/files#diff-dfcc91a2765ccd42f814d348224f3bc20b343386851e14fd99132000aa56fd78R13

Ideally would be nice to have types that can automatically flatten themselves eg, promise<promise<t>> automatically becomes promise<t> on the type system level. The same thing for the option type and others. But I can imagine that it's not easy to do.

Well, I don't have anything else to add. I'm more into leaving things as they are right now. Both approaches have pros and cons. I'm not a decision-maker here.

}
`)

/** Type-safe t-first catch */
let catch: (promise<'a>, error => promise<'a>) => promise<'a> = %raw(`
function(p, cont) {
Promise.resolve(p).catch(cont)
}
`)

@deprecated("Use then instead")
let then_ = then
3 changes: 2 additions & 1 deletion jscomp/others/release.ninja
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ o others/js_obj.cmi others/js_obj.cmj : cc others/js_obj.ml | others/belt_intern
o others/js_option.cmj : cc_cmi others/js_option.ml | others/belt_internals.cmi others/js.cmi others/js_exn.cmj others/js_option.cmi $bsc
o others/js_option.cmi : cc others/js_option.mli | others/belt_internals.cmi others/js.cmi $bsc
o others/js_promise.cmi others/js_promise.cmj : cc others/js_promise.ml | others/belt_internals.cmi others/js.cmi $bsc
o others/js_promise2.cmi others/js_promise2.cmj : cc others/js_promise2.res | others/belt_internals.cmi others/js.cmi others/js_promise.cmj $bsc
o others/js_re.cmi others/js_re.cmj : cc others/js_re.ml | others/belt_internals.cmi others/js.cmi others/js.cmj $bsc
o others/js_result.cmj : cc_cmi others/js_result.ml | others/belt_internals.cmi others/js.cmi others/js_result.cmi $bsc
o others/js_result.cmi : cc others/js_result.mli | others/belt_internals.cmi others/js.cmi $bsc
Expand All @@ -67,7 +68,7 @@ o others/jsx.cmi others/jsx.cmj : cc others/jsx.ml | others/belt_internals.cmi o
o others/jsxDOM.cmi others/jsxDOM.cmj : cc others/jsxDOM.ml | others/belt_internals.cmi others/js.cmi others/jsx.cmj others/jsxDOMStyle.cmj others/jsxEvent.cmj $bsc
o others/jsxDOMStyle.cmi others/jsxDOMStyle.cmj : cc others/jsxDOMStyle.ml | others/belt_internals.cmi others/js.cmi $bsc
o others/jsxEvent.cmi others/jsxEvent.cmj : cc others/jsxEvent.ml | others/belt_internals.cmi others/js.cmi $bsc
o js_pkg : phony others/js_OO.cmi others/js_OO.cmj others/js_array.cmi others/js_array.cmj others/js_array2.cmi others/js_array2.cmj others/js_bigint.cmi others/js_bigint.cmj others/js_cast.cmi others/js_cast.cmj others/js_console.cmi others/js_console.cmj others/js_date.cmi others/js_date.cmj others/js_dict.cmi others/js_dict.cmj others/js_exn.cmi others/js_exn.cmj others/js_float.cmi others/js_float.cmj others/js_global.cmi others/js_global.cmj others/js_int.cmi others/js_int.cmj others/js_json.cmi others/js_json.cmj others/js_list.cmi others/js_list.cmj others/js_map.cmi others/js_map.cmj others/js_mapperRt.cmi others/js_mapperRt.cmj others/js_math.cmi others/js_math.cmj others/js_null.cmi others/js_null.cmj others/js_null_undefined.cmi others/js_null_undefined.cmj others/js_obj.cmi others/js_obj.cmj others/js_option.cmi others/js_option.cmj others/js_promise.cmi others/js_promise.cmj others/js_re.cmi others/js_re.cmj others/js_result.cmi others/js_result.cmj others/js_set.cmi others/js_set.cmj others/js_string.cmi others/js_string.cmj others/js_string2.cmi others/js_string2.cmj others/js_typed_array.cmi others/js_typed_array.cmj others/js_typed_array2.cmi others/js_typed_array2.cmj others/js_types.cmi others/js_types.cmj others/js_undefined.cmi others/js_undefined.cmj others/js_vector.cmi others/js_vector.cmj others/js_weakmap.cmi others/js_weakmap.cmj others/js_weakset.cmi others/js_weakset.cmj others/jsx.cmi others/jsx.cmj others/jsxDOM.cmi others/jsxDOM.cmj others/jsxDOMStyle.cmi others/jsxDOMStyle.cmj others/jsxEvent.cmi others/jsxEvent.cmj
o js_pkg : phony others/js_OO.cmi others/js_OO.cmj others/js_array.cmi others/js_array.cmj others/js_array2.cmi others/js_array2.cmj others/js_bigint.cmi others/js_bigint.cmj others/js_cast.cmi others/js_cast.cmj others/js_console.cmi others/js_console.cmj others/js_date.cmi others/js_date.cmj others/js_dict.cmi others/js_dict.cmj others/js_exn.cmi others/js_exn.cmj others/js_float.cmi others/js_float.cmj others/js_global.cmi others/js_global.cmj others/js_int.cmi others/js_int.cmj others/js_json.cmi others/js_json.cmj others/js_list.cmi others/js_list.cmj others/js_map.cmi others/js_map.cmj others/js_mapperRt.cmi others/js_mapperRt.cmj others/js_math.cmi others/js_math.cmj others/js_null.cmi others/js_null.cmj others/js_null_undefined.cmi others/js_null_undefined.cmj others/js_obj.cmi others/js_obj.cmj others/js_option.cmi others/js_option.cmj others/js_promise.cmi others/js_promise.cmj others/js_promise2.cmi others/js_promise2.cmj others/js_re.cmi others/js_re.cmj others/js_result.cmi others/js_result.cmj others/js_set.cmi others/js_set.cmj others/js_string.cmi others/js_string.cmj others/js_string2.cmi others/js_string2.cmj others/js_typed_array.cmi others/js_typed_array.cmj others/js_typed_array2.cmi others/js_typed_array2.cmj others/js_types.cmi others/js_types.cmj others/js_undefined.cmi others/js_undefined.cmj others/js_vector.cmi others/js_vector.cmj others/js_weakmap.cmi others/js_weakmap.cmj others/js_weakset.cmi others/js_weakset.cmj others/jsx.cmi others/jsx.cmj others/jsxDOM.cmi others/jsxDOM.cmj others/jsxDOMStyle.cmi others/jsxDOMStyle.cmj others/jsxEvent.cmi others/jsxEvent.cmj
o others/belt_Array.cmj : cc_cmi others/belt_Array.ml | others/belt.cmi others/belt_Array.cmi others/belt_internals.cmi others/js.cmi others/js.cmj others/js_math.cmj $bsc js_pkg
o others/belt_Array.cmi : cc others/belt_Array.mli | others/belt.cmi others/belt_internals.cmi others/js.cmi others/js.cmj $bsc js_pkg
o others/belt_Float.cmj : cc_cmi others/belt_Float.ml | others/belt.cmi others/belt_Float.cmi others/belt_internals.cmi others/js.cmi $bsc js_pkg
Expand Down
3 changes: 3 additions & 0 deletions jscomp/runtime/js.ml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ module Re = Js_re
module Promise = Js_promise
(** Provide bindings to JS promise *)

module Promise2 = Js_promise2
(** Provide bindings to JS promise *)

module Date = Js_date
(** Provide bindings for JS Date *)

Expand Down
35 changes: 35 additions & 0 deletions jscomp/test/SafePromises.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

var Js_promise2 = require("../../lib/js/js_promise2.js");

async function nestedPromise(xxx) {
var xx = await xxx;
Js_promise2.then(xx, (function (x) {
return Promise.resolve((console.log("Promise2.then", x), undefined));
}));
Js_promise2.$$catch(xx, (function (x) {
console.log("Promise2.catch_", x);
return Promise.resolve(0);
}));
var arg1 = function (x) {
return Promise.resolve((console.log("Promise.then_", x), undefined));
};
xx.then(arg1);
}

async function create(x) {
console.log("create", x);
return x;
}

var xx = create(10);

var xxx = create(xx);

nestedPromise(xxx);

exports.nestedPromise = nestedPromise;
exports.create = create;
exports.xx = xx;
exports.xxx = xxx;
/* xx Not a pure module */
23 changes: 23 additions & 0 deletions jscomp/test/SafePromises.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*** Problematic example of nested promises is safe with Js.Promise2 */

let nestedPromise = async (xxx: promise<promise<int>>) => {
let xx = await xxx

let _ = xx->Js.Promise2.then(x => Js.log2("Promise2.then", x) |> Js.Promise.resolve)
let _ = xx->Js.Promise2.catch(x => {
Js.log2("Promise2.catch_", x)
0 |> Js.Promise.resolve
})

// This crashes
let _ = Js.Promise.then_(x => Js.log2("Promise.then_", x) |> Js.Promise.resolve, xx)
}

let create = async (x) => {
Js.log2("create", x)
x
}

let xx = create(10)
let xxx = create(xx)
let _ = nestedPromise(xxx)
3 changes: 2 additions & 1 deletion jscomp/test/build.ninja

Large diffs are not rendered by default.

Loading