From c73485102e7167989711ec41f789624649ece88d Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Jul 2026 17:03:33 -0400 Subject: [PATCH 1/4] format: spec the inlined-call virtual activation contract Document how an inlined internal call is represented and how a debugger reconstructs its virtual activation, per the inlining design (gnidan rulings: reuse jump:true, omit arg/return value pointers in v1, adopt DWARF "activation" naming). - invoke.mdx: add "Inlined internal calls" (jump:true = call kind not a literal JUMP; target omitted; transform:["inline"]) and a "Reconstructing activations" section defining the real-vs-virtual activation distinction, annotation-driven per-instruction membership (robust to non-contiguous inlined bodies), graceful identity degradation, and v1 value resolution (structure + locals; no arguments/data pointers). - transform.mdx: enrich the inline id (virtual activation, cross- link) and add an inlined-call-site worked example noting that a helper inlined at N sites yields N virtual activations. No schema change. Scope-agnostic re: the single- vs multi-site heuristic (bugc pass scope, decision #3, still open). --- .../spec/program/context/function/invoke.mdx | 83 +++++++++++++++++++ .../web/spec/program/context/transform.mdx | 27 +++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index af1c1da7a..0a454070a 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -65,6 +65,33 @@ than physically invoked. pointer="#/$defs/InternalCall" /> +### 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](#reconstructing-activations)). + +Because inlining may copy a small callee into several call sites, +the _same_ callee can produce several independent virtual +activations across a trace — one per inlined site. + ## External call An external call represents a call to another contract via CALL, @@ -86,3 +113,59 @@ presence of `salt` implies CREATE2. schema={{ id: "schema:ethdebug/format/program/context/function/invoke" }} pointer="#/$defs/ContractCreation" /> + +## 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. + +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: + +- A **real activation** comes from an `invoke` that carries a + `target`. It is corroborated by runtime state — a return address + on the EVM stack — and occupies a real stack region. +- A **virtual activation** comes from an `invoke` with **no + `target`** carrying `transform: ["inline"]`. It has **no runtime + corroboration** and occupies no EVM stack region; it exists only + in the debug annotations. + +### Activation membership + +An instruction belongs to the innermost open virtual activation if +and only if it carries `transform: ["inline"]`; the nesting depth +is the number of `"inline"` occurrences (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. + +### 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. + +A virtual activation reliably carries structural information +(identity, declaration, and per-instruction source ranges) and can +resolve inlined locals that the compiler homed in addressable +memory, via [`variables`](/spec/program/context/variables) +contexts. It does **not** carry `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. diff --git a/packages/web/spec/program/context/transform.mdx b/packages/web/spec/program/context/transform.mdx index e2b4b3cd7..1a784a3a7 100644 --- a/packages/web/spec/program/context/transform.mdx +++ b/packages/web/spec/program/context/transform.mdx @@ -42,9 +42,12 @@ optimization-aware presentations: Four identifiers are recognized in v1: - **`"inline"`** — the marked instruction is part of an inlined - function body. Surrounding invoke/return contexts name the - inlined callee; this marker tells the debugger the physical - code does not correspond to a separate activation record. + function body. A surrounding `invoke`/`return` pair names the + inlined callee, and a debugger reconstructs a _virtual + activation_ for it (see + [inlined internal calls](/spec/program/context/function/invoke#inlined-internal-calls)). + This marker tells the debugger the physical code has no separate + runtime activation record. - **`"tailcall"`** — the marked instruction is a tail-call-optimized back-edge JUMP or continuation, where the call was realized without pushing/popping a full activation. @@ -112,6 +115,24 @@ The `return` and `invoke` state the source-level facts `transform` explains how the compiler realized that pair as a single JUMP. +An inlined call site combines an invoke with an inline transform. +The invoke marks the call kind with `jump: true` but omits +`target`, because the JUMP was elided: + +```yaml +invoke: + jump: true + identifier: "square" + declaration: { ... } +transform: ["inline"] +``` + +Each instruction of the inlined body also carries +`transform: ["inline"]`, and a matching `return` closes the +[virtual activation](/spec/program/context/function/invoke#reconstructing-activations). +A small helper inlined at several call sites produces one such +`invoke`/`return` pair — and one virtual activation — per site. + Reach for [`gather`](/spec/program/context/gather) only when two contexts would collide on the same key — e.g., two independent `variables` blocks or two From 2d12a94b6c84a14981ec5a7bd44b0ada892af7dd Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Jul 2026 17:05:48 -0400 Subject: [PATCH 2/4] format: describe inlining eligibility qualitatively (decision #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reflect gnidan's #3 heuristic (leaf or small, non-recursive, internal, at all call sites) as compiler-typical, keeping the threshold qualitative — the format is invariant to the eligibility rule. --- packages/web/spec/program/context/function/invoke.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index 0a454070a..506ce81ea 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -88,9 +88,11 @@ the debugger's call stack: the debugger reconstructs a **virtual activation** for it (see [Reconstructing activations](#reconstructing-activations)). -Because inlining may copy a small callee into several call sites, -the _same_ callee can produce several independent virtual -activations across a trace — one per inlined site. +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 From c1ce9ce3e8261b1ad060c95edfcfa6acc389dd9a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Jul 2026 18:04:36 -0400 Subject: [PATCH 3/4] format: sharpen activation discriminator + membership wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply debugger review precision notes on #229: - Real-vs-virtual: the reliable discriminator is the inline transform marker, not target-absence — a real internal call may also omit target (#213), so no-target does NOT imply virtual. Reword the real/virtual clauses accordingly. - Membership: reword "iff it carries transform:["inline"]" (read as exact array equality) to "carries an inline identifier in its transform list" so composed markers like ["inline","fold"] still confer membership. --- .../spec/program/context/function/invoke.mdx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index 506ce81ea..7ded542cb 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -129,22 +129,30 @@ uniform whether or not the call was inlined: 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: - -- A **real activation** comes from an `invoke` that carries a - `target`. It is corroborated by runtime state — a return address - on the EVM stack — and occupies a real stack region. -- A **virtual activation** comes from an `invoke` with **no - `target`** carrying `transform: ["inline"]`. It has **no runtime - corroboration** and occupies no EVM stack region; it exists only - in the debug annotations. +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](#internal-call)). The + reliable discriminator is the `inline` marker. ### Activation membership An instruction belongs to the innermost open virtual activation if -and only if it carries `transform: ["inline"]`; the nesting depth -is the number of `"inline"` occurrences (doubly-inlined code -carries `["inline", "inline"]`). Membership is determined +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 From 3a209a1fd024434394e30ace51942e4302d29413 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 2 Jul 2026 18:05:52 -0400 Subject: [PATCH 4/4] format: frame virtual-activation structure as compiler-typical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply debugger note (a): "reliably carries ... identity" contradicted the graceful-degradation paragraph (identity is optional). Reword to "an inlining compiler typically preserves declaration and per-instruction source ranges; identity fields remain optional and degrade gracefully" — a compiler-behavior expectation, not a format guarantee. --- packages/web/spec/program/context/function/invoke.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx index 7ded542cb..4e6d88bd3 100644 --- a/packages/web/spec/program/context/function/invoke.mdx +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -165,11 +165,12 @@ Every function-identity field (`identifier`, `declaration`, from full identity down to an anonymous inlined frame — with no fabricated data. A debugger renders whatever is present. -A virtual activation reliably carries structural information -(identity, declaration, and per-instruction source ranges) and can -resolve inlined locals that the compiler homed in addressable -memory, via [`variables`](/spec/program/context/variables) -contexts. It does **not** carry `invoke.arguments` or +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`](/spec/program/context/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