Evaluate a let before the callee of the call it feeds
Why
Lam_util.refine_let rewrote let x = e; f(x) to f(e) whenever f did not mention x. JavaScript evaluates the callee of a call before the arguments, so e then ran after f: a let that read state saw what the callee had changed, and an effect of the let came after the effects of the callee, or after its await. The same happened to the binding Lam_beta_reduce makes for an argument of an inlined function, and to the binding Lam_pass_sroa makes for a field of a local record. The rewrite now applies only when the two commute: one of them has no side effects and reads nothing that may change, or neither has side effects. The rewrite of let x = e; p(x) for a primitive p did the same to the call of an external function or class, which JavaScript reads from its global or module before the argument; it now applies to such a call only when e has no side effects. refine_let takes the assigned variables for this, and motion and conflict move from Lam_beta_reduce_util to Lam_analysis.
modifiedJS let read before a callee that changes it
type counter = {mutable n: int}let c = {n: 0}let next = () => { c.n = c.n + 1 (x: int) => x}let readBeforeCall = () => { let before = c.n next()(before)}
js
modifiedJS let with an effect before an awaited callee
let effectBeforeAwait = async () => { let x = log("argument") (await getHandler())(x)}
js
modifiedJS let that replaces the field the callee is read from
type handler = {mutable call: int => int}let h = {call: x => x + 1}let swap = () => { h.call = x => x + 100 1}let effectBeforeFieldRead = () => { let x = swap() h.call(x)}
js
modifiedJS field of a local record
type snapshot = {mutable unused: int, saved: int}let recordField = () => { let s = {unused: 0, saved: c.n} next()(s.saved)}
js
modifiedJS let that replaces an external function
@scope("globalThis") @val external hook: int => int = "letBeforeCalleeHook"let effectBeforeExternal = () => { let x = install() hook(x)}
js
Review
- How were the changes tested?
let_before_callee_testhas nine tests. Compiled with master, eight fail. One of them is for an@newexternal.inline_arg_order_cross_module_testgains a test that fails with master as well. - Which calls still take the value in place? Those where the order cannot matter:
let x = swap(); cb(x)with a parametercbcompiles tocb(swap()), andlet x = c.n; h.call(x)toh.call(c.n), since neither the field read norc.nhas side effects. - Which checked-in output changes? One, in
Stdlib_PromiseTest:let s = switch e {...}; resolve(s)keepssinstead of assigning the result of theswitchto atmp, because aswitchcounts as having side effects andresolveis the externalPromise.resolve. - Why does a call through a field of an immutable record keep the binding, as in
let x = f(); r.call(x)? The field may still change: a record with a mutable field can be coerced to a type where the field is immutable, sof()may assign it through the other type. - Where do the assigned variables matter?
Lam_pass_sroamakes theLassigns and is the last Lambda pass, so the set is empty inLam_pass_deep_flatten,Lam_pass_lets_dceand the inliner today; they compute it anyway, asLam_pass_remove_aliasdoes. It is not empty whereLam_compileinlines a function of another module under-bs-cross-module-opt, which happens afterLam_pass_sroa. There an argument that reads a local ref, by then an assigned variable, stays bound before a callee that assigns it; the new test ininline_arg_order_cross_module_testcovers that.