Pass undefined for an optional external argument or JSX key that is None
Why
An optional argument of an external passed as ~x=?e, where e is a call, a dict or array lookup, a conditional or anything else that cannot be read twice to test it first, is compiled to Primitive_option.valFromOption(e). valFromOption reads a property of its argument to find a nested None, so it threw a TypeError when e was None, though its doc comment says that it keeps undefined. #8380 replaced Primitive_option.toUndefined, which tested for undefined before calling valFromOption, on that premise. The same call is emitted for a JSX key, also with -bs-jsx-preserve, for the argument of an inlined function that forwards ~x? to an external, and for an optional @int argument, or @string argument with @as, which switches on it. valFromOption now returns undefined for undefined, as it does for null, which fixes all of them.
modifiedJS Primitive_option.valFromOption
js
modifiedOutput optional argument that is None
%%raw(`function greet(name) { return name === undefined ? "Hello" : "Hello " + name }`)@val external greet: (~name: string=?, unit) => string = "greet" let greetUser = (names: dict<string>, id) => greet(~name=?names->Dict.get(id), ())Console.log(greetUser(dict{"1": "Ann"}, "2"))
js
function greetUser(names, id) { return greet(Primitive_option.valFromOption(names[id]));}
text
Review
- Why change the runtime and not the compiler? Every path that emits the unchecked call goes through
valFromOption:Arg_optionalinocaml_to_js_eff, the JSX key inLam_compile_primitive, inlined functions, and@intand@asarguments, which switch on its result.get_default_undefined_from_optionalreturns an expression, so a test in the compiler needs the argument bound to a temporary: a statement for each such argument, throughhold_argfor calls and separately for JSX keys, plus temporaries for the arguments to its left (seesequence_arguments). Bringing backtoUndefinedwould undo #8380 for one comparison. This way the generated code stays as it is, and code that 13.0.0-alpha.5 or later already compiled works once the runtime is updated. - Does the extra test cost the other callers anything? They pass a value known to be
Some: a match on a nested option (Pval_from_option), and the branch afterx !== undefinedof an@objassignment or of an optional argument that can be read twice. For themx == nullreplacesx === null, still one comparison; in a micro-benchmark of 2×10^8 calls the difference is within noise for objects or numbers alone and under 0.3 ns a call for mixed inputs. The runtime grows by one character, and no other compiled module of the runtime, the stdlib or the tests changes. - Could an argument that can be read twice now be compiled to
valFromOption(x)alone, withoutx !== undefined ? … : undefined? It could, but that would change much output and call the runtime forNone. The next commit does it only for a JS property read, which may run a getter.