Semantic Diff

#8936 Evaluate partial application arguments once

6 commits, drawn from their messages.

d19633d38ce7f8850e138913ffa24a8bf6327465..pr-8936

4882045df · Christoph Knittel · Oct 20, 2026

Evaluate partial application arguments once

Why

f(a, ...) put its callee and arguments inside the function it creates, so they ran on every call, and an await in them ended up in a non-async arrow, a JavaScript syntax error.

They are now bound outside that function, as the optional-argument path already did. Variables, constants, closures, external values and module paths stay in place, so such a function can still be inlined. Fixes #8932.

modifiedJS partial application

let add = (a, b, c) => a + b + clet f = async p => {  let h = add(await p, ...)  h(1, 2) + h(3, 4)}
js
Before
After
async function f(p) {async function f(p) { let h = (extra, extra$1) => add(await p, extra, extra$1); return h(1, 2) + h(3, 4) | 0; let arg = await p; return add(arg, 1, 2) + add(arg, 3, 4) | 0;}}

Review

  • Is evaluating arguments once a behaviour change? Yes, for arguments with side effects (earger_curry_test shows one). They now run once, where the application is, like the arguments of any other call.
with Claude Opus 5.5
be72a815b · Christoph Knittel · Oct 21, 2026

Bind closures and external values of partial applications too

Each evaluation of a closure is a new one, and an external value can be reassigned or be a getter, so neither may be re-evaluated on each call.

with Claude Opus 5.5
42e741c60 · Christoph Knittel · Oct 21, 2026

Generalize a partial application only when its parts are values

Now that f(a, ...) evaluates its callee and arguments once, the function it creates shares them, so treating every partial application as nonexpansive let first([], ...) return one array at two element types.

modifiedTyping value restriction for partial application

let first = (x, _y) => xlet h = first([], ...)let ints: array<int> = h(0)let strings: array<string> = h(0)
rule
Before
nonexpansive(f(a₁, …, aₙ, ...))
(nonexp-partial)
After
nonexpansive(f)nonexpansive(a₁) … nonexpansive(aₙ)
nonexpansive(f(a₁, …, aₙ, ...))
(nonexp-partial)
diagnostic
After
This has type: array<int>But it's expected to have type: array<string>
with Claude Opus 5.5
ecb2c6678 · Christoph Knittel · Oct 21, 2026

Format is_nonexpansive

with Claude Opus 5.5
885afb2c0 · Christoph Knittel · Oct 21, 2026

Bind allocating constants of partial applications too

A constant block such as a tuple is emitted as a new JavaScript value on each evaluation, so only constants that allocate nothing stay in place.

with Claude Opus 5.5
4ff19a3d2 · Christoph Knittel · Oct 21, 2026

Test the Lambda of partial applications

The callee and allocating arguments are strict bindings around the generated function, callee first; variables and constants stay inside.

with Claude Opus 5.5