Skip to main content

Invocation contexts

An invoke context marks an instruction that enters a function. The schema distinguishes three kinds of invocation — internal calls (via JUMP), external message calls (CALL/DELEGATECALL/ STATICCALL), and contract creations (CREATE/CREATE2) — each with fields appropriate to the call mechanism.

All variants extend the function identity schema and may include pointers to the call target, arguments, gas, value, and input data as applicable. See Tracing execution for worked examples showing how debuggers use invoke and return contexts to reconstruct call stacks.

Pointer evaluation and instruction placement

An instruction's context describes what is known following that instruction's execution: the fact that a function was invoked holds from that point forward. Pointers within the context reference the machine state at the instruction's trace step — the state a debugger observes when it encounters the instruction.

For internal calls, this context is typically placed on the callee's entry JUMPDEST rather than the caller's JUMP. JUMP consumes its destination operand from the stack; at the entry JUMPDEST, the remaining stack (return address followed by arguments) is stable and directly addressable.

For external calls and contract creations, this context marks the CALL/DELEGATECALL/STATICCALL/CREATE/CREATE2 instruction itself, where the call parameters are visible on the stack.

Loading ....

Internal call

An internal call represents a function call within the same contract. This context is typically placed on the callee's entry JUMPDEST; the caller's JUMP has already consumed the destination from the stack, so pointer slot values reflect the post-JUMP layout. The target points to a code location and arguments are passed on the stack.

The target field is optional. It may be omitted when there is no meaningful code pointer to record — most notably at the first instruction of an inlined function body, where the inlining pass has elided the JUMP that would normally carry the target. The callee identity (identifier, declaration, type) remains meaningful in this case; a sibling transform: ["inline"] key on the same context indicates that the call was inlined rather than physically invoked.

Loading ....

Inlined internal calls

When the compiler inlines a callee, there is no JUMP and no runtime activation record: the callee's instructions are spliced directly into the caller. The call still happened at the source level, so it is still marked with an invoke context — one that describes the kind of call without a physical target:

  • jump: true marks the invocation as an internal call kind (as opposed to a message call or contract creation). It does not assert that a JUMP instruction executes here — an inlined call has none.
  • target is omitted: there is no code location to point at, because the JUMP that would carry it was elided.
  • a sibling transform: ["inline"] key marks the instruction as belonging to an inlined body.

The callee identity (identifier, declaration, type, all optional) is preserved, so the inlined function still appears on the debugger's call stack: the debugger reconstructs a virtual activation for it (see Reconstructing activations).

Compilers typically inline small or leaf non-recursive callees at every call site, so the same callee can produce several independent virtual activations across a trace — one per inlined site. (The precise eligibility rule is a compiler choice; the format is the same however inlining decisions are made.)

External call

An external call represents a call to another contract via CALL, DELEGATECALL, or STATICCALL. The type of call may be indicated by setting delegate or static to true. If neither flag is present, the invocation represents a regular CALL.

Loading ....

Contract creation

A contract creation represents a CREATE or CREATE2 operation. The presence of salt implies CREATE2.

Loading ....

Reconstructing activations

A debugger reconstructs the logical call stack from invoke and return contexts. Each entry on that stack is an activation (the DWARF term for a call-stack entry). Activation handling is uniform whether or not the call was inlined:

  • Push an activation when an invoke context is encountered and pop it when the matching return context is encountered, in trace order. The invoke opens the activation inclusive of its instruction; the return closes it after its instruction, so the instruction bearing return is still inside the activation.

Because push/pop is driven by where the invoke and return contexts sit, a compiler must emit them as a bracket: the invoke on the first instruction of the body and the return on its last. A compiler must not duplicate invoke or return across a body's interior instructions — repeating them would push or pop spurious activations. The sole exception is a body that compiles to a single instruction, whose entry and exit coincide: that one instruction legitimately carries both invoke and return, and a debugger processes them in order (push, then pop). This bracket rule is what keeps the guarantee below — that a debugger ignoring transform still sees a coherent invoke/return pair — true for inlined calls.

An inlined callee therefore appears on the call stack exactly as a non-inlined one does. Two kinds of activation differ only in how they are backed, distinguished by the presence of an inline transform marker — not by whether target is present:

  • A real activation comes from an invoke without an inline transform marker. It corresponds to an actual call at runtime, corroborated by machine state — a return address on the EVM stack — and occupies a real stack region.
  • A virtual activation comes from an invoke whose context carries transform: ["inline"] (an inline identifier in its transform list). It has no runtime corroboration and occupies no EVM stack region; it exists only in the debug annotations. Its target is typically omitted (the JUMP was elided), but target-absence is not itself the signal — a real internal call may also omit target (see Internal call). The reliable discriminator is the inline marker.

Activation membership

Push/pop and membership answer two different questions. Push/pop (above) determines when a virtual activation is open — its lifetime on the call stack. Membership determines which open activation a given instruction belongs to. The two are independent, and a debugger uses both.

An instruction belongs to the innermost open virtual activation if and only if its context carries an inline identifier in its transform list — so composed markers such as ["inline", "fold"] still confer membership. The nesting depth is the number of "inline" occurrences in the list (doubly-inlined code carries ["inline", "inline"]). Membership is determined per-instruction from this marker, not from instruction ranges: optimization passes may relocate or interleave an inlined body, so a positional "everything between the invoke and the return" rule would be unsound.

This is why membership is separate from lifetime. An activation opened by an invoke stays open until its return, even across instructions that are not its members — for example, caller code an optimizer interleaved into the body's trace span. Such a non-member instruction (no inline marker for that depth) is attributed to the enclosing activation, not the inlined one, even while the virtual activation remains on the stack.

Correlating with name

The push/pop and membership rules above reconstruct activations without any correlation identifier: they rely on invoke/return appearing in a well-nested order and on the inline marker to attribute instructions. That is sufficient for typical compiler output. It has two blind spots, both arising because the marker alone can't tell one activation from another of the same function:

  • Adjacent activations of the same function. Two inlined copies of the same callee placed back-to-back, with no intervening caller instruction, read as one activation to a debugger that groups a consecutive run of inline-marked instructions.
  • Reordered or interleaved bodies. An optimizer that moves a return marker ahead of its invoke, or interleaves two activations non-nested, defeats strict push/pop pairing.

A name closes both. When an invoke context carries a name, it declares that activation; the matching return, and the body instructions that belong to it, carry the same name to reference it. Because each name is declared by exactly one invoke, the pairing is explicit and order-independent: adjacent same-function activations have distinct names, and a reference resolves to its declaration regardless of trace order.

When names are present they are authoritative for activation structure — which invoke pairs with which return, and which instructions belong to which activation. Push/pop, the inline marker, and the marker-count depth remain the fallback a name-less debugger uses; in well-nested output the two agree. Where they cannot — the two blind spots above — the names are correct. A compiler that emits names should therefore keep them consistent with the push/pop structure wherever both are determinate, so the two views never silently disagree.

Identity and values

Every function-identity field (identifier, declaration, type) is optional, so a virtual activation degrades gracefully — from full identity down to an anonymous inlined frame — with no fabricated data. A debugger renders whatever is present.

An inlining compiler typically preserves the callee's declaration and per-instruction source ranges for a virtual activation, and can resolve inlined locals that it homed in addressable memory, via variables contexts. Identity fields remain optional and degrade gracefully as described above. Such a compiler does not emit invoke.arguments or return.data pointers in this first version; individual parameter values may still be inspectable as locals inside the body where they are memory-homed. A virtual activation with no resolvable values is still a valid, displayable frame.

A debugger that ignores transform contexts still sees a coherent invoke/return pair and a sound source-level call stack. One that understands them can present virtual activations distinctly — for example, collapsible and tied to the callee's source location.