diff --git a/changes/external-int-argument-retention.fix.md b/changes/external-int-argument-retention.fix.md new file mode 100644 index 0000000000..31f60c7fb5 --- /dev/null +++ b/changes/external-int-argument-retention.fix.md @@ -0,0 +1 @@ +- Fix an `@int` or `@as` argument of an external keeping what it reads alive for as long as a closure created next to it. diff --git a/changes/external-unit-argument-effects-in-place.fix.md b/changes/external-unit-argument-effects-in-place.fix.md new file mode 100644 index 0000000000..6c6f7fbe4f --- /dev/null +++ b/changes/external-unit-argument-effects-in-place.fix.md @@ -0,0 +1,3 @@ +- Evaluate a `unit` or `@ignore` argument of an external in source order, as in `setX(t, f(), g())`. +- Call an external whose arguments are all `@ignore` without them, like other externals. +- Keep the effects of the `@ignore` arguments of a `@module` external that is the module itself. diff --git a/changes/optional-external-argument-none.fix.md b/changes/optional-external-argument-none.fix.md new file mode 100644 index 0000000000..f4682822ea --- /dev/null +++ b/changes/optional-external-argument-none.fix.md @@ -0,0 +1 @@ +- Fix a `TypeError` when an optional external argument such as `~x=?dict->Dict.get(k)`, or an optional JSX key, is `None`. diff --git a/changes/optional-external-argument-once.fix.md b/changes/optional-external-argument-once.fix.md new file mode 100644 index 0000000000..aec248a958 --- /dev/null +++ b/changes/optional-external-argument-once.fix.md @@ -0,0 +1,2 @@ +- Evaluate an optional `@obj` argument such as `~x=?f()` in source order. +- Read an optional external argument that is a JS property, such as a `@get` getter, only once. diff --git a/compiler/core/js_analyzer.ml b/compiler/core/js_analyzer.ml index 85f46a68a8..9eab7e1e39 100644 --- a/compiler/core/js_analyzer.ml +++ b/compiler/core/js_analyzer.ml @@ -365,8 +365,13 @@ let rev_toplevel_flatten block = is_constant a && is_constant b | _ -> false *) -let rec is_okay_to_duplicate (e : J.expression) = +let rec is_okay_to_duplicate ?(property_reads = true) (e : J.expression) = match e.expression_desc with + | Var (Id id) + when (not property_reads) && Ext_ident.is_js id + && String.contains id.name '.' -> + false | Var _ | Bool _ | Str _ | Template_literal _ | Number _ -> true - | Static_index (e, _s, _off) -> is_okay_to_duplicate e + | Static_index (e, _s, off) -> + (property_reads || off <> None) && is_okay_to_duplicate ~property_reads e | _ -> false diff --git a/compiler/core/js_analyzer.mli b/compiler/core/js_analyzer.mli index 281ec6c412..e325033937 100644 --- a/compiler/core/js_analyzer.mli +++ b/compiler/core/js_analyzer.mli @@ -70,8 +70,9 @@ val rev_toplevel_flatten : J.block -> J.block (* val is_constant : J.expression -> bool *) -(** Simple expression, - no computation involved so that it is okay to be duplicated -*) - -val is_okay_to_duplicate : J.expression -> bool +val is_okay_to_duplicate : ?property_reads:bool -> J.expression -> bool +(** Simple expression, no computation involved so that it is okay to be + duplicated. With [~property_reads:false], reading it twice also gives the + same value twice: it reads no JS property, which may run a getter each + time, as one from a [@get] external or a [@val] name with a dot does. + Fields of records count. *) diff --git a/compiler/core/js_ast_util.ml b/compiler/core/js_ast_util.ml index c761a06006..ad164470fe 100644 --- a/compiler/core/js_ast_util.ml +++ b/compiler/core/js_ast_util.ml @@ -26,8 +26,9 @@ module S = Js_stmt_make -let named_expression (e : J.expression) : (J.statement * Ident.t) option = - if Js_analyzer.is_okay_to_duplicate e then None +let named_expression ?property_reads (e : J.expression) : + (J.statement * Ident.t) option = + if Js_analyzer.is_okay_to_duplicate ?property_reads e then None else let obj = Ext_ident.create_tmp () in let obj_code = S.define_variable ~kind:Strict obj e in diff --git a/compiler/core/js_ast_util.mli b/compiler/core/js_ast_util.mli index 3977f0e483..24cde0e181 100644 --- a/compiler/core/js_ast_util.mli +++ b/compiler/core/js_ast_util.mli @@ -22,4 +22,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *) -val named_expression : J.expression -> (J.statement * Ident.t) option +val named_expression : + ?property_reads:bool -> J.expression -> (J.statement * Ident.t) option +(** A temporary bound to the expression, unless + {!Js_analyzer.is_okay_to_duplicate} holds for it *) diff --git a/compiler/core/js_exp_make.ml b/compiler/core/js_exp_make.ml index ae1a4273ae..84d4e6c256 100644 --- a/compiler/core/js_exp_make.ml +++ b/compiler/core/js_exp_make.ml @@ -406,8 +406,6 @@ let rec seq ?comment (e0 : t) (e1 : t) : t = | (Number _ | Var _ | Undefined _), _ -> e1 | _ -> {expression_desc = Seq (e0, e1); comment; source_loc = None} -let fuse_to_seq x xs = if xs = [] then x else Ext_list.fold_left xs x seq - let zero_int_literal : t = { expression_desc = Number (Int {i = 0l; c = None}); @@ -1767,30 +1765,6 @@ let bigint_mod ~checked (e0 : t) (e1 : t) = if checked then runtime_call Primitive_modules.bigint "mod_" [e0; e1] else bigint_op Mod e0 e1 -(* TODO -- alpha conversion - remember to add parens.. -*) -let of_block block : t = - let return_unit = false in - (* This case is not hit that much*) - call ~info:Js_call_info.ml_full_call - { - comment = None; - source_loc = None; - expression_desc = - Fun - { - is_method = false; - params = []; - body = block; - env = Js_fun_env.make 0; - return_unit; - async = false; - directive = None; - }; - } - [] - let is_null (x : t) = triple_equal x nil let is_null_undefined_constant (x : t) = diff --git a/compiler/core/js_exp_make.mli b/compiler/core/js_exp_make.mli index 2bfc4709c5..5994f2a648 100644 --- a/compiler/core/js_exp_make.mli +++ b/compiler/core/js_exp_make.mli @@ -269,8 +269,6 @@ val make_block : val seq : ?comment:string -> t -> t -> t -val fuse_to_seq : t -> t list -> t - val object_entries : ?dup:J.expression -> J.property_map -> J.object_entry list val obj : ?dup:J.expression -> J.property_map -> t @@ -307,9 +305,6 @@ val in_ : t -> t -> t val dummy_obj : Lambda.tag_info -> t -val of_block : J.statement list -> t -(** convert a block to expresion by using IIFE *) - val raw_js_code : Js_raw_info.code_info -> string -> t val nil : t diff --git a/compiler/core/js_of_lam_option.ml b/compiler/core/js_of_lam_option.ml index ef9afc1fa3..5ad8ee143a 100644 --- a/compiler/core/js_of_lam_option.ml +++ b/compiler/core/js_of_lam_option.ml @@ -74,10 +74,12 @@ let get_default_undefined_from_optional (arg : J.expression) : J.expression = match desc with | Optional_block (x, _) -> x (* invariant: option encoding *) | _ -> - if Js_analyzer.is_okay_to_duplicate arg then + if Js_analyzer.is_okay_to_duplicate ~property_reads:false arg then (* FIXME: no need do such inlining*) E.econd (is_not_none arg) (val_from_option arg) E.undefined - else E.runtime_call Primitive_modules.option "valFromOption" [arg] + else + (* [valFromOption] returns [undefined] for [None] as well *) + E.runtime_call Primitive_modules.option "valFromOption" [arg] let option_unwrap (arg : J.expression) : J.expression = let desc = arg.expression_desc in diff --git a/compiler/core/js_of_lam_variant.ml b/compiler/core/js_of_lam_variant.ml index 4386120982..97d44c9b26 100644 --- a/compiler/core/js_of_lam_variant.ml +++ b/compiler/core/js_of_lam_variant.ml @@ -27,34 +27,55 @@ module S = Js_stmt_make type arg_expression = Splice0 | Splice1 of E.t | Splice2 of E.t * E.t -(* [eval] and [eval_as_int] read an argument other than a literal once, - inside the function that dispatches on it. That is the same as reading it - in place when the read has no effect and cannot await. *) -let eval_in_place (arg : J.expression) = - Js_analyzer.no_side_effect_expression arg - (* [eval_as_event] reads an argument other than a literal twice, for the tag - (inside the function that dispatches on it, with [@as]) and for the - payload. That is the same as reading it once, in place, for constants, - variables, and fields, array elements and conditionals of them. A JS global - ([@val]) or a JS property read, as from a [@get] external, may run a getter - each time, so it does not count. *) + and for the payload. That is the same as reading it once, in place, when + [Js_analyzer.is_okay_to_duplicate ~property_reads:false] holds for it: for + constants, variables and fields of records. A JS global ([@val]) counts as + a variable, like a value of a JS module ([@module]), though either may be + a getter. A JS property read does not count: a [@get] external is there to + run a getter, and a [@val] or [@module] value with [@scope], or a [@val] + with a dotted name such as ["window.ev"], is a property read as well, + which cannot be told from it. Array elements and conditionals of such + arguments, and their fields, count too, though [is_okay_to_duplicate] + leaves them out: here the alternative is a temporary, a statement that + also binds the arguments to its left that have side effects. *) let eval_as_event_in_place (arg : J.expression) = let rec duplicable (e : J.expression) = match e.expression_desc with - | Undefined _ | Bool _ | Number _ | Str _ | Template_literal _ - | Var (Qualified _) -> - true - | Var (Id id) -> not (Ext_ident.is_js id) | Static_index (e, _, Some _) -> duplicable e | Array_index (a, b) -> duplicable a && duplicable b | Cond (a, b, c) -> duplicable a && duplicable b && duplicable c - | _ -> false + | _ -> Js_analyzer.is_okay_to_duplicate ~property_reads:false e in match arg.expression_desc with | Caml_block ([{expression_desc = Str _}; _], _, Blk_poly_var) -> true | _ -> duplicable arg +(* [(x => { switch (x) { case s: return r; ... } })(arg)]: the result [r] for + the case [s] that [arg] matches. [arg] is an argument of the call, so it is + evaluated once and in its place, and an [await] in it belongs to the code + around it. The function captures no variable of that code, so it keeps + none of them alive for the closures that code creates. *) +let dispatch (arg : J.expression) (cases : (string * 'a) list) + (result : 'a -> J.expression) : J.expression = + let x = Ident.create "x" in + E.call ~info:Js_call_info.ml_full_call + (E.ocaml_fun ~return_unit:false ~async:false ~one_unit_arg:false [x] + [ + S.string_switch (E.var x) + (Ext_list.map cases (fun (s, r) -> + ( Variant_runtime.Literal (String s), + J. + { + switch_body = [S.return_stmt (result r)]; + should_break = false; + (* FIXME: if true, still print break*) + comment = None; + source_loc = None; + } ))); + ]) + [arg] + (* we need destruct [undefined] when input is optional *) let eval (arg : J.expression) (dispatches : (string * string) list) : E.t = if arg == E.undefined then E.undefined @@ -63,21 +84,7 @@ let eval (arg : J.expression) (dispatches : (string * string) list) : E.t = | Str txt -> let s = Ext_list.assoc_by_string dispatches txt None in E.str s - | _ -> - E.of_block - [ - S.string_switch arg - (Ext_list.map dispatches (fun (s, r) -> - ( Variant_runtime.Literal (String s), - J. - { - switch_body = [S.return_stmt (E.str r)]; - should_break = false; - (* FIXME: if true, still print break*) - comment = None; - source_loc = None; - } ))); - ] + | _ -> dispatch arg dispatches E.str (* invariant: optional is not allowed in this case *) (* arg is a polyvar *) @@ -99,21 +106,7 @@ let eval_as_event (arg : J.expression) Splice2 ( (match dispatches with | Some dispatches -> - E.of_block - [ - S.string_switch - (E.poly_var_tag_access arg) - (Ext_list.map dispatches (fun (s, r) -> - ( Variant_runtime.Literal (String s), - J. - { - switch_body = [S.return_stmt (E.str r)]; - should_break = false; - (* FIXME: if true, still print break*) - comment = None; - source_loc = None; - } ))); - ] + dispatch (E.poly_var_tag_access arg) dispatches E.str | None -> E.poly_var_tag_access arg), E.poly_var_value_access arg ) @@ -124,21 +117,7 @@ let eval_as_int (arg : J.expression) (dispatches : (string * int) list) : E.t = match arg.expression_desc with | Str txt -> E.int (Int32.of_int (Ext_list.assoc_by_string dispatches txt None)) - | _ -> - E.of_block - [ - S.string_switch arg - (Ext_list.map dispatches (fun (s, r) -> - ( Variant_runtime.Literal (String s), - J. - { - switch_body = [S.return_stmt (E.int (Int32.of_int r))]; - should_break = false; - (* FIXME: if true, still print break*) - comment = None; - source_loc = None; - } ))); - ] + | _ -> dispatch arg dispatches (fun r -> E.int (Int32.of_int r)) let eval_as_unwrap (arg : J.expression) : E.t = match arg.expression_desc with diff --git a/compiler/core/js_of_lam_variant.mli b/compiler/core/js_of_lam_variant.mli index 47f3cf7673..03fc722053 100644 --- a/compiler/core/js_of_lam_variant.mli +++ b/compiler/core/js_of_lam_variant.mli @@ -29,19 +29,19 @@ type arg_expression = | Splice1 of J.expression | Splice2 of J.expression * J.expression -val eval_in_place : J.expression -> bool -(** Whether [eval] and [eval_as_int] can be given the argument as it is, - rather than a variable bound to it: they may read it inside a function *) - val eval_as_event_in_place : J.expression -> bool (** Whether [eval_as_event] can be given the argument as it is, rather than a variable bound to it: it may read it twice *) val eval : J.expression -> (string * string) list -> J.expression +(** The string that a [@string] argument stands for. The argument is + evaluated once, in its place: a function that switches on it is called + with it. *) val eval_as_event : J.expression -> (string * string) list option -> arg_expression val eval_as_int : J.expression -> (string * int) list -> J.expression +(** The integer that an [@int] argument stands for, evaluated like [eval] *) val eval_as_unwrap : J.expression -> J.expression diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index f4841957cb..a6b8a51751 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -282,7 +282,8 @@ let rec delay_safe ~assignable (e : J.expression) = Building an array, block or object literal is pure, so a literal stays in place and its elements are evaluated now instead. That also keeps the literal visible to translations that look into their arguments, such as - the rows of a dict literal or a [@variadic] array. *) + the rows of a dict literal or a [@variadic] array. So does an + [Optional_block], which keeps an optional argument known to be present. *) let rec evaluate_now ~assignable (e : J.expression) : J.block * J.expression = match e.expression_desc with | _ when delay_safe ~assignable e -> ([], e) @@ -291,6 +292,9 @@ let rec evaluate_now ~assignable (e : J.expression) : J.block * J.expression = if Js_analyzer.no_side_effect_expression a then (rest, b) else ({J.statement_desc = Exp a; comment = None; source_loc = None} :: rest, b) + | Optional_block (a, identity) -> + let held, a = evaluate_now ~assignable a in + (held, {e with expression_desc = Optional_block (a, identity)}) | Array xs -> let held, xs = evaluate_all_now ~assignable xs in (held, {e with expression_desc = Array xs}) @@ -327,12 +331,29 @@ and evaluate_all_now ~assignable (xs : J.expression list) : let blocks, xs = Ext_list.split_map xs (evaluate_now ~assignable) in (List.concat blocks, xs) +(* Whether evaluating [e] runs no code: it has no side effects and reads no JS + property, which may run a getter. A record field counts. *) +let runs_no_code (e : J.expression) = + Js_analyzer.is_okay_to_duplicate ~property_reads:false e + +(* Whether [block] only defines variables as values that run no code, as a + temporary bound to a record field does *) +let defines_pure_values (block : J.block) = + Ext_list.for_all block (fun (s : J.statement) -> + match s.statement_desc with + | Variable {value = None} -> true + | Variable {value = Some e} -> runs_no_code e + | _ -> false) + (* Assembles compiled arguments for a call or a literal that evaluates them left to right. Each argument comes with the statements that have to run before its value is read. Putting all statements first and then the values would move the statements of an argument in front of the values of the arguments to its left, so when an argument has statements, every - argument to its left is evaluated before them with [evaluate_now]. + argument to its left is evaluated before them with [evaluate_now]. An + argument that runs no code stays in place when the statements after it + only define values that run none either, since then neither can change + the other. With [~method_callee:true], the first argument is a JS method read [obj.m] being called: the object is held and the property read stays in @@ -351,7 +372,8 @@ let sequence_arguments ?(method_callee = false) ~assignable Ext_list.fold_right args (last, [], []) (fun (block, value) (i, code, values) -> let held, value = - if code = [] then ([], value) + if code = [] || (runs_no_code value && defines_pure_values code) + then ([], value) else match value.expression_desc with | Static_index (obj, name, pos) when method_callee && i = 0 -> @@ -1908,7 +1930,10 @@ let compile output_prefix = | {primitive = Pjs_object_create labels; args} -> let args_code, args_expr = compile_arguments - ~hold:(Lam_compile_external_obj.hold_obj_args labels) + ~hold: + (Lam_compile_external_obj.hold_obj_args + ~delay_safe:(delay_safe ~assignable:lambda_cxt.meta.assignable) + labels) lambda_cxt args in let block, exp = @@ -1917,9 +1942,9 @@ let compile output_prefix = Js_output.output_of_block_and_expression lambda_cxt.continuation (Ext_list.append args_code block) exp - | {primitive = Pjs_call {arg_types} as primitive; args; loc} -> + | {primitive = Pjs_call {arg_types; ffi} as primitive; args; loc} -> compile_primitive_default - ~hold:(Lam_compile_external_call.hold_args arg_types) + ~hold:(Lam_compile_external_call.hold_args ffi arg_types) primitive args loc | {primitive; args; loc} -> compile_primitive_default primitive args loc and collect_dup_overrides (copy_id : Ident.t) (lam : Lambda.t) diff --git a/compiler/core/lam_compile_external_call.ml b/compiler/core/lam_compile_external_call.ml index 1a63ba8642..4e6d02c085 100644 --- a/compiler/core/lam_compile_external_call.ml +++ b/compiler/core/lam_compile_external_call.ml @@ -47,31 +47,14 @@ let append_list x xs = | Splice1 a -> a :: xs | Splice2 (a, b) -> a :: b :: xs -(* The first return value is value, the second argument is side effect expressions - Only the [unit] with no label will be ignored - When we are passing a boxed value to external(optional), we need - unbox it in the first place. - - Note when optional value is not passed, the unboxed value would be - [undefined], with the combination of `[@int]` it would be still be - [undefined], this by default is still correct.. - {[ - (function () { - switch (undefined) { - case 97 : - return "a"; - case 98 : - return "b"; - - } - }()) === undefined - ]} - - This would not work with [NonNullString] -*) -let ocaml_to_js_eff ~(arg_label : External_arg_spec.label_noname) - ~(arg_type : External_arg_spec.attr) (raw_arg : E.t) : - arg_expression * E.t list = +(* The JS arguments that a compiled argument of an external is passed as. A + [unit] argument, which has no label, and an [@ignore] argument are left + out: [hold_arg] has already made their effects a statement of the + argument. An optional argument is passed as its payload, or [undefined] + when it is absent, which an [@int] or [@as] dispatch passes on as + [undefined]. *) +let ocaml_to_js ~(arg_label : External_arg_spec.label_noname) + ~(arg_type : External_arg_spec.attr) (raw_arg : E.t) : arg_expression = let arg = match arg_label with | Arg_optional -> @@ -81,21 +64,15 @@ let ocaml_to_js_eff ~(arg_label : External_arg_spec.label_noname) match arg_type with | Arg_cst _ -> assert false (* has to be preprocessed by {!Lambda} first *) - | Extern_unit -> - ( (if arg_label = Arg_empty then Splice0 else Splice1 E.unit), - if Js_analyzer.no_side_effect_expression arg then [] else [arg] ) - (* leave up later to decide *) - | Ignore -> - (Splice0, if Js_analyzer.no_side_effect_expression arg then [] else [arg]) - | Poly_var_string {descr} -> (Splice1 (Js_of_lam_variant.eval arg descr), []) - | Poly_var {descr} -> (Js_of_lam_variant.eval_as_event arg descr, []) + | Extern_unit | Ignore -> Splice0 + | Poly_var_string {descr} -> Splice1 (Js_of_lam_variant.eval arg descr) + | Poly_var {descr} -> Js_of_lam_variant.eval_as_event arg descr (* FIXME: encode invariant below in the signature*) (* length of 2 - the poly var tag - the value *) - | Int dispatches -> - (Splice1 (Js_of_lam_variant.eval_as_int arg dispatches), []) + | Int dispatches -> Splice1 (Js_of_lam_variant.eval_as_int arg dispatches) | Unwrap -> let single_arg = match arg_label with @@ -112,61 +89,66 @@ let ocaml_to_js_eff ~(arg_label : External_arg_spec.label_noname) Js_of_lam_option.option_unwrap raw_arg | _ -> Js_of_lam_variant.eval_as_unwrap raw_arg in - (Splice1 single_arg, []) - | Nothing -> (Splice1 arg, []) + Splice1 single_arg + | Nothing -> Splice1 arg + +(* Prepares a compiled argument, with the statements that run before its value + is read, for [ocaml_to_js], so that it is evaluated once and in its place + among the arguments (see [sequence_arguments] in [Lam_compile]). -(* The translation of a polymorphic variant argument other than a literal - reads it twice, as [e.NAME] and [e.VAL], or inside the function that a - dispatch on its tag builds, where an [await] is a syntax error. So unless - the translation can read it in place, the argument is bound to a temporary - after its statements, which keeps it in its place among the arguments (see - [sequence_arguments] in [Lam_compile]). Of an optional argument known to be - present, only the payload is bound, so that the translation still knows it - is present. *) -let hold_arg ~(arg_label : External_arg_spec.label_noname) - ~(arg_type : External_arg_spec.attr) + - The translation of an event-style polymorphic variant argument other + than a literal reads it twice, as [e.NAME] and [e.VAL]. So unless + reading it twice is the same as reading it once, the argument is bound + to a temporary after its statements. + - An [@int] or [@as] argument is passed, in its place, to a function that + switches on it. + - A [unit] or [@ignore] argument is not passed, so its effects become a + statement of the argument, and its value [undefined]. Of an optional + one, only the payload is evaluated, since its presence does not + matter. *) +let hold_arg ~(arg_type : External_arg_spec.attr) ((block, arg) as compiled : J.block * E.t) : J.block * E.t = - let needs_temporary (e : E.t) = - match arg_type with - | Poly_var _ -> not (Js_of_lam_variant.eval_as_event_in_place e) - | Poly_var_string _ | Int _ -> not (Js_of_lam_variant.eval_in_place e) - | Arg_cst _ | Extern_unit | Nothing | Ignore | Unwrap -> false - in - let hold (e : E.t) = + match arg_type with + | Poly_var _ when not (Js_of_lam_variant.eval_as_event_in_place arg) -> let tmp = Ext_ident.create_tmp () in - (Ext_list.append_one block (S.define_variable ~kind:Strict tmp e), E.var tmp) - in - match (arg_label, arg.expression_desc) with - | Arg_optional, Optional_block (x, identity) -> - if needs_temporary x then - let block, x = hold x in - (block, {arg with expression_desc = Optional_block (x, identity)}) - else compiled - | (Arg_optional | Arg_label | Arg_empty), _ -> - if needs_temporary arg then hold arg else compiled + ( Ext_list.append_one block (S.define_variable ~kind:Strict tmp arg), + E.var tmp ) + | Extern_unit | Ignore -> + let effects = + match arg.expression_desc with + | Optional_block (x, _) -> x + | _ -> arg + in + ( (if Js_analyzer.no_side_effect_expression effects then block + else Ext_list.append_one block (S.exp effects)), + E.unit ) + | Poly_var _ | Poly_var_string _ | Int _ | Arg_cst _ | Nothing | Unwrap -> + compiled (* [hold_arg] for the compiled arguments of a call, which have the statements that run before their values are read. The specs pair with the arguments as in [assemble_args_no_splice], and no spec translates an - argument past them. *) -let hold_args (arg_types : External_arg_spec.params) - (args : (J.block * E.t) list) = + argument past them. The object of a [@send] call is left as it is, since + [translate_ffi] calls the method on it whatever its spec, such as + [@ignore], says. *) +let hold_args (decl : External_ffi_types.external_decl) + (arg_types : External_arg_spec.params) (args : (J.block * E.t) list) = let rec aux (arg_types : External_arg_spec.params) args = match (arg_types, args) with | {arg_type = Arg_cst _; _} :: arg_types, args -> aux arg_types args - | {arg_label; arg_type} :: arg_types, arg :: args -> - hold_arg ~arg_label ~arg_type arg :: aux arg_types args + | {arg_type; _} :: arg_types, arg :: args -> + hold_arg ~arg_type arg :: aux arg_types args | [], args -> args | _ :: _, [] -> assert false in - aux arg_types args - -let empty_pair = ([], []) - -let add_eff eff e = - match eff with - | None -> e - | Some v -> E.seq v e + match decl.kind with + | Decl_send _ -> ( + match (arg_types, args) with + | _self_type :: arg_types, self :: args -> self :: aux arg_types args + | [], _ | _, [] -> assert false) + | Decl_val _ | Decl_new _ | Decl_get _ | Decl_set _ | Decl_get_index + | Decl_set_index -> + aux arg_types args type specs = External_arg_spec.params @@ -198,62 +180,42 @@ let keep_non_undefined_args (arg_types : specs) (args : exprs) = otherwise, we should provide a good error message here, no compiler failure here Invariant : Array encoding - @return arguments and effect *) -let assemble_args_no_splice (arg_types : specs) (args : exprs) : - exprs * E.t option = - let rec aux (labels : specs) (args : exprs) : exprs * exprs = +let assemble_args_no_splice (arg_types : specs) (args : exprs) : exprs = + let rec aux (labels : specs) (args : exprs) : exprs = match (labels, args) with | [], _ -> assert (args = []); - empty_pair + [] | {arg_type = Arg_cst cst; _} :: labels, args -> (* can not be Optional *) - let accs, eff = aux labels args in - (Lam_compile_const.translate_arg_cst cst :: accs, eff) + Lam_compile_const.translate_arg_cst cst :: aux labels args | {arg_label; arg_type} :: labels, arg :: args -> - let accs, eff = aux labels args in - let acc, new_eff = ocaml_to_js_eff ~arg_label ~arg_type arg in - (append_list acc accs, Ext_list.append new_eff eff) + append_list (ocaml_to_js ~arg_label ~arg_type arg) (aux labels args) | _ :: _, [] -> assert false in - let args, eff = aux arg_types args in - ( keep_non_undefined_args arg_types args, - match eff with - | [] -> None - | x :: xs -> - (* FIXME: the order of effects? *) - Some (E.fuse_to_seq x xs) ) + keep_non_undefined_args arg_types (aux arg_types args) -let assemble_args_has_splice (arg_types : specs) (args : exprs) : - exprs * E.t option * bool = +let assemble_args_has_splice (arg_types : specs) (args : exprs) : exprs * bool = let dynamic = ref false in - let rec aux (labels : specs) (args : exprs) = + let rec aux (labels : specs) (args : exprs) : exprs = match (labels, args) with | [], _ -> assert (args = []); - empty_pair + [] | {arg_type = Arg_cst cst; _} :: labels, args -> - let accs, eff = aux labels args in - (Lam_compile_const.translate_arg_cst cst :: accs, eff) + Lam_compile_const.translate_arg_cst cst :: aux labels args | {arg_label; arg_type} :: labels, arg :: args -> ( - let accs, eff = aux labels args in + let accs = aux labels args in match (args, (arg : E.t)) with - | [], {expression_desc = Array ls; _} -> (Ext_list.append ls accs, eff) + | [], {expression_desc = Array ls; _} -> Ext_list.append ls accs | _ -> if args = [] then dynamic := true; - let acc, new_eff = ocaml_to_js_eff ~arg_type ~arg_label arg in - (append_list acc accs, Ext_list.append new_eff eff)) + append_list (ocaml_to_js ~arg_type ~arg_label arg) accs) | _ :: _, [] -> assert false in - let args, eff = aux arg_types args in - ( args, - (match eff with - | [] -> None - | x :: xs -> - (* FIXME: the order of effects? *) - Some (E.fuse_to_seq x xs)), - !dynamic ) + let args = aux arg_types args in + (args, !dynamic) let translate_scoped_module_val (module_name : External_ffi_types.external_module_name option) (fn : string) @@ -326,6 +288,13 @@ let translate_ffi (cxt : Lam_compile_context.t) arg_types ~(prim_name : string) | Some (Module_named emn) -> Some emn | Some Module_itself | None -> None in + (* the JS arguments of a call, spread when [@variadic] needs it *) + let assemble_args arg_types args = + if splice then + let args, dynamic = assemble_args_has_splice arg_types args in + if dynamic then E.variadic_args args else args + else assemble_args_no_splice arg_types args + in let mark_new_object () = (* handle [@@new]: mark the bound identifier (if any) as an object; order matters, since the property affects later code generation *) @@ -340,53 +309,37 @@ let translate_ffi (cxt : Lam_compile_context.t) arg_types ~(prim_name : string) | Decl_val _, Some Module_itself -> (* the module itself, called as a function *) let fn = external_var (module_itself_name ()) in - if splice then - let args, eff, dynamic = assemble_args_has_splice arg_types args in - let args = if dynamic then E.variadic_args args else args in - add_eff eff (E.call ~info:Js_call_info.na_full_call fn args) - else - let args, eff = assemble_args_no_splice arg_types args in - (* TODO: fix in rest calling convention *) - add_eff eff (E.call ~info:Js_call_info.na_full_call fn args) + E.call ~info:Js_call_info.na_full_call fn (assemble_args arg_types args) | Decl_new _, Some Module_itself -> (* the module itself, as a class *) let fn = external_var (module_itself_name ()) in - let args, eff = assemble_args_no_splice arg_types args in + let args = assemble_args_no_splice arg_types args in (* TODO: fix in rest calling convention *) - add_eff eff - (mark_new_object (); - E.new_ fn args) + mark_new_object (); + E.new_ fn args | ( (Decl_send _ | Decl_get _ | Decl_set _ | Decl_get_index | Decl_set_index), Some Module_itself ) -> assert false (* rejected during digestion *) - | Decl_val {name}, _ when decl.effective_arity = 0 -> - (* a global value *) + | Decl_val {name}, _ when decl.effective_arity = 0 -> ( + (* a global value. A call applies it to the arguments of a function type + that the external does not spell out, or to the [@ignore] arguments + of one that it does, which are not passed. *) let e = translate_scoped_module_val named_module name scopes in - if args = [] then e else E.call ~info:Js_call_info.na_full_call e args + match (arg_types, args) with + | _, [] -> e + | [], _ -> E.call ~info:Js_call_info.na_full_call e args + | _ :: _, _ -> + E.call ~info:Js_call_info.na_full_call e + (assemble_args_no_splice arg_types args)) | Decl_val {name = fn}, _ -> (* a global (possibly module-scoped) function call *) let fn = translate_scoped_module_val named_module fn scopes in - if splice then - let args, eff, dynamic = assemble_args_has_splice arg_types args in - let args = if dynamic then E.variadic_args args else args in - add_eff eff (E.call ~info:Js_call_info.na_full_call fn args) - else - let args, eff = assemble_args_no_splice arg_types args in - add_eff eff @@ E.call ~info:Js_call_info.na_full_call fn args + E.call ~info:Js_call_info.na_full_call fn (assemble_args arg_types args) | Decl_new {name = fn}, _ -> - if splice then - let args, eff, dynamic = assemble_args_has_splice arg_types args in - let args = if dynamic then E.variadic_args args else args in - let fn = translate_scoped_module_val named_module fn scopes in - add_eff eff - (mark_new_object (); - E.new_ fn args) - else - let args, eff = assemble_args_no_splice arg_types args in - let fn = translate_scoped_module_val named_module fn scopes in - add_eff eff - (mark_new_object (); - E.new_ fn args) + let args = assemble_args arg_types args in + let fn = translate_scoped_module_val named_module fn scopes in + mark_new_object (); + E.new_ fn args | Decl_send {name}, _ -> ( match args with | self :: args -> @@ -397,23 +350,12 @@ let translate_ffi (cxt : Lam_compile_context.t) arg_types ~(prim_name : string) | _self_type :: arg_types -> arg_types | [] -> assert false in - if splice then - let args, eff, dynamic = assemble_args_has_splice arg_types args in - let args = if dynamic then E.variadic_args args else args in - add_eff eff - (let self = translate_scoped_access scopes self in - E.call ~info:Js_call_info.na_full_call (E.dot self name) args) - else - let args, eff = assemble_args_no_splice arg_types args in - add_eff eff - (let self = translate_scoped_access scopes self in - E.call ~info:Js_call_info.na_full_call (E.dot self name) args) + let args = assemble_args arg_types args in + let self = translate_scoped_access scopes self in + E.call ~info:Js_call_info.na_full_call (E.dot self name) args | _ -> assert false) | Decl_get {name}, _ -> ( - let args, cur_eff = assemble_args_no_splice arg_types args in - add_eff cur_eff - @@ - match args with + match assemble_args_no_splice arg_types args with | [obj] -> let obj = translate_scoped_access scopes obj in E.dot obj name @@ -422,29 +364,20 @@ let translate_ffi (cxt : Lam_compile_context.t) arg_types ~(prim_name : string) invalid_arg "Internal compiler error: @get external called with wrong number of \ arguments. Expected exactly one object argument." - (* Note these assertion happens in call site *)) + (* Note these assertion happens in call site *)) | Decl_set {name}, _ -> ( - let args, cur_eff = assemble_args_no_splice arg_types args in - add_eff cur_eff - @@ - match (args, arg_types) with - | [obj; v], _ -> + match assemble_args_no_splice arg_types args with + | [obj; v] -> let obj = translate_scoped_access scopes obj in E.assign (E.dot obj name) v | _ -> assert false) | Decl_get_index, _ -> ( - let args, cur_eff = assemble_args_no_splice arg_types args in - add_eff cur_eff - @@ - match args with + match assemble_args_no_splice arg_types args with | [obj; v] -> Js_of_lam_array.ref_array (translate_scoped_access scopes obj) v | _ -> assert false) | Decl_set_index, _ -> ( - let args, cur_eff = assemble_args_no_splice arg_types args in - add_eff cur_eff - @@ - match args with + match assemble_args_no_splice arg_types args with | [obj; v; value] -> Js_of_lam_array.set_array (translate_scoped_access scopes obj) v value | _ -> assert false) diff --git a/compiler/core/lam_compile_external_call.mli b/compiler/core/lam_compile_external_call.mli index 2b73daa216..e893272720 100644 --- a/compiler/core/lam_compile_external_call.mli +++ b/compiler/core/lam_compile_external_call.mli @@ -22,27 +22,33 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *) -val ocaml_to_js_eff : +val ocaml_to_js : arg_label:External_arg_spec.label_noname -> arg_type:External_arg_spec.attr -> J.expression -> - Js_of_lam_variant.arg_expression * J.expression list -(** Compile ocaml external function call to JS IR. *) + Js_of_lam_variant.arg_expression +(** The JS arguments that an argument of an external, prepared by + [hold_arg], is passed as *) val hold_arg : - arg_label:External_arg_spec.label_noname -> arg_type:External_arg_spec.attr -> J.block * J.expression -> J.block * J.expression -(** Binds a compiled argument, after its statements, to a temporary unless - [ocaml_to_js_eff] can read it in place: it reads a polymorphic variant - argument twice, or inside a function it builds *) +(** Prepares a compiled argument and its statements for [ocaml_to_js], so + that it is evaluated once and in its place among the arguments. An + event-style polymorphic variant argument, which [ocaml_to_js] reads + twice, is bound to a temporary after the statements unless reading it + twice is the same as reading it once. The effects of a [unit] or + [@ignore] argument, which [ocaml_to_js] leaves out, become a statement + of the argument. *) val hold_args : + External_ffi_types.external_decl -> External_arg_spec.params -> (J.block * J.expression) list -> (J.block * J.expression) list -(** [hold_arg] for the compiled arguments of a call to an external *) +(** [hold_arg] for the compiled arguments of a call to an external, but for + the object of a [@send] call, which [translate_ffi] uses as it is *) val translate_ffi : Lam_compile_context.t -> diff --git a/compiler/core/lam_compile_external_obj.ml b/compiler/core/lam_compile_external_obj.ml index ebb97c50c8..b7bff47d0e 100644 --- a/compiler/core/lam_compile_external_obj.ml +++ b/compiler/core/lam_compile_external_obj.ml @@ -36,24 +36,69 @@ module S = Js_stmt_make But the default to be undefined seems reasonable *) +(* Whether [assemble_obj_args] assigns the optional argument [arg] after the + object literal, since its presence is not known *) +let presence_unknown (arg : J.expression) = + Js_of_lam_option.destruct_optional arg ~for_sure_none:false + ~for_sure_some:(fun _ -> false) + ~not_sure:(fun () -> true) + +(* The arguments that [assemble_obj_args] evaluates in the object literal or + before it, which are all but the optional ones whose presence is not + known *) +let rec literal_args (labels : External_arg_spec.obj_params) + (args : (J.block * J.expression) list) = + match (labels, args) with + | {obj_arg_label = Obj_label _; obj_arg_type = Arg_cst _} :: labels, args -> + literal_args labels args + | {obj_arg_label = Obj_optional _} :: labels, (_, arg) :: args + when presence_unknown arg -> + literal_args labels args + | ( {obj_arg_label = Obj_empty | Obj_label _ | Obj_optional _} :: labels, + (_, arg) :: args ) -> + arg :: literal_args labels args + | [], [] -> [] + | [], _ :: _ | _ :: _, [] -> assert false + (* [Lam_compile_external_call.hold_arg] for the compiled arguments of an object-creating external, which pair with the labels as in - [assemble_obj_args] *) -let rec hold_obj_args (labels : External_arg_spec.obj_params) + [assemble_obj_args]. That function assigns an optional argument whose + presence is not known after the object literal, so it evaluates the + argument after the arguments that follow it. Unless that gives the same + values and effects, the argument is bound to a temporary after its + statements, which keeps it in its place. It does when the argument is + [delay_safe], and when each argument that the literal evaluates after it + is [delay_safe] or neither of the two has side effects. *) +let rec hold_obj_args ~(delay_safe : J.expression -> bool) + (labels : External_arg_spec.obj_params) (args : (J.block * J.expression) list) = match (labels, args) with | {obj_arg_label = Obj_label _; obj_arg_type = Arg_cst _} :: labels, args -> - hold_obj_args labels args - | {obj_arg_label = Obj_empty} :: labels, arg :: args -> - arg :: hold_obj_args labels args - | {obj_arg_label = Obj_label _; obj_arg_type} :: labels, arg :: args -> - Lam_compile_external_call.hold_arg ~arg_label:Arg_label - ~arg_type:obj_arg_type arg - :: hold_obj_args labels args + hold_obj_args ~delay_safe labels args + | ( {obj_arg_label = Obj_empty | Obj_label _; obj_arg_type} :: labels, + arg :: args ) -> + Lam_compile_external_call.hold_arg ~arg_type:obj_arg_type arg + :: hold_obj_args ~delay_safe labels args | {obj_arg_label = Obj_optional _; obj_arg_type} :: labels, arg :: args -> - Lam_compile_external_call.hold_arg ~arg_label:Arg_optional - ~arg_type:obj_arg_type arg - :: hold_obj_args labels args + let ((block, value) as held) = + Lam_compile_external_call.hold_arg ~arg_type:obj_arg_type arg + in + let order_matters (later : J.expression) = + not + (delay_safe later + || Js_analyzer.no_side_effect_expression value + && Js_analyzer.no_side_effect_expression later) + in + (if + presence_unknown value + && (not (delay_safe value)) + && List.exists order_matters (literal_args labels args) + then + let tmp = Ext_ident.create_tmp () in + ( Ext_list.append_one block (S.define_variable ~kind:Strict tmp value), + E.var tmp ) + else held) + :: hold_obj_args ~delay_safe labels args | [], [] -> [] | [], _ :: _ | _ :: _, [] -> assert false @@ -61,67 +106,54 @@ let rec hold_obj_args (labels : External_arg_spec.obj_params) let assemble_obj_args (labels : External_arg_spec.obj_params) (args : J.expression list) : J.block * J.expression = let rec aux (labels : External_arg_spec.obj_params) args : - (Js_op.property_name * E.t) list * J.expression list * _ = + (Js_op.property_name * E.t) list * _ = match (labels, args) with - | [], [] -> ([], [], []) + | [], [] -> ([], []) | ( {obj_arg_label = Obj_label {name = label}; obj_arg_type = Arg_cst cst} :: labels, args ) -> - let accs, eff, assign = aux labels args in + let accs, assign = aux labels args in ( (Js_op.Lit label, Lam_compile_const.translate_arg_cst cst) :: accs, - eff, assign ) (* | {obj_arg_label = EmptyCst _ } :: rest , args -> assert false *) - | {obj_arg_label = Obj_empty} :: labels, arg :: args -> - (* unit type*) - let ((accs, eff, assign) as r) = aux labels args in - if Js_analyzer.no_side_effect_expression arg then r - else (accs, arg :: eff, assign) + | {obj_arg_label = Obj_empty} :: labels, _ :: args -> + (* a [unit] or [@ignore] argument, whose effects [hold_obj_args] made a + statement *) + aux labels args | ( ({obj_arg_label = Obj_label {name = label}} as arg_kind) :: labels, arg :: args ) -> ( - let accs, eff, assign = aux labels args in - let acc, new_eff = - Lam_compile_external_call.ocaml_to_js_eff ~arg_label:Arg_label + let accs, assign = aux labels args in + match + Lam_compile_external_call.ocaml_to_js ~arg_label:Arg_label ~arg_type:arg_kind.obj_arg_type arg - in - match acc with + with | Splice2 _ | Splice0 -> assert false - | Splice1 x -> - ((Js_op.Lit label, x) :: accs, Ext_list.append new_eff eff, assign) + | Splice1 x -> ((Js_op.Lit label, x) :: accs, assign) (* evaluation order is undefined *)) | ( ({obj_arg_label = Obj_optional {name = label}; obj_arg_type} as arg_kind) :: labels, arg :: args ) -> - let ((accs, eff, assign) as r) = aux labels args in + let ((accs, assign) as r) = aux labels args in Js_of_lam_option.destruct_optional arg ~for_sure_none:r ~for_sure_some:(fun x -> - let acc, new_eff = - Lam_compile_external_call.ocaml_to_js_eff ~arg_label:Arg_label + match + Lam_compile_external_call.ocaml_to_js ~arg_label:Arg_label ~arg_type:obj_arg_type x - in - match acc with + with | Splice2 _ | Splice0 -> assert false - | Splice1 x -> - ((Js_op.Lit label, x) :: accs, Ext_list.append new_eff eff, assign)) - ~not_sure:(fun _ -> (accs, eff, (arg_kind, arg) :: assign)) + | Splice1 x -> ((Js_op.Lit label, x) :: accs, assign)) + ~not_sure:(fun _ -> (accs, (arg_kind, arg) :: assign)) | {obj_arg_label = Obj_empty | Obj_label _ | Obj_optional _} :: _, [] -> assert false | [], _ :: _ -> assert false in - let map, eff, assignment = aux labels args in + let map, assignment = aux labels args in match assignment with - | [] -> - ( [], - match eff with - | [] -> E.obj map - | x :: xs -> E.seq (E.fuse_to_seq x xs) (E.obj map) ) + | [] -> ([], E.obj map) | _ -> let v = Ext_ident.create_tmp () in let var_v = E.var v in - ( S.define_variable ~kind:Variable v - (match eff with - | [] -> E.obj map - | x :: xs -> E.seq (E.fuse_to_seq x xs) (E.obj map)) + ( S.define_variable ~kind:Variable v (E.obj map) :: Ext_list.flat_map assignment (fun ((xlabel : External_arg_spec.obj_param), (arg : J.expression)) -> @@ -130,53 +162,39 @@ let assemble_obj_args (labels : External_arg_spec.obj_params) obj_arg_label = Obj_optional {name = label; for_sure_no_nested_option}; } -> ( - (* Need make sure whether assignment is effectful or not - to avoid code duplication - *) - match Js_ast_util.named_expression arg with + (* [arg] is read for its presence and for its value, so it is + bound to a temporary unless reading it twice gives the + same value twice *) + match Js_ast_util.named_expression ~property_reads:false arg with | None -> ( - let acc, new_eff = - Lam_compile_external_call.ocaml_to_js_eff - ~arg_label:Arg_empty ~arg_type:xlabel.obj_arg_type + match + Lam_compile_external_call.ocaml_to_js ~arg_label:Arg_empty + ~arg_type:xlabel.obj_arg_type (if for_sure_no_nested_option then arg else Js_of_lam_option.val_from_option arg) - in - match acc with + with | Splice1 v -> [ S.if_ (Js_of_lam_option.is_not_none arg) - [ - S.exp - (E.assign (E.dot var_v label) - (match new_eff with - | [] -> v - | x :: xs -> E.seq (E.fuse_to_seq x xs) v)); - ]; + [S.exp (E.assign (E.dot var_v label) v)]; ] | Splice0 | Splice2 _ -> assert false) | Some (st, id) -> ( (* FIXME: see #2503 *) let arg = E.var id in - let acc, new_eff = - Lam_compile_external_call.ocaml_to_js_eff - ~arg_label:Arg_empty ~arg_type:xlabel.obj_arg_type + match + Lam_compile_external_call.ocaml_to_js ~arg_label:Arg_empty + ~arg_type:xlabel.obj_arg_type (if for_sure_no_nested_option then arg else Js_of_lam_option.val_from_option arg) - in - match acc with + with | Splice1 v -> [ st; S.if_ (Js_of_lam_option.is_not_none arg) - [ - S.exp - (E.assign (E.dot var_v label) - (match new_eff with - | [] -> v - | x :: xs -> E.seq (E.fuse_to_seq x xs) v)); - ]; + [S.exp (E.assign (E.dot var_v label) v)]; ] | Splice0 | Splice2 _ -> assert false)) | _ -> assert false), diff --git a/compiler/core/lam_compile_external_obj.mli b/compiler/core/lam_compile_external_obj.mli index 22ef026293..86f9b40f90 100644 --- a/compiler/core/lam_compile_external_obj.mli +++ b/compiler/core/lam_compile_external_obj.mli @@ -26,11 +26,16 @@ object literal. *) val hold_obj_args : + delay_safe:(J.expression -> bool) -> External_arg_spec.obj_params -> (J.block * J.expression) list -> (J.block * J.expression) list (** {!Lam_compile_external_call.hold_arg} for the compiled arguments of an - object-creating external *) + object-creating external. An optional argument whose presence is not + known is also bound to a temporary when evaluating it after the object + literal, as {!assemble_obj_args} does, could change a value or the order + of effects. [delay_safe] tells whether evaluating an argument after other + code gives the same value and effects. *) val assemble_obj_args : External_arg_spec.obj_params -> J.expression list -> J.block * J.expression diff --git a/packages/@rescript/runtime/Primitive_option.res b/packages/@rescript/runtime/Primitive_option.res index 05b4e65f71..d348909a6d 100644 --- a/packages/@rescript/runtime/Primitive_option.res +++ b/packages/@rescript/runtime/Primitive_option.res @@ -63,7 +63,7 @@ let fromNull = (type t, x: Primitive_js_extern.null): option => /** Preserves `None`/`undefined` and unwraps one outer option layer. Nested `Some` values stay encoded one level deeper. */ let valFromOption = (x: Obj.t): Obj.t => - if x !== Obj.repr(Primitive_js_extern.null) && isNested(x) { + if !Primitive_js_extern.testAny(x) && isNested(x) { let {depth}: nested = Obj.magic(x) if depth == 0 { Obj.magic(None) diff --git a/packages/@rescript/runtime/lib/es6/Primitive_option.mjs b/packages/@rescript/runtime/lib/es6/Primitive_option.mjs index 9fe7522ae0..13be2c44f1 100644 --- a/packages/@rescript/runtime/lib/es6/Primitive_option.mjs +++ b/packages/@rescript/runtime/lib/es6/Primitive_option.mjs @@ -46,7 +46,7 @@ function fromNull(x) { } function valFromOption(x) { - if (x === null || x.BS_PRIVATE_NESTED_SOME_NONE === undefined) { + if ((x == null) || x.BS_PRIVATE_NESTED_SOME_NONE === undefined) { return x; } let depth = x.BS_PRIVATE_NESTED_SOME_NONE; diff --git a/packages/@rescript/runtime/lib/js/Primitive_option.cjs b/packages/@rescript/runtime/lib/js/Primitive_option.cjs index 5b50ebc668..e297a59a40 100644 --- a/packages/@rescript/runtime/lib/js/Primitive_option.cjs +++ b/packages/@rescript/runtime/lib/js/Primitive_option.cjs @@ -46,7 +46,7 @@ function fromNull(x) { } function valFromOption(x) { - if (x === null || x.BS_PRIVATE_NESTED_SOME_NONE === undefined) { + if ((x == null) || x.BS_PRIVATE_NESTED_SOME_NONE === undefined) { return x; } let depth = x.BS_PRIVATE_NESTED_SOME_NONE; diff --git a/tests/gentype_tests/typescript-react-example/src/ImportJsValue.res.js b/tests/gentype_tests/typescript-react-example/src/ImportJsValue.res.js index c31daa8a0c..f9224b1ff3 100644 --- a/tests/gentype_tests/typescript-react-example/src/ImportJsValue.res.js +++ b/tests/gentype_tests/typescript-react-example/src/ImportJsValue.res.js @@ -48,21 +48,21 @@ function useColor(prim) { } function useEscapedInlineVariant(prim) { - return ImportJsValueGen.useEscapedInlineVariant((() => { - switch (prim) { + return ImportJsValueGen.useEscapedInlineVariant((x => { + switch (x) { case "illegalName" : return "Illegal\"Name"; } - })()); + })(prim)); } function useUtf8InlineVariant(prim) { - return ImportJsValueGen.useUtf8InlineVariant((() => { - switch (prim) { + return ImportJsValueGen.useUtf8InlineVariant((x => { + switch (x) { case "utf8" : return "café\npath\\name"; } - })()); + })(prim)); } function higherOrder(prim) { diff --git a/tests/tests/src/bs_ignore_effect_test.mjs b/tests/tests/src/bs_ignore_effect_test.mjs index a9ca8204ea..947f87ff90 100644 --- a/tests/tests/src/bs_ignore_effect_test.mjs +++ b/tests/tests/src/bs_ignore_effect_test.mjs @@ -1,6 +1,7 @@ // Generated by ReScript, PLEASE EDIT WITH CARE import * as Mocha from "mocha"; +import * as Nodepath from "node:path"; import * as Test_utils from "./test_utils.mjs"; function add(x,y){ @@ -12,21 +13,137 @@ let v = { contents: 0 }; -let h = (v.contents = v.contents + 1 | 0, { +v.contents = v.contents + 1 | 0; + +let h = { hi: 2, lo: 0 +}; + +v.contents = v.contents + 1 | 0; + +let z = add(3.0, 2.0); + +let defineCountArgs = (() => { + globalThis.bsIgnoreEffectCountArgs = (...args) => args.length }); -let z = (v.contents = v.contents + 1 | 0, "Float", add(3.0, 2.0)); +let counter = { + contents: 0 +}; + +function bump() { + counter.contents = counter.contents + 1 | 0; +} Mocha.describe("Bs_ignore_effect_test", () => { - Mocha.test("ignore effect 1", () => Test_utils.eq("File \"bs_ignore_effect_test.res\", line 32, characters 35-42", v.contents, 2)); - Mocha.test("ignore effect 2", () => Test_utils.eq("File \"bs_ignore_effect_test.res\", line 33, characters 35-42", z, 5.0)); + Mocha.test("ignore effect 1", () => Test_utils.eq("File \"bs_ignore_effect_test.res\", line 54, characters 35-42", v.contents, 2)); + Mocha.test("ignore effect 2", () => Test_utils.eq("File \"bs_ignore_effect_test.res\", line 55, characters 35-42", z, 5.0)); + Mocha.test("unit and @ignore arguments after others", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let arr = []; + let tmp = mark("value", 1); + mark("unit", undefined); + arr.push(tmp); + let tmp$1 = mark("value", 2); + mark("ignored", 0); + arr.push(tmp$1); + let tmp$2 = mark("field", 3); + mark("unit", undefined); + let o = { + a: tmp$2 + }; + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 67, characters 7-14", [ + "value", + "unit", + "value", + "ignored", + "field", + "unit" + ], log); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 68, characters 7-14", [ + 1, + 2 + ], arr); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 69, characters 7-14", 3, o.a); + }); + Mocha.test("an optional @ignore argument that is absent", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let arr = []; + let tmp = mark("value", 1); + mark("absent", undefined); + arr.push(tmp); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 80, characters 7-14", [ + "value", + "absent" + ], log); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 81, characters 7-14", [1], arr); + }); + Mocha.test("a unit argument that changes an earlier argument", () => { + let arr = []; + let tmp = counter.contents; + bump(); + arr.push(tmp); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 87, characters 7-14", [0], arr); + }); + Mocha.test("the object of a @send call marked @ignore", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let arr = []; + arr.push(1); + mark("object", arr).push(2); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 99, characters 7-14", ["object"], log); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 100, characters 7-14", [ + 1, + 2 + ], arr); + }); + Mocha.test("an external whose arguments are all @ignore", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + defineCountArgs(); + let pure = bsIgnoreEffectCountArgs(); + mark("first", 1); + mark("second", 2); + let effectful = bsIgnoreEffectCountArgs(); + mark("module", 0); + let path = Nodepath; + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 113, characters 7-14", [ + 0, + 0 + ], [ + pure, + effectful + ]); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 114, characters 7-14", [ + "first", + "second", + "module" + ], log); + Test_utils.eq("File \"bs_ignore_effect_test.res\", line 115, characters 7-14", true, path.sep === "/" || path.sep === "\\"); + }); }); export { v, h, z, + defineCountArgs, + counter, + bump, } /* Not a pure module */ diff --git a/tests/tests/src/bs_ignore_effect_test.res b/tests/tests/src/bs_ignore_effect_test.res index c0e2e91f02..736fba1ace 100644 --- a/tests/tests/src/bs_ignore_effect_test.res +++ b/tests/tests/src/bs_ignore_effect_test.res @@ -28,7 +28,90 @@ let z = add( 2.0, ) +// The effects of a unit or @ignore argument run in its place among the +// arguments, after the arguments to its left. +@send external pushThen: (array, int, unit) => int = "push" +@send external pushIgnoring: (array, int, @ignore int) => int = "push" +@send external pushIgnoringOpt: (array, int, ~extra: @ignore int=?, unit) => int = "push" +@obj external makeThen: (~a: int, unit) => {"a": int} = "" + +// The object of a @send call is the object whatever its spec says. +@send external pushOnIgnored: (@ignore array, int) => int = "push" + +// An external whose arguments are all @ignore is called without them, or is +// a value when it is a JS module. +@val external countArgs: (@ignore int, @ignore int) => int = "bsIgnoreEffectCountArgs" +@module external pathModule: @ignore int => {"sep": string} = "node:path" + +let defineCountArgs: unit => unit = %raw(`() => { + globalThis.bsIgnoreEffectCountArgs = (...args) => args.length +}`) + +let counter = ref(0) +let bump = () => counter := counter.contents + 1 + describe(__MODULE__, () => { test("ignore effect 1", () => eq(__LOC__, v.contents, 2)) test("ignore effect 2", () => eq(__LOC__, z, 5.0)) + + test("unit and @ignore arguments after others", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let arr = [] + arr->pushThen(mark("value", 1), mark("unit", ()))->ignore + arr->pushIgnoring(mark("value", 2), mark("ignored", 0))->ignore + let o = makeThen(~a=mark("field", 3), mark("unit", ())) + eq(__LOC__, ["value", "unit", "value", "ignored", "field", "unit"], log) + eq(__LOC__, [1, 2], arr) + eq(__LOC__, 3, o["a"]) + }) + + test("an optional @ignore argument that is absent", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let arr = [] + arr->pushIgnoringOpt(mark("value", 1), ~extra=?mark("absent", None), ())->ignore + eq(__LOC__, ["value", "absent"], log) + eq(__LOC__, [1], arr) + }) + + test("a unit argument that changes an earlier argument", () => { + let arr = [] + arr->pushThen(counter.contents, bump())->ignore + eq(__LOC__, [0], arr) + }) + + test("the object of a @send call marked @ignore", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let arr = [] + arr->pushOnIgnored(1)->ignore + mark("object", arr)->pushOnIgnored(2)->ignore + eq(__LOC__, ["object"], log) + eq(__LOC__, [1, 2], arr) + }) + + test("an external whose arguments are all @ignore", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + defineCountArgs() + let pure = countArgs(1, 2) + let effectful = countArgs(mark("first", 1), mark("second", 2)) + let path = pathModule(mark("module", 0)) + eq(__LOC__, (0, 0), (pure, effectful)) + eq(__LOC__, ["first", "second", "module"], log) + eq(__LOC__, true, path["sep"] == "/" || path["sep"] == "\\") + }) }) diff --git a/tests/tests/src/optional_external_arg_none_test.mjs b/tests/tests/src/optional_external_arg_none_test.mjs new file mode 100644 index 0000000000..b7474fac07 --- /dev/null +++ b/tests/tests/src/optional_external_arg_none_test.mjs @@ -0,0 +1,148 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.mjs"; +import * as JsxRuntime from "react/jsx-runtime"; + +function greet(name) { + return name === undefined ? "Hello" : "Hello " + name +} + +function Greeting(name) { + this.text = greet(name) +} +; + +let makeTarget = (() => { + const calls = [] + const record = (...args) => { + calls.push(args.map(String).join(" ")) + } + return { calls, greet, setLevel: record, setMode: record } +}); + +let opaque = (x => x); + +function greetUser(names, id) { + return greet(Primitive_option.valFromOption(names[id])); +} + +function hello(name, param) { + return greet(name !== undefined ? Primitive_option.valFromOption(name) : undefined); +} + +function helloUser(names, id) { + return greet(Primitive_option.valFromOption(names[id])); +} + +function item(keys, id) { + return JsxRuntime.jsx("li", { + children: id + }, Primitive_option.valFromOption(keys[id])); +} + +Mocha.describe("Optional_external_arg_none_test", () => { + Mocha.test("None from a call, a lookup or a conditional", () => { + let names = { + ann: "Ann" + }; + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 68, characters 7-14", "Hello Ann", greetUser(names, "ann")); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 69, characters 7-14", "Hello", greetUser(names, "bob")); + let list = ["Ann"]; + let at = i => greet(Primitive_option.valFromOption(list[i])); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 72, characters 7-14", [ + "Hello Ann", + "Hello" + ], [ + at(0), + at(1) + ]); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 73, characters 7-14", "Hello", greet(Primitive_option.valFromOption(opaque(undefined)))); + let pick = (b, name) => greet(Primitive_option.valFromOption(b ? name : undefined)); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 75, characters 7-14", [ + "Hello Ann", + "Hello" + ], [ + pick(true, "Ann"), + pick(false, "Ann") + ]); + }); + Mocha.test("None passed to @send and @new externals", () => { + let t = makeTarget(); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 80, characters 7-14", "Hello", t.greet(Primitive_option.valFromOption(opaque(undefined)))); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 81, characters 7-14", "Hello", new Greeting(Primitive_option.valFromOption(opaque(undefined))).text); + }); + Mocha.test("None forwarded by an inlined function", () => { + let names = { + ann: "Ann" + }; + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 86, characters 7-14", [ + "Hello Ann", + "Hello" + ], [ + helloUser(names, "ann"), + helloUser(names, "bob") + ]); + }); + Mocha.test("None for @int and @string arguments", () => { + let t = makeTarget(); + let levels = ["high"]; + let modes = ["dark"]; + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(levels[0]))); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(levels[1]))); + t.setMode((x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(Primitive_option.valFromOption(modes[0]))); + t.setMode((x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(Primitive_option.valFromOption(modes[1]))); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 97, characters 7-14", [ + "10", + "undefined", + "dark-mode", + "undefined" + ], t.calls); + }); + Mocha.test("a JSX key that is None", () => { + let keys = { + a: "key-a" + }; + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 102, characters 7-14", "key-a", Primitive_option.fromNullable(item(keys, "a").key)); + Test_utils.eq("File \"optional_external_arg_none_test.res\", line 103, characters 7-14", undefined, Primitive_option.fromNullable(item(keys, "b").key)); + }); +}); + +export { + makeTarget, + opaque, + greetUser, + hello, + helloUser, + item, +} +/* Not a pure module */ diff --git a/tests/tests/src/optional_external_arg_none_test.res b/tests/tests/src/optional_external_arg_none_test.res new file mode 100644 index 0000000000..9ffedc5967 --- /dev/null +++ b/tests/tests/src/optional_external_arg_none_test.res @@ -0,0 +1,105 @@ +@@config({flags: ["-bs-jsx", "4"]}) + +open Mocha +open Test_utils + +// An optional argument of an external passed as `~x=?e` is left out, that +// is passed as undefined, when `e` is None, and so is a JSX key. A call, a +// lookup or a conditional cannot be read twice to test it first, so it is +// unwrapped by Primitive_option.valFromOption, which keeps undefined. + +%%raw(` +function greet(name) { + return name === undefined ? "Hello" : "Hello " + name +} + +function Greeting(name) { + this.text = greet(name) +} +`) + +@val external greet: (~name: string=?, unit) => string = "greet" + +type greeting +@new external makeGreeting: (~name: string=?, unit) => greeting = "Greeting" +@get external text: greeting => string = "text" + +type target + +let makeTarget: unit => target = %raw(`() => { + const calls = [] + const record = (...args) => { + calls.push(args.map(String).join(" ")) + } + return { calls, greet, setLevel: record, setMode: record } +}`) + +@get external calls: target => array = "calls" + +@send external greetWith: (target, ~name: string=?, unit) => string = "greet" + +@send +external setLevel: (target, ~level: @int [#low | @as(10) #high]=?, unit) => unit = "setLevel" + +@send +external setMode: ( + target, + ~mode: @string [@as("light-mode") #light | @as("dark-mode") #dark]=?, + unit, +) => unit = "setMode" + +// Opaque, so that the optimizer cannot tell the argument is None. +let opaque: 'a => 'a = %raw(`x => x`) + +let greetUser = (names: dict, id) => greet(~name=?names->Dict.get(id), ()) + +// Inlined into its callers, where the argument replaces [name]. +let hello = (~name=?, ()) => greet(~name?, ()) + +let helloUser = (names: dict, id) => hello(~name=?names->Dict.get(id), ()) + +let item = (keys: dict, id) =>
  • Dict.get(id)}>{React.string(id)}
  • + +@get external elementKey: React.element => Nullable.t = "key" + +describe(__MODULE__, () => { + test("None from a call, a lookup or a conditional", () => { + let names = dict{"ann": "Ann"} + eq(__LOC__, "Hello Ann", greetUser(names, "ann")) + eq(__LOC__, "Hello", greetUser(names, "bob")) + let list = ["Ann"] + let at = i => greet(~name=?list->Array.get(i), ()) + eq(__LOC__, ("Hello Ann", "Hello"), (at(0), at(1))) + eq(__LOC__, "Hello", greet(~name=?opaque(None), ())) + let pick = (b, name) => greet(~name=?b ? Some(name) : None, ()) + eq(__LOC__, ("Hello Ann", "Hello"), (pick(true, "Ann"), pick(false, "Ann"))) + }) + + test("None passed to @send and @new externals", () => { + let t = makeTarget() + eq(__LOC__, "Hello", t->greetWith(~name=?opaque(None), ())) + eq(__LOC__, "Hello", makeGreeting(~name=?opaque(None), ())->text) + }) + + test("None forwarded by an inlined function", () => { + let names = dict{"ann": "Ann"} + eq(__LOC__, ("Hello Ann", "Hello"), (helloUser(names, "ann"), helloUser(names, "bob"))) + }) + + test("None for @int and @string arguments", () => { + let t = makeTarget() + let levels = [#high] + let modes = [#dark] + t->setLevel(~level=?levels->Array.get(0), ()) + t->setLevel(~level=?levels->Array.get(1), ()) + t->setMode(~mode=?modes->Array.get(0), ()) + t->setMode(~mode=?modes->Array.get(1), ()) + eq(__LOC__, ["10", "undefined", "dark-mode", "undefined"], calls(t)) + }) + + test("a JSX key that is None", () => { + let keys = dict{"a": "key-a"} + eq(__LOC__, Some("key-a"), item(keys, "a")->elementKey->Nullable.toOption) + eq(__LOC__, None, item(keys, "b")->elementKey->Nullable.toOption) + }) +}) diff --git a/tests/tests/src/optional_external_arg_once_test.mjs b/tests/tests/src/optional_external_arg_once_test.mjs new file mode 100644 index 0000000000..1085effc19 --- /dev/null +++ b/tests/tests/src/optional_external_arg_once_test.mjs @@ -0,0 +1,440 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.mjs"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.mjs"; +import * as JsxRuntime from "react/jsx-runtime"; + +let makeSource = (() => ({ + reads: 0, + value: "first", + get name() { + this.reads++ + return this.value + }, + get level() { + this.reads++ + return this.reads === 1 ? "high" : undefined + } +})); + +let makeBumper = (c => ({ + get bump() { + c.count = c.count + 1 + return "bumped" + } +})); + +let setGlobalSource = (s => { + globalThis.optionalExternalArgOnceSource = s +}); + +let setGlobalName = (v => { + globalThis.optionalExternalArgOnceName = v +}); + +function greet(name) { + return name === undefined ? "Hello" : "Hello " + name +} +; + +let makeTarget = (() => { + const calls = [] + return { calls, setLevel: (...args) => { calls.push(args.map(String).join(" ")) } } +}); + +let call = (f => f()); + +function keys(o) { + return Object.keys(o); +} + +Mocha.describe("Optional_external_arg_once_test", () => { + Mocha.test("a getter passed as an optional argument", () => { + let s = makeSource(); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 150, characters 7-14", [ + "Hello first", + 1 + ], [ + greet(Primitive_option.valFromOption(s.name)), + s.reads + ]); + let s$1 = makeSource(); + let t = makeTarget(); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(s$1.level))); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 155, characters 7-14", [ + ["10"], + 1 + ], [ + t.calls, + s$1.reads + ]); + let s$2 = makeSource(); + let key = JsxRuntime.jsx("li", { + children: "item" + }, Primitive_option.valFromOption(s$2.name)).key; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 159, characters 7-14", [ + "first", + 1 + ], [ + (key == null) ? undefined : Primitive_option.some(key), + s$2.reads + ]); + let s$3 = makeSource(); + let tmp = { + mode: "m" + }; + let tmp$1 = s$3.name; + if (tmp$1 !== undefined) { + tmp.name = tmp$1; + } + let o = tmp; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 163, characters 7-14", [ + "first", + 1 + ], [ + o.name, + s$3.reads + ]); + }); + Mocha.test("a global with a dot passed as an optional argument", () => { + let s = makeSource(); + setGlobalSource(s); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 169, characters 7-14", [ + "Hello first", + 1 + ], [ + greet(Primitive_option.valFromOption(optionalExternalArgOnceSource.name)), + s.reads + ]); + let s$1 = makeSource(); + setGlobalSource(s$1); + let tmp = { + mode: "m" + }; + let tmp$1 = optionalExternalArgOnceSource.name; + if (tmp$1 !== undefined) { + tmp.name = tmp$1; + } + let o = tmp; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 174, characters 7-14", [ + "first", + 1 + ], [ + o.name, + s$1.reads + ]); + }); + Mocha.test("an optional field before a field with side effects", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let tmp = mark("size", 1); + let tmp$1 = { + mode: mark("mode", "dark") + }; + if (tmp !== undefined) { + tmp$1.size = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 184, characters 7-14", [ + "size", + "mode" + ], log); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 185, characters 7-14", [ + 1, + "dark" + ], [ + o.size, + o.mode + ]); + }); + Mocha.test("an optional field between fields with side effects", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let tmp = mark("first", "a"); + let tmp$1 = mark("size", undefined); + let tmp$2 = { + first: tmp, + mode: mark("mode", "b") + }; + if (tmp$1 !== undefined) { + tmp$2.size = tmp$1; + } + let o = tmp$2; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 200, characters 7-14", [ + "first", + "size", + "mode" + ], log); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 201, characters 7-14", [ + "a", + undefined, + "b" + ], [ + o.first, + o.size, + o.mode + ]); + }); + Mocha.test("an optional field before a unit argument with side effects", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let tmp = mark("size", 2); + mark("unit", undefined); + let tmp$1 = {}; + if (tmp !== undefined) { + tmp$1.size = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 211, characters 7-14", [ + "size", + "unit" + ], log); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 212, characters 7-14", 2, o.size); + }); + Mocha.test("an optional field known to be present stays in the literal", () => { + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + let tmp = mark("first", 1); + let tmp$1 = mark("size", 2); + let tmp$2 = { + first: tmp, + mode: mark("mode", "m") + }; + if (tmp$1 !== undefined) { + tmp$2.size = tmp$1; + } + let o = tmp$2; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 227, characters 7-14", [ + "first", + "size", + "mode" + ], log); + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 228, characters 7-14", [ + "first", + "mode", + "size" + ], Object.keys(o)); + }); + Mocha.test("a getter that a later field changes", () => { + let s = makeSource(); + let tmp = s.name; + let tmp$1 = { + mode: call(() => { + s.value = "second"; + return "getter"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 243, characters 7-14", "first", o.name); + }); + Mocha.test("a field before a getter that changes it", () => { + let c = { + count: 1 + }; + let s = makeBumper(c); + let tmp = c.count; + let tmp$1 = s.bump; + let tmp$2 = { + count: tmp, + mode: call(() => "m") + }; + if (tmp$1 !== undefined) { + tmp$2.name = tmp$1; + } + let o = tmp$2; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 250, characters 7-14", [ + 1, + "bumped", + 2 + ], [ + o.count, + o.name, + c.count + ]); + }); + Mocha.test("a global that a later field changes", () => { + setGlobalName("first"); + let tmp = optionalExternalArgOnceName; + let tmp$1 = { + mode: call(() => { + setGlobalName("second"); + return "global"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 265, characters 7-14", "first", o.name); + }); + Mocha.test("a dict entry that a later field changes", () => { + let d = { + k: "first" + }; + let tmp = d["k"]; + let tmp$1 = { + mode: call(() => { + Stdlib_Dict.$$delete(d, "k"); + return "dict"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 280, characters 7-14", "first", o.name); + }); + Mocha.test("an array element that a later field changes", () => { + let a = ["first"]; + let tmp = a[0]; + let tmp$1 = { + mode: call(() => { + a[0] = "second"; + return "array"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 295, characters 7-14", "first", o.name); + }); + Mocha.test("a ref that a later field changes", () => { + let r = "first"; + let tmp = r; + let tmp$1 = { + mode: call(() => { + r = undefined; + return "ref"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 310, characters 7-14", "first", o.name); + }); + Mocha.test("a record field that a later field changes", () => { + let item = call(() => ({ + label: "item", + name: "first" + })); + let tmp = item.name; + let tmp$1 = { + label: item.label, + mode: call(() => { + item.name = undefined; + return "record"; + }) + }; + if (tmp !== undefined) { + tmp$1.name = tmp; + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 326, characters 7-14", [ + "item", + "first", + "record" + ], [ + o.label, + o.name, + o.mode + ]); + }); + Mocha.test("optional polymorphic variant fields", () => { + let level = "high"; + let tmp = level; + let tmp$1 = { + mode: call(() => { + level = "low"; + return "level"; + }) + }; + if (tmp !== undefined) { + tmp$1.level = (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(tmp); + } + let o = tmp$1; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 341, characters 7-14", [ + 10, + "level" + ], [ + o.level, + o.mode + ]); + let mode = "light"; + let tmp$2 = mode; + let tmp$3 = { + level: (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(call(() => { + mode = "dark"; + return "high"; + })) + }; + if (tmp$2 !== undefined) { + tmp$3.mode = (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(tmp$2); + } + let o$1 = tmp$3; + Test_utils.eq("File \"optional_external_arg_once_test.res\", line 354, characters 7-14", [ + "light-mode", + 10 + ], [ + o$1.mode, + o$1.level + ]); + }); +}); + +export { + makeSource, + makeBumper, + setGlobalSource, + setGlobalName, + makeTarget, + call, + keys, +} +/* Not a pure module */ diff --git a/tests/tests/src/optional_external_arg_once_test.res b/tests/tests/src/optional_external_arg_once_test.res new file mode 100644 index 0000000000..3cb13c2113 --- /dev/null +++ b/tests/tests/src/optional_external_arg_once_test.res @@ -0,0 +1,356 @@ +@@config({flags: ["-bs-jsx", "4"]}) + +open Mocha +open Test_utils + +// An optional argument of an external whose presence is not known is +// evaluated once, in source order. A JS property read, which may run a getter, +// is read once, to test it for None and to unwrap it. An object-creating +// external assigns such an argument after the object literal is built, but +// evaluates it before the arguments that follow it when they have side +// effects, or read what its side effects change. The arguments before it, and +// optional arguments known to be present, stay in the literal. + +@obj +external make: (~size: int=?, ~mode: string, unit) => {"size": option, "mode": string} = "" + +@obj +external makeBetween: ( + ~first: string, + ~size: int=?, + ~mode: string, + unit, +) => {"first": string, "size": option, "mode": string} = "" + +@obj +external makeKnown: ( + ~first: int=?, + ~size: int=?, + ~mode: string, + unit, +) => {"first": option, "size": option, "mode": string} = "" + +@obj external makeSize: (~size: int=?, unit) => {"size": option} = "" + +@obj +external makeName: ( + ~name: string=?, + ~mode: string, + unit, +) => {"name": option, "mode": string} = "" + +@obj +external makeCounted: ( + ~count: int, + ~name: string=?, + ~mode: string, + unit, +) => {"count": int, "name": option, "mode": string} = "" + +@obj +external makeLabelled: ( + ~label: string, + ~name: string=?, + ~mode: string, + unit, +) => {"label": string, "name": option, "mode": string} = "" + +@obj +external makeLevel: ( + ~level: @int [#low | @as(10) #high]=?, + ~mode: string, + unit, +) => {"level": option, "mode": string} = "" + +@obj +external makeModeFirst: ( + ~mode: @string [@as("light-mode") #light | @as("dark-mode") #dark]=?, + ~level: @int [#low | @as(10) #high], + unit, +) => {"mode": option, "level": int} = "" + +type source + +// [level] is present on the first read only. +let makeSource: unit => source = %raw(`() => ({ + reads: 0, + value: "first", + get name() { + this.reads++ + return this.value + }, + get level() { + this.reads++ + return this.reads === 1 ? "high" : undefined + } +})`) + +@get external name: source => option = "name" + +type counter = {mutable count: int} + +// Reading [bump] increments the count of the counter it was made with. +let makeBumper: counter => source = %raw(`c => ({ + get bump() { + c.count = c.count + 1 + return "bumped" + } +})`) + +@get external bump: source => option = "bump" +@get external level: source => option<[#low | #high]> = "level" +@get external reads: source => int = "reads" +@set external setValue: (source, string) => unit = "value" + +let setGlobalSource: source => unit = %raw(`s => { + globalThis.optionalExternalArgOnceSource = s +}`) + +// A property read, though it compiles to a JS name. +@val external globalSourceName: option = "optionalExternalArgOnceSource.name" + +@val external globalName: option = "optionalExternalArgOnceName" + +let setGlobalName: string => unit = %raw(`v => { + globalThis.optionalExternalArgOnceName = v +}`) + +%%raw(` +function greet(name) { + return name === undefined ? "Hello" : "Hello " + name +} +`) + +@val external greet: (~name: string=?, unit) => string = "greet" + +type target + +let makeTarget: unit => target = %raw(`() => { + const calls = [] + return { calls, setLevel: (...args) => { calls.push(args.map(String).join(" ")) } } +}`) + +@get external calls: target => array = "calls" + +@send +external setLevel: (target, ~level: @int [#low | @as(10) #high]=?, unit) => unit = "setLevel" + +@get external elementKey: React.element => Nullable.t = "key" + +type item = {label: string, mutable name: option} + +// Calls the function, which the optimizer cannot inline then. +let call: (unit => 'a) => 'a = %raw(`f => f()`) + +let keys = (o: {..}) => Object.keysToArray(Obj.magic(o)) + +describe(__MODULE__, () => { + test("a getter passed as an optional argument", () => { + let s = makeSource() + eq(__LOC__, ("Hello first", 1), (greet(~name=?name(s), ()), reads(s))) + + let s = makeSource() + let t = makeTarget() + t->setLevel(~level=?level(s), ()) + eq(__LOC__, (["10"], 1), (calls(t), reads(s))) + + let s = makeSource() + let key =
  • {React.string("item")}
  • ->elementKey + eq(__LOC__, (Some("first"), 1), (key->Nullable.toOption, reads(s))) + + let s = makeSource() + let o = makeName(~name=?name(s), ~mode="m", ()) + eq(__LOC__, (Some("first"), 1), (o["name"], reads(s))) + }) + + test("a global with a dot passed as an optional argument", () => { + let s = makeSource() + setGlobalSource(s) + eq(__LOC__, ("Hello first", 1), (greet(~name=?globalSourceName, ()), reads(s))) + + let s = makeSource() + setGlobalSource(s) + let o = makeName(~name=?globalSourceName, ~mode="m", ()) + eq(__LOC__, (Some("first"), 1), (o["name"], reads(s))) + }) + + test("an optional field before a field with side effects", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let o = make(~size=?mark("size", Some(1)), ~mode=mark("mode", "dark"), ()) + eq(__LOC__, ["size", "mode"], log) + eq(__LOC__, (Some(1), "dark"), (o["size"], o["mode"])) + }) + + test("an optional field between fields with side effects", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let o = makeBetween( + ~first=mark("first", "a"), + ~size=?mark("size", None), + ~mode=mark("mode", "b"), + (), + ) + eq(__LOC__, ["first", "size", "mode"], log) + eq(__LOC__, ("a", None, "b"), (o["first"], o["size"], o["mode"])) + }) + + test("an optional field before a unit argument with side effects", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let o = makeSize(~size=?mark("size", Some(2)), mark("unit", ())) + eq(__LOC__, ["size", "unit"], log) + eq(__LOC__, Some(2), o["size"]) + }) + + test("an optional field known to be present stays in the literal", () => { + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + let o = makeKnown( + ~first=mark("first", 1), + ~size=?mark("size", Some(2)), + ~mode=mark("mode", "m"), + (), + ) + eq(__LOC__, ["first", "size", "mode"], log) + eq(__LOC__, ["first", "mode", "size"], keys(o)) + }) + + test("a getter that a later field changes", () => { + let s = makeSource() + let o = makeName( + ~name=?name(s), + ~mode=call( + () => { + setValue(s, "second") + "getter" + }, + ), + (), + ) + eq(__LOC__, Some("first"), o["name"]) + }) + + test("a field before a getter that changes it", () => { + let c = {count: 1} + let s = makeBumper(c) + let o = makeCounted(~count=c.count, ~name=?bump(s), ~mode=call(() => "m"), ()) + eq(__LOC__, (1, Some("bumped"), 2), (o["count"], o["name"], c.count)) + }) + + test("a global that a later field changes", () => { + setGlobalName("first") + let o = makeName( + ~name=?globalName, + ~mode=call( + () => { + setGlobalName("second") + "global" + }, + ), + (), + ) + eq(__LOC__, Some("first"), o["name"]) + }) + + test("a dict entry that a later field changes", () => { + let d = dict{"k": "first"} + let o = makeName( + ~name=?d->Dict.get("k"), + ~mode=call( + () => { + d->Dict.delete("k") + "dict" + }, + ), + (), + ) + eq(__LOC__, Some("first"), o["name"]) + }) + + test("an array element that a later field changes", () => { + let a = ["first"] + let o = makeName( + ~name=?a->Array.get(0), + ~mode=call( + () => { + a[0] = "second" + "array" + }, + ), + (), + ) + eq(__LOC__, Some("first"), o["name"]) + }) + + test("a ref that a later field changes", () => { + let r = ref(Some("first")) + let o = makeName( + ~name=?r.contents, + ~mode=call( + () => { + r := None + "ref" + }, + ), + (), + ) + eq(__LOC__, Some("first"), o["name"]) + }) + + test("a record field that a later field changes", () => { + let item = call(() => {label: "item", name: Some("first")}) + let o = makeLabelled( + ~label=item.label, + ~name=?item.name, + ~mode=call( + () => { + item.name = None + "record" + }, + ), + (), + ) + eq(__LOC__, ("item", Some("first"), "record"), (o["label"], o["name"], o["mode"])) + }) + + test("optional polymorphic variant fields", () => { + let level = ref(Some(#high)) + let o = makeLevel( + ~level=?level.contents, + ~mode=call( + () => { + level := Some(#low) + "level" + }, + ), + (), + ) + eq(__LOC__, (Some(10), "level"), (o["level"], o["mode"])) + + let mode = ref(Some(#light)) + let o = makeModeFirst( + ~mode=?mode.contents, + ~level=call( + () => { + mode := Some(#dark) + #high + }, + ), + (), + ) + eq(__LOC__, (Some("light-mode"), 10), (o["mode"], o["level"])) + }) +}) diff --git a/tests/tests/src/polyvar_external_arg_test.mjs b/tests/tests/src/polyvar_external_arg_test.mjs index 4990ffe260..65a6f14e77 100644 --- a/tests/tests/src/polyvar_external_arg_test.mjs +++ b/tests/tests/src/polyvar_external_arg_test.mjs @@ -1,6 +1,8 @@ // Generated by ReScript, PLEASE EDIT WITH CARE import * as Mocha from "mocha"; +import * as Nodev8 from "node:v8"; +import * as Nodevm from "node:vm"; import * as Test_utils from "./test_utils.mjs"; import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.mjs"; @@ -13,7 +15,10 @@ let makeTarget = (() => { record(...args) return args[0] === "click" } - return { calls, has, on: record, onAfter: record, onBefore: record, setMode: record, setLevel: record, setLevelOpt: record } + return { + calls, has, on: record, onAfter: record, onBefore: record, + setMode: record, setModeBetween: record, setLevel: record, setLevelBetween: record, setLevelOpt: record + } }); let makeSource = (() => ({ @@ -36,6 +41,21 @@ let defineGlobalEvent = (() => { return () => reads }); +let setCurrentEvent = (e => { + globalThis.polyvarExternalArgCurrentEvent = e +}); + +let defineScopedEvent = (() => { + let reads = 0 + globalThis.polyvarExternalArgScope = { + get event() { + reads++ + return { NAME: "keydown", VAL: x => "keydown " + x } + } + } + return () => reads +}); + let opaque = (x => x); function click(x) { @@ -53,6 +73,41 @@ let lastEvent = { }) }; +function setLevelAndKeep(t, levels) { + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(levels.level)); + return () => t; +} + +function keepClosure(t) { + let levels_level = opaque("high"); + let levels_data = [ + 1, + 2, + 3 + ]; + let levels = { + level: levels_level, + data: levels_data + }; + return [ + new WeakRef(levels), + setLevelAndKeep(t, levels) + ]; +} + +function nextTask() { + return new Promise((resolve, param) => { + setTimeout(() => resolve(), 0); + }); +} + Mocha.describe("Polyvar_external_arg_test", () => { Mocha.test("an event-style argument with side effects", () => { let t = makeTarget(); @@ -70,19 +125,19 @@ Mocha.describe("Polyvar_external_arg_test", () => { NAME: "click", VAL: click })); - t.on((() => { - switch (tmp$1.NAME) { + t.on((x => { + switch (x) { case "click" : return "click!"; case "keydown" : return "keydown!"; } - })(), tmp$1.VAL); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 121, characters 7-14", [ + })(tmp$1.NAME), tmp$1.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 217, characters 7-14", [ "event", "dispatch" ], log); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 122, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 218, characters 7-14", [ "keydown keydown 1", "click! click 1" ], t.calls); @@ -95,10 +150,175 @@ Mocha.describe("Polyvar_external_arg_test", () => { return v; }; t.on("click", mark("payload", click)); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 133, characters 7-14", ["payload"], log); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 134, characters 7-14", ["click click 1"], t.calls); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 229, characters 7-14", ["payload"], log); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 230, characters 7-14", ["click click 1"], t.calls); }); - Mocha.test("@string and @int arguments with side effects in temporaries", () => { + Mocha.test("@string and @int arguments with side effects", () => { + let t = makeTarget(); + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + t.setMode((x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(mark("mode", opaque("dark")))); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(mark("level", opaque("high")))); + t.setLevelOpt((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(mark("optional", opaque("low"))))); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 243, characters 7-14", [ + "mode", + "level", + "optional" + ], log); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 244, characters 7-14", [ + "dark-mode", + "10", + "0" + ], t.calls); + }); + Mocha.test("optional @string and @int arguments whose presence is not known", () => { + let t = makeTarget(); + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + t.setLevelOpt((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(mark("some", opaque("high"))))); + t.setLevelOpt((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(mark("none", opaque(undefined))))); + let tmp = mark("mode field", opaque("dark")); + let tmp$1 = { + level: (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(mark("level field", opaque("high"))) + }; + if (tmp !== undefined) { + tmp$1.mode = (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(tmp); + } + let options = tmp$1; + let tmp$2 = mark("no mode field", opaque(undefined)); + let tmp$3 = { + level: (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(mark("low field", opaque("low"))) + }; + if (tmp$2 !== undefined) { + tmp$3.mode = (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(tmp$2); + } + let noMode = tmp$3; + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 266, characters 7-14", [ + "some", + "none", + "mode field", + "level field", + "no mode field", + "low field" + ], log); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 267, characters 7-14", [ + "10", + "undefined" + ], t.calls); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 268, characters 7-14", [ + 10, + "dark-mode" + ], [ + options.level, + options.mode + ]); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 269, characters 7-14", [ + 0, + undefined + ], [ + noMode.level, + noMode.mode + ]); + }); + Mocha.test("an @int argument that holds an async function", () => { + let t = makeTarget(); + let pending = []; + let defer = (f, v) => { + pending.push(f); + return v; + }; + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(defer(async () => await Promise.resolve(), opaque("high")))); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 281, characters 7-14", 1, pending.length); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 282, characters 7-14", ["10"], t.calls); + }); + Mocha.test("a closure created next to an @int argument does not keep it alive", async () => { + Nodev8.setFlagsFromString("--expose-gc"); + let gc = Nodevm.runInNewContext("gc"); + let t = makeTarget(); + let match = keepClosure(t); + await nextTask(); + gc(); + Test_utils.ok("File \"polyvar_external_arg_test.res\", line 293, characters 7-14", match[0].deref() === undefined); + Test_utils.ok("File \"polyvar_external_arg_test.res\", line 294, characters 7-14", match[1]() === t); + return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 295, characters 7-14", ["10"], t.calls); + }); + Mocha.test("the effects of unit and @ignore arguments after @string and @int arguments", () => { let t = makeTarget(); let log = []; let mark = (s, v) => { @@ -106,42 +326,186 @@ Mocha.describe("Polyvar_external_arg_test", () => { return v; }; let tmp = mark("mode", opaque("dark")); - t.setMode((() => { - switch (tmp) { + mark("unit", undefined); + t.setMode((x => { + switch (x) { case "light" : return "light-mode"; case "dark" : return "dark-mode"; } - })()); + })(tmp)); let tmp$1 = mark("level", opaque("high")); - t.setLevel((() => { - switch (tmp$1) { + mark("unit", undefined); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(tmp$1)); + let tmp$2 = mark("level", opaque("low")); + mark("ignored", 1); + t.setLevel((x => { + switch (x) { case "low" : return 0; case "high" : return 10; } - })()); - let tmp$2 = mark("optional", opaque("low")); - t.setLevelOpt((() => { - switch (tmp$2 !== undefined ? Primitive_option.valFromOption(tmp$2) : undefined) { + })(tmp$2)); + let tmp$3 = mark("labelled", opaque("high")); + mark("unit", undefined); + t.setLevel((x => { + switch (x) { case "low" : return 0; case "high" : return 10; } - })()); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 147, characters 7-14", [ + })(tmp$3)); + let tmp$4 = mark("optional", opaque("low")); + mark("unit", undefined); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(tmp$4)); + let tmp$5 = mark("mode field", opaque("dark")); + let tmp$6 = mark("level field", opaque("high")); + mark("unit", undefined); + let options = { + mode: (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(tmp$5), + level: (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(tmp$6) + }; + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 316, characters 6-13", [ "mode", + "unit", "level", - "optional" + "unit", + "level", + "ignored", + "labelled", + "unit", + "optional", + "unit", + "mode field", + "level field", + "unit" ], log); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 148, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 334, characters 7-14", [ "dark-mode", "10", + "0", + "10", "0" ], t.calls); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 336, characters 7-14", [ + "mode", + "level" + ], Object.keys(options)); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 337, characters 7-14", [ + 10, + "dark-mode" + ], [ + options.level, + options.mode + ]); + }); + Mocha.test("a later argument that changes an event-style JS global", () => { + let t = makeTarget(); + setCurrentEvent({ + NAME: "click", + VAL: click + }); + let tmp = polyvarExternalArgCurrentEvent; + setCurrentEvent({ + NAME: "keydown", + VAL: keydown + }); + t.on(tmp.NAME, tmp.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 344, characters 7-14", ["click click 1"], t.calls); + }); + Mocha.test("the arguments around @string and @int arguments run in order", () => { + let t = makeTarget(); + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + t.setModeBetween(mark("first", "a"), (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(mark("mode", opaque("light"))), mark("second", "b")); + t.setLevelBetween(mark("third", "c"), (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(mark("level", opaque("high"))), mark("fourth", "d")); + let options = { + level: (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(mark("level field", opaque("low"))), + mode: (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(mark("mode field", opaque("dark"))) + }; + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 362, characters 6-13", [ + "first", + "mode", + "second", + "third", + "level", + "fourth", + "level field", + "mode field" + ], log); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 366, characters 7-14", [ + "a light-mode b", + "c 10 d" + ], t.calls); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 367, characters 7-14", [ + 0, + "dark-mode" + ], [ + options.level, + options.mode + ]); }); Mocha.test("the arguments around an event-style argument run in order", () => { let t = makeTarget(); @@ -161,13 +525,13 @@ Mocha.describe("Polyvar_external_arg_test", () => { VAL: keydown })); t.onBefore(tmp$2.NAME, tmp$2.VAL, mark("fourth", "b")); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 160, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 379, characters 7-14", [ "first", "second", "third", "fourth" ], log); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 161, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 380, characters 7-14", [ "a click click 1", "keydown keydown 1 b" ], t.calls); @@ -177,14 +541,40 @@ Mocha.describe("Polyvar_external_arg_test", () => { let s = makeSource(); let tmp = s.current; t.on(tmp.NAME, tmp.VAL); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 168, characters 7-14", 1, s.reads); + let tmp$1 = s.current; + t.on((x => { + switch (x) { + case "click" : + return "click!"; + case "keydown" : + return "keydown!"; + } + })(tmp$1.NAME), tmp$1.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 388, characters 7-14", 2, s.reads); let globalReads = defineGlobalEvent(); - let tmp$1 = polyvarExternalArgEvent; - t.on(tmp$1.NAME, tmp$1.VAL); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 171, characters 7-14", 1, globalReads()); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 172, characters 7-14", [ + t.on(polyvarExternalArgEvent.NAME, polyvarExternalArgEvent.VAL); + t.on((x => { + switch (x) { + case "click" : + return "click!"; + case "keydown" : + return "keydown!"; + } + })(polyvarExternalArgEvent.NAME), polyvarExternalArgEvent.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 393, characters 7-14", 4, globalReads()); + let scopedReads = defineScopedEvent(); + let tmp$2 = polyvarExternalArgScope.event; + t.on(tmp$2.NAME, tmp$2.VAL); + let tmp$3 = polyvarExternalArgScope.event; + t.on(tmp$3.NAME, tmp$3.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 398, characters 7-14", 2, scopedReads()); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 400, characters 6-13", [ + "keydown keydown 1", + "keydown! keydown 1", + "click click 1", + "click! click 1", "keydown keydown 1", - "click click 1" + "keydown keydown 1" ], t.calls); }); Mocha.test("refs read as event-style arguments", () => { @@ -198,28 +588,28 @@ Mocha.describe("Polyvar_external_arg_test", () => { NAME: "keydown", VAL: keydown }); - t.on((() => { - switch (r.NAME) { + t.on((x => { + switch (x) { case "click" : return "click!"; case "keydown" : return "keydown!"; } - })(), r.VAL); + })(r.NAME), r.VAL); t.on(lastEvent.contents.NAME, lastEvent.contents.VAL); lastEvent.contents = opaque({ NAME: "click", VAL: click }); - t.on((() => { - switch (lastEvent.contents.NAME) { + t.on((x => { + switch (x) { case "click" : return "click!"; case "keydown" : return "keydown!"; } - })(), lastEvent.contents.VAL); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 185, characters 6-13", [ + })(lastEvent.contents.NAME), lastEvent.contents.VAL); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 423, characters 6-13", [ "click click 1", "keydown! keydown 1", "keydown keydown 1", @@ -242,42 +632,95 @@ Mocha.describe("Polyvar_external_arg_test", () => { NAME: "click", VAL: click }))); - t.on((() => { - switch (tmp$1.NAME) { + t.on((x => { + switch (x) { case "click" : return "click!"; case "keydown" : return "keydown!"; } - })(), tmp$1.VAL); - let tmp$2 = await mark("mode", Promise.resolve(opaque("light"))); - t.setMode((() => { - switch (tmp$2) { + })(tmp$1.NAME), tmp$1.VAL); + t.setMode((x => { + switch (x) { case "light" : return "light-mode"; case "dark" : return "dark-mode"; } - })()); - let tmp$3 = await mark("level", Promise.resolve(opaque("high"))); - t.setLevel((() => { - switch (tmp$3) { + })(await mark("mode", Promise.resolve(opaque("light"))))); + t.setLevel((x => { + switch (x) { case "low" : return 0; case "high" : return 10; } - })()); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 202, characters 7-14", [ + })(await mark("level", Promise.resolve(opaque("high"))))); + t.setLevelOpt((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(Primitive_option.valFromOption(await mark("optional", Promise.resolve(opaque("high")))))); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 441, characters 7-14", [ "event", "dispatch", "mode", - "level" + "level", + "optional" ], log); - return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 203, characters 7-14", [ + return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 442, characters 7-14", [ "keydown keydown 1", "click! click 1", "light-mode", + "10", + "10" + ], t.calls); + }); + Mocha.test("awaits in @string and @int arguments between others", async () => { + let t = makeTarget(); + let log = []; + let mark = (s, v) => { + log.push(s); + return v; + }; + t.setModeBetween(mark("first", "a"), (x => { + switch (x) { + case "light" : + return "light-mode"; + case "dark" : + return "dark-mode"; + } + })(mark("mode", await Promise.resolve(opaque("dark")))), mark("second", "b")); + t.setLevelBetween(mark("third", "c"), (x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })(await mark("level", Promise.resolve(opaque("low")))), mark("fourth", "d")); + t.setLevel((x => { + switch (x) { + case "low" : + return 0; + case "high" : + return 10; + } + })((await Promise.resolve("high")))); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 466, characters 7-14", [ + "first", + "mode", + "second", + "third", + "level", + "fourth" + ], log); + return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 467, characters 7-14", [ + "a dark-mode b", + "c 0 d", "10" ], t.calls); }); @@ -318,8 +761,8 @@ Mocha.describe("Polyvar_external_arg_test", () => { ]); }; }; - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 231, characters 7-14", 3, i); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 232, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 495, characters 7-14", 3, i); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 496, characters 7-14", [ [ 1, 1 @@ -329,7 +772,7 @@ Mocha.describe("Polyvar_external_arg_test", () => { 1 ] ], seen); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 233, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 497, characters 7-14", [ "event", "event", "event" @@ -357,8 +800,8 @@ Mocha.describe("Polyvar_external_arg_test", () => { } n = n + 1 | 0; }; - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 247, characters 7-14", 3, n); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 248, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 511, characters 7-14", 3, n); + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 512, characters 7-14", [ "event", "event", "event" @@ -377,7 +820,7 @@ Mocha.describe("Polyvar_external_arg_test", () => { while (await Promise.resolve(true) && i < events.length && t.has(events[i].NAME, events[i].VAL)) { i = i + 1 | 0; }; - return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 260, characters 7-14", 1, i); + return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 524, characters 7-14", 1, i); }); Mocha.test("awaited fields of an object-creating external", async () => { let log = []; @@ -385,63 +828,60 @@ Mocha.describe("Polyvar_external_arg_test", () => { log.push(s); return v; }; - let tmp = await mark("level", Promise.resolve(opaque("high"))); - let tmp$1 = await mark("mode", Promise.resolve(opaque("dark"))); let options = { - level: (() => { - switch (tmp) { + level: (x => { + switch (x) { case "low" : return 0; case "high" : return 10; } - })(), - mode: (() => { - switch (tmp$1) { + })(await mark("level", Promise.resolve(opaque("high")))), + mode: (x => { + switch (x) { case "light" : return "light-mode"; case "dark" : return "dark-mode"; } - })() + })(await mark("mode", Promise.resolve(opaque("dark")))) }; - let tmp$2 = await mark("level", Promise.resolve(opaque("low"))); - let tmp$3 = await mark("mode", Promise.resolve(opaque("light"))); - let tmp$4 = { - level: (() => { - switch (tmp$2) { + let tmp = { + level: (x => { + switch (x) { case "low" : return 0; case "high" : return 10; } - })() + })(await mark("level", Promise.resolve(opaque("low")))) }; - if (tmp$3 !== undefined) { - tmp$4.mode = (() => { - switch (tmp$3) { + let tmp$1 = await mark("mode", Promise.resolve(opaque("light"))); + if (tmp$1 !== undefined) { + tmp.mode = (x => { + switch (x) { case "light" : return "light-mode"; case "dark" : return "dark-mode"; } - })(); + })(tmp$1); } - let optional = tmp$4; - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 279, characters 7-14", [ + let optional = tmp; + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 543, characters 7-14", [ "level", "mode", "level", "mode" ], log); - Test_utils.eq("File \"polyvar_external_arg_test.res\", line 280, characters 7-14", [ + Test_utils.eq("File \"polyvar_external_arg_test.res\", line 544, characters 7-14", [ 10, "dark-mode" ], [ options.level, options.mode ]); - return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 281, characters 7-14", [ + return Test_utils.eq("File \"polyvar_external_arg_test.res\", line 545, characters 7-14", [ 0, "light-mode" ], [ @@ -455,9 +895,14 @@ export { makeTarget, makeSource, defineGlobalEvent, + setCurrentEvent, + defineScopedEvent, opaque, click, keydown, lastEvent, + setLevelAndKeep, + keepClosure, + nextTask, } /* lastEvent Not a pure module */ diff --git a/tests/tests/src/polyvar_external_arg_test.res b/tests/tests/src/polyvar_external_arg_test.res index cb57b3e50b..973a353788 100644 --- a/tests/tests/src/polyvar_external_arg_test.res +++ b/tests/tests/src/polyvar_external_arg_test.res @@ -5,12 +5,15 @@ open Test_utils external testAsync: (string, unit => promise) => unit = "test" // A polymorphic variant argument of an external is evaluated once, in its -// place among the arguments, and not inside a function the compiler creates. -// An event-style argument is passed as its tag and its payload, and an @as or -// @int dispatch switches on the argument in a function, so an argument with -// side effects is read from a temporary, as is a getter passed as an -// event-style argument. In a while condition, the temporary is bound inside -// the loop. +// place among the arguments. An event-style argument is passed as its tag and +// its payload, so an argument with side effects is read from a temporary, as +// is a JS property read such as a @get getter or a @val with a dotted name, +// while a JS global is read in place, like a value of a JS module. An @as or +// @int dispatch is a function that switches on the argument and is called +// with it, so the argument, an await in it included, is evaluated where it +// is. Like any argument, it is read from a temporary only if an argument after +// it has statements to run first. In a while condition, the temporary is +// bound inside the loop. type target @@ -23,7 +26,10 @@ let makeTarget: unit => target = %raw(`() => { record(...args) return args[0] === "click" } - return { calls, has, on: record, onAfter: record, onBefore: record, setMode: record, setLevel: record, setLevelOpt: record } + return { + calls, has, on: record, onAfter: record, onBefore: record, + setMode: record, setModeBetween: record, setLevel: record, setLevelBetween: record, setLevelOpt: record + } }`) @get external calls: target => array = "calls" @@ -44,8 +50,20 @@ external onAs: ( external setMode: (target, @string [@as("light-mode") #light | @as("dark-mode") #dark]) => unit = "setMode" +@send +external setModeBetween: ( + target, + string, + @string [@as("light-mode") #light | @as("dark-mode") #dark], + string, +) => unit = "setModeBetween" + @send external setLevel: (target, @int [#low | @as(10) #high]) => unit = "setLevel" +@send +external setLevelBetween: (target, string, @int [#low | @as(10) #high], string) => unit = + "setLevelBetween" + @send external setLevelOpt: (target, ~level: @int [#low | @as(10) #high]=?, unit) => unit = "setLevelOpt" @@ -63,6 +81,29 @@ external onBefore: ( string, ) => unit = "onBefore" +@send +external onThen: (target, @string [#click(int => string) | #keydown(int => string)], unit) => unit = + "on" + +@send +external setModeThen: ( + target, + @string [@as("light-mode") #light | @as("dark-mode") #dark], + unit, +) => unit = "setMode" + +@send +external setLevelThen: (target, @int [#low | @as(10) #high], unit) => unit = "setLevel" + +@send +external setLevelIgnoring: (target, @int [#low | @as(10) #high], @ignore int) => unit = "setLevel" + +@send +external setLevelLabelled: (target, ~level: @int [#low | @as(10) #high], unit) => unit = "setLevel" + +@send +external setLevelOptThen: (target, ~level: @int [#low | @as(10) #high]=?, unit) => unit = "setLevel" + @obj external makeOptions: ( ~level: @int [#low | @as(10) #high], @@ -70,6 +111,13 @@ external makeOptions: ( unit, ) => _ = "" +@obj +external makeModeFirst: ( + ~mode: @string [@as("light-mode") #light | @as("dark-mode") #dark]=?, + ~level: @int [#low | @as(10) #high], + unit, +) => _ = "" + type source let makeSource: unit => source = %raw(`() => ({ @@ -98,6 +146,32 @@ let defineGlobalEvent: unit => unit => int = %raw(`() => { return () => reads }`) +@val +external currentEvent: [#click(int => string) | #keydown(int => string)] = + "polyvarExternalArgCurrentEvent" + +let setCurrentEvent: [#click(int => string) | #keydown(int => string)] => unit = %raw(`e => { + globalThis.polyvarExternalArgCurrentEvent = e +}`) + +@val +external dottedEvent: [#click(int => string) | #keydown(int => string)] = + "polyvarExternalArgScope.event" + +@val @scope("polyvarExternalArgScope") +external scopedEvent: [#click(int => string) | #keydown(int => string)] = "event" + +let defineScopedEvent: unit => unit => int = %raw(`() => { + let reads = 0 + globalThis.polyvarExternalArgScope = { + get event() { + reads++ + return { NAME: "keydown", VAL: x => "keydown " + x } + } + } + return () => reads +}`) + // Opaque, so that the optimizer cannot tell the argument is a literal. let opaque: 'a => 'a = %raw(`x => x`) @@ -108,6 +182,28 @@ let keydown = x => "keydown " ++ Int.toString(x) let lastEvent = ref(opaque(#keydown(keydown))) +type weakRef<'a> +@new external makeWeakRef: 'a => weakRef<'a> = "WeakRef" +@send external deref: weakRef<'a> => option<'a> = "deref" +@module("node:v8") external setV8Flags: string => unit = "setFlagsFromString" +@module("node:vm") external runInNewContext: string => unit => unit = "runInNewContext" + +type levels = {level: [#low | #high], data: array} + +// The function that switches on the argument is called with it and captures +// no variable, so the closure returned does not keep [levels] alive. +let setLevelAndKeep = (t, levels) => { + setLevel(t, levels.level) + () => t +} + +let keepClosure = t => { + let levels = {level: opaque(#high), data: [1, 2, 3]} + (makeWeakRef(levels), setLevelAndKeep(t, levels)) +} + +let nextTask = () => Promise.make((resolve, _) => setTimeout(() => resolve(), 0)->ignore) + describe(__MODULE__, () => { test("an event-style argument with side effects", () => { let t = makeTarget() @@ -134,7 +230,7 @@ describe(__MODULE__, () => { eq(__LOC__, ["click click 1"], calls(t)) }) - test("@string and @int arguments with side effects in temporaries", () => { + test("@string and @int arguments with side effects", () => { let t = makeTarget() let log = [] let mark = (s, v) => { @@ -148,6 +244,129 @@ describe(__MODULE__, () => { eq(__LOC__, ["dark-mode", "10", "0"], calls(t)) }) + test("optional @string and @int arguments whose presence is not known", () => { + let t = makeTarget() + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + setLevelOpt(t, ~level=?mark("some", opaque(Some(#high))), ()) + setLevelOpt(t, ~level=?mark("none", opaque(None)), ()) + let options = makeModeFirst( + ~mode=?mark("mode field", opaque(Some(#dark))), + ~level=mark("level field", opaque(#high)), + (), + ) + let noMode = makeModeFirst( + ~mode=?mark("no mode field", opaque(None)), + ~level=mark("low field", opaque(#low)), + (), + ) + eq(__LOC__, ["some", "none", "mode field", "level field", "no mode field", "low field"], log) + eq(__LOC__, ["10", "undefined"], calls(t)) + eq(__LOC__, (10, Some("dark-mode")), (options["level"], options["mode"]->toOption)) + eq(__LOC__, (0, None), (noMode["level"], noMode["mode"]->toOption)) + }) + + test("an @int argument that holds an async function", () => { + let t = makeTarget() + let pending = [] + let defer = (f, v) => { + pending->Array.push(f) + v + } + // An await in a function of the argument stays there + setLevel(t, defer(async () => await Promise.resolve(), opaque(#high))) + eq(__LOC__, 1, Array.length(pending)) + eq(__LOC__, ["10"], calls(t)) + }) + + testAsync("a closure created next to an @int argument does not keep it alive", async () => { + setV8Flags("--expose-gc") + let gc = runInNewContext("gc") + let t = makeTarget() + let (levels, keep) = keepClosure(t) + // A WeakRef keeps its target until the end of the current task + await nextTask() + gc() + ok(__LOC__, levels->deref == None) + ok(__LOC__, keep() === t) + eq(__LOC__, ["10"], calls(t)) + }) + + test("the effects of unit and @ignore arguments after @string and @int arguments", () => { + let t = makeTarget() + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + setModeThen(t, mark("mode", opaque(#dark)), mark("unit", ())) + setLevelThen(t, mark("level", opaque(#high)), mark("unit", ())) + setLevelIgnoring(t, mark("level", opaque(#low)), mark("ignored", 1)) + setLevelLabelled(t, ~level=mark("labelled", opaque(#high)), mark("unit", ())) + setLevelOptThen(t, ~level=mark("optional", opaque(#low)), mark("unit", ())) + let options = makeModeFirst( + ~mode=mark("mode field", opaque(#dark)), + ~level=mark("level field", opaque(#high)), + mark("unit", ()), + ) + eq( + __LOC__, + [ + "mode", + "unit", + "level", + "unit", + "level", + "ignored", + "labelled", + "unit", + "optional", + "unit", + "mode field", + "level field", + "unit", + ], + log, + ) + eq(__LOC__, ["dark-mode", "10", "0", "10", "0"], calls(t)) + // The optional field is known to be present, so it stays in the literal + eq(__LOC__, ["mode", "level"], Object.keysToArray(Obj.magic(options))) + eq(__LOC__, (10, Some("dark-mode")), (options["level"], options["mode"]->toOption)) + }) + + test("a later argument that changes an event-style JS global", () => { + let t = makeTarget() + setCurrentEvent(#click(click)) + onThen(t, currentEvent, setCurrentEvent(#keydown(keydown))) + eq(__LOC__, ["click click 1"], calls(t)) + }) + + test("the arguments around @string and @int arguments run in order", () => { + let t = makeTarget() + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + setModeBetween(t, mark("first", "a"), mark("mode", opaque(#light)), mark("second", "b")) + setLevelBetween(t, mark("third", "c"), mark("level", opaque(#high)), mark("fourth", "d")) + let options = makeOptions( + ~level=mark("level field", opaque(#low)), + ~mode=mark("mode field", opaque(#dark)), + (), + ) + eq( + __LOC__, + ["first", "mode", "second", "third", "level", "fourth", "level field", "mode field"], + log, + ) + eq(__LOC__, ["a light-mode b", "c 10 d"], calls(t)) + eq(__LOC__, (0, Some("dark-mode")), (options["level"], options["mode"]->toOption)) + }) + test("the arguments around an event-style argument run in order", () => { let t = makeTarget() let log = [] @@ -165,11 +384,30 @@ describe(__MODULE__, () => { let t = makeTarget() let s = makeSource() on(t, current(s)) - eq(__LOC__, 1, reads(s)) + onAs(t, current(s)) + eq(__LOC__, 2, reads(s)) + // A JS global is read in place, for the tag and for the payload let globalReads = defineGlobalEvent() on(t, globalEvent) - eq(__LOC__, 1, globalReads()) - eq(__LOC__, ["keydown keydown 1", "click click 1"], calls(t)) + onAs(t, globalEvent) + eq(__LOC__, 4, globalReads()) + // A @val with a dotted name is a property read, like one with @scope + let scopedReads = defineScopedEvent() + on(t, dottedEvent) + on(t, scopedEvent) + eq(__LOC__, 2, scopedReads()) + eq( + __LOC__, + [ + "keydown keydown 1", + "keydown! keydown 1", + "click click 1", + "click! click 1", + "keydown keydown 1", + "keydown keydown 1", + ], + calls(t), + ) }) test("refs read as event-style arguments", () => { @@ -199,8 +437,34 @@ describe(__MODULE__, () => { onAs(t, await mark("dispatch", Promise.resolve(opaque(#click(click))))) setMode(t, await mark("mode", Promise.resolve(opaque(#light)))) setLevel(t, await mark("level", Promise.resolve(opaque(#high)))) - eq(__LOC__, ["event", "dispatch", "mode", "level"], log) - eq(__LOC__, ["keydown keydown 1", "click! click 1", "light-mode", "10"], calls(t)) + setLevelOpt(t, ~level=?await mark("optional", Promise.resolve(opaque(Some(#high)))), ()) + eq(__LOC__, ["event", "dispatch", "mode", "level", "optional"], log) + eq(__LOC__, ["keydown keydown 1", "click! click 1", "light-mode", "10", "10"], calls(t)) + }) + + testAsync("awaits in @string and @int arguments between others", async () => { + let t = makeTarget() + let log = [] + let mark = (s, v) => { + log->Array.push(s) + v + } + setModeBetween( + t, + mark("first", "a"), + mark("mode", await Promise.resolve(opaque(#dark))), + mark("second", "b"), + ) + setLevelBetween( + t, + mark("third", "c"), + await mark("level", Promise.resolve(opaque(#low))), + mark("fourth", "d"), + ) + // Raw JS code that awaits is evaluated in its place as well + setLevel(t, %raw(`await Promise.resolve("high")`)) + eq(__LOC__, ["first", "mode", "second", "third", "level", "fourth"], log) + eq(__LOC__, ["a dark-mode b", "c 0 d", "10"], calls(t)) }) test("a while condition with a temporary, and break and continue", () => {