Semantic Diff

#9217 Evaluate a polymorphic variant argument of an external once, in place

1 commit

227b697763ca1c08773343bd9e4da9629b920037..pr-9217

8b9c6cbb8 · mununki · Nov 04, 2026, 05:19 UTC

Evaluate a polymorphic variant argument of an external once, in place

Why

The translation of a polymorphic variant argument of an external read an argument other than a literal twice, as arg.NAME and arg.VAL of an event-style @string, or inside the arrow function that switches on it for @int and @as. So on(t, next()) called next() twice, and an await in the argument was a syntax error in the arrow function. The functions in Js_of_lam_variant return an expression and cannot bind the argument, so Lam_compile now binds it to a temporary among the statements of the call's arguments, unless reading it twice or in the function is the same as reading it once.

modifiedJS polymorphic variant argument

type t@send external on: (t, @string [#click(int => unit) | #keydown(string => unit)]) => unit = "on"@send external setLevel: (t, @int [#low | #high]) => unit = "setLevel" let f = (t, next) => on(t, next())let g = async (t, p) => setLevel(t, await p)
js
Before g is a SyntaxError when the module loads
After
function f(t, next) {function f(t, next) { t.on(next().NAME, next().VAL); let tmp = next(); t.on(tmp.NAME, tmp.VAL);}}async function g(t, p) {async function g(t, p) { let tmp = await p; t.setLevel((() => { t.setLevel((() => { switch (await p) { switch (tmp) { … … } } })()); })());}}

modifiedJS literal with a payload

type t@send external on: (t, @string [#click(int => unit) | #keydown(string => unit)]) => unit = "on"@val external wrap: (int => unit) => int => unit = "wrap" let l = (t, handler) => on(t, #click(wrap(handler)))
js
Before
After
function l(t, handler) {function l(t, handler) { t.on(({ NAME: "click", VAL: wrap(handler) }).NAME, ({ NAME: "click", VAL: wrap(handler) }).VAL); t.on("click", wrap(handler)); }}

modifiedJS while condition with statements

type t@send external has: (t, @string [#click(int => unit) | #keydown(string => unit)]) => bool = "has" let w = async (t, p) => {  let n = ref(0)  while has(t, await p) {    n := n.contents + 1  }  n.contents}
js
Before
After
async function w(t, p) {async function w(t, p) { let n = 0; let n = 0; while (t.has((await p).NAME, (await p).VAL)) { while (true) { let tmp = await p; if (!t.has(tmp.NAME, tmp.VAL)) { break; } n = n + 1 | 0; n = n + 1 | 0; }; }; return n; return n;}}

Review

  • Which arguments keep their output? Literals, constants and variables, and for an @int or @string dispatch, which reads the argument once, any argument without side effects. An event-style argument is read twice, so fields, array elements and conditionals of variables stay in place, but a @val global or a @get property read is bound, since it may run a getter. gpr_2633_test.mjs changes because it pinned foo.on(h(event).NAME, h(event).VAL).
  • Why does compile_while change? The temporaries are statements, and it put a condition with statements into a closure called on each iteration, where an await, break or continue is a syntax error, so while has(t, await p) would stop loading. Such a condition now compiles to while (true) { <statements>; if (!cond) break; <body> }. The change is the same as in #9177, with the same new output in complex_while_loop.mjs and test_while_side_effect.mjs.