Skip to content

A callee that is not a name - #972

Merged
onatozmenn merged 1 commit into
mainfrom
a-callee-that-is-not-a-name
Aug 10, 2026
Merged

A callee that is not a name#972
onatozmenn merged 1 commit into
mainfrom
a-callee-that-is-not-a-name

Conversation

@onatozmenn

Copy link
Copy Markdown
Collaborator

Both engines refused to call a function value unless the callee was written as a bare name, and both accepted the same value one line earlier:

let held = Holder { step: twice }
let f = held.step
f(2)              // ran
held.step(2)      // "the interpreter cannot run this call"
chooser()(2)      // the same

deed check said nothing about either, which is what makes it a bug rather than a rule: the type checker accepts a function value wherever a function type fits, and where the callee happens to be written is not something a type is about. The interpreter asked what the callee resolved to, and a value resolves to nothing. The backend refused to lower anything but a name.

The interpreter

It now tells the two apart before it evaluates anything. A callee that names a function, an operation, a builtin or an import is dispatched on the declaration as before; everything else is a value, and is evaluated.

That subsumes the special case for a bare name holding a closure, and with it bound_value, which existed because calling a name asked the frame while reading one knew that handler state lives in the handler instance. Going through the ordinary read is what that workaround was standing in for, and a_closure_kept_in_handler_state_is_called_later still passes without it.

The backend

The same shape lowers to the CallIndirect it already had, with the callee held in a local first. The code generator reads a CallIndirect callee twice, once for the environment and once for the code pointer — free while the callee was always a local, and a second run of chooser() otherwise. Without the local the order program below answers 1121 instead of 12, which is the silent kind of wrong.

Which part runs first

Until now the callee could not be an expression with anything in it to run, so nothing had to decide. It is the callee, in the order the two are written. Both engines are held to it by a program whose answer says which order it took rather than by a sentence: a handler accumulates seen * 10 + n, the callee marks 1 and the argument marks 2, and the answer is 12.

Held by

Three programs in agreement.rs, each run twice and compared: a function value held in a field, a callee that is a call, and the order.

Break-verified four ways, each failing by name: restoring the backend refusal, dropping the local, swapping the order in the interpreter, and each shape on its own.

Both engines refused to call a function value unless the callee was written as
a bare name, and both accepted the same value one line earlier:

    let held = Holder { step: twice }
    let f = held.step
    f(2)              ran
    held.step(2)      "the interpreter cannot run this call"
    chooser()(2)      the same

`deed check` said nothing about either, which is what makes this a bug rather
than a rule: the type checker accepts a function value wherever a function type
fits, and where the callee happens to be written is not something a type is
about. The interpreter asked what the callee resolved to, and a value resolves
to nothing. The backend refused to lower anything but a name.

The interpreter now tells the two apart before it evaluates anything: a callee
that names a function, an operation, a builtin or an import is dispatched on
the declaration as before, and everything else is a value and is evaluated.
That subsumes the special case for a bare name holding a closure, and with it
`bound_value`, which existed because calling a name asked the frame while
reading one knew that handler state lives elsewhere. Going through the ordinary
read is what that workaround was standing in for.

The backend lowers the same shape to the `CallIndirect` it already had, with
the callee held in a local first. The code generator reads a `CallIndirect`
callee twice, once for the environment and once for the code pointer, which was
free while the callee was always a local and would have run `chooser()` twice
otherwise. That is the break-verification worth keeping: without the local the
new order program answers 1121 instead of 12.

This settles which of the callee and the arguments runs first, because until
now the callee could not be an expression with anything in it to run. It is the
callee, in the order the two are written, and both engines are held to it by a
program whose answer says which order it took rather than by a sentence.

Three programs in `agreement.rs`: a function value held in a field, a callee
that is a call, and the order. Break-verified four ways, each failing by name:
restoring the backend refusal, dropping the local, swapping the order in the
interpreter, and each of the two shapes on its own.
@onatozmenn
onatozmenn merged commit c3b247f into main Aug 10, 2026
10 checks passed
@onatozmenn
onatozmenn deleted the a-callee-that-is-not-a-name branch August 10, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant