11.4 Function Calls and Returns in IR
A source-level call combines several contracts: evaluate arguments, convert them to parameter types, transfer control, preserve required machine state, and receive either a value or an exceptional exit. IR makes those obligations explicit enough for later lowering to a calling convention.
At a target-neutral level, a call may look like %r = call i32 @mix(i32 %a, i32 %b). A lower IR may assign arguments to registers or aligned stack slots, attach calling-convention metadata, and describe which memory or registers the call may modify.
The function type is part of the call instruction, not just documentation. It tells the verifier how many arguments are expected, which conversions are legal, whether a result exists, and whether an indirect function value may be called with that signature. Calling i32(i32, i32) with one argument or treating its result as f64 should make the IR invalid immediately.
Build the call packet in source order
Argument lowering should separate semantic order from physical placement:
1. evaluate arguments in the order required by the language;
2. perform implicit conversions and record each resulting IR value;
3. place those values according to the function type and ABI;
4. emit the call and capture its result if it has one.
This separation prevents register assignment from accidentally reordering side effects. Variadic calls, structure returns, closures with hidden environment parameters, and methods with a hidden receiver all extend the same call contract.
For obj.mix(read(), 3.5, next()), suppose the source language evaluates receiver and arguments from left to right. Target-neutral lowering first produces values in that order:
%recv = lower obj
%a0 = call @read()
%a1 = const.f64 3.5
%a2 = call @next()
%result = call @mix(%recv, %a0, %a1, %a2)Only a later ABI-lowering step decides that %recv, %a0, and %a1 use registers while %a2 goes in a stack slot. Physical placement may happen in a different order as long as it moves already-computed values and does not reorder the source-level calls.
Some calls contain hidden arguments. A closure call may pass an environment pointer before explicit arguments. A method may pass the receiver as self. A large structure result may be implemented by a hidden pointer to caller-allocated storage. The IR function type or call metadata must describe these conventions consistently at both caller and callee.
Returns close every normal path
A return e lowers e, converts it to the declared result type, performs required cleanup, and terminates the current block. A function with multiple source returns may keep multiple IR returns or branch them to a unified epilogue. The latter reduces duplicated cleanup but can require a return-value phi or temporary.
For example, two source returns can be unified as follows:
fast:
br epilogue(%cached)
slow:
%computed = call @compute()
br epilogue(%computed)
epilogue(%answer):
call @release(%resource)
ret %answerBlock parameters or a phi choose the incoming answer. More importantly, release runs exactly once on both normal paths. If compute() may throw, its exceptional edge needs a cleanup path too; it cannot simply reuse a normal epilogue that expects a return value.
Calls alter the CFG and dataflow
A call that may throw has a normal successor and an exceptional successor. A tail call can reuse the current frame only when the calling convention, return shape, cleanup obligations, and language semantics permit it. These are semantic and ABI proofs—not text substitutions.
Calls also carry effect information useful to optimizers. A pure call may read no mutable memory; a readonly call may read but not write; a general call may invalidate memory facts. Incorrectly marking an effectful call as pure can make common-subexpression elimination or code motion miscompile the program, so attributes must be conservative and verified.
Before accepting a function's IR, verify that every reachable normal path ends with a compatible ret or another terminator, every call's argument and result types match its signature, and cleanup occurs exactly once along each exit path.