Semantic Diff

#9177 Evaluate the arguments of an inlined call left to right

2 commits

0f92894adb6ac53fd1e52b3429dc33853ae5373f..pr-9177

87fc65ad9 · mununki · Nov 03, 2026, 02:45 UTC

Evaluate the arguments of an inlined call left to right

Why

Inlining a function whose body is one primitive or call put each argument where the body uses its parameter, so the arguments ran in the body's order, after the ones it ignores (#9175). A read of mutable state such as r.contents then moved past another argument's side effects or past an await, and an argument for the right operand of && or || could be skipped. Larger bodies substituted such reads the same way. Inlining now puts an argument into the body only when that cannot change what anything computes, and otherwise binds the arguments in call order. The bindings can make a while condition compile to statements, which went into a closure called on each iteration, where an await is a syntax error; such a condition now goes into the loop.

modifiedJS inlined calls

let r = ref(0)let bump = () => {  r := r.contents + 10  1}let minus = (a: int, b: int) => b - alet first = (a: int, _b: int) => Math.Int.abs(a) let g = () => minus(r.contents, bump())let f = async p => minus(await p, r.contents)let h = () => first(r.contents, bump())
js
Before g returns -9, f reads r before the await, h returns 10
After g returns 1, f reads r after the await, h returns 0
function g() {function g() { return bump() - r.contents | 0; let a = r.contents; let b = bump(); return b - a | 0;}} async function f(p) {async function f(p) { return r.contents - await p | 0; let a = await p; return r.contents - a | 0;}} function h() {function h() { let a = r.contents; bump(); bump(); return Math.abs(r.contents); return Math.abs(a);}}

modifiedJS right operand of &&

let logTrue = () => {  Console.log("logTrue")  true}let both = (a: bool, b: bool) => a && blet t = both(false, logTrue())
js
Before logTrue is skipped
After logTrue runs, as in the call
let t = false && (console.log("logTrue"), true); console.log("logTrue"); let t = false;

modifiedJS while condition with statements

let f = async p => {  let n = ref(0)  while {    let v = await p    v > n.contents  } {    n := n.contents + 1  }  n.contents}
js
Before a SyntaxError when the module loads
After
async function f(p) {async function f(p) { let n = 0; let n = 0; while ((() => { let v = await p; return v > n; })()) { while (true) { let v = await p; if (v <= n) { break; } n = n + 1 | 0; n = n + 1 | 0; }; }; return n; return n;}}

Review

  • Which arguments still go into the body? Constants, and variables that no Lassign assigns, functions, module fields and pure operators over them (Lam_analysis.delay_safe, the Lambda counterpart of the check from #9159), always; another argument without side effects when nothing it moves past has side effects. So minus(x, r.contents) keeps its JS. Lambda does not mark record fields mutable or not, so a read of any field counts as a read of state. Lambda.apply no longer turns ((x, y) => x && y)(a, b) into a && b unless b is a constant or a variable.
  • Does the output change elsewhere? In the test corpus, inline_unused_arg_order_test.mjs, whose test pinned the old order [2, 3, 1], and the block conditions of complex_while_loop.mjs and test_while_side_effect.mjs, which no longer use a closure. The runtime and Belt compile to the same JavaScript. Labeled arguments given out of order now run in parameter order when inlined, as they do in a call that is not. A break or continue in a while condition, a syntax error in the closure, now works, with a label when it targets an enclosing loop.
3e925cc7b · mununki · Nov 04, 2026, 17:27 UTC

Shorten the changelog fragment

The review asked for one short sentence per bullet; the commit message that adds the fragment keeps the details.