diff --git a/changes/let-before-callee.fix.md b/changes/let-before-callee.fix.md new file mode 100644 index 0000000000..628dda1ea4 --- /dev/null +++ b/changes/let-before-callee.fix.md @@ -0,0 +1 @@ +- Fix `let x = e; f(x)` running `e` after it evaluates the callee `f`, as in `let x = c.n; next()(x)`. diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index 2eaba3240d..0a2f23c194 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -192,6 +192,23 @@ let rec delay_safe ~assignable (lam : Lambda.t) : bool = | Lfor _ | Lfor_of _ | Lfor_await_of _ | Lassign _ | Lbreak | Lcontinue -> false +let reads_callee (primitive : Lambda.primitive) = + match primitive with + | Pjs_call {ffi = {kind = Decl_val _ | Decl_new _}} -> true + | _ -> false + +type motion = Anywhere | Reads | Effects + +let motion ~assignable lam : motion = + if delay_safe ~assignable lam then Anywhere + else if no_side_effects lam then Reads + else Effects + +let conflict (a : motion) (b : motion) = + match (a, b) with + | Anywhere, _ | _, Anywhere | Reads, Reads -> false + | Effects, (Reads | Effects) | Reads, Effects -> true + (* Size of a lambda that is too big to inline, e.g. one containing a loop *) let too_big = 1000 diff --git a/compiler/core/lam_analysis.mli b/compiler/core/lam_analysis.mli index e707a9b602..4807870f1f 100644 --- a/compiler/core/lam_analysis.mli +++ b/compiler/core/lam_analysis.mli @@ -32,6 +32,22 @@ val delay_safe : assignable:Set_ident.t -> Lambda.t -> bool after other code, gives the same value. [assignable] are the variables some [Lassign] assigns. *) +val reads_callee : Lambda.primitive -> bool +(** The call of an external function or class, which reads it from its global + or module before the arguments run *) + +(** How the evaluation of a term may move with respect to other code *) +type motion = + | Anywhere (** no side effects, and depends on no state that may change *) + | Reads (** no side effects, but reads state that may change *) + | Effects + +val motion : assignable:Set_ident.t -> Lambda.t -> motion + +val conflict : motion -> motion -> bool +(** Whether code of the two motions can give other results when it runs in + the other order *) + val size_upto : limit:int -> Lambda.t -> int (** The size of a lambda for inlining decisions if it is below [limit], [limit] otherwise. [limit] must be at most 1000. *) diff --git a/compiler/core/lam_beta_reduce.ml b/compiler/core/lam_beta_reduce.ml index f9ff3412cd..9905521736 100644 --- a/compiler/core/lam_beta_reduce.ml +++ b/compiler/core/lam_beta_reduce.ml @@ -150,7 +150,7 @@ let propagate_beta_reduce (meta : Lam_stats.t) (params : Ident.t list) | Lprim {primitive = Psome | Psome_not_nest; args = [v]; _} -> Hash_ident.replace meta.ident_tbl param (Normal_optional v) | _ -> ()); - Lam_util.refine_let ~kind:Strict param arg l) + Lam_util.refine_let ~assignable ~kind:Strict param arg l) let propagate_beta_reduce_with_map (meta : Lam_stats.t) (map : Lam_var_stats.stats Map_ident.t) params body args = @@ -201,7 +201,7 @@ let propagate_beta_reduce_with_map (meta : Lam_stats.t) | Lprim {primitive = Psome | Psome_not_nest; args = [v]} -> Hash_ident.replace meta.ident_tbl param (Normal_optional v) | _ -> ()); - Lam_util.refine_let ~kind:Strict param arg l) + Lam_util.refine_let ~assignable ~kind:Strict param arg l) let no_names_beta_reduce ~assignable params body args = match @@ -212,4 +212,4 @@ let no_names_beta_reduce ~assignable params body args = (* Fold right, so that the first parameter's binding is outermost and the arguments evaluate in call order *) Ext_list.fold_right2 params args body (fun param arg l -> - Lam_util.refine_let ~kind:Strict param arg l) + Lam_util.refine_let ~assignable ~kind:Strict param arg l) diff --git a/compiler/core/lam_beta_reduce_util.ml b/compiler/core/lam_beta_reduce_util.ml index 28af87d516..89b62f53f9 100644 --- a/compiler/core/lam_beta_reduce_util.ml +++ b/compiler/core/lam_beta_reduce_util.ml @@ -38,25 +38,12 @@ 3. an argument with side effects is not put in an operand that may not be evaluated, the right one of [&&] and [||] *) -(* How the evaluation of an argument may move with respect to other code *) -type motion = - | Anywhere (** no side effects, and depends on no state that may change *) - | Reads (** no side effects, but reads state that may change *) - | Effects - -let motion ~assignable lam : motion = - if Lam_analysis.delay_safe ~assignable lam then Anywhere - else if Lam_analysis.no_side_effects lam then Reads - else Effects - -(* Whether code of motions [a] and [b] can give other results when it runs in - the other order *) -let conflict (a : motion) (b : motion) = - match (a, b) with - | Anywhere, _ | _, Anywhere | Reads, Reads -> false - | Effects, (Reads | Effects) | Reads, Effects -> true - -type value = {mutable used: bool; lambda: Lambda.t; rank: int; motion: motion} +type value = { + mutable used: bool; + lambda: Lambda.t; + rank: int; + motion: Lam_analysis.motion; +} (* Sequence the unused arguments before [body], in parameter order *) let seq_unused param_hash params body = @@ -90,7 +77,7 @@ let simple_beta_reduce ~assignable params body args = (* The code the reduced body evaluates, latest first, with its rank in the call *) let evaluated = ref [] in - let evaluate ~conditional rank (motion : motion) = + let evaluate ~conditional rank (motion : Lam_analysis.motion) = match motion with | Effects when conditional -> raise_notrace Not_simple_apply | Anywhere | Reads | Effects -> evaluated := (rank, motion) :: !evaluated @@ -102,7 +89,7 @@ let simple_beta_reduce ~assignable params body args = evaluate ~conditional exp.rank exp.motion; exp.lambda | None -> - evaluate ~conditional body_rank (motion ~assignable opt); + evaluate ~conditional body_rank (Lam_analysis.motion ~assignable opt); opt in (* Substitutes in the operands from left to right. The operands from @@ -131,7 +118,7 @@ let simple_beta_reduce ~assignable params body args = | [] -> true | (rank, motion) :: later -> Ext_list.for_all later (fun (rank', motion') -> - rank <= rank' || not (conflict motion motion')) + rank <= rank' || not (Lam_analysis.conflict motion motion')) && in_order later in if not (in_order (unused @ List.rev !evaluated)) then @@ -141,7 +128,12 @@ let simple_beta_reduce ~assignable params body args = List.iteri (fun rank (p, a) -> Hash_ident.add param_hash p - {lambda = a; used = false; rank; motion = motion ~assignable a}) + { + lambda = a; + used = false; + rank; + motion = Lam_analysis.motion ~assignable a; + }) (List.combine params args) in match (body : Lambda.t) with diff --git a/compiler/core/lam_pass_deep_flatten.ml b/compiler/core/lam_pass_deep_flatten.ml index a9292fe8b2..51a22abd1c 100644 --- a/compiler/core/lam_pass_deep_flatten.ml +++ b/compiler/core/lam_pass_deep_flatten.ml @@ -100,12 +100,13 @@ let rec eliminate_tuple (id : Ident.t) (lam : Lambda.t) acc = - also for function compilation, flattening should be done first - [compile_group] and [compile] become mutually recursive function *) -let lambda_of_groups ~(rev_bindings : Lam_group.t list) (result : Lambda.t) : - Lambda.t = +let lambda_of_groups ~assignable ~(rev_bindings : Lam_group.t list) + (result : Lambda.t) : Lambda.t = Ext_list.fold_left rev_bindings result (fun acc x -> match x with | Nop l -> Lambda.seq l acc - | Single (kind, ident, lam) -> Lam_util.refine_let ~kind ident lam acc + | Single (kind, ident, lam) -> + Lam_util.refine_let ~assignable ~kind ident lam acc | Recursive bindings -> Lambda.letrec bindings acc) (* TODO: @@ -155,6 +156,9 @@ let regroups_binding (str : Lambda.let_kind) (id : Ident.t) (arg : Lambda.t) = | _ -> false let deep_flatten (lam : Lambda.t) : Lambda.t = + (* See [Lam_pass_remove_alias.simplify_alias] *) + let assignable = Lambda_traverse.assigned_variables lam in + let lambda_of_groups = lambda_of_groups ~assignable in let rec flatten (acc : Lam_group.t list) (lam : Lambda.t) : Lambda.t * Lam_group.t list = match lam with @@ -225,7 +229,8 @@ let deep_flatten (lam : Lambda.t) : Lambda.t = and aux (lam : Lambda.t) : Lambda.t = match lam with | Llet (str, id, arg, body) when not (regroups_binding str id arg) -> - Lam_util.refine_let ~original:lam ~kind:str id (aux arg) (aux body) + Lam_util.refine_let ~original:lam ~assignable ~kind:str id (aux arg) + (aux body) | Llet _ -> let res, groups = flatten [] lam in lambda_of_groups res ~rev_bindings:groups diff --git a/compiler/core/lam_pass_lets_dce.ml b/compiler/core/lam_pass_lets_dce.ml index 07d50fe928..7dc130fb43 100644 --- a/compiler/core/lam_pass_lets_dce.ml +++ b/compiler/core/lam_pass_lets_dce.ml @@ -93,7 +93,9 @@ let lets_helper ~assignable (count_var : Ident.t -> Lam_pass_count.used_info) Hash_ident.add string_table v s; (* we need move [simplif lbody] later, since adding Hash does have side effect *) Lambda.let_ Alias v l1 (simplif lbody) - | _ -> Lam_util.refine_let ~original:lam ~kind v l1 (simplif lbody) + | _ -> + Lam_util.refine_let ~original:lam ~assignable ~kind v l1 + (simplif lbody) (* TODO: check if it is correct rollback to [StrictOpt]? *)) | Llet (((Strict | Variable) as kind), v, l1, l2) -> ( if not (used v) then @@ -107,7 +109,8 @@ let lets_helper ~assignable (count_var : Ident.t -> Lam_pass_count.used_info) | Strict, Lconst (Const_string s) -> Hash_ident.add string_table v s; Lambda.let_ Alias v l1 (simplif l2) - | _ -> Lam_util.refine_let ~original:lam ~kind v l1 (simplif l2)) + | _ -> + Lam_util.refine_let ~original:lam ~assignable ~kind v l1 (simplif l2)) | Lapply {ap_func = Lfunction ({params; body} as lfunction); ap_args = args; _} when Ext_list.same_length params args diff --git a/compiler/core/lam_pass_sroa.ml b/compiler/core/lam_pass_sroa.ml index e5deff7fe9..17b7b4357a 100644 --- a/compiler/core/lam_pass_sroa.ml +++ b/compiler/core/lam_pass_sroa.ml @@ -109,6 +109,9 @@ let replace ~block ~info ~initializers body = else let fields = fields_for_block block info field_count in let body = rewrite block fields uses body in + (* The fields that are written, of this block and of the ones replaced + in [body] *) + let assignable = lazy (Lambda_traverse.assigned_variables body) in let rec bind_fields index initializers body = match initializers with | [] -> body @@ -121,7 +124,8 @@ let replace ~block ~info ~initializers body = still needs a mutable scalar binding. *) if not use.read then discard_value init body else if not use.written then - Lam_util.refine_let ~kind:Strict fields.(index) init body + Lam_util.refine_let ~assignable:(Lazy.force assignable) ~kind:Strict + fields.(index) init body else Lambda.let_ Variable fields.(index) init body in Some (bind_fields 0 initializers body) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 0bab8c65e4..596f4c32c9 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -41,6 +41,7 @@ - (Return) [let[k] x = e in x] ⟶ e - (Prim) [let[k] x = e in prim p x] ⟶ prim p e (p ≠ makeblock) - (Call) [let[k] x = e in f x] ⟶ f e (x not captured in f) + both when e may run after what the call evaluates first - (Alias) [let[k] x = e in body] ⟶ let[Alias] x = e in body when k ∈ {Strict, StrictOpt} and SafeAlias(e) - (Strict λ) [let[Strict] x = fn in body] ⟶ let[StrictOpt] x = fn in body @@ -50,15 +51,21 @@ evaluation strategy downstream, so we keep its predicate intentionally syntactic and narrow. *) (* [original] is the binding this one was taken apart from, if any. When - nothing is refined it is handed back untouched rather than rebuilt. *) -let refine_let ?original ~kind param (arg : Lambda.t) (l : Lambda.t) : Lambda.t - = + nothing is refined it is handed back untouched rather than rebuilt. + [assignable] are the variables some [Lassign] assigns. *) +let refine_let ?original ~assignable ~kind param (arg : Lambda.t) (l : Lambda.t) + : Lambda.t = (* A JSX element whose only argument is its tag keeps the tag bound: a function bound by [let] gets the binding's name, which React shows *) let keeps_binding = function | Lambda.Pmakeblock _ | Pjsx _ -> true | _ -> false in + (* JavaScript evaluates the callee of a call before the arguments, so + [f(e)] runs [e] after [f], while the binding ran it before *) + let may_follow (callee : Lam_analysis.motion) = + not (Lam_analysis.conflict callee (Lam_analysis.motion ~assignable arg)) + in (* SafeAlias is the predicate that justifies the (Alias) rewrite let[k] x = e in body --> let[Alias] x = e in body for strict bindings. Turning a binding into [Alias] authorises JS codegen @@ -101,17 +108,23 @@ let refine_let ?original ~kind param (arg : Lambda.t) (l : Lambda.t) : Lambda.t cannot refer back to `x`, and we avoid generating a redundant local. *) arg | _, _, Lprim {primitive; args = [Lvar w]; loc; _} - when Ident.same w param && not (keeps_binding primitive) -> + when Ident.same w param + && (not (keeps_binding primitive)) + && ((not (Lam_analysis.reads_callee primitive)) || may_follow Reads) -> (* When we immediately feed the binding into a primitive, like `{ let x = value; Array.length(x) }`, we inline the primitive call - with `value`. This only happens for primitives that are pure and do not - allocate new blocks, so evaluation order and side effects stay the same. *) + with `value`. Only the call of an external function does something + before its argument, which is to read the function, so evaluation + order and side effects stay the same. *) Lambda.prim ~primitive ~args:[arg] loc | _, _, Lapply {ap_func = fn; ap_args = [Lvar w]; ap_loc} - when Ident.same w param && not (Lam_hit.hit_variable param fn) -> + when Ident.same w param + && (not (Lam_hit.hit_variable param fn)) + && may_follow (Lam_analysis.motion ~assignable fn) -> (* For a function call such as `{ let x = value; someFn(x) }`, we can - rewrite to `someFn(value)` as long as the callee does not capture `x`. - This removes the temporary binding while preserving the call semantics. *) + rewrite to `someFn(value)` as long as the callee does not capture `x` + and `value` may run after it: not `{ let x = c.n; next()(x) }`, where + `next()` may assign `c.n`. *) Lambda.apply fn [arg] ap_loc | (Strict | StrictOpt), arg, _ when is_safe_to_alias arg -> (* `Strict` and `StrictOpt` bindings both evaluate the RHS immediately diff --git a/compiler/core/lam_util.mli b/compiler/core/lam_util.mli index 99be5d6059..a2fa6dc64e 100644 --- a/compiler/core/lam_util.mli +++ b/compiler/core/lam_util.mli @@ -54,12 +54,14 @@ val alias_ident_or_global : val refine_let : ?original:Lambda.t -> + assignable:Set_ident.t -> kind:Lambda.let_kind -> Ident.t -> Lambda.t -> Lambda.t -> Lambda.t (** [original] is the binding being rebuilt, when there is one. It is returned - unchanged if no refinement applies. *) + unchanged if no refinement applies. [assignable] are the variables some + [Lassign] assigns. *) val not_function : Lambda.t -> bool diff --git a/compiler/ml/lambda_traverse.ml b/compiler/ml/lambda_traverse.ml index 027eb9dd88..6672d4f432 100644 --- a/compiler/ml/lambda_traverse.ml +++ b/compiler/ml/lambda_traverse.ml @@ -245,11 +245,16 @@ let free_variables l = let assigned_variables l = let assigned = ref Set_ident.empty in + (* [visit] is the one callback of the walk, where [iter] would allocate a + closure for each node *) let rec walk l = (match l with | Lassign (id, _) -> assigned := Set_ident.add id !assigned | _ -> ()); - iter walk l + ignore (shallow_exists visit l) + and visit l = + walk l; + false in walk l; !assigned diff --git a/tests/tests/src/inline_arg_order_cross_module_lib.mjs b/tests/tests/src/inline_arg_order_cross_module_lib.mjs index c4ac75114b..7455b3a44e 100644 --- a/tests/tests/src/inline_arg_order_cross_module_lib.mjs +++ b/tests/tests/src/inline_arg_order_cross_module_lib.mjs @@ -5,7 +5,12 @@ function minus3(_x, a, b) { return b - a | 0; } +function callNext(next, x) { + return next()(x); +} + export { minus3, + callNext, } /* No side effect */ diff --git a/tests/tests/src/inline_arg_order_cross_module_lib.res b/tests/tests/src/inline_arg_order_cross_module_lib.res index d087603a9a..fe71f1da3c 100644 --- a/tests/tests/src/inline_arg_order_cross_module_lib.res +++ b/tests/tests/src/inline_arg_order_cross_module_lib.res @@ -2,3 +2,5 @@ // Inlined by inline_arg_order_cross_module_test.res let minus3 = (_x: int, a: int, b: int) => b - a + +let callNext = (next: unit => int => int, x: int) => next()(x) diff --git a/tests/tests/src/inline_arg_order_cross_module_test.mjs b/tests/tests/src/inline_arg_order_cross_module_test.mjs index c545847199..5435def96e 100644 --- a/tests/tests/src/inline_arg_order_cross_module_test.mjs +++ b/tests/tests/src/inline_arg_order_cross_module_test.mjs @@ -24,13 +24,25 @@ function bumpThenRead(x) { return s - a | 0; } +function readThenCall() { + let s = 0; + let next = () => { + s = s + 1 | 0; + return y => y; + }; + let x = s; + return next()(x); +} + Mocha.describe("Inline_arg_order_cross_module_test", () => { - Mocha.test("a read of a local ref before an argument that assigns it", () => Test_utils.eq("File \"inline_arg_order_cross_module_test.res\", line 30, characters 7-14", 1, readThenBump(0))); - Mocha.test("a read of a local ref after an argument that assigns it", () => Test_utils.eq("File \"inline_arg_order_cross_module_test.res\", line 34, characters 7-14", 9, bumpThenRead(0))); + Mocha.test("a read of a local ref before an argument that assigns it", () => Test_utils.eq("File \"inline_arg_order_cross_module_test.res\", line 38, characters 7-14", 1, readThenBump(0))); + Mocha.test("a read of a local ref after an argument that assigns it", () => Test_utils.eq("File \"inline_arg_order_cross_module_test.res\", line 42, characters 7-14", 9, bumpThenRead(0))); + Mocha.test("a read of a local ref before a callee that assigns it", () => Test_utils.eq("File \"inline_arg_order_cross_module_test.res\", line 46, characters 7-14", 0, readThenCall())); }); export { readThenBump, bumpThenRead, + readThenCall, } /* Not a pure module */ diff --git a/tests/tests/src/inline_arg_order_cross_module_test.res b/tests/tests/src/inline_arg_order_cross_module_test.res index 8b3f57b1c2..ae05c45a0d 100644 --- a/tests/tests/src/inline_arg_order_cross_module_test.res +++ b/tests/tests/src/inline_arg_order_cross_module_test.res @@ -25,6 +25,14 @@ let bumpThenRead = x => { Inline_arg_order_cross_module_lib.minus3(x, bumpS(), s.contents) } +let readThenCall = () => { + let s = ref(0) + Inline_arg_order_cross_module_lib.callNext(() => { + s := s.contents + 1 + y => y + }, s.contents) +} + describe(__MODULE__, () => { test("a read of a local ref before an argument that assigns it", () => { eq(__LOC__, 1, readThenBump(0)) @@ -33,4 +41,8 @@ describe(__MODULE__, () => { test("a read of a local ref after an argument that assigns it", () => { eq(__LOC__, 9, bumpThenRead(0)) }) + + test("a read of a local ref before a callee that assigns it", () => { + eq(__LOC__, 0, readThenCall()) + }) }) diff --git a/tests/tests/src/let_before_callee_test.mjs b/tests/tests/src/let_before_callee_test.mjs new file mode 100644 index 0000000000..8fd7638dba --- /dev/null +++ b/tests/tests/src/let_before_callee_test.mjs @@ -0,0 +1,183 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +let c = { + n: 0 +}; + +function next() { + c.n = c.n + 1 | 0; + return x => x; +} + +let h = { + call: x => x + 1 | 0 +}; + +function swap() { + h.call = x => x + 100 | 0; + return 1; +} + +let events = []; + +function log(s) { + events.push(s); + return s; +} + +async function getHandler() { + events.push("getHandler"); + return s => s; +} + +globalThis.letBeforeCalleeHook = x => x + 1 +; + +let install = (() => { + globalThis.letBeforeCalleeHook = x => x + 100 + return 1 +}); + +globalThis.LetBeforeCalleeBox = class { + constructor(x) { + this.v = x + 1 + } +} +; + +let installBox = (() => { + globalThis.LetBeforeCalleeBox = class { + constructor(x) { + this.v = x + 100 + } + } + return 1 +}); + +function readBeforeCall() { + let before = c.n; + return next()(before); +} + +function effectBeforeFieldRead() { + let x = swap(); + return h.call(x); +} + +async function effectBeforeAwait() { + let x = log("argument"); + return (await getHandler())(x); +} + +function callNext(x) { + return next()(x); +} + +function inlined() { + let x = c.n; + return next()(x); +} + +function recordField() { + let s_saved = c.n; + return next()(s_saved); +} + +function assignedVariable(replace) { + let f = x => x + 1 | 0; + let x; + if (replace) { + f = x => x + 100 | 0; + x = 1; + } else { + x = 2; + } + return f(x); +} + +function effectBeforeExternal() { + let x = install(); + return globalThis.letBeforeCalleeHook(x); +} + +function effectBeforeNew() { + let x = installBox(); + return new (globalThis.LetBeforeCalleeBox)(x); +} + +function fold(cb) { + return cb(swap()); +} + +function foldRead() { + return h.call(c.n); +} + +Mocha.describe("Let_before_callee_test", () => { + Mocha.test("a let that reads what the callee changes", () => { + c.n = 0; + Test_utils.eq("File \"let_before_callee_test.res\", line 130, characters 7-14", readBeforeCall(), 0); + Test_utils.eq("File \"let_before_callee_test.res\", line 131, characters 7-14", c.n, 1); + }); + Mocha.test("a let that changes the field the callee is read from", () => { + h.call = x => x + 1 | 0; + Test_utils.eq("File \"let_before_callee_test.res\", line 136, characters 7-14", effectBeforeFieldRead(), 101); + }); + Mocha.test("a let before an awaited callee", async () => { + events.splice(0, events.length); + let p = effectBeforeAwait(); + Test_utils.eq("File \"let_before_callee_test.res\", line 142, characters 7-14", events, [ + "argument", + "getHandler" + ]); + return Test_utils.eq("File \"let_before_callee_test.res\", line 143, characters 7-14", await p, "argument"); + }); + Mocha.test("an argument of an inlined function", () => { + c.n = 0; + let x = c.n; + Test_utils.eq("File \"let_before_callee_test.res\", line 148, characters 7-14", next()(x), 0); + }); + Mocha.test("a field of a local record", () => { + c.n = 0; + Test_utils.eq("File \"let_before_callee_test.res\", line 153, characters 7-14", recordField(), 0); + }); + Mocha.test("a let that assigns the ref the callee is read from", () => { + Test_utils.eq("File \"let_before_callee_test.res\", line 157, characters 7-14", assignedVariable(true), 101); + Test_utils.eq("File \"let_before_callee_test.res\", line 158, characters 7-14", assignedVariable(false), 3); + }); + Mocha.test("a let that replaces an external function", () => Test_utils.eq("File \"let_before_callee_test.res\", line 161, characters 60-67", effectBeforeExternal(), 101)); + Mocha.test("a let that replaces an external class", () => Test_utils.eq("File \"let_before_callee_test.res\", line 163, characters 57-64", effectBeforeNew().v, 101)); + Mocha.test("a callee that stays the same", () => { + h.call = x => x + 1 | 0; + Test_utils.eq("File \"let_before_callee_test.res\", line 167, characters 7-14", (swap() << 1), 2); + c.n = 5; + Test_utils.eq("File \"let_before_callee_test.res\", line 169, characters 7-14", h.call(c.n), 105); + }); +}); + +export { + c, + next, + h, + swap, + events, + log, + getHandler, + install, + installBox, + readBeforeCall, + effectBeforeFieldRead, + effectBeforeAwait, + callNext, + inlined, + recordField, + assignedVariable, + effectBeforeExternal, + effectBeforeNew, + fold, + foldRead, +} +/* Not a pure module */ diff --git a/tests/tests/src/let_before_callee_test.res b/tests/tests/src/let_before_callee_test.res new file mode 100644 index 0000000000..91e2374ee6 --- /dev/null +++ b/tests/tests/src/let_before_callee_test.res @@ -0,0 +1,171 @@ +open Mocha +open Test_utils + +@module("mocha") +external testAsync: (string, unit => promise) => unit = "test" + +// `let x = e; f(x)` runs `e` before it evaluates `f`. JavaScript evaluates the +// callee of `f(e)` before the argument, so the generated JS keeps the binding +// unless `e` gives the same value after `f`, or `f` the same function after +// `e`: `fold` and `foldRead` below still compile to one call. + +type counter = {mutable n: int} + +type handler = {mutable call: int => int} + +type snapshot = {mutable unused: int, saved: int} + +let c = {n: 0} + +let next = () => { + c.n = c.n + 1 + (x: int) => x +} + +let h = {call: x => x + 1} + +let swap = () => { + h.call = x => x + 100 + 1 +} + +let events: array = [] + +let log = (s: string) => { + events->Array.push(s) + s +} + +let getHandler = async () => { + events->Array.push("getHandler") + (s: string) => s +} + +%%raw(`globalThis.letBeforeCalleeHook = x => x + 1`) + +@scope("globalThis") @val external hook: int => int = "letBeforeCalleeHook" + +let install: unit => int = %raw(`() => { + globalThis.letBeforeCalleeHook = x => x + 100 + return 1 +}`) + +type box = {v: int} + +%%raw(`globalThis.LetBeforeCalleeBox = class { + constructor(x) { + this.v = x + 1 + } +}`) + +@scope("globalThis") @new external makeBox: int => box = "LetBeforeCalleeBox" + +let installBox: unit => int = %raw(`() => { + globalThis.LetBeforeCalleeBox = class { + constructor(x) { + this.v = x + 100 + } + } + return 1 +}`) + +let readBeforeCall = () => { + let before = c.n + next()(before) +} + +let effectBeforeFieldRead = () => { + let x = swap() + h.call(x) +} + +let effectBeforeAwait = async () => { + let x = log("argument") + (await getHandler())(x) +} + +let callNext = (x: int) => next()(x) + +let inlined = () => callNext(c.n) + +let recordField = () => { + let s = {unused: 0, saved: c.n} + next()(s.saved) +} + +let assignedVariable = replace => { + let f = ref((x: int) => x + 1) + let x = if replace { + f := x => x + 100 + 1 + } else { + 2 + } + f.contents(x) +} + +let effectBeforeExternal = () => { + let x = install() + hook(x) +} + +let effectBeforeNew = () => { + let x = installBox() + makeBox(x) +} + +let fold = (cb: int => int) => { + let x = swap() + cb(x) +} + +let foldRead = () => { + let x = c.n + h.call(x) +} + +describe(__MODULE__, () => { + test("a let that reads what the callee changes", () => { + c.n = 0 + eq(__LOC__, readBeforeCall(), 0) + eq(__LOC__, c.n, 1) + }) + + test("a let that changes the field the callee is read from", () => { + h.call = x => x + 1 + eq(__LOC__, effectBeforeFieldRead(), 101) + }) + + testAsync("a let before an awaited callee", async () => { + events->Array.splice(~start=0, ~remove=Array.length(events), ~insert=[]) + let p = effectBeforeAwait() + eq(__LOC__, events, ["argument", "getHandler"]) + eq(__LOC__, await p, "argument") + }) + + test("an argument of an inlined function", () => { + c.n = 0 + eq(__LOC__, inlined(), 0) + }) + + test("a field of a local record", () => { + c.n = 0 + eq(__LOC__, recordField(), 0) + }) + + test("a let that assigns the ref the callee is read from", () => { + eq(__LOC__, assignedVariable(true), 101) + eq(__LOC__, assignedVariable(false), 3) + }) + + test("a let that replaces an external function", () => eq(__LOC__, effectBeforeExternal(), 101)) + + test("a let that replaces an external class", () => eq(__LOC__, effectBeforeNew().v, 101)) + + test("a callee that stays the same", () => { + h.call = x => x + 1 + eq(__LOC__, fold(x => x * 2), 2) + c.n = 5 + eq(__LOC__, foldRead(), 105) + }) +}) diff --git a/tests/tests/src/stdlib/Stdlib_PromiseTest.mjs b/tests/tests/src/stdlib/Stdlib_PromiseTest.mjs index 8bfeb94f8a..dc00b0d4b9 100644 --- a/tests/tests/src/stdlib/Stdlib_PromiseTest.mjs +++ b/tests/tests/src/stdlib/Stdlib_PromiseTest.mjs @@ -207,9 +207,8 @@ function thenAfterCatch() { RE_EXN_ID: TestError, _1: "some rejected value" })), e => { - let tmp; - tmp = e.RE_EXN_ID === TestError && e._1 === "some rejected value" ? "success" : "not a test error"; - return Promise.resolve(tmp); + let s = e.RE_EXN_ID === TestError && e._1 === "some rejected value" ? "success" : "not a test error"; + return Promise.resolve(s); }).then(msg => { Test.run([ [