From 07865237fdfbd889f7a4ea8b46999755a7817924 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 29 May 2025 15:41:48 +0100 Subject: [PATCH 01/18] Draft function call context schemas --- schemas/program/context.schema.yaml | 12 + schemas/program/context/invoke.schema.yaml | 250 +++++++++++++++++++++ schemas/program/context/return.schema.yaml | 69 ++++++ schemas/program/context/revert.schema.yaml | 58 +++++ 4 files changed, 389 insertions(+) create mode 100644 schemas/program/context/invoke.schema.yaml create mode 100644 schemas/program/context/return.schema.yaml create mode 100644 schemas/program/context/revert.schema.yaml diff --git a/schemas/program/context.schema.yaml b/schemas/program/context.schema.yaml index ba5a4ee87..3361594b9 100644 --- a/schemas/program/context.schema.yaml +++ b/schemas/program/context.schema.yaml @@ -70,6 +70,18 @@ allOf: intermediary representation) to associate a context with a particular compiler step. $ref: "schema:ethdebug/format/program/context/frame" + - if: + required: ["invoke"] + then: + $ref: "schema:ethdebug/format/program/context/invoke" + - if: + required: ["return"] + then: + $ref: "schema:ethdebug/format/program/context/return" + - if: + required: ["revert"] + then: + $ref: "schema:ethdebug/format/program/context/revert" unevaluatedProperties: false diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/invoke.schema.yaml new file mode 100644 index 000000000..820c55f98 --- /dev/null +++ b/schemas/program/context/invoke.schema.yaml @@ -0,0 +1,250 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/program/context/invoke" + +title: ethdebug/format/program/context/invoke +description: | + Schema for representing function invocation context at a specific point in + program execution. + + This context captures information about function calls, including both + internal function calls (via JUMP) and external contract calls (via CALL, + DELEGATECALL, STATICCALL, etc.). The schema distinguishes between these + different invocation types through the use of `internal` and `external` + boolean properties. + +type: object +properties: + invoke: + type: object + title: Function invocation + description: | + Represents a function invocation, either internal (via JUMP) or external + (via CALL opcodes). The schema enforces that exactly one of `internal` + or `external` must be true. + + For internal calls, only `target` and `arguments` are valid. + For external calls, `gas`, `value`, `input`, `salt`, `delegate`, + `static`, `create`, and `create2` may be used as appropriate. + + properties: + target: + type: object + title: Invocation target + description: | + Pointer to the target of the invocation. For internal calls, this + typically points to a code location. For external calls, this points + to the address being called. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + internal: + type: boolean + description: | + Indicates this is an internal function call (JUMP/JUMPI). + + external: + type: boolean + description: | + Indicates this is an external contract call (CALL/DELEGATECALL/etc). + + arguments: + type: object + title: Function arguments + description: | + Pointer to the arguments for an internal function call. + Only valid for internal calls. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + gas: + type: object + title: Gas allocation + description: | + Pointer to the gas allocated for an external call. + Only valid for external calls. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + value: + type: object + title: ETH value + description: | + Pointer to the amount of ETH being sent with an external call. + Only valid for external calls. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + input: + type: object + title: Call input data + description: | + Pointer to the input data for an external call. + Only valid for external calls. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + salt: + type: object + title: CREATE2 salt + description: | + Pointer to the salt value for CREATE2. + Only valid when create2 is true. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + delegate: + type: boolean + description: | + Indicates this external call is a DELEGATECALL. + Only valid when external is true. + + static: + type: boolean + description: | + Indicates this external call is a STATICCALL. + Only valid when external is true. + + create: + type: boolean + description: | + Indicates this external call creates a new contract (CREATE). + Only valid when external is true. + + create2: + type: boolean + description: | + Indicates this external call creates a new contract (CREATE2). + Only valid when external is true. + + required: + - target + + oneOf: + - properties: + internal: + const: true + required: + - internal + - properties: + external: + const: true + required: + - external + +required: + - invoke + +additionalProperties: false + +examples: + - invoke: + target: + pointer: + location: code + offset: 291 + length: 2 + internal: true + arguments: + pointer: + location: stack + slot: 0 + length: 3 + + - invoke: + target: + pointer: + location: stack + slot: 0 + external: true + value: + pointer: + location: stack + slot: 1 + gas: + pointer: + location: stack + slot: 2 + input: + pointer: + location: memory + offset: 128 + length: 68 + + - invoke: + target: + pointer: + location: stack + slot: 0 + external: true + delegate: true + gas: + pointer: + location: stack + slot: 1 + input: + pointer: + location: calldata + offset: 4 + length: 68 + + - invoke: + target: + pointer: + location: stack + slot: 0 + external: true + create2: true + salt: + pointer: + location: stack + slot: 1 + value: + pointer: + location: stack + slot: 2 + input: + pointer: + location: memory + offset: 0 + length: 200 + + - invoke: + target: + pointer: + location: stack + slot: 0 + external: true + static: true + gas: + pointer: + location: stack + slot: 1 + input: + pointer: + location: calldata + offset: 4 + length: 36 diff --git a/schemas/program/context/return.schema.yaml b/schemas/program/context/return.schema.yaml new file mode 100644 index 000000000..091893132 --- /dev/null +++ b/schemas/program/context/return.schema.yaml @@ -0,0 +1,69 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/program/context/return" + +title: ethdebug/format/program/context/return +description: | + Schema for representing function return context at a specific point in + program execution. + + This context captures information about successful function returns, + including the return data and, for external calls, the success status. + +type: object +properties: + return: + type: object + properties: + data: + type: object + title: Return data + description: | + Pointer to the data being returned from the function. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + + success: + type: object + title: Call success status + description: | + Pointer to the success status of an external call. + Typically points to a boolean value on the stack. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + +required: + - return + +additionalProperties: false + +examples: + - return: + data: + pointer: + location: memory + offset: 0x40 + length: 0x20 + + - return: + data: + pointer: + location: memory + offset: 0x40 + length: 0x20 + success: + pointer: + location: stack + slot: 0 + + - return: + data: + pointer: + location: returndata + offset: 0 + length: 32 diff --git a/schemas/program/context/revert.schema.yaml b/schemas/program/context/revert.schema.yaml new file mode 100644 index 000000000..032b10c1c --- /dev/null +++ b/schemas/program/context/revert.schema.yaml @@ -0,0 +1,58 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/program/context/revert" + +title: ethdebug/format/program/context/revert +description: | + Schema for representing function revert context at a specific point in + program execution. + + This context captures information about function reverts, including + revert reason data or panic codes. + +type: object +properties: + revert: + type: object + properties: + reason: + type: object + title: Revert reason + description: | + Pointer to the revert reason data. This typically contains an + ABI-encoded error message or custom error data. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + + panic: + type: integer + title: Panic code + description: | + Numeric panic code for built-in assertion failures. + Languages may define their own panic code conventions + (e.g., Solidity uses codes like 0x11 for arithmetic overflow). + +required: + - revert + +additionalProperties: false + +examples: + - revert: + reason: + pointer: + location: memory + offset: 0x40 + length: 0x60 + + - revert: + panic: 0x11 + + - revert: + reason: + pointer: + location: returndata + offset: 0 + length: 100 From aeb180be77054fdf11657ca7adc43e8104591ab2 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 31 May 2025 14:19:56 +0100 Subject: [PATCH 02/18] Organize schema a bit --- schemas/program/context/invoke.schema.yaml | 74 +++++++++++----------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/invoke.schema.yaml index 820c55f98..70c703d7c 100644 --- a/schemas/program/context/invoke.schema.yaml +++ b/schemas/program/context/invoke.schema.yaml @@ -33,7 +33,7 @@ properties: description: | Pointer to the target of the invocation. For internal calls, this typically points to a code location. For external calls, this points - to the address being called. + to the address and/or selector being called. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -41,15 +41,26 @@ properties: - pointer additionalProperties: false + oneOf: + - $ref: "#/$defs/InternalFunctionInvocation" + - $ref: "#/$defs/ExternalFunctionInvocation" + + required: + - target + +required: + - invoke + +additionalProperties: false + +$defs: + InternalFunctionInvocation: + type: object + properties: internal: - type: boolean description: | Indicates this is an internal function call (JUMP/JUMPI). - - external: - type: boolean - description: | - Indicates this is an external contract call (CALL/DELEGATECALL/etc). + const: true arguments: type: object @@ -63,13 +74,21 @@ properties: required: - pointer additionalProperties: false + required: [internal] + + ExternalFunctionInvocation: + type: object + properties: + external: + description: | + Indicates this is an external contract call (CALL/DELEGATECALL/etc). + const: true gas: type: object title: Gas allocation description: | - Pointer to the gas allocated for an external call. - Only valid for external calls. + Pointer to the gas allocated for an external call properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -82,7 +101,6 @@ properties: title: ETH value description: | Pointer to the amount of ETH being sent with an external call. - Only valid for external calls. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -90,12 +108,12 @@ properties: - pointer additionalProperties: false - input: + salt: type: object - title: Call input data + title: CREATE2 salt description: | - Pointer to the input data for an external call. - Only valid for external calls. + Pointer to the salt value for CREATE2. + Only valid when create2 is true. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -103,12 +121,12 @@ properties: - pointer additionalProperties: false - salt: + input: type: object - title: CREATE2 salt + title: Call input data description: | - Pointer to the salt value for CREATE2. - Only valid when create2 is true. + Pointer to the input data for an external call. + Only valid for external calls. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -140,25 +158,7 @@ properties: Indicates this external call creates a new contract (CREATE2). Only valid when external is true. - required: - - target - - oneOf: - - properties: - internal: - const: true - required: - - internal - - properties: - external: - const: true - required: - - external - -required: - - invoke - -additionalProperties: false + required: [external] examples: - invoke: From 02fa43d63503d1bf22ba12ad51cdfaa3583ca71b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 31 May 2025 15:00:35 +0100 Subject: [PATCH 03/18] Disable unevaluated properties --- schemas/program/context/invoke.schema.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/invoke.schema.yaml index 70c703d7c..31d8060c0 100644 --- a/schemas/program/context/invoke.schema.yaml +++ b/schemas/program/context/invoke.schema.yaml @@ -45,6 +45,8 @@ properties: - $ref: "#/$defs/InternalFunctionInvocation" - $ref: "#/$defs/ExternalFunctionInvocation" + unevaluatedProperties: false + required: - target From 31c34f6c91ad1f9b50894d4d149f80f17c9f3c60 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 31 May 2025 15:00:58 +0100 Subject: [PATCH 04/18] Allow additionalProperties at top-level --- schemas/program/context/invoke.schema.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/invoke.schema.yaml index 31d8060c0..e0ca257c4 100644 --- a/schemas/program/context/invoke.schema.yaml +++ b/schemas/program/context/invoke.schema.yaml @@ -53,8 +53,6 @@ properties: required: - invoke -additionalProperties: false - $defs: InternalFunctionInvocation: type: object From 0267d162bcabf7ad6fe0ffc598dd32c4b57cffc2 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 3 Mar 2026 20:30:35 -0500 Subject: [PATCH 05/18] Add function identity fields to invoke/return/revert contexts Introduce a shared context/function schema defining identifier, declaration, and type properties (mirroring Variable's pattern) and compose it into invoke, return, and revert via allOf. Switch return and revert from additionalProperties to unevaluatedProperties to support allOf composition. --- packages/format/src/schemas/examples.test.ts | 1 + packages/web/spec/program/context/invoke.mdx | 38 ++++ packages/web/spec/program/context/return.mdx | 11 + packages/web/spec/program/context/revert.mdx | 11 + packages/web/src/schemas.ts | 25 ++- .../components/UnnecessaryComposition.tsx | 1 + schemas/program/context.schema.yaml | 9 + schemas/program/context/function.schema.yaml | 29 +++ schemas/program/context/invoke.schema.yaml | 211 ++++++++++-------- schemas/program/context/return.schema.yaml | 31 ++- schemas/program/context/revert.schema.yaml | 15 +- 11 files changed, 273 insertions(+), 109 deletions(-) create mode 100644 packages/web/spec/program/context/invoke.mdx create mode 100644 packages/web/spec/program/context/return.mdx create mode 100644 packages/web/spec/program/context/revert.mdx create mode 100644 schemas/program/context/function.schema.yaml diff --git a/packages/format/src/schemas/examples.test.ts b/packages/format/src/schemas/examples.test.ts index 0b77fbd01..7f4da0d56 100644 --- a/packages/format/src/schemas/examples.test.ts +++ b/packages/format/src/schemas/examples.test.ts @@ -13,6 +13,7 @@ const idsOfSchemasAllowedToOmitExamples = new Set([ "schema:ethdebug/format/type/elementary", "schema:ethdebug/format/pointer/region", "schema:ethdebug/format/pointer/collection", + "schema:ethdebug/format/program/context/function", ]); describe("Examples", () => { diff --git a/packages/web/spec/program/context/invoke.mdx b/packages/web/spec/program/context/invoke.mdx new file mode 100644 index 000000000..f67e610fb --- /dev/null +++ b/packages/web/spec/program/context/invoke.mdx @@ -0,0 +1,38 @@ +--- +sidebar_position: 8 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Invocation contexts + + + +## Internal function invocation + +An internal function invocation represents a call within the same +contract via JUMP/JUMPI. The target points to a code location and +arguments are passed on the stack. + + + +## External function invocation + +An external function invocation represents a call to another contract +via CALL, DELEGATECALL, STATICCALL, CREATE, or CREATE2. The type of +call may be indicated by setting exactly one of `delegate`, `static`, +`create`, or `create2` to `true`. If none of these flags is present, +the invocation represents a regular CALL. + +For CREATE and CREATE2 operations, the `target` field is forbidden +since the creation bytecode is specified via `input` instead. + + diff --git a/packages/web/spec/program/context/return.mdx b/packages/web/spec/program/context/return.mdx new file mode 100644 index 000000000..80601939b --- /dev/null +++ b/packages/web/spec/program/context/return.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 9 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Return contexts + + diff --git a/packages/web/spec/program/context/revert.mdx b/packages/web/spec/program/context/revert.mdx new file mode 100644 index 000000000..eb606b07c --- /dev/null +++ b/packages/web/spec/program/context/revert.mdx @@ -0,0 +1,11 @@ +--- +sidebar_position: 10 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Revert contexts + + diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index 619c35b04..f25d0e26e 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -224,13 +224,36 @@ const programSchemaIndex: SchemaIndex = { href: "/spec/program/context", }, - ...["name", "code", "variables", "remark", "pick", "gather", "frame"] + ...[ + "name", + "code", + "variables", + "remark", + "pick", + "gather", + "frame", + "invoke", + "return", + "revert", + ] .map((name) => ({ [`schema:ethdebug/format/program/context/${name}`]: { href: `/spec/program/context/${name}`, }, })) .reduce((a, b) => ({ ...a, ...b }), {}), + + "schema:ethdebug/format/program/context/invoke#/$defs/InternalFunctionInvocation": + { + title: "Internal function invocation schema", + href: "/spec/program/context/invoke#internal-function-invocation", + }, + + "schema:ethdebug/format/program/context/invoke#/$defs/ExternalFunctionInvocation": + { + title: "External function invocation schema", + href: "/spec/program/context/invoke#external-function-invocation", + }, }; const infoSchemaIndex: SchemaIndex = { diff --git a/packages/web/src/theme/JSONSchemaViewer/components/UnnecessaryComposition.tsx b/packages/web/src/theme/JSONSchemaViewer/components/UnnecessaryComposition.tsx index 8640d9a47..d468c6a15 100644 --- a/packages/web/src/theme/JSONSchemaViewer/components/UnnecessaryComposition.tsx +++ b/packages/web/src/theme/JSONSchemaViewer/components/UnnecessaryComposition.tsx @@ -2,6 +2,7 @@ import React from "react"; import Link from "@docusaurus/Link"; import CreateNodes from "@theme/JSONSchemaViewer/components/CreateNodes"; import { SchemaHierarchyComponent } from "@theme-original/JSONSchemaViewer/contexts"; +import { Collapsible } from "@theme/JSONSchemaViewer/components"; import { GenerateFriendlyName, QualifierMessages, diff --git a/schemas/program/context.schema.yaml b/schemas/program/context.schema.yaml index 3361594b9..ef1686928 100644 --- a/schemas/program/context.schema.yaml +++ b/schemas/program/context.schema.yaml @@ -73,14 +73,23 @@ allOf: - if: required: ["invoke"] then: + description: | + Function invocation context, representing an internal function call + (via JUMP) or an external contract call (via CALL opcodes). $ref: "schema:ethdebug/format/program/context/invoke" - if: required: ["return"] then: + description: | + Function return context, representing the data returned from a + function and, for external calls, the success status. $ref: "schema:ethdebug/format/program/context/return" - if: required: ["revert"] then: + description: | + Function revert context, representing revert reason data or a + panic code for built-in assertion failures. $ref: "schema:ethdebug/format/program/context/revert" unevaluatedProperties: false diff --git a/schemas/program/context/function.schema.yaml b/schemas/program/context/function.schema.yaml new file mode 100644 index 000000000..d28e2d6fa --- /dev/null +++ b/schemas/program/context/function.schema.yaml @@ -0,0 +1,29 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/program/context/function" + +title: ethdebug/format/program/context/function +description: | + Common properties for identifying the function associated with + an invoke, return, or revert context. These properties mirror + the Variable schema's identity fields (identifier, declaration, + type). + +type: object +properties: + identifier: + type: string + description: | + The function's name in the source language. + + declaration: + description: | + Source range where the function is declared. + $ref: "schema:ethdebug/format/materials/source-range" + + type: + description: | + The function's type, specified either as a full + ethdebug/format/type representation or a type reference. + oneOf: + - $ref: "schema:ethdebug/format/type" + - $ref: "schema:ethdebug/format/type/reference" diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/invoke.schema.yaml index e0ca257c4..f61521594 100644 --- a/schemas/program/context/invoke.schema.yaml +++ b/schemas/program/context/invoke.schema.yaml @@ -3,14 +3,12 @@ $id: "schema:ethdebug/format/program/context/invoke" title: ethdebug/format/program/context/invoke description: | - Schema for representing function invocation context at a specific point in - program execution. + Schema for representing function invocation context at a specific + point in program execution. - This context captures information about function calls, including both - internal function calls (via JUMP) and external contract calls (via CALL, - DELEGATECALL, STATICCALL, etc.). The schema distinguishes between these - different invocation types through the use of `internal` and `external` - boolean properties. + This context captures information about function calls, including + both internal function calls (via JUMP) and external contract calls + (via CALL, DELEGATECALL, STATICCALL, etc.). type: object properties: @@ -18,22 +16,39 @@ properties: type: object title: Function invocation description: | - Represents a function invocation, either internal (via JUMP) or external - (via CALL opcodes). The schema enforces that exactly one of `internal` - or `external` must be true. + Represents a function invocation, either internal (via JUMP) or + external (via CALL opcodes). - For internal calls, only `target` and `arguments` are valid. - For external calls, `gas`, `value`, `input`, `salt`, `delegate`, - `static`, `create`, and `create2` may be used as appropriate. + allOf: + - $ref: "schema:ethdebug/format/program/context/function" + - oneOf: + - required: [internal] + - required: [external] + - if: + required: [internal] + then: + $ref: "#/$defs/InternalFunctionInvocation" + - if: + required: [external] + then: + $ref: "#/$defs/ExternalFunctionInvocation" + unevaluatedProperties: false + +required: + - invoke + +$defs: + InternalFunctionInvocation: + title: Internal function invocation + type: object properties: target: type: object title: Invocation target description: | - Pointer to the target of the invocation. For internal calls, this - typically points to a code location. For external calls, this points - to the address and/or selector being called. + Pointer to the target of the invocation. For internal + calls, this typically points to a code location. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -41,22 +56,6 @@ properties: - pointer additionalProperties: false - oneOf: - - $ref: "#/$defs/InternalFunctionInvocation" - - $ref: "#/$defs/ExternalFunctionInvocation" - - unevaluatedProperties: false - - required: - - target - -required: - - invoke - -$defs: - InternalFunctionInvocation: - type: object - properties: internal: description: | Indicates this is an internal function call (JUMP/JUMPI). @@ -67,28 +66,51 @@ $defs: title: Function arguments description: | Pointer to the arguments for an internal function call. - Only valid for internal calls. properties: pointer: $ref: "schema:ethdebug/format/pointer" required: - pointer additionalProperties: false - required: [internal] + required: [internal, target] ExternalFunctionInvocation: + title: External function invocation + description: | + Represents an external contract call. The type of call may be + indicated by setting exactly one of `delegate`, `static`, + `create`, or `create2` to `true`. If none of these flags is + present, the invocation represents a regular CALL. type: object properties: + target: + type: object + title: Invocation target + description: | + Pointer to the target of the invocation. For external + calls, this points to the address and/or selector + being called. + + Not used for contract creation operations + (CREATE/CREATE2), where the creation bytecode is + specified via input instead. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + external: description: | - Indicates this is an external contract call (CALL/DELEGATECALL/etc). + Indicates this is an external contract call. const: true gas: type: object title: Gas allocation description: | - Pointer to the gas allocated for an external call + Pointer to the gas allocated for the external call. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -100,7 +122,7 @@ $defs: type: object title: ETH value description: | - Pointer to the amount of ETH being sent with an external call. + Pointer to the amount of ETH being sent with the call. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -113,7 +135,6 @@ $defs: title: CREATE2 salt description: | Pointer to the salt value for CREATE2. - Only valid when create2 is true. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -125,8 +146,7 @@ $defs: type: object title: Call input data description: | - Pointer to the input data for an external call. - Only valid for external calls. + Pointer to the input data for the external call. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -135,116 +155,123 @@ $defs: additionalProperties: false delegate: - type: boolean description: | Indicates this external call is a DELEGATECALL. - Only valid when external is true. + const: true static: - type: boolean description: | Indicates this external call is a STATICCALL. - Only valid when external is true. + const: true create: - type: boolean description: | - Indicates this external call creates a new contract (CREATE). - Only valid when external is true. + Indicates this external call creates a new contract + (CREATE). + const: true create2: - type: boolean description: | - Indicates this external call creates a new contract (CREATE2). - Only valid when external is true. + Indicates this external call creates a new contract + (CREATE2). + const: true + + allOf: + - if: + anyOf: + - required: [create] + - required: [create2] + then: + properties: + target: false + else: + required: [target] required: [external] examples: - invoke: + identifier: "transfer" + declaration: + source: + id: 1 + range: + offset: 256 + length: 80 + type: + id: 42 target: pointer: location: code - offset: 291 - length: 2 + offset: "0x100" + length: 1 internal: true arguments: pointer: - location: stack - slot: 0 - length: 3 + group: + - name: "arg0" + location: stack + slot: 0 + - name: "arg1" + location: stack + slot: 1 - invoke: target: pointer: location: stack - slot: 0 + slot: 1 external: true - value: + gas: pointer: location: stack - slot: 1 - gas: + slot: 0 + value: pointer: location: stack slot: 2 input: pointer: - location: memory - offset: 128 - length: 68 + group: + - name: "selector" + location: memory + offset: "0x80" + length: 4 + - name: "arguments" + location: memory + offset: "0x84" + length: "0x40" - invoke: target: pointer: location: stack - slot: 0 + slot: 1 external: true delegate: true gas: pointer: location: stack - slot: 1 + slot: 0 input: pointer: - location: calldata - offset: 4 - length: 68 + location: memory + offset: "0x80" + length: "0x24" - invoke: - target: - pointer: - location: stack - slot: 0 external: true create2: true - salt: - pointer: - location: stack - slot: 1 value: - pointer: - location: stack - slot: 2 - input: - pointer: - location: memory - offset: 0 - length: 200 - - - invoke: - target: pointer: location: stack slot: 0 - external: true - static: true - gas: + salt: pointer: location: stack slot: 1 input: pointer: - location: calldata - offset: 4 - length: 36 + location: memory + offset: "0x80" + length: "0x200" diff --git a/schemas/program/context/return.schema.yaml b/schemas/program/context/return.schema.yaml index 091893132..f373216e7 100644 --- a/schemas/program/context/return.schema.yaml +++ b/schemas/program/context/return.schema.yaml @@ -13,6 +13,8 @@ type: object properties: return: type: object + allOf: + - $ref: "schema:ethdebug/format/program/context/function" properties: data: type: object @@ -37,25 +39,35 @@ properties: required: - pointer + required: + - data + + unevaluatedProperties: false + required: - return -additionalProperties: false - examples: - return: + identifier: "transfer" + declaration: + source: + id: 1 + range: + offset: 256 + length: 80 data: pointer: location: memory - offset: 0x40 - length: 0x20 + offset: "0x80" + length: "0x20" - return: data: pointer: - location: memory - offset: 0x40 - length: 0x20 + location: returndata + offset: 0 + length: "0x20" success: pointer: location: stack @@ -64,6 +76,5 @@ examples: - return: data: pointer: - location: returndata - offset: 0 - length: 32 + location: stack + slot: 0 diff --git a/schemas/program/context/revert.schema.yaml b/schemas/program/context/revert.schema.yaml index 032b10c1c..b863b6c0e 100644 --- a/schemas/program/context/revert.schema.yaml +++ b/schemas/program/context/revert.schema.yaml @@ -13,6 +13,8 @@ type: object properties: revert: type: object + allOf: + - $ref: "schema:ethdebug/format/program/context/function" properties: reason: type: object @@ -34,25 +36,26 @@ properties: Languages may define their own panic code conventions (e.g., Solidity uses codes like 0x11 for arithmetic overflow). + unevaluatedProperties: false + required: - revert -additionalProperties: false - examples: - revert: + identifier: "transfer" reason: pointer: location: memory - offset: 0x40 - length: 0x60 + offset: "0x80" + length: "0x64" - revert: - panic: 0x11 + panic: 17 - revert: reason: pointer: location: returndata offset: 0 - length: 100 + length: "0x64" From 711125935999a155ef3b23b75335d22eb85ed65a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 3 Mar 2026 20:54:52 -0500 Subject: [PATCH 06/18] Fix Playground crash on page refresh in dev server Wrap the Monaco-based Playground component in BrowserOnly with React.lazy so it only loads client-side, preventing SSR-related initialization errors that caused [object Object] crashes on refresh. --- packages/web/src/components/SchemaViewer.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/web/src/components/SchemaViewer.tsx b/packages/web/src/components/SchemaViewer.tsx index db803ddef..93dd9627b 100644 --- a/packages/web/src/components/SchemaViewer.tsx +++ b/packages/web/src/components/SchemaViewer.tsx @@ -1,16 +1,18 @@ -import React from "react"; +import React, { Suspense } from "react"; import type { URL } from "url"; import type { JSONSchema } from "json-schema-typed/draft-2020-12"; import JSONSchemaViewer from "@theme/JSONSchemaViewer"; import CodeBlock from "@theme/CodeBlock"; import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; +import BrowserOnly from "@docusaurus/BrowserOnly"; import { type DescribeSchemaOptions, describeSchema } from "@ethdebug/format"; import { schemaIndex } from "@site/src/schemas"; import { SchemaContext, internalIdKey } from "@site/src/contexts/SchemaContext"; import ReactMarkdown from "react-markdown"; import SchemaListing from "./SchemaListing"; -import Playground from "./Playground"; + +const Playground = React.lazy(() => import("./Playground")); export interface SchemaViewerProps extends DescribeSchemaOptions {} @@ -83,7 +85,13 @@ export default function SchemaViewer(props: SchemaViewerProps): JSX.Element { - + Loading playground...}> + {() => ( + Loading playground...}> + + + )} + ); From 0f345caa69263e7730eac881c315f5e2497c5de7 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 00:23:02 -0500 Subject: [PATCH 07/18] Nest invoke/return/revert under function/ category Move function call lifecycle contexts (invoke, return, revert) into a function/ subcategory and extract shared identity fields (identifier, declaration, type) into a base function schema that each context extends via $ref. --- .../program/context/function/_category_.json | 4 + .../program/context/function/function.mdx | 14 ++ .../spec/program/context/function/invoke.mdx | 44 +++++ .../program/context/{ => function}/return.mdx | 4 +- .../program/context/{ => function}/revert.mdx | 4 +- packages/web/spec/program/context/invoke.mdx | 38 ---- packages/web/src/schemas.ts | 48 ++++-- schemas/program/context.schema.yaml | 6 +- schemas/program/context/function.schema.yaml | 29 +++- .../context/{ => function}/invoke.schema.yaml | 162 ++++++++++-------- .../context/{ => function}/return.schema.yaml | 17 +- .../context/{ => function}/revert.schema.yaml | 24 +-- 12 files changed, 234 insertions(+), 160 deletions(-) create mode 100644 packages/web/spec/program/context/function/_category_.json create mode 100644 packages/web/spec/program/context/function/function.mdx create mode 100644 packages/web/spec/program/context/function/invoke.mdx rename packages/web/spec/program/context/{ => function}/return.mdx (52%) rename packages/web/spec/program/context/{ => function}/revert.mdx (52%) delete mode 100644 packages/web/spec/program/context/invoke.mdx rename schemas/program/context/{ => function}/invoke.schema.yaml (66%) rename schemas/program/context/{ => function}/return.schema.yaml (77%) rename schemas/program/context/{ => function}/revert.schema.yaml (65%) diff --git a/packages/web/spec/program/context/function/_category_.json b/packages/web/spec/program/context/function/_category_.json new file mode 100644 index 000000000..8c013b27b --- /dev/null +++ b/packages/web/spec/program/context/function/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Function contexts", + "link": null +} diff --git a/packages/web/spec/program/context/function/function.mdx b/packages/web/spec/program/context/function/function.mdx new file mode 100644 index 000000000..0b00b3a6d --- /dev/null +++ b/packages/web/spec/program/context/function/function.mdx @@ -0,0 +1,14 @@ +--- +sidebar_position: 0 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Function identity + +Function contexts (invoke, return, revert) share a common set of +identity fields for the function being called. + + diff --git a/packages/web/spec/program/context/function/invoke.mdx b/packages/web/spec/program/context/function/invoke.mdx new file mode 100644 index 000000000..553e88f03 --- /dev/null +++ b/packages/web/spec/program/context/function/invoke.mdx @@ -0,0 +1,44 @@ +--- +sidebar_position: 1 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Invocation contexts + + + +## Internal call + +An internal call represents a function call within the same contract +via JUMP/JUMPI. The target points to a code location and arguments +are passed on the stack. + + + +## 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. + + + +## Contract creation + +A contract creation represents a CREATE or CREATE2 operation. The +presence of `salt` implies CREATE2. + + diff --git a/packages/web/spec/program/context/return.mdx b/packages/web/spec/program/context/function/return.mdx similarity index 52% rename from packages/web/spec/program/context/return.mdx rename to packages/web/spec/program/context/function/return.mdx index 80601939b..44a0f1591 100644 --- a/packages/web/spec/program/context/return.mdx +++ b/packages/web/spec/program/context/function/return.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 2 --- import SchemaViewer from "@site/src/components/SchemaViewer"; @@ -7,5 +7,5 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Return contexts diff --git a/packages/web/spec/program/context/revert.mdx b/packages/web/spec/program/context/function/revert.mdx similarity index 52% rename from packages/web/spec/program/context/revert.mdx rename to packages/web/spec/program/context/function/revert.mdx index eb606b07c..4b18b518b 100644 --- a/packages/web/spec/program/context/revert.mdx +++ b/packages/web/spec/program/context/function/revert.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 3 --- import SchemaViewer from "@site/src/components/SchemaViewer"; @@ -7,5 +7,5 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Revert contexts diff --git a/packages/web/spec/program/context/invoke.mdx b/packages/web/spec/program/context/invoke.mdx deleted file mode 100644 index f67e610fb..000000000 --- a/packages/web/spec/program/context/invoke.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -sidebar_position: 8 ---- - -import SchemaViewer from "@site/src/components/SchemaViewer"; - -# Invocation contexts - - - -## Internal function invocation - -An internal function invocation represents a call within the same -contract via JUMP/JUMPI. The target points to a code location and -arguments are passed on the stack. - - - -## External function invocation - -An external function invocation represents a call to another contract -via CALL, DELEGATECALL, STATICCALL, CREATE, or CREATE2. The type of -call may be indicated by setting exactly one of `delegate`, `static`, -`create`, or `create2` to `true`. If none of these flags is present, -the invocation represents a regular CALL. - -For CREATE and CREATE2 operations, the `target` field is forbidden -since the creation bytecode is specified via `input` instead. - - diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index f25d0e26e..84ba8c701 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -14,6 +14,10 @@ const typeSchemaIndex: SchemaIndex = { title: "Base type wrapper schema", href: "/spec/type/base#base-type-wrapper-schema", }, + "schema:ethdebug/format/type/specifier": { + title: "Type specifier schema", + href: "/spec/type/concepts#type-specifier-schema", + }, "schema:ethdebug/format/type/wrapper": { title: "Type wrapper schema", href: "/spec/type/concepts#type-wrapper-schema", @@ -224,18 +228,7 @@ const programSchemaIndex: SchemaIndex = { href: "/spec/program/context", }, - ...[ - "name", - "code", - "variables", - "remark", - "pick", - "gather", - "frame", - "invoke", - "return", - "revert", - ] + ...["name", "code", "variables", "remark", "pick", "gather", "frame"] .map((name) => ({ [`schema:ethdebug/format/program/context/${name}`]: { href: `/spec/program/context/${name}`, @@ -243,16 +236,35 @@ const programSchemaIndex: SchemaIndex = { })) .reduce((a, b) => ({ ...a, ...b }), {}), - "schema:ethdebug/format/program/context/invoke#/$defs/InternalFunctionInvocation": + "schema:ethdebug/format/program/context/function": { + title: "Function identity schema", + href: "/spec/program/context/function", + }, + + ...["invoke", "return", "revert"] + .map((name) => ({ + [`schema:ethdebug/format/program/context/function/${name}`]: { + href: `/spec/program/context/function/${name}`, + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}), + + "schema:ethdebug/format/program/context/function/invoke#/$defs/InternalCall": + { + title: "Internal call schema", + href: "/spec/program/context/function/invoke#internal-call", + }, + + "schema:ethdebug/format/program/context/function/invoke#/$defs/ExternalCall": { - title: "Internal function invocation schema", - href: "/spec/program/context/invoke#internal-function-invocation", + title: "External call schema", + href: "/spec/program/context/function/invoke#external-call", }, - "schema:ethdebug/format/program/context/invoke#/$defs/ExternalFunctionInvocation": + "schema:ethdebug/format/program/context/function/invoke#/$defs/ContractCreation": { - title: "External function invocation schema", - href: "/spec/program/context/invoke#external-function-invocation", + title: "Contract creation schema", + href: "/spec/program/context/function/invoke#contract-creation", }, }; diff --git a/schemas/program/context.schema.yaml b/schemas/program/context.schema.yaml index ef1686928..2be1016d4 100644 --- a/schemas/program/context.schema.yaml +++ b/schemas/program/context.schema.yaml @@ -76,21 +76,21 @@ allOf: description: | Function invocation context, representing an internal function call (via JUMP) or an external contract call (via CALL opcodes). - $ref: "schema:ethdebug/format/program/context/invoke" + $ref: "schema:ethdebug/format/program/context/function/invoke" - if: required: ["return"] then: description: | Function return context, representing the data returned from a function and, for external calls, the success status. - $ref: "schema:ethdebug/format/program/context/return" + $ref: "schema:ethdebug/format/program/context/function/return" - if: required: ["revert"] then: description: | Function revert context, representing revert reason data or a panic code for built-in assertion failures. - $ref: "schema:ethdebug/format/program/context/revert" + $ref: "schema:ethdebug/format/program/context/function/revert" unevaluatedProperties: false diff --git a/schemas/program/context/function.schema.yaml b/schemas/program/context/function.schema.yaml index d28e2d6fa..375cde728 100644 --- a/schemas/program/context/function.schema.yaml +++ b/schemas/program/context/function.schema.yaml @@ -3,15 +3,19 @@ $id: "schema:ethdebug/format/program/context/function" title: ethdebug/format/program/context/function description: | - Common properties for identifying the function associated with - an invoke, return, or revert context. These properties mirror - the Variable schema's identity fields (identifier, declaration, - type). + Properties for identifying a function in the source language. + Used by function context schemas (invoke, return, revert) to + associate a compile-time function identity with runtime + execution events. + + All properties are optional so that compilers may provide as + much or as little information as is available. type: object properties: identifier: type: string + minLength: 1 description: | The function's name in the source language. @@ -24,6 +28,17 @@ properties: description: | The function's type, specified either as a full ethdebug/format/type representation or a type reference. - oneOf: - - $ref: "schema:ethdebug/format/type" - - $ref: "schema:ethdebug/format/type/reference" + $ref: "schema:ethdebug/format/type/specifier" + +examples: + - identifier: "transfer" + declaration: + source: + id: 1 + range: + offset: 256 + length: 80 + type: + id: 42 + - identifier: "balanceOf" + - {} diff --git a/schemas/program/context/invoke.schema.yaml b/schemas/program/context/function/invoke.schema.yaml similarity index 66% rename from schemas/program/context/invoke.schema.yaml rename to schemas/program/context/function/invoke.schema.yaml index f61521594..d8e5cbeff 100644 --- a/schemas/program/context/invoke.schema.yaml +++ b/schemas/program/context/function/invoke.schema.yaml @@ -1,14 +1,14 @@ $schema: "https://json-schema.org/draft/2020-12/schema" -$id: "schema:ethdebug/format/program/context/invoke" +$id: "schema:ethdebug/format/program/context/function/invoke" -title: ethdebug/format/program/context/invoke +title: ethdebug/format/program/context/function/invoke description: | - Schema for representing function invocation context at a specific - point in program execution. + Context for a function being invoked. Covers internal calls + (JUMP), external contract calls (CALL, DELEGATECALL, STATICCALL), + and contract creation (CREATE, CREATE2). - This context captures information about function calls, including - both internal function calls (via JUMP) and external contract calls - (via CALL, DELEGATECALL, STATICCALL, etc.). + Extends the function identity schema with variant-specific + fields such as call targets, gas, value, and input data. type: object properties: @@ -16,22 +16,29 @@ properties: type: object title: Function invocation description: | - Represents a function invocation, either internal (via JUMP) or - external (via CALL opcodes). + Represents a function invocation: an internal call (via JUMP), + an external call (via CALL opcodes), or a contract creation + (via CREATE/CREATE2). + + $ref: "schema:ethdebug/format/program/context/function" allOf: - - $ref: "schema:ethdebug/format/program/context/function" - oneOf: - required: [internal] - - required: [external] + - required: [call] + - required: [create] - if: required: [internal] then: - $ref: "#/$defs/InternalFunctionInvocation" + $ref: "#/$defs/InternalCall" + - if: + required: [call] + then: + $ref: "#/$defs/ExternalCall" - if: - required: [external] + required: [create] then: - $ref: "#/$defs/ExternalFunctionInvocation" + $ref: "#/$defs/ContractCreation" unevaluatedProperties: false @@ -39,10 +46,18 @@ required: - invoke $defs: - InternalFunctionInvocation: - title: Internal function invocation + InternalCall: + title: Internal call + description: | + Represents an internal function call within the same contract + (via JUMP/JUMPI). type: object properties: + internal: + description: | + Indicates this is an internal function call (JUMP/JUMPI). + const: true + target: type: object title: Invocation target @@ -56,11 +71,6 @@ $defs: - pointer additionalProperties: false - internal: - description: | - Indicates this is an internal function call (JUMP/JUMPI). - const: true - arguments: type: object title: Function arguments @@ -72,17 +82,23 @@ $defs: required: - pointer additionalProperties: false + required: [internal, target] - ExternalFunctionInvocation: - title: External function invocation + ExternalCall: + title: External call description: | - Represents an external contract call. The type of call may be - indicated by setting exactly one of `delegate`, `static`, - `create`, or `create2` to `true`. If none of these flags is - present, the invocation represents a regular CALL. + Represents an external contract call 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. type: object properties: + call: + description: | + Indicates this is an external contract call. + const: true + target: type: object title: Invocation target @@ -90,10 +106,6 @@ $defs: Pointer to the target of the invocation. For external calls, this points to the address and/or selector being called. - - Not used for contract creation operations - (CREATE/CREATE2), where the creation bytecode is - specified via input instead. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -101,11 +113,6 @@ $defs: - pointer additionalProperties: false - external: - description: | - Indicates this is an external contract call. - const: true - gas: type: object title: Gas allocation @@ -130,18 +137,6 @@ $defs: - pointer additionalProperties: false - salt: - type: object - title: CREATE2 salt - description: | - Pointer to the salt value for CREATE2. - properties: - pointer: - $ref: "schema:ethdebug/format/pointer" - required: - - pointer - additionalProperties: false - input: type: object title: Call input data @@ -164,30 +159,60 @@ $defs: Indicates this external call is a STATICCALL. const: true + required: [call, target] + + ContractCreation: + title: Contract creation + description: | + Represents a contract creation operation via CREATE or + CREATE2. The presence of `salt` implies CREATE2. + type: object + properties: create: description: | - Indicates this external call creates a new contract - (CREATE). + Indicates this is a contract creation operation + (CREATE or CREATE2). const: true - create2: + value: + type: object + title: ETH value description: | - Indicates this external call creates a new contract - (CREATE2). - const: true + Pointer to the amount of ETH being sent with the + creation. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false - allOf: - - if: - anyOf: - - required: [create] - - required: [create2] - then: - properties: - target: false - else: - required: [target] + salt: + type: object + title: CREATE2 salt + description: | + Pointer to the salt value for CREATE2. Its presence + implies this is a CREATE2 operation. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false + + input: + type: object + title: Creation bytecode + description: | + Pointer to the creation bytecode for the new contract. + properties: + pointer: + $ref: "schema:ethdebug/format/pointer" + required: + - pointer + additionalProperties: false - required: [external] + required: [create] examples: - invoke: @@ -221,7 +246,7 @@ examples: pointer: location: stack slot: 1 - external: true + call: true gas: pointer: location: stack @@ -247,7 +272,7 @@ examples: pointer: location: stack slot: 1 - external: true + call: true delegate: true gas: pointer: @@ -260,8 +285,7 @@ examples: length: "0x24" - invoke: - external: true - create2: true + create: true value: pointer: location: stack diff --git a/schemas/program/context/return.schema.yaml b/schemas/program/context/function/return.schema.yaml similarity index 77% rename from schemas/program/context/return.schema.yaml rename to schemas/program/context/function/return.schema.yaml index f373216e7..35cf256c6 100644 --- a/schemas/program/context/return.schema.yaml +++ b/schemas/program/context/function/return.schema.yaml @@ -1,20 +1,19 @@ $schema: "https://json-schema.org/draft/2020-12/schema" -$id: "schema:ethdebug/format/program/context/return" +$id: "schema:ethdebug/format/program/context/function/return" -title: ethdebug/format/program/context/return +title: ethdebug/format/program/context/function/return description: | - Schema for representing function return context at a specific point in - program execution. - - This context captures information about successful function returns, - including the return data and, for external calls, the success status. + Context for a function returning successfully. Extends the + function identity schema with the return data pointer and, + for external calls, the success status. type: object properties: return: type: object - allOf: - - $ref: "schema:ethdebug/format/program/context/function" + + $ref: "schema:ethdebug/format/program/context/function" + properties: data: type: object diff --git a/schemas/program/context/revert.schema.yaml b/schemas/program/context/function/revert.schema.yaml similarity index 65% rename from schemas/program/context/revert.schema.yaml rename to schemas/program/context/function/revert.schema.yaml index b863b6c0e..03a7afc6a 100644 --- a/schemas/program/context/revert.schema.yaml +++ b/schemas/program/context/function/revert.schema.yaml @@ -1,27 +1,26 @@ $schema: "https://json-schema.org/draft/2020-12/schema" -$id: "schema:ethdebug/format/program/context/revert" +$id: "schema:ethdebug/format/program/context/function/revert" -title: ethdebug/format/program/context/revert +title: ethdebug/format/program/context/function/revert description: | - Schema for representing function revert context at a specific point in - program execution. - - This context captures information about function reverts, including - revert reason data or panic codes. + Context for a function that reverts. Extends the function + identity schema with optional revert reason data and/or a + numeric panic code. type: object properties: revert: type: object - allOf: - - $ref: "schema:ethdebug/format/program/context/function" + + $ref: "schema:ethdebug/format/program/context/function" + properties: reason: type: object title: Revert reason description: | - Pointer to the revert reason data. This typically contains an - ABI-encoded error message or custom error data. + Pointer to the revert reason data. This typically contains + an ABI-encoded error message or custom error data. properties: pointer: $ref: "schema:ethdebug/format/pointer" @@ -34,7 +33,8 @@ properties: description: | Numeric panic code for built-in assertion failures. Languages may define their own panic code conventions - (e.g., Solidity uses codes like 0x11 for arithmetic overflow). + (e.g., Solidity uses codes like 0x11 for arithmetic + overflow). unevaluatedProperties: false From 4c67ad4e3f589e59a58444ff141f08e88c14b0be Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 00:37:38 -0500 Subject: [PATCH 08/18] Rename invoke discriminant fields: internal->jump, call->message Reduces ambiguity: `call` could mean any function call, and `internal` vs `create` doesn't clearly convey the mechanism. The new names (jump/message/create) map directly to the three EVM invocation mechanisms. Also enforces mutual exclusivity of delegate/static on external calls via `not: required: [delegate, static]`. --- .../context/function/invoke.schema.yaml | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/schemas/program/context/function/invoke.schema.yaml b/schemas/program/context/function/invoke.schema.yaml index d8e5cbeff..8bab859d7 100644 --- a/schemas/program/context/function/invoke.schema.yaml +++ b/schemas/program/context/function/invoke.schema.yaml @@ -24,15 +24,15 @@ properties: allOf: - oneOf: - - required: [internal] - - required: [call] + - required: [jump] + - required: [message] - required: [create] - if: - required: [internal] + required: [jump] then: $ref: "#/$defs/InternalCall" - if: - required: [call] + required: [message] then: $ref: "#/$defs/ExternalCall" - if: @@ -53,7 +53,7 @@ $defs: (via JUMP/JUMPI). type: object properties: - internal: + jump: description: | Indicates this is an internal function call (JUMP/JUMPI). const: true @@ -83,7 +83,7 @@ $defs: - pointer additionalProperties: false - required: [internal, target] + required: [jump, target] ExternalCall: title: External call @@ -94,9 +94,10 @@ $defs: the invocation represents a regular CALL. type: object properties: - call: + message: description: | - Indicates this is an external contract call. + Indicates this is an external message call (CALL, + DELEGATECALL, or STATICCALL). const: true target: @@ -159,7 +160,11 @@ $defs: Indicates this external call is a STATICCALL. const: true - required: [call, target] + not: + description: Only one of `delegate` and `static` can be set at a time. + required: [delegate, static] + + required: [message, target] ContractCreation: title: Contract creation @@ -230,7 +235,7 @@ examples: location: code offset: "0x100" length: 1 - internal: true + jump: true arguments: pointer: group: @@ -246,7 +251,7 @@ examples: pointer: location: stack slot: 1 - call: true + message: true gas: pointer: location: stack @@ -272,7 +277,7 @@ examples: pointer: location: stack slot: 1 - call: true + message: true delegate: true gas: pointer: From 9d80ea82ae4bba98d16dac129dd1fbc3064eb7ef Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 00:40:52 -0500 Subject: [PATCH 09/18] Rewrite function context descriptions Frame descriptions from the perspective of contexts marking instructions rather than "representing" things. A context indicates association with a function lifecycle event; it does not represent the event itself. --- schemas/program/context.schema.yaml | 10 +++--- schemas/program/context/function.schema.yaml | 10 +++--- .../context/function/invoke.schema.yaml | 35 ++++++++++--------- .../context/function/return.schema.yaml | 7 ++-- .../context/function/revert.schema.yaml | 7 ++-- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/schemas/program/context.schema.yaml b/schemas/program/context.schema.yaml index 2be1016d4..a57fce654 100644 --- a/schemas/program/context.schema.yaml +++ b/schemas/program/context.schema.yaml @@ -74,22 +74,20 @@ allOf: required: ["invoke"] then: description: | - Function invocation context, representing an internal function call - (via JUMP) or an external contract call (via CALL opcodes). + Indicates association with a function invocation (internal call, + external message call, or contract creation). $ref: "schema:ethdebug/format/program/context/function/invoke" - if: required: ["return"] then: description: | - Function return context, representing the data returned from a - function and, for external calls, the success status. + Indicates association with a successful function return. $ref: "schema:ethdebug/format/program/context/function/return" - if: required: ["revert"] then: description: | - Function revert context, representing revert reason data or a - panic code for built-in assertion failures. + Indicates association with a function revert. $ref: "schema:ethdebug/format/program/context/function/revert" unevaluatedProperties: false diff --git a/schemas/program/context/function.schema.yaml b/schemas/program/context/function.schema.yaml index 375cde728..f16d9b9aa 100644 --- a/schemas/program/context/function.schema.yaml +++ b/schemas/program/context/function.schema.yaml @@ -3,13 +3,13 @@ $id: "schema:ethdebug/format/program/context/function" title: ethdebug/format/program/context/function description: | - Properties for identifying a function in the source language. - Used by function context schemas (invoke, return, revert) to - associate a compile-time function identity with runtime - execution events. + Properties for identifying a source-language function. Function + context schemas (invoke, return, revert) extend this schema so + that each context can optionally indicate which function it + pertains to. All properties are optional so that compilers may provide as - much or as little information as is available. + much or as little detail as is available. type: object properties: diff --git a/schemas/program/context/function/invoke.schema.yaml b/schemas/program/context/function/invoke.schema.yaml index 8bab859d7..1bf401926 100644 --- a/schemas/program/context/function/invoke.schema.yaml +++ b/schemas/program/context/function/invoke.schema.yaml @@ -3,12 +3,14 @@ $id: "schema:ethdebug/format/program/context/function/invoke" title: ethdebug/format/program/context/function/invoke description: | - Context for a function being invoked. Covers internal calls - (JUMP), external contract calls (CALL, DELEGATECALL, STATICCALL), - and contract creation (CREATE, CREATE2). + This context indicates that the marked instruction is + associated with a function invocation. The invocation is one + of three kinds: an internal call via JUMP, an external message + call (CALL / DELEGATECALL / STATICCALL), or a contract + creation (CREATE / CREATE2). - Extends the function identity schema with variant-specific - fields such as call targets, gas, value, and input data. + Extends the function identity schema with kind-specific fields + such as call targets, gas, value, and input data. type: object properties: @@ -16,9 +18,10 @@ properties: type: object title: Function invocation description: | - Represents a function invocation: an internal call (via JUMP), - an external call (via CALL opcodes), or a contract creation - (via CREATE/CREATE2). + Describes the function invocation associated with this + context. Must indicate exactly one invocation kind: `jump` + for an internal call, `message` for an external call, or + `create` for a contract creation. $ref: "schema:ethdebug/format/program/context/function" @@ -49,8 +52,8 @@ $defs: InternalCall: title: Internal call description: | - Represents an internal function call within the same contract - (via JUMP/JUMPI). + An internal function call within the same contract, entered + via JUMP/JUMPI. type: object properties: jump: @@ -88,10 +91,10 @@ $defs: ExternalCall: title: External call description: | - Represents an external contract call 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. + An external message call to another contract via CALL, + DELEGATECALL, or STATICCALL. Set `delegate` or `static` to + `true` to indicate the call variant; if neither is present + the call is a regular CALL. type: object properties: message: @@ -169,8 +172,8 @@ $defs: ContractCreation: title: Contract creation description: | - Represents a contract creation operation via CREATE or - CREATE2. The presence of `salt` implies CREATE2. + A contract creation via CREATE or CREATE2. The presence + of `salt` distinguishes CREATE2 from CREATE. type: object properties: create: diff --git a/schemas/program/context/function/return.schema.yaml b/schemas/program/context/function/return.schema.yaml index 35cf256c6..6885cb1f2 100644 --- a/schemas/program/context/function/return.schema.yaml +++ b/schemas/program/context/function/return.schema.yaml @@ -3,9 +3,10 @@ $id: "schema:ethdebug/format/program/context/function/return" title: ethdebug/format/program/context/function/return description: | - Context for a function returning successfully. Extends the - function identity schema with the return data pointer and, - for external calls, the success status. + This context indicates that the marked instruction is + associated with a successful function return. Extends the + function identity schema with a pointer to the return data + and, for external calls, the success status. type: object properties: diff --git a/schemas/program/context/function/revert.schema.yaml b/schemas/program/context/function/revert.schema.yaml index 03a7afc6a..483805f25 100644 --- a/schemas/program/context/function/revert.schema.yaml +++ b/schemas/program/context/function/revert.schema.yaml @@ -3,9 +3,10 @@ $id: "schema:ethdebug/format/program/context/function/revert" title: ethdebug/format/program/context/function/revert description: | - Context for a function that reverts. Extends the function - identity schema with optional revert reason data and/or a - numeric panic code. + This context indicates that the marked instruction is + associated with a function revert. Extends the function + identity schema with an optional pointer to revert reason + data and/or a numeric panic code. type: object properties: From 950a742255534cab8332e9a532b4d9d1d0a1c74c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 01:09:40 -0500 Subject: [PATCH 10/18] Rewrite function context examples with realistic scenarios Each example now describes a concrete EVM execution scenario: which instruction the context marks, what the stack and memory layout looks like, and why each pointer points where it does. --- schemas/program/context/function.schema.yaml | 16 +++- .../context/function/invoke.schema.yaml | 92 +++++++++++++++---- .../context/function/return.schema.yaml | 37 ++++++-- .../context/function/revert.schema.yaml | 24 +++++ 4 files changed, 142 insertions(+), 27 deletions(-) diff --git a/schemas/program/context/function.schema.yaml b/schemas/program/context/function.schema.yaml index f16d9b9aa..52da86c23 100644 --- a/schemas/program/context/function.schema.yaml +++ b/schemas/program/context/function.schema.yaml @@ -31,14 +31,22 @@ properties: $ref: "schema:ethdebug/format/type/specifier" examples: + # All three identity fields provided: the compiler knows the + # function name, where it was declared, and its type. - identifier: "transfer" declaration: source: - id: 1 + id: 0 range: - offset: 256 - length: 80 + offset: 128 + length: 95 type: - id: 42 + id: 7 + + # Only the function name is known. - identifier: "balanceOf" + + # No identity information. The compiler knows that a function + # context applies but cannot attribute it to a specific + # function (e.g., an indirect call through a function pointer). - {} diff --git a/schemas/program/context/function/invoke.schema.yaml b/schemas/program/context/function/invoke.schema.yaml index 1bf401926..e1779a733 100644 --- a/schemas/program/context/function/invoke.schema.yaml +++ b/schemas/program/context/function/invoke.schema.yaml @@ -223,38 +223,68 @@ $defs: required: [create] examples: + # ----------------------------------------------------------- + # Internal call: transfer(address, uint256) + # ----------------------------------------------------------- + # This context would mark the JUMP instruction that enters + # the function. Before the jump, the compiler has arranged + # the stack as follows (top first): + # + # slot 0: jump destination (entry PC of `transfer`) + # slot 1: return label + # slot 2: first argument (`to`) + # slot 3: second argument (`amount`) + # + # The `target` pointer reads the jump destination from the + # stack; `arguments` uses a group to name each argument's + # stack position. - invoke: identifier: "transfer" declaration: source: - id: 1 + id: 0 range: - offset: 256 - length: 80 + offset: 128 + length: 95 type: - id: 42 + id: 7 + jump: true target: pointer: - location: code - offset: "0x100" - length: 1 - jump: true + location: stack + slot: 0 arguments: pointer: group: - - name: "arg0" + - name: "to" location: stack - slot: 0 - - name: "arg1" + slot: 2 + - name: "amount" location: stack - slot: 1 + slot: 3 + # ----------------------------------------------------------- + # External CALL: token.balanceOf(account) + # ----------------------------------------------------------- + # This context would mark the CALL instruction. The EVM + # expects the stack to contain (top first): + # + # slot 0: gas to forward + # slot 1: target contract address + # slot 2: value (0 — balanceOf is non-payable) + # + # The ABI-encoded calldata has already been written to + # memory at 0x80: + # + # 0x80..0x83: function selector (4 bytes) + # 0x84..0xa3: abi-encoded `account` (32 bytes) - invoke: + identifier: "balanceOf" + message: true target: pointer: location: stack slot: 1 - message: true gas: pointer: location: stack @@ -273,15 +303,31 @@ examples: - name: "arguments" location: memory offset: "0x84" - length: "0x40" + length: "0x20" + # ----------------------------------------------------------- + # DELEGATECALL: proxy forwarding calldata + # ----------------------------------------------------------- + # This context would mark a DELEGATECALL instruction in a + # proxy contract. The call executes the implementation's + # code within the proxy's storage context. + # + # DELEGATECALL takes no value parameter. Stack layout + # (top first): + # + # slot 0: gas + # slot 1: implementation address + # + # The original calldata has been copied into memory: + # + # 0x80..0xe3: forwarded calldata (100 bytes) - invoke: + message: true + delegate: true target: pointer: location: stack slot: 1 - message: true - delegate: true gas: pointer: location: stack @@ -290,8 +336,20 @@ examples: pointer: location: memory offset: "0x80" - length: "0x24" + length: "0x64" + # ----------------------------------------------------------- + # CREATE2: deploying a child contract + # ----------------------------------------------------------- + # This context would mark the CREATE2 instruction. Stack + # layout (top first): + # + # slot 0: value (ETH to send to the new contract) + # slot 1: salt (for deterministic address derivation) + # + # The init code has been placed in memory: + # + # 0x80..0x027f: creation bytecode (512 bytes) - invoke: create: true value: diff --git a/schemas/program/context/function/return.schema.yaml b/schemas/program/context/function/return.schema.yaml index 6885cb1f2..dd274f67d 100644 --- a/schemas/program/context/function/return.schema.yaml +++ b/schemas/program/context/function/return.schema.yaml @@ -48,20 +48,37 @@ required: - return examples: + # ----------------------------------------------------------- + # Internal return: transfer(address, uint256) returns (bool) + # ----------------------------------------------------------- + # This context would mark the JUMP instruction that returns + # control to the caller. The function has left its return + # value on the stack: + # + # slot 0: return value (`bool success`) - return: identifier: "transfer" declaration: source: - id: 1 + id: 0 range: - offset: 256 - length: 80 + offset: 128 + length: 95 data: pointer: - location: memory - offset: "0x80" - length: "0x20" + location: stack + slot: 0 + # ----------------------------------------------------------- + # External call return: processing result of a CALL + # ----------------------------------------------------------- + # This context would mark an instruction after a CALL that + # completed successfully. The EVM places a success flag on + # the stack, and the callee's return data is accessible via + # the returndata buffer: + # + # stack slot 0: success flag (1 = success) + # returndata 0x00..0x1f: ABI-encoded return value (32 bytes) - return: data: pointer: @@ -73,6 +90,14 @@ examples: location: stack slot: 0 + # ----------------------------------------------------------- + # Minimal return: only the data pointer + # ----------------------------------------------------------- + # When the compiler cannot attribute the return to a named + # function, the context may contain only the return data. + # Here, a single stack value is being returned. + # + # slot 0: return value - return: data: pointer: diff --git a/schemas/program/context/function/revert.schema.yaml b/schemas/program/context/function/revert.schema.yaml index 483805f25..9aecc9026 100644 --- a/schemas/program/context/function/revert.schema.yaml +++ b/schemas/program/context/function/revert.schema.yaml @@ -43,6 +43,15 @@ required: - revert examples: + # ----------------------------------------------------------- + # Revert with reason: require() failure in transfer + # ----------------------------------------------------------- + # This context would mark the REVERT instruction after a + # failed require(). The compiler has written the ABI-encoded + # Error(string) revert reason into memory: + # + # 0x80..0xe3: ABI-encoded Error(string) (100 bytes) + # selector 0x08c379a0 + offset + length + data - revert: identifier: "transfer" reason: @@ -51,9 +60,24 @@ examples: offset: "0x80" length: "0x64" + # ----------------------------------------------------------- + # Panic: arithmetic overflow (code 0x11) + # ----------------------------------------------------------- + # A built-in safety check detected an arithmetic overflow. + # The panic code alone identifies the failure; no pointer to + # revert data is needed since the compiler inserts the check + # itself. - revert: panic: 17 + # ----------------------------------------------------------- + # External call revert: processing a failed CALL + # ----------------------------------------------------------- + # This context would mark an instruction after a CALL that + # reverted. The callee's revert reason is accessible via the + # returndata buffer: + # + # returndata 0x00..0x63: ABI-encoded revert reason - revert: reason: pointer: From c3e7b673504a40736c1f912ee3cf9c14e7683bb5 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 01:10:14 -0500 Subject: [PATCH 11/18] Add type specifier schema and use it across the format Introduce schema:ethdebug/format/type/specifier to formalize the "type or type reference" pattern. Update type wrapper, variables, and doc pages to use the new schema instead of inline oneOf/if-then discrimination. Remove the now-unnecessary allow-list entry for the function context schema. --- packages/format/src/schemas/examples.test.ts | 1 - packages/web/spec/type/concepts.mdx | 41 ++++++++++++------- schemas/program/context/variables.schema.yaml | 10 ++--- schemas/type/specifier.schema.yaml | 20 +++++++++ schemas/type/wrapper.schema.yaml | 9 +--- 5 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 schemas/type/specifier.schema.yaml diff --git a/packages/format/src/schemas/examples.test.ts b/packages/format/src/schemas/examples.test.ts index 7f4da0d56..0b77fbd01 100644 --- a/packages/format/src/schemas/examples.test.ts +++ b/packages/format/src/schemas/examples.test.ts @@ -13,7 +13,6 @@ const idsOfSchemasAllowedToOmitExamples = new Set([ "schema:ethdebug/format/type/elementary", "schema:ethdebug/format/pointer/region", "schema:ethdebug/format/pointer/collection", - "schema:ethdebug/format/program/context/function", ]); describe("Examples", () => { diff --git a/packages/web/spec/type/concepts.mdx b/packages/web/spec/type/concepts.mdx index e87a516f4..554baa560 100644 --- a/packages/web/spec/type/concepts.mdx +++ b/packages/web/spec/type/concepts.mdx @@ -158,18 +158,24 @@ possibly includes other fields alongside `"type"`. -## Type wrappers and type references +## Type specifiers, wrappers, and references -This schema defines the concept of a type wrapper and the related concept of a -type reference. +This schema defines three related concepts for working with types +indirectly: **type specifiers**, **type wrappers**, and +**type references**. -Type wrappers serve to encapsulate a type representation alongside other fields -in the same object, and to facilitate discriminating which polymorphic form is -used for a particular complex type. +A **type specifier** is either a complete type representation or a +reference to a known type by ID. Type specifiers appear wherever a +type or reference to a type is needed—as the value of a type +wrapper's `"type"` field, as properties on variable and function +context schemas, etc. -Type wrappers are any object of the form -`{ "type": , ...otherProperties }`, where `` is either a complete -type representation or a reference to another type by ID. +A **type wrapper** is any object of the form +`{ "type": , ...otherProperties }`, where `` +is a type specifier. Type wrappers serve to encapsulate a type +specifier alongside other fields in the same object, and to +facilitate discriminating which polymorphic form is used for a +particular complex type.
Example type wrapper with complete type representation @@ -198,9 +204,17 @@ type representation or a reference to another type by ID.
-Note that **ethdebug/format/type** places no restriction on IDs other than -that they must be either a number or a string. Other components of this format -at-large may impose restrictions, however. +A **type reference** is the simplest form of type specifier: an +object containing only an `"id"` field. Note that +**ethdebug/format/type** places no restriction on IDs other than +that they must be either a number or a string. Other components +of this format at-large may impose restrictions, however. + +### Type specifier schema + + ### Type wrapper schema @@ -208,9 +222,6 @@ at-large may impose restrictions, however. ### Type reference schema -A type reference is an object containing the single `"id"` field. This field -must be a string or a number. - ## Sometimes types are defined in code diff --git a/schemas/program/context/variables.schema.yaml b/schemas/program/context/variables.schema.yaml index a64b43725..9987a1856 100644 --- a/schemas/program/context/variables.schema.yaml +++ b/schemas/program/context/variables.schema.yaml @@ -58,12 +58,10 @@ $defs: type: description: | - The variable's static type, if it exists. This **must** be specified - either as a full **ethdebug/format/type** representation, or an - `{ "id": "..." }` type reference object. - oneOf: - - $ref: "schema:ethdebug/format/type" - - $ref: "schema:ethdebug/format/type/reference" + The variable's static type, if it exists. This **must** be + specified either as a full **ethdebug/format/type** + representation, or an `{ "id": "..." }` type reference. + $ref: "schema:ethdebug/format/type/specifier" pointer: description: | diff --git a/schemas/type/specifier.schema.yaml b/schemas/type/specifier.schema.yaml new file mode 100644 index 000000000..8781c890e --- /dev/null +++ b/schemas/type/specifier.schema.yaml @@ -0,0 +1,20 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/type/specifier" + +title: ethdebug/format/type/specifier +description: | + A type specifier: either a complete type representation or a + reference to a known type by ID. This schema discriminates + between the two forms based on the presence of an `id` field. + +if: + required: [id] +then: + $ref: "schema:ethdebug/format/type/reference" +else: + $ref: "schema:ethdebug/format/type" + +examples: + - kind: uint + bits: 256 + - id: 42 diff --git a/schemas/type/wrapper.schema.yaml b/schemas/type/wrapper.schema.yaml index eefca6a56..8e9a277f0 100644 --- a/schemas/type/wrapper.schema.yaml +++ b/schemas/type/wrapper.schema.yaml @@ -9,14 +9,7 @@ description: type: object properties: type: - # Discriminate between reference and type based on presence of `id` - if: - required: - - id - then: - $ref: "schema:ethdebug/format/type/reference" - else: - $ref: "schema:ethdebug/format/type" + $ref: "schema:ethdebug/format/type/specifier" required: - type From b42da9fc58a92fa9b91b0f9894997199fc9adf01 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 4 Mar 2026 01:15:39 -0500 Subject: [PATCH 12/18] Format --- packages/web/spec/type/concepts.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web/spec/type/concepts.mdx b/packages/web/spec/type/concepts.mdx index 554baa560..e42bcbf7f 100644 --- a/packages/web/spec/type/concepts.mdx +++ b/packages/web/spec/type/concepts.mdx @@ -212,9 +212,7 @@ of this format at-large may impose restrictions, however. ### Type specifier schema - + ### Type wrapper schema From f966ef5129527ea8585eda955d22e998a7ef058b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 03:40:04 -0400 Subject: [PATCH 13/18] format: add TypeScript types for function call contexts (#186) Add Type.Reference, Type.Specifier, and type guards for the new function call context schemas (invoke, return, revert). - Type.Reference: { id: string | number } for type references - Type.Specifier: Type | Reference union (matches type/specifier schema) - Context.Function.Identity: shared function identity fields - Context.Invoke: internal calls, external calls, contract creation - Context.Return: function return with data and optional success - Context.Revert: function revert with optional reason/panic - Update Variable.type to accept Type.Specifier (was Type only) - Update Type.Wrapper to use Type.Specifier --- .../format/src/types/program/context.test.ts | 16 ++ packages/format/src/types/program/context.ts | 172 +++++++++++++++++- packages/format/src/types/type/index.test.ts | 15 ++ packages/format/src/types/type/index.ts | 20 +- 4 files changed, 216 insertions(+), 7 deletions(-) diff --git a/packages/format/src/types/program/context.test.ts b/packages/format/src/types/program/context.test.ts index 47185c7ca..4470a322d 100644 --- a/packages/format/src/types/program/context.test.ts +++ b/packages/format/src/types/program/context.test.ts @@ -30,4 +30,20 @@ testSchemaGuards("ethdebug/format/program/context", [ schema: "schema:ethdebug/format/program/context/frame", guard: Context.isFrame, }, + { + schema: "schema:ethdebug/format/program/context/function", + guard: Context.Function.isIdentity, + }, + { + schema: "schema:ethdebug/format/program/context/function/invoke", + guard: Context.isInvoke, + }, + { + schema: "schema:ethdebug/format/program/context/function/return", + guard: Context.isReturn, + }, + { + schema: "schema:ethdebug/format/program/context/function/revert", + guard: Context.isRevert, + }, ] as const); diff --git a/packages/format/src/types/program/context.ts b/packages/format/src/types/program/context.ts index 6a552734a..9388fea7b 100644 --- a/packages/format/src/types/program/context.ts +++ b/packages/format/src/types/program/context.ts @@ -1,5 +1,5 @@ import { Materials } from "#types/materials"; -import { Type, isType } from "#types/type"; +import { Type } from "#types/type"; import { Pointer, isPointer } from "#types/pointer"; export type Context = @@ -8,7 +8,10 @@ export type Context = | Context.Remark | Context.Pick | Context.Gather - | Context.Frame; + | Context.Frame + | Context.Invoke + | Context.Return + | Context.Revert; export const isContext = (value: unknown): value is Context => [ @@ -18,6 +21,9 @@ export const isContext = (value: unknown): value is Context => Context.isPick, Context.isFrame, Context.isGather, + Context.isInvoke, + Context.isReturn, + Context.isRevert, ].some((guard) => guard(value)); export namespace Context { @@ -47,7 +53,7 @@ export namespace Context { export interface Variable { identifier?: string; declaration?: Materials.SourceRange; - type?: Type; + type?: Type.Specifier; pointer?: Pointer; } @@ -66,7 +72,7 @@ export namespace Context { (!("identifier" in value) || typeof value.identifier === "string") && (!("declaration" in value) || Materials.isSourceRange(value.declaration)) && - (!("type" in value) || isType(value.type)) && + (!("type" in value) || Type.isSpecifier(value.type)) && (!("pointer" in value) || isPointer(value.pointer)); } @@ -111,4 +117,162 @@ export namespace Context { !!value && "frame" in value && typeof value.frame === "string"; + + export namespace Function { + export interface Identity { + identifier?: string; + declaration?: Materials.SourceRange; + type?: Type.Specifier; + } + + export const isIdentity = (value: unknown): value is Identity => + typeof value === "object" && + !!value && + (!("identifier" in value) || typeof value.identifier === "string") && + (!("declaration" in value) || + Materials.isSourceRange(value.declaration)) && + (!("type" in value) || Type.isSpecifier(value.type)); + + export interface PointerRef { + pointer: Pointer; + } + + export const isPointerRef = (value: unknown): value is PointerRef => + typeof value === "object" && + !!value && + "pointer" in value && + isPointer(value.pointer); + } + + export interface Invoke { + invoke: Invoke.Invocation; + } + + export const isInvoke = (value: unknown): value is Invoke => + typeof value === "object" && + !!value && + "invoke" in value && + Invoke.isInvocation(value.invoke); + + export namespace Invoke { + export type Invocation = Function.Identity & + ( + | Invocation.InternalCall + | Invocation.ExternalCall + | Invocation.ContractCreation + ); + + export const isInvocation = (value: unknown): value is Invocation => + Function.isIdentity(value) && + (Invocation.isInternalCall(value) || + Invocation.isExternalCall(value) || + Invocation.isContractCreation(value)); + + export namespace Invocation { + export interface InternalCall extends Function.Identity { + jump: true; + target: Function.PointerRef; + arguments?: Function.PointerRef; + } + + export const isInternalCall = (value: unknown): value is InternalCall => + typeof value === "object" && + !!value && + "jump" in value && + value.jump === true && + "target" in value && + Function.isPointerRef(value.target) && + (!("arguments" in value) || Function.isPointerRef(value.arguments)); + + export interface ExternalCall extends Function.Identity { + message: true; + target: Function.PointerRef; + gas?: Function.PointerRef; + value?: Function.PointerRef; + input?: Function.PointerRef; + delegate?: true; + static?: true; + } + + export const isExternalCall = (value: unknown): value is ExternalCall => + typeof value === "object" && + !!value && + "message" in value && + value.message === true && + "target" in value && + Function.isPointerRef(value.target) && + (!("gas" in value) || Function.isPointerRef(value.gas)) && + (!("value" in value) || Function.isPointerRef(value.value)) && + (!("input" in value) || Function.isPointerRef(value.input)) && + (!("delegate" in value) || value.delegate === true) && + (!("static" in value) || value.static === true); + + export interface ContractCreation extends Function.Identity { + create: true; + value?: Function.PointerRef; + salt?: Function.PointerRef; + input?: Function.PointerRef; + } + + export const isContractCreation = ( + value: unknown, + ): value is ContractCreation => + typeof value === "object" && + !!value && + "create" in value && + value.create === true && + (!("value" in value) || Function.isPointerRef(value.value)) && + (!("salt" in value) || Function.isPointerRef(value.salt)) && + (!("input" in value) || Function.isPointerRef(value.input)); + } + } + + export interface Return { + return: Return.Info; + } + + export const isReturn = (value: unknown): value is Return => + typeof value === "object" && + !!value && + "return" in value && + Return.isInfo(value.return); + + export namespace Return { + export interface Info extends Function.Identity { + data: Function.PointerRef; + success?: Function.PointerRef; + } + + export const isInfo = (value: unknown): value is Info => + Function.isIdentity(value) && + typeof value === "object" && + !!value && + "data" in value && + Function.isPointerRef(value.data) && + (!("success" in value) || Function.isPointerRef(value.success)); + } + + export interface Revert { + revert: Revert.Info; + } + + export const isRevert = (value: unknown): value is Revert => + typeof value === "object" && + !!value && + "revert" in value && + Revert.isInfo(value.revert); + + export namespace Revert { + export interface Info extends Function.Identity { + reason?: Function.PointerRef; + panic?: number; + } + + export const isInfo = (value: unknown): value is Info => + Function.isIdentity(value) && + typeof value === "object" && + !!value && + (!("reason" in value) || Function.isPointerRef(value.reason)) && + (!("panic" in value) || typeof value.panic === "number"); + } } diff --git a/packages/format/src/types/type/index.test.ts b/packages/format/src/types/type/index.test.ts index 61a5349c3..cc461540a 100644 --- a/packages/format/src/types/type/index.test.ts +++ b/packages/format/src/types/type/index.test.ts @@ -80,4 +80,19 @@ testSchemaGuards("ethdebug/format/type", [ schema: "schema:ethdebug/format/type/complex/struct", guard: Type.Complex.isStruct, }, + + // type reference and specifier + + { + schema: "schema:ethdebug/format/type/reference", + guard: Type.isReference, + }, + { + schema: "schema:ethdebug/format/type/specifier", + guard: Type.isSpecifier, + }, + { + schema: "schema:ethdebug/format/type/wrapper", + guard: Type.isWrapper, + }, ] as const); diff --git a/packages/format/src/types/type/index.ts b/packages/format/src/types/type/index.ts index c53d0230b..066f3a6d5 100644 --- a/packages/format/src/types/type/index.ts +++ b/packages/format/src/types/type/index.ts @@ -32,16 +32,30 @@ export namespace Type { (typeof value.contains === "object" && Object.values(value.contains).every(Type.isWrapper))); + export interface Reference { + id: string | number; + } + + export const isReference = (value: unknown): value is Reference => + typeof value === "object" && + !!value && + "id" in value && + (typeof value.id === "string" || typeof value.id === "number"); + + export type Specifier = Type | Reference; + + export const isSpecifier = (value: unknown): value is Specifier => + isType(value) || isReference(value); + export interface Wrapper { - type: Type | { id: any }; + type: Specifier; } export const isWrapper = (value: unknown): value is Wrapper => typeof value === "object" && !!value && "type" in value && - (isType(value.type) || - (typeof value.type === "object" && !!value.type && "id" in value.type)); + isSpecifier(value.type); export type Elementary = | Elementary.Uint From da00fb816e253fc56be0adb102152561c436e27f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 03:54:11 -0400 Subject: [PATCH 14/18] bugc: emit invoke/return contexts for internal function calls (#185) Add debug context annotations for function call boundaries using the typed Format.Program.Context.Invoke and Context.Return interfaces: - Invoke context on caller's JUMP instruction with target pointer and argument group pointers (stack slots) - Return context on continuation JUMPDEST with data pointer to return value at stack slot 0 - Invoke context on callee entry JUMPDEST with target and argument pointers Includes tests verifying context emission for single-arg, multi-arg, nested, and void function call scenarios. --- .../bugc/src/evmgen/call-contexts.test.ts | 358 ++++++++++++++++++ packages/bugc/src/evmgen/generation/block.ts | 20 +- .../generation/control-flow/terminator.ts | 37 +- .../bugc/src/evmgen/generation/function.ts | 33 +- 4 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 packages/bugc/src/evmgen/call-contexts.test.ts diff --git a/packages/bugc/src/evmgen/call-contexts.test.ts b/packages/bugc/src/evmgen/call-contexts.test.ts new file mode 100644 index 000000000..c0a66d549 --- /dev/null +++ b/packages/bugc/src/evmgen/call-contexts.test.ts @@ -0,0 +1,358 @@ +import { describe, it, expect } from "vitest"; + +import { compile } from "#compiler"; +import type * as Format from "@ethdebug/format"; + +/** + * Compile a BUG source and return the runtime program + */ +async function compileProgram(source: string): Promise { + const result = await compile({ + to: "bytecode", + source, + }); + + if (!result.success) { + const errors = result.messages.error ?? []; + const msgs = errors + .map((e: { message?: string }) => e.message ?? String(e)) + .join("\n"); + throw new Error(`Compilation failed:\n${msgs}`); + } + + return result.value.bytecode.runtimeProgram; +} + +/** + * Find instructions matching a predicate + */ +function findInstructions( + program: Format.Program, + predicate: (instr: Format.Program.Instruction) => boolean, +): Format.Program.Instruction[] { + return program.instructions.filter(predicate); +} + +describe("function call debug contexts", () => { + const source = `name CallContextTest; + +define { + function add(a: uint256, b: uint256) -> uint256 { + return a + b; + }; +} + +storage { + [0] result: uint256; +} + +create { + result = 0; +} + +code { + result = add(10, 20); +}`; + + it("should emit invoke context on caller JUMP", async () => { + const program = await compileProgram(source); + + // Find JUMP instructions with invoke context + const invokeJumps = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMP" && + !!(instr.context as Record)?.invoke, + ); + + expect(invokeJumps.length).toBeGreaterThanOrEqual(1); + + const ctx = (invokeJumps[0].context as Record)!; + const invoke = ctx.invoke as Record; + + expect(invoke.jump).toBe(true); + expect(invoke.identifier).toBe("add"); + + // Should have target pointer + const target = invoke.target as Record; + expect(target.pointer).toBeDefined(); + + // Should have argument pointers + const args = invoke.arguments as Record; + const pointer = args.pointer as Record; + const group = pointer.group as Array>; + + expect(group).toHaveLength(2); + // First arg (a) is deepest on stack + expect(group[0]).toEqual({ + location: "stack", + slot: 1, + }); + // Second arg (b) is on top + expect(group[1]).toEqual({ + location: "stack", + slot: 0, + }); + }); + + it("should emit return context on continuation JUMPDEST", async () => { + const program = await compileProgram(source); + + // Find JUMPDEST instructions with return context + const returnJumpdests = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMPDEST" && + !!(instr.context as Record)?.return, + ); + + expect(returnJumpdests.length).toBeGreaterThanOrEqual(1); + + const ctx = (returnJumpdests[0].context as Record)!; + const ret = ctx.return as Record; + + expect(ret.identifier).toBe("add"); + + // Should have data pointer to return value at + // TOS (stack slot 0) + const data = ret.data as Record; + const pointer = data.pointer as Record; + expect(pointer).toEqual({ + location: "stack", + slot: 0, + }); + }); + + it("should emit invoke context on callee entry JUMPDEST", async () => { + const program = await compileProgram(source); + + // Find JUMPDEST instructions with invoke context + // (the callee entry point, not the continuation) + const invokeJumpdests = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMPDEST" && + !!(instr.context as Record)?.invoke, + ); + + expect(invokeJumpdests.length).toBeGreaterThanOrEqual(1); + + const ctx = (invokeJumpdests[0].context as Record)!; + const invoke = ctx.invoke as Record; + + expect(invoke.jump).toBe(true); + expect(invoke.identifier).toBe("add"); + + // Should have argument pointers matching + // function parameters + const args = invoke.arguments as Record; + const pointer = args.pointer as Record; + const group = pointer.group as Array>; + + expect(group).toHaveLength(2); + }); + + it("should emit contexts in correct instruction order", async () => { + const program = await compileProgram(source); + + // The caller JUMP should come before the + // continuation JUMPDEST + const invokeJump = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMP" && + !!(instr.context as Record)?.invoke, + )[0]; + + const returnJumpdest = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMPDEST" && + !!(instr.context as Record)?.return, + )[0]; + + expect(invokeJump).toBeDefined(); + expect(returnJumpdest).toBeDefined(); + + // Invoke JUMP offset should be less than + // return JUMPDEST offset (caller comes first + // in bytecode) + expect(Number(invokeJump.offset)).toBeLessThan( + Number(returnJumpdest.offset), + ); + }); + + describe("void function calls", () => { + const voidSource = `name VoidCallTest; + +define { + function setVal( + s: uint256, v: uint256 + ) -> uint256 { + return v; + }; +} + +storage { + [0] result: uint256; +} + +create { + result = 0; +} + +code { + result = setVal(0, 42); +}`; + + it( + "should emit return context without data pointer " + "for void functions", + async () => { + // This tests that when a function returns a + // value, the return context includes data. + // (All our test functions return values, so + // data should always be present here.) + const program = await compileProgram(voidSource); + + const returnJumpdests = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMPDEST" && + !!(instr.context as Record)?.return, + ); + + expect(returnJumpdests.length).toBeGreaterThanOrEqual(1); + + const ctx = (returnJumpdests[0].context as Record)!; + const ret = ctx.return as Record; + expect(ret.identifier).toBe("setVal"); + // Since setVal returns a value, data should + // be present + expect(ret.data).toBeDefined(); + }, + ); + }); + + describe("nested function calls", () => { + const nestedSource = `name NestedCallTest; + +define { + function add( + a: uint256, b: uint256 + ) -> uint256 { + return a + b; + }; + function addThree( + x: uint256, y: uint256, z: uint256 + ) -> uint256 { + let sum1 = add(x, y); + let sum2 = add(sum1, z); + return sum2; + }; +} + +storage { + [0] result: uint256; +} + +create { + result = 0; +} + +code { + result = addThree(1, 2, 3); +}`; + + it("should emit invoke/return contexts for " + "nested calls", async () => { + const program = await compileProgram(nestedSource); + + // Should have invoke contexts for: + // 1. main -> addThree + // 2. addThree -> add (first call) + // 3. addThree -> add (second call) + // Plus callee entry JUMPDESTs + const invokeJumps = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMP" && + !!(instr.context as Record)?.invoke, + ); + + // At least 3 invoke JUMPs (main->addThree, + // addThree->add x2) + expect(invokeJumps.length).toBeGreaterThanOrEqual(3); + + // Check we have invokes for both functions + const invokeIds = invokeJumps.map( + (instr) => + ( + (instr.context as Record).invoke as Record< + string, + unknown + > + ).identifier, + ); + expect(invokeIds).toContain("addThree"); + expect(invokeIds).toContain("add"); + + // Should have return contexts for all + // continuation points + const returnJumpdests = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMPDEST" && + !!(instr.context as Record)?.return, + ); + + expect(returnJumpdests.length).toBeGreaterThanOrEqual(3); + }); + }); + + describe("single-arg function", () => { + const singleArgSource = `name SingleArgTest; + +define { + function double(x: uint256) -> uint256 { + return x + x; + }; +} + +storage { + [0] result: uint256; +} + +create { + result = 0; +} + +code { + result = double(7); +}`; + + it("should emit single-element argument group", async () => { + const program = await compileProgram(singleArgSource); + + const invokeJumps = findInstructions( + program, + (instr) => + instr.operation?.mnemonic === "JUMP" && + !!(instr.context as Record)?.invoke, + ); + + expect(invokeJumps.length).toBeGreaterThanOrEqual(1); + + const ctx = (invokeJumps[0].context as Record)!; + const invoke = ctx.invoke as Record; + const args = invoke.arguments as Record; + const pointer = args.pointer as Record; + const group = pointer.group as Array>; + + // Single arg at stack slot 0 + expect(group).toHaveLength(1); + expect(group[0]).toEqual({ + location: "stack", + slot: 0, + }); + }); + }); +}); diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index e425212fb..6b0c2189d 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -2,6 +2,7 @@ * Block-level code generation */ +import type * as Format from "@ethdebug/format"; import * as Ir from "#ir"; import type { Stack } from "#evm"; @@ -69,11 +70,24 @@ export function generate( // Add JUMPDEST with continuation annotation if applicable if (isContinuation) { - const continuationDebug = { - context: { - remark: `call-continuation: resume after call to ${calledFunction}`, + // Return context describes state after JUMPDEST + // executes: TOS is the return value (if any). + // data pointer is required by the schema; for + // void returns, slot 0 is still valid (empty). + const returnCtx: Format.Program.Context.Return = { + return: { + identifier: calledFunction, + data: { + pointer: { + location: "stack" as const, + slot: 0, + }, + }, }, }; + const continuationDebug = { + context: returnCtx as Format.Program.Context, + }; result = result.then(JUMPDEST({ debug: continuationDebug })); } else { result = result.then(JUMPDEST()); diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index 44c4792da..fe39eddfc 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -1,3 +1,4 @@ +import type * as Format from "@ethdebug/format"; import type * as Ir from "#ir"; import type { Stack } from "#evm"; import type { State } from "#evmgen/state"; @@ -225,13 +226,45 @@ export function generateCallTerminator( currentState = loadValue(arg, { debug: argsDebug })(currentState); } - // Push function address and jump + // Push function address and jump. + // The JUMP gets an invoke context: after JUMP executes, + // the function has been entered with args on the stack. const funcAddrPatchIndex = currentState.instructions.length; const invocationDebug = { context: { remark: `call-invocation: jump to function ${funcName}`, }, }; + + // Build argument pointers: after the JUMP, the callee + // sees args on the stack in order (first arg deepest). + const argPointers = args.map((_arg, i) => ({ + location: "stack" as const, + slot: args.length - 1 - i, + })); + + // Invoke context describes state after JUMP executes: + // the callee has been entered with args on the stack. + // target points to the function address at stack slot 0 + // (consumed by JUMP, but describes the call target). + const invoke: Format.Program.Context.Invoke = { + invoke: { + jump: true as const, + identifier: funcName, + target: { + pointer: { location: "stack" as const, slot: 0 }, + }, + ...(argPointers.length > 0 && { + arguments: { + pointer: { + group: argPointers, + }, + }, + }), + }, + }; + const invokeContext = { context: invoke as Format.Program.Context }; + currentState = { ...currentState, instructions: [ @@ -242,7 +275,7 @@ export function generateCallTerminator( immediates: [0, 0], debug: invocationDebug, }, - { mnemonic: "JUMP", opcode: 0x56 }, + { mnemonic: "JUMP", opcode: 0x56, debug: invokeContext }, ], patches: [ ...currentState.patches, diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 59e068bf8..70b31d8aa 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -2,6 +2,7 @@ * Function-level code generation */ +import type * as Format from "@ethdebug/format"; import * as Ir from "#ir"; import type * as Evm from "#evm"; import type { Stack } from "#evm"; @@ -27,12 +28,36 @@ function generatePrologue( return ((state: State): State => { let currentState = state; - // Add JUMPDEST with function entry annotation - const entryDebug = { - context: { - remark: `function-entry: ${func.name || "anonymous"}`, + // Add JUMPDEST with function entry annotation. + // After this JUMPDEST executes, the callee's args are + // on the stack (first arg deepest). + const argPointers = params.map((_p, i) => ({ + location: "stack" as const, + slot: params.length - 1 - i, + })); + + const entryInvoke: Format.Program.Context.Invoke = { + invoke: { + jump: true as const, + identifier: func.name || "anonymous", + target: { + pointer: { + location: "stack" as const, + slot: 0, + }, + }, + ...(argPointers.length > 0 && { + arguments: { + pointer: { + group: argPointers, + }, + }, + }), }, }; + const entryDebug = { + context: entryInvoke as Format.Program.Context, + }; currentState = { ...currentState, instructions: [ From 17a7b1310b72076c3c09152414e210dc642b55df Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 04:28:14 -0400 Subject: [PATCH 15/18] bugc: add source maps for call setup instructions (#188) The call setup sequence (POP cleanup, PUSH return address, MSTORE, push arguments, PUSH function address) was using remark-only debug contexts, leaving these instructions unmapped in tracing output. Now threads the call terminator's operationDebug (which carries the source code range for the call expression) through all setup instructions, matching how other instruction generators use operationDebug. --- .../generation/control-flow/terminator.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts index fe39eddfc..1f418c9b4 100644 --- a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -159,6 +159,10 @@ export function generateCallTerminator( return ((state: State): State => { let currentState: State = state as State; + // All call setup instructions map back to the call + // expression source location via operationDebug. + const debug = term.operationDebug; + // Clean the stack before setting up the call. // Values produced by block instructions that are only // used as call arguments will have been DUP'd by @@ -166,17 +170,12 @@ export function generateCallTerminator( // block terminator, all current stack values are dead // after the call — POP them so the function receives a // clean stack with only its arguments. - const cleanupDebug = { - context: { - remark: `call-preparation: clean stack for ${funcName}`, - }, - }; while (currentState.stack.length > 0) { currentState = { ...currentState, instructions: [ ...currentState.instructions, - { mnemonic: "POP", opcode: 0x50, debug: cleanupDebug }, + { mnemonic: "POP", opcode: 0x50, debug }, ], stack: currentState.stack.slice(1), brands: currentState.brands.slice(1) as Stack, @@ -186,11 +185,6 @@ export function generateCallTerminator( const returnPcPatchIndex = currentState.instructions.length; // Store return PC to memory at 0x60 - const returnPcDebug = { - context: { - remark: `call-preparation: store return address for ${funcName}`, - }, - }; currentState = { ...currentState, instructions: [ @@ -199,10 +193,15 @@ export function generateCallTerminator( mnemonic: "PUSH2", opcode: 0x61, immediates: [0, 0], - debug: returnPcDebug, + debug, }, - { mnemonic: "PUSH1", opcode: 0x60, immediates: [0x60] }, - { mnemonic: "MSTORE", opcode: 0x52 }, + { + mnemonic: "PUSH1", + opcode: 0x60, + immediates: [0x60], + debug, + }, + { mnemonic: "MSTORE", opcode: 0x52, debug }, ], patches: [ ...currentState.patches, @@ -217,24 +216,14 @@ export function generateCallTerminator( // Push arguments using loadValue. // Stack is clean, so loadValue will reload from memory // (for temps) or re-push (for consts). - const argsDebug = { - context: { - remark: `call-arguments: push ${args.length} argument(s) for ${funcName}`, - }, - }; for (const arg of args) { - currentState = loadValue(arg, { debug: argsDebug })(currentState); + currentState = loadValue(arg, { debug })(currentState); } // Push function address and jump. // The JUMP gets an invoke context: after JUMP executes, // the function has been entered with args on the stack. const funcAddrPatchIndex = currentState.instructions.length; - const invocationDebug = { - context: { - remark: `call-invocation: jump to function ${funcName}`, - }, - }; // Build argument pointers: after the JUMP, the callee // sees args on the stack in order (first arg deepest). @@ -273,7 +262,7 @@ export function generateCallTerminator( mnemonic: "PUSH2", opcode: 0x61, immediates: [0, 0], - debug: invocationDebug, + debug, }, { mnemonic: "JUMP", opcode: 0x56, debug: invokeContext }, ], From 8864511fc7b61ab6e582906366396c623527a263 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 04:35:54 -0400 Subject: [PATCH 16/18] Add function call tracing documentation (#187) * Add function call tracing documentation Add invoke/return/revert context documentation across concept, reference, and spec pages: - concepts/programs.mdx: new "Function call contexts" section explaining the three context types with a SchemaExample - tracing.mdx: walkthrough of tracing through an internal function call (Adder contract), plus external call and revert examples - Spec pages: added intro prose to function.mdx, return.mdx, revert.mdx * Make function call tracing example interactive Replace the static BUG code block with an interactive TraceExample component that lets readers compile and step through the Adder contract, seeing invoke/return contexts at function boundaries. Static SchemaExample blocks are kept for the narrative walkthrough and for external call/revert examples (which BUG can't demonstrate). * Fix BUG source indentation in tracing examples Add 2-space indentation inside block bodies (storage, create, code, if) to match the canonical style used in .bug example files. * Revert BUG indentation to match prettier formatting Prettier strips indentation inside template literal strings in MDX JSX props. Revert to the unindented style that prettier enforces. --- packages/web/docs/concepts/programs.mdx | 49 +++++ .../docs/core-schemas/programs/tracing.mdx | 172 ++++++++++++++++++ .../program/context/function/function.mdx | 6 +- .../spec/program/context/function/return.mdx | 5 + .../spec/program/context/function/revert.mdx | 5 + 5 files changed, 236 insertions(+), 1 deletion(-) diff --git a/packages/web/docs/concepts/programs.mdx b/packages/web/docs/concepts/programs.mdx index ec4b3abe4..93c844c27 100644 --- a/packages/web/docs/concepts/programs.mdx +++ b/packages/web/docs/concepts/programs.mdx @@ -157,6 +157,55 @@ Contexts can be composed using: This composition enables describing complex scenarios like conditional variable assignments or function inlining. +## Function call contexts + +Programs answer "what function are we in?" through three context types +that track function boundaries during execution: + +- **invoke** — marks an instruction that enters a function. Indicates + the invocation kind (internal jump, external message call, or + contract creation) and provides pointers to call arguments, target + address, gas, and value as appropriate. +- **return** — marks an instruction associated with a successful + return from a function. Provides a pointer to the return data. +- **revert** — marks an instruction associated with a failed call. + May include a pointer to revert reason data or a numeric panic + code. + +All three extend a common **function identity** schema with optional +fields for the function's name, declaration source range, and type. +This lets compilers provide as much or as little attribution as +available — from a fully identified `transfer` call down to an +anonymous indirect invocation through a function pointer. + + + {`{ + "invoke": { + "identifier": "transfer", + "jump": true, + "target": { + "pointer": { "location": "stack", "slot": 0 } + }, + "arguments": { + "pointer": { + "group": [ + { "name": "to", "location": "stack", "slot": 2 }, + { "name": "amount", "location": "stack", "slot": 3 } + ] + } + } + } +}`} + + +A debugger uses these contexts to reconstruct call stacks, show +function names in stepping UI, and display argument/return values +alongside source code. + ## What tracing enables By following contexts through execution, debuggers can provide: diff --git a/packages/web/docs/core-schemas/programs/tracing.mdx b/packages/web/docs/core-schemas/programs/tracing.mdx index efbae0cfc..55ec6ca8b 100644 --- a/packages/web/docs/core-schemas/programs/tracing.mdx +++ b/packages/web/docs/core-schemas/programs/tracing.mdx @@ -121,6 +121,178 @@ b = b + 1; }`} /> +## Tracing through a function call + +The examples above trace simple straight-line code. Real programs +make function calls. **invoke** and **return** contexts let a +debugger follow execution across function boundaries. + +Click **"Try it"** on the example below, then step through the +trace. Watch for **invoke** contexts on the JUMP into `add` and +**return** contexts on the JUMP back to the caller: + + uint256 { +return a + b; +}; +} + +storage { +[0] result: uint256; +} + +create { +result = 0; +} + +code { +result = add(3, 4); +}`} +/> + +As you step through, three phases are visible: + +### Before the call — setting up arguments + +At the call site, the compiler pushes arguments onto the stack and +prepares the jump. The JUMP instruction carries an **invoke** +context identifying the function, its target, and the argument +locations: + + + {`{ + "invoke": { + "identifier": "add", + "jump": true, + "target": { + "pointer": { "location": "stack", "slot": 0 } + }, + "arguments": { + "pointer": { + "group": [ + { "name": "a", "location": "stack", "slot": 2 }, + { "name": "b", "location": "stack", "slot": 3 } + ] + } + } + } +}`} + + +The debugger now knows it's entering `add` with arguments at stack +slots 2 and 3. A trace viewer can show `add(3, 4)` in the call +stack. + +### Inside the function — normal tracing + +Inside `add`, instructions carry their own `code` and `variables` +contexts as usual. The debugger shows the source range within the +function body, and parameters `a` and `b` appear as in-scope +variables. + +### Returning — the result + +When `add` finishes, the JUMP back to the caller carries a +**return** context with a pointer to the result: + + + {`{ + "return": { + "identifier": "add", + "data": { + "pointer": { "location": "stack", "slot": 0 } + } + } +}`} + + +The debugger pops `add` from the call stack and can display the +return value (7). + +### External calls and reverts + +The same pattern applies to external message calls, but with +additional fields. An external CALL instruction carries gas, value, +and input data pointers: + + + {`{ + "invoke": { + "identifier": "balanceOf", + "message": true, + "target": { + "pointer": { "location": "stack", "slot": 1 } + }, + "gas": { + "pointer": { "location": "stack", "slot": 0 } + }, + "input": { + "pointer": { + "group": [ + { "name": "selector", "location": "memory", + "offset": "0x80", "length": 4 }, + { "name": "arguments", "location": "memory", + "offset": "0x84", "length": "0x20" } + ] + } + } + } +}`} + + +If the call reverts, a **revert** context captures the reason: + + + {`{ + "revert": { + "identifier": "transfer", + "reason": { + "pointer": { + "location": "memory", + "offset": "0x80", + "length": "0x64" + } + } + } +}`} + + +For built-in assertion failures, the compiler can provide a panic +code instead of (or alongside) a reason pointer: + + + {`{ + "revert": { + "panic": 17 + } +}`} + + ## Trace data structure A trace step captures the EVM state at a single point: diff --git a/packages/web/spec/program/context/function/function.mdx b/packages/web/spec/program/context/function/function.mdx index 0b00b3a6d..69dcad9d9 100644 --- a/packages/web/spec/program/context/function/function.mdx +++ b/packages/web/spec/program/context/function/function.mdx @@ -7,7 +7,11 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Function identity Function contexts (invoke, return, revert) share a common set of -identity fields for the function being called. +identity fields for the function being called. All fields are +optional, allowing compilers to provide as much or as little +detail as available — from a fully named function with source +location and type, down to an empty object for an anonymous +indirect call. diff --git a/packages/web/spec/program/context/function/revert.mdx b/packages/web/spec/program/context/function/revert.mdx index 4b18b518b..98d980988 100644 --- a/packages/web/spec/program/context/function/revert.mdx +++ b/packages/web/spec/program/context/function/revert.mdx @@ -6,6 +6,11 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Revert contexts +A revert context marks an instruction associated with a function +revert. It extends the function identity schema with an optional +pointer to revert reason data and/or a numeric panic code for +built-in assertion failures. + From 2c36ef8ddfcd7ad7a66a0d0c0767806c942a6f5b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 05:10:14 -0400 Subject: [PATCH 17/18] bugc: add debug contexts to all unmapped bytecodes (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bugc: add debug contexts to all remaining unmapped bytecodes Thread remark/code contexts through all compiler-generated instructions that previously lacked debug info: - Free memory pointer initialization (remark) - Return value spill after call continuation (call expr source range) - STOP guard between main and user functions (remark) - Function prologue MSTORE for param storage (thread existing remark) - Function prologue return PC save sequence (thread existing remark) - Deployment wrapper CODECOPY+RETURN (remark) All 82 instructions across runtime and create programs now carry debug contexts (previously 22 were unmapped). * bugc: add code contexts with source ranges to compiler-generated instructions Add source location info (loc, sourceId) to Ir.Function so EVM codegen can build code contexts for compiler-generated instructions. Instructions that map to a source location now use gather contexts combining both a remark (for debugger tooling) and a code context (for source highlighting): - Free memory pointer init → code block / create block range - Function prologue (param stores, return PC save) → function decl range - STOP guard → code block range Deployment wrapper remains remark-only (no corresponding source). Return value spill already had correct source mapping (call expr). --- packages/bugc/src/evmgen/generation/block.ts | 71 ++++++++++++++---- .../bugc/src/evmgen/generation/function.ts | 73 +++++++++++++++---- packages/bugc/src/evmgen/generation/module.ts | 50 +++++++++++-- packages/bugc/src/ir/spec/function.ts | 4 + packages/bugc/src/irgen/generate/function.ts | 4 + 5 files changed, 165 insertions(+), 37 deletions(-) diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts index 6b0c2189d..5fea7a285 100644 --- a/packages/bugc/src/evmgen/generation/block.ts +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -2,6 +2,7 @@ * Block-level code generation */ +import type * as Ast from "#ast"; import type * as Format from "@ethdebug/format"; import * as Ir from "#ir"; import type { Stack } from "#evm"; @@ -47,9 +48,13 @@ export function generate( // Initialize memory for first block if (isFirstBlock) { - // Always initialize the free memory pointer for consistency - // This ensures dynamic allocations start after static ones - result = result.then(initializeMemory(state.memory.nextStaticOffset)); + const sourceInfo = + func?.sourceId && func?.loc + ? { sourceId: func.sourceId, loc: func.loc } + : undefined; + result = result.then( + initializeMemory(state.memory.nextStaticOffset, sourceInfo), + ); } // Set JUMPDEST for non-first blocks @@ -104,6 +109,7 @@ export function generate( predBlock.terminator.dest ) { const destId = predBlock.terminator.dest; + const spillDebug = predBlock.terminator.operationDebug; result = result.then(annotateTop(destId)).then((s) => { const allocation = s.memory.allocations[destId]; if (!allocation) return s; @@ -112,7 +118,11 @@ export function generate( ...s, instructions: [ ...s.instructions, - { mnemonic: "DUP1" as const, opcode: 0x80 }, + { + mnemonic: "DUP1" as const, + opcode: 0x80, + debug: spillDebug, + }, { mnemonic: "PUSH2" as const, opcode: 0x61, @@ -120,8 +130,13 @@ export function generate( (allocation.offset >> 8) & 0xff, allocation.offset & 0xff, ], + debug: spillDebug, + }, + { + mnemonic: "MSTORE" as const, + opcode: 0x52, + debug: spillDebug, }, - { mnemonic: "MSTORE" as const, opcode: 0x52 }, ], }; }); @@ -205,21 +220,45 @@ function generatePhi( /** * Initialize the free memory pointer at runtime - * Sets the value at 0x40 to the next available memory location after static allocations + * Sets the value at 0x40 to the next available memory location + * after static allocations */ function initializeMemory( nextStaticOffset: number, + sourceInfo?: { sourceId: string; loc: Ast.SourceLocation }, ): Transition { const { PUSHn, MSTORE } = operations; - return ( - pipe() - // Push the static offset value (the value to store) - .then(PUSHn(BigInt(nextStaticOffset)), { as: "value" }) - // Push the free memory pointer location (0x40) (the offset) - .then(PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER)), { as: "offset" }) - // Store the initial free pointer (expects [value, offset] on stack) - .then(MSTORE()) - .done() - ); + const debug = sourceInfo + ? { + context: { + gather: [ + { remark: "initialize free memory pointer" }, + { + code: { + source: { id: sourceInfo.sourceId }, + range: sourceInfo.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: "initialize free memory pointer", + } as Format.Program.Context, + }; + + return pipe() + .then(PUSHn(BigInt(nextStaticOffset), { debug }), { + as: "value", + }) + .then( + PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER), { + debug, + }), + { as: "offset" }, + ) + .then(MSTORE({ debug })) + .done(); } diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts index 70b31d8aa..9f397e7bc 100644 --- a/packages/bugc/src/evmgen/generation/function.ts +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -71,11 +71,28 @@ function generatePrologue( // Return PC is already in memory at 0x60 (stored by caller) // Pop and store each arg from argN down to arg0 - const prologueDebug = { - context: { - remark: `prologue: store ${params.length} parameter(s) to memory`, - }, - }; + const prologueDebug = + func.sourceId && func.loc + ? { + context: { + gather: [ + { + remark: `prologue: store ${params.length} parameter(s) to memory`, + }, + { + code: { + source: { id: func.sourceId }, + range: func.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: `prologue: store ${params.length} parameter(s) to memory`, + } as Format.Program.Context, + }; for (let i = params.length - 1; i >= 0; i--) { const param = params[i]; @@ -104,7 +121,11 @@ function generatePrologue( ...currentState, instructions: [ ...currentState.instructions, - { mnemonic: "MSTORE", opcode: 0x52 }, + { + mnemonic: "MSTORE", + opcode: 0x52, + debug: prologueDebug, + }, ], }; } @@ -113,11 +134,28 @@ function generatePrologue( // so nested function calls don't clobber it. const savedPcOffset = currentState.memory.savedReturnPcOffset; if (savedPcOffset !== undefined) { - const savePcDebug = { - context: { - remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, - }, - }; + const savePcDebug = + func.sourceId && func.loc + ? { + context: { + gather: [ + { + remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, + }, + { + code: { + source: { id: func.sourceId }, + range: func.loc, + }, + }, + ], + } as Format.Program.Context, + } + : { + context: { + remark: `prologue: save return PC to 0x${savedPcOffset.toString(16)}`, + } as Format.Program.Context, + }; const highByte = (savedPcOffset >> 8) & 0xff; const lowByte = savedPcOffset & 0xff; currentState = { @@ -130,13 +168,22 @@ function generatePrologue( immediates: [0x60], debug: savePcDebug, }, - { mnemonic: "MLOAD", opcode: 0x51 }, + { + mnemonic: "MLOAD", + opcode: 0x51, + debug: savePcDebug, + }, { mnemonic: "PUSH2", opcode: 0x61, immediates: [highByte, lowByte], + debug: savePcDebug, + }, + { + mnemonic: "MSTORE", + opcode: 0x52, + debug: savePcDebug, }, - { mnemonic: "MSTORE", opcode: 0x52 }, ], }; } diff --git a/packages/bugc/src/evmgen/generation/module.ts b/packages/bugc/src/evmgen/generation/module.ts index 92c306f8a..a0e26b229 100644 --- a/packages/bugc/src/evmgen/generation/module.ts +++ b/packages/bugc/src/evmgen/generation/module.ts @@ -105,9 +105,37 @@ export function generate( // Insert STOP between main and user functions to prevent // fall-through when the main function's last block omits // STOP (the isLastBlock optimization). + const stopGuardDebug = + module.main.sourceId && module.main.loc + ? { + context: { + gather: [ + { + remark: "guard: prevent fall-through into functions", + }, + { + code: { + source: { id: module.main.sourceId }, + range: module.main.loc, + }, + }, + ], + }, + } + : { + context: { + remark: "guard: prevent fall-through into functions", + }, + }; const stopGuard: Evm.Instruction[] = patchedFunctions.length > 0 - ? [{ mnemonic: "STOP" as const, opcode: 0x00 }] + ? [ + { + mnemonic: "STOP" as const, + opcode: 0x00, + debug: stopGuardDebug, + }, + ] : []; const stopGuardBytes: number[] = patchedFunctions.length > 0 ? [0x00] : []; @@ -243,13 +271,19 @@ function buildDeploymentInstructions( function deploymentTransition(runtimeOffset: bigint, runtimeLength: bigint) { const { PUSHn, CODECOPY, RETURN } = operations; + const debug = { + context: { + remark: "deployment: copy runtime bytecode and return", + }, + }; + return pipe() - .then(PUSHn(runtimeLength), { as: "size" }) - .then(PUSHn(runtimeOffset), { as: "offset" }) - .then(PUSHn(0n), { as: "destOffset" }) - .then(CODECOPY()) - .then(PUSHn(runtimeLength), { as: "size" }) - .then(PUSHn(0n), { as: "offset" }) - .then(RETURN()) + .then(PUSHn(runtimeLength, { debug }), { as: "size" }) + .then(PUSHn(runtimeOffset, { debug }), { as: "offset" }) + .then(PUSHn(0n, { debug }), { as: "destOffset" }) + .then(CODECOPY({ debug })) + .then(PUSHn(runtimeLength, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(RETURN({ debug })) .done(); } diff --git a/packages/bugc/src/ir/spec/function.ts b/packages/bugc/src/ir/spec/function.ts index df58c6ac8..49be241c5 100644 --- a/packages/bugc/src/ir/spec/function.ts +++ b/packages/bugc/src/ir/spec/function.ts @@ -17,6 +17,10 @@ export interface Function { blocks: Map; /** SSA variable metadata mapping temp IDs to original variables */ ssaVariables?: Map; + /** Source location of the function body */ + loc?: Ast.SourceLocation; + /** Source identifier for debug info */ + sourceId?: string; } export namespace Function { diff --git a/packages/bugc/src/irgen/generate/function.ts b/packages/bugc/src/irgen/generate/function.ts index 7e39aacfd..59e152768 100644 --- a/packages/bugc/src/irgen/generate/function.ts +++ b/packages/bugc/src/irgen/generate/function.ts @@ -46,12 +46,16 @@ export function* buildFunction( // Collect SSA variable metadata const ssaVariables = yield* Process.Functions.collectSsaMetadata(); + const module_ = yield* Process.Modules.current(); + const function_: Ir.Function = { name, parameters: params, entry: "entry", blocks, ssaVariables: ssaVariables.size > 0 ? ssaVariables : undefined, + loc: body.loc ?? undefined, + sourceId: module_.sourceId, }; return function_; From 9cf00f414262211b71941490c392e893b79c3d22 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 11 Mar 2026 05:21:17 -0400 Subject: [PATCH 18/18] Add call stack breadcrumb and call info panel (#189) * Add call stack breadcrumb and call info panel components Surface invoke/return/revert context information in the trace viewer: a breadcrumb showing the current call stack, and a panel showing call details with async-resolved pointer ref values. New components: CallStackDisplay, CallInfoPanel New utilities: extractCallInfoFromInstruction, buildCallStack, buildPcToInstructionMap New types: CallInfo, CallFrame, ResolvedCallInfo, ResolvedPointerRef * Integrate CallStackDisplay and CallInfoPanel into TraceViewer The components were exported from programs-react but never rendered in the web package's TraceViewer. Add them to the layout: call stack breadcrumb in the header, call info panel at the top of the right sidebar. * Add call stack breadcrumb and call info banner to TraceDrawer The deploy preview uses TraceDrawer (not TraceViewer) for the interactive trace playground. Add call context display directly to TraceDrawer: a breadcrumb bar showing nested call frames with clickable navigation, and a colored banner showing invoke/return/revert status at the current step. * Show always-visible call stack and fix duplicate frame bug - Call stack bar now always visible with "(top level)" empty state so users know the feature exists - Fix duplicate call stack frames: compiler emits invoke context on both the caller JUMP and callee entry JUMPDEST, so skip push if top frame already matches the same call - Applied fix to both TraceDrawer and programs-react utility --- .../src/components/CallInfoPanel.css | 85 ++++++++ .../src/components/CallInfoPanel.tsx | 131 ++++++++++++ .../src/components/CallStackDisplay.css | 46 +++++ .../src/components/CallStackDisplay.tsx | 62 ++++++ .../src/components/TraceContext.tsx | 127 ++++++++++++ .../programs-react/src/components/index.ts | 9 + packages/programs-react/src/index.ts | 12 ++ packages/programs-react/src/utils/index.ts | 4 + .../programs-react/src/utils/mockTrace.ts | 189 ++++++++++++++++++ .../theme/ProgramExample/CallInfoPanel.css | 85 ++++++++ .../theme/ProgramExample/CallStackDisplay.css | 46 +++++ .../src/theme/ProgramExample/TraceDrawer.css | 77 +++++++ .../src/theme/ProgramExample/TraceDrawer.tsx | 170 ++++++++++++++++ .../src/theme/ProgramExample/TraceViewer.tsx | 7 + .../web/src/theme/ProgramExample/index.ts | 10 + 15 files changed, 1060 insertions(+) create mode 100644 packages/programs-react/src/components/CallInfoPanel.css create mode 100644 packages/programs-react/src/components/CallInfoPanel.tsx create mode 100644 packages/programs-react/src/components/CallStackDisplay.css create mode 100644 packages/programs-react/src/components/CallStackDisplay.tsx create mode 100644 packages/web/src/theme/ProgramExample/CallInfoPanel.css create mode 100644 packages/web/src/theme/ProgramExample/CallStackDisplay.css diff --git a/packages/programs-react/src/components/CallInfoPanel.css b/packages/programs-react/src/components/CallInfoPanel.css new file mode 100644 index 000000000..1adfe5dcc --- /dev/null +++ b/packages/programs-react/src/components/CallInfoPanel.css @@ -0,0 +1,85 @@ +.call-info-panel { + font-size: 0.9em; +} + +.call-info-banner { + padding: 6px 10px; + border-radius: 4px; + font-weight: 500; + margin-bottom: 6px; +} + +.call-info-banner-invoke { + background: var(--programs-invoke-bg, #e8f4fd); + color: var(--programs-invoke-text, #0969da); + border-left: 3px solid var(--programs-invoke-accent, #0969da); +} + +.call-info-banner-return { + background: var(--programs-return-bg, #dafbe1); + color: var(--programs-return-text, #1a7f37); + border-left: 3px solid var(--programs-return-accent, #1a7f37); +} + +.call-info-banner-revert { + background: var(--programs-revert-bg, #ffebe9); + color: var(--programs-revert-text, #cf222e); + border-left: 3px solid var(--programs-revert-accent, #cf222e); +} + +.call-info-refs { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; +} + +.call-info-ref { + display: flex; + align-items: baseline; + gap: 6px; + padding: 2px 0; +} + +.call-info-ref-label { + font-weight: 500; + color: var(--programs-text-muted, #888); + min-width: 80px; + text-align: right; + flex-shrink: 0; +} + +.call-info-ref-resolved { + font-family: monospace; + font-size: 0.9em; + word-break: break-all; +} + +.call-info-ref-error { + color: var(--programs-error, #cf222e); + font-style: italic; +} + +.call-info-ref-pending { + color: var(--programs-text-muted, #888); + font-style: italic; +} + +.call-info-ref-pointer { + margin-top: 2px; +} + +.call-info-ref-pointer summary { + cursor: pointer; + color: var(--programs-text-muted, #888); + font-size: 0.85em; +} + +.call-info-ref-pointer-json { + font-size: 0.8em; + padding: 4px 8px; + background: var(--programs-bg-code, #f6f8fa); + border-radius: 3px; + overflow-x: auto; + max-height: 200px; +} diff --git a/packages/programs-react/src/components/CallInfoPanel.tsx b/packages/programs-react/src/components/CallInfoPanel.tsx new file mode 100644 index 000000000..b18e58d78 --- /dev/null +++ b/packages/programs-react/src/components/CallInfoPanel.tsx @@ -0,0 +1,131 @@ +/** + * Panel showing call context info for the current instruction. + * + * Displays a banner for invoke/return/revert events and + * lists resolved pointer ref values (arguments, return + * data, etc.). + */ + +import React from "react"; +import { + useTraceContext, + type ResolvedCallInfo, + type ResolvedPointerRef, +} from "./TraceContext.js"; + +// CSS is expected to be imported by the consuming application +// import "./CallInfoPanel.css"; + +export interface CallInfoPanelProps { + /** Whether to show raw pointer JSON */ + showPointers?: boolean; + /** Custom class name */ + className?: string; +} + +function formatBanner(info: ResolvedCallInfo): string { + const name = info.identifier || "(anonymous)"; + + if (info.kind === "invoke") { + const prefix = + info.callType === "external" + ? "Calling (external)" + : info.callType === "create" + ? "Creating" + : "Calling"; + return `${prefix} ${name}()`; + } + + if (info.kind === "return") { + return `Returned from ${name}()`; + } + + // revert + if (info.panic !== undefined) { + return `Reverted: panic 0x${info.panic.toString(16)}`; + } + return `Reverted in ${name}()`; +} + +function bannerClassName(kind: ResolvedCallInfo["kind"]): string { + if (kind === "invoke") { + return "call-info-banner-invoke"; + } + if (kind === "return") { + return "call-info-banner-return"; + } + return "call-info-banner-revert"; +} + +/** + * Shows call context info when the current instruction + * has an invoke, return, or revert context. + */ +export function CallInfoPanel({ + showPointers = false, + className = "", +}: CallInfoPanelProps): JSX.Element | null { + const { currentCallInfo } = useTraceContext(); + + if (!currentCallInfo) { + return null; + } + + return ( +
+
+ {formatBanner(currentCallInfo)} +
+ + {currentCallInfo.pointerRefs.length > 0 && ( +
+ {currentCallInfo.pointerRefs.map((ref) => ( + + ))} +
+ )} +
+ ); +} + +interface PointerRefItemProps { + ref_: ResolvedPointerRef; + showPointer: boolean; +} + +function PointerRefItem({ + ref_, + showPointer, +}: PointerRefItemProps): JSX.Element { + return ( +
+ {ref_.label}: + + {ref_.error ? ( + + Error: {ref_.error} + + ) : ref_.value !== undefined ? ( + {ref_.value} + ) : ( + (resolving...) + )} + + + {showPointer && !!ref_.pointer && ( +
+ Pointer +
+            {JSON.stringify(ref_.pointer, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/packages/programs-react/src/components/CallStackDisplay.css b/packages/programs-react/src/components/CallStackDisplay.css new file mode 100644 index 000000000..5d928c292 --- /dev/null +++ b/packages/programs-react/src/components/CallStackDisplay.css @@ -0,0 +1,46 @@ +.call-stack { + font-size: 0.85em; + padding: 4px 8px; +} + +.call-stack-empty { + color: var(--programs-text-muted, #888); +} + +.call-stack-breadcrumb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 2px; +} + +.call-stack-separator { + color: var(--programs-text-muted, #888); + user-select: none; +} + +.call-stack-frame { + display: inline-flex; + align-items: center; + background: none; + border: 1px solid transparent; + border-radius: 3px; + padding: 1px 4px; + cursor: pointer; + font-family: inherit; + font-size: inherit; + color: var(--programs-link, #0366d6); +} + +.call-stack-frame:hover { + background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05)); + border-color: var(--programs-border, #ddd); +} + +.call-stack-name { + font-weight: 500; +} + +.call-stack-parens { + color: var(--programs-text-muted, #888); +} diff --git a/packages/programs-react/src/components/CallStackDisplay.tsx b/packages/programs-react/src/components/CallStackDisplay.tsx new file mode 100644 index 000000000..fbc55c994 --- /dev/null +++ b/packages/programs-react/src/components/CallStackDisplay.tsx @@ -0,0 +1,62 @@ +/** + * Displays the current call stack as a breadcrumb trail. + */ + +import React from "react"; +import { useTraceContext } from "./TraceContext.js"; + +// CSS is expected to be imported by the consuming application +// import "./CallStackDisplay.css"; + +export interface CallStackDisplayProps { + /** Custom class name */ + className?: string; +} + +/** + * Renders the call stack as a breadcrumb. + * + * Shows function names separated by arrows, e.g.: + * main() -> transfer() -> _update() + */ +export function CallStackDisplay({ + className = "", +}: CallStackDisplayProps): JSX.Element { + const { callStack, jumpToStep } = useTraceContext(); + + if (callStack.length === 0) { + return ( +
+ (top level) +
+ ); + } + + return ( +
+
+ {callStack.map((frame, index) => ( + + {index > 0 && ( + {" -> "} + )} + + + ))} +
+
+ ); +} diff --git a/packages/programs-react/src/components/TraceContext.tsx b/packages/programs-react/src/components/TraceContext.tsx index c21874722..4a4ef9d4c 100644 --- a/packages/programs-react/src/components/TraceContext.tsx +++ b/packages/programs-react/src/components/TraceContext.tsx @@ -14,8 +14,12 @@ import type { Pointer, Program } from "@ethdebug/format"; import { dereference, Data } from "@ethdebug/pointers"; import { type TraceStep, + type CallInfo, + type CallFrame, extractVariablesFromInstruction, + extractCallInfoFromInstruction, buildPcToInstructionMap, + buildCallStack, } from "#utils/mockTrace"; import { traceStepToMachineState } from "#utils/traceState"; @@ -35,6 +39,36 @@ export interface ResolvedVariable { error?: string; } +/** + * A resolved pointer ref with its label and value. + */ +export interface ResolvedPointerRef { + /** Label for this pointer (e.g., "target", "arguments") */ + label: string; + /** The raw pointer */ + pointer: unknown; + /** Resolved hex value */ + value?: string; + /** Error if resolution failed */ + error?: string; +} + +/** + * Call info with resolved pointer values. + */ +export interface ResolvedCallInfo { + /** The kind of call event */ + kind: "invoke" | "return" | "revert"; + /** Function name */ + identifier?: string; + /** Call variant for invoke contexts */ + callType?: "internal" | "external" | "create"; + /** Panic code for revert contexts */ + panic?: number; + /** Resolved pointer refs */ + pointerRefs: ResolvedPointerRef[]; +} + /** * State provided by the Trace context. */ @@ -53,6 +87,10 @@ export interface TraceState { currentInstruction: Program.Instruction | undefined; /** Variables in scope at current step */ currentVariables: ResolvedVariable[]; + /** Call stack at current step */ + callStack: CallFrame[]; + /** Call info for current instruction (if any) */ + currentCallInfo: ResolvedCallInfo | undefined; /** Whether we're at the first step */ isAtStart: boolean; /** Whether we're at the last step */ @@ -241,6 +279,93 @@ export function TraceProvider({ }; }, [extractedVars, currentStep, shouldResolve, templates]); + // Build call stack by scanning instructions up to current step + const callStack = useMemo( + () => buildCallStack(trace, pcToInstruction, currentStepIndex), + [trace, pcToInstruction, currentStepIndex], + ); + + // Extract call info for current instruction (synchronous) + const extractedCallInfo = useMemo((): CallInfo | undefined => { + if (!currentInstruction) { + return undefined; + } + return extractCallInfoFromInstruction(currentInstruction); + }, [currentInstruction]); + + // Async call info pointer resolution + const [currentCallInfo, setCurrentCallInfo] = useState< + ResolvedCallInfo | undefined + >(undefined); + + useEffect(() => { + if (!extractedCallInfo) { + setCurrentCallInfo(undefined); + return; + } + + // Immediately show call info without resolved values + const initial: ResolvedCallInfo = { + kind: extractedCallInfo.kind, + identifier: extractedCallInfo.identifier, + callType: extractedCallInfo.callType, + panic: extractedCallInfo.panic, + pointerRefs: extractedCallInfo.pointerRefs.map((ref) => ({ + label: ref.label, + pointer: ref.pointer, + value: undefined, + error: undefined, + })), + }; + setCurrentCallInfo(initial); + + if (!shouldResolve || !currentStep) { + return; + } + + let cancelled = false; + const resolved = [...initial.pointerRefs]; + + const promises = extractedCallInfo.pointerRefs.map(async (ref, index) => { + try { + const value = await resolveVariableValue( + ref.pointer as Pointer, + currentStep, + templates, + ); + if (!cancelled) { + resolved[index] = { + ...resolved[index], + value, + }; + setCurrentCallInfo({ + ...initial, + pointerRefs: [...resolved], + }); + } + } catch (err) { + if (!cancelled) { + resolved[index] = { + ...resolved[index], + error: err instanceof Error ? err.message : String(err), + }; + setCurrentCallInfo({ + ...initial, + pointerRefs: [...resolved], + }); + } + } + }); + + Promise.all(promises).catch(() => { + // Individual errors already handled + }); + + return () => { + cancelled = true; + }; + }, [extractedCallInfo, currentStep, shouldResolve, templates]); + const stepForward = useCallback(() => { setCurrentStepIndex((prev) => Math.min(prev + 1, trace.length - 1)); }, [trace.length]); @@ -272,6 +397,8 @@ export function TraceProvider({ currentStep, currentInstruction, currentVariables, + callStack, + currentCallInfo, isAtStart: currentStepIndex === 0, isAtEnd: currentStepIndex >= trace.length - 1, stepForward, diff --git a/packages/programs-react/src/components/index.ts b/packages/programs-react/src/components/index.ts index 886c22d62..222acf051 100644 --- a/packages/programs-react/src/components/index.ts +++ b/packages/programs-react/src/components/index.ts @@ -22,6 +22,8 @@ export { type TraceState, type TraceProviderProps, type ResolvedVariable, + type ResolvedCallInfo, + type ResolvedPointerRef, } from "./TraceContext.js"; export { @@ -37,3 +39,10 @@ export { type VariableInspectorProps, type StackInspectorProps, } from "./VariableInspector.js"; + +export { + CallStackDisplay, + type CallStackDisplayProps, +} from "./CallStackDisplay.js"; + +export { CallInfoPanel, type CallInfoPanelProps } from "./CallInfoPanel.js"; diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts index 599e8ff1c..2fa17ad36 100644 --- a/packages/programs-react/src/index.ts +++ b/packages/programs-react/src/index.ts @@ -26,13 +26,19 @@ export { TraceProgress, VariableInspector, StackInspector, + CallStackDisplay, + CallInfoPanel, type TraceState, type TraceProviderProps, type ResolvedVariable, + type ResolvedCallInfo, + type ResolvedPointerRef, type TraceControlsProps, type TraceProgressProps, type VariableInspectorProps, type StackInspectorProps, + type CallStackDisplayProps, + type CallInfoPanelProps, } from "#components/index"; // Shiki utilities @@ -51,7 +57,11 @@ export { createMockTrace, findInstructionAtPc, extractVariablesFromInstruction, + extractCallInfoFromInstruction, buildPcToInstructionMap, + buildCallStack, + type CallInfo, + type CallFrame, type DynamicInstruction, type DynamicContext, type ContextThunk, @@ -67,3 +77,5 @@ export { // import "@ethdebug/programs-react/components/SourceContents.css"; // import "@ethdebug/programs-react/components/TraceControls.css"; // import "@ethdebug/programs-react/components/VariableInspector.css"; +// import "@ethdebug/programs-react/components/CallStackDisplay.css"; +// import "@ethdebug/programs-react/components/CallInfoPanel.css"; diff --git a/packages/programs-react/src/utils/index.ts b/packages/programs-react/src/utils/index.ts index 5f750e9a0..a79f07b08 100644 --- a/packages/programs-react/src/utils/index.ts +++ b/packages/programs-react/src/utils/index.ts @@ -17,9 +17,13 @@ export { createMockTrace, findInstructionAtPc, extractVariablesFromInstruction, + extractCallInfoFromInstruction, buildPcToInstructionMap, + buildCallStack, type TraceStep, type MockTraceSpec, + type CallInfo, + type CallFrame, } from "./mockTrace.js"; export { traceStepToMachineState } from "./traceState.js"; diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts index 6adef9e05..f3234e735 100644 --- a/packages/programs-react/src/utils/mockTrace.ts +++ b/packages/programs-react/src/utils/mockTrace.ts @@ -100,6 +100,195 @@ function extractVariablesFromContext( return []; } +/** + * Info about a function call context on an instruction. + */ +export interface CallInfo { + /** The kind of call event */ + kind: "invoke" | "return" | "revert"; + /** Function name (from Function.Identity) */ + identifier?: string; + /** Call variant for invoke contexts */ + callType?: "internal" | "external" | "create"; + /** Panic code for revert contexts */ + panic?: number; + /** Named pointer refs to resolve */ + pointerRefs: Array<{ + label: string; + pointer: unknown; + }>; +} + +/** + * Extract call info (invoke/return/revert) from an + * instruction's context tree. + */ +export function extractCallInfoFromInstruction( + instruction: Program.Instruction, +): CallInfo | undefined { + if (!instruction.context) { + return undefined; + } + return extractCallInfoFromContext(instruction.context); +} + +function extractCallInfoFromContext( + context: Program.Context, +): CallInfo | undefined { + // Use unknown intermediate to avoid strict type checks + // on the context union — we discriminate by key presence + const ctx = context as unknown as Record; + + if ("invoke" in ctx) { + const inv = ctx.invoke as Record; + const pointerRefs: CallInfo["pointerRefs"] = []; + + let callType: CallInfo["callType"]; + if ("jump" in inv) { + callType = "internal"; + collectPointerRef(pointerRefs, "target", inv.target); + collectPointerRef(pointerRefs, "arguments", inv.arguments); + } else if ("message" in inv) { + callType = "external"; + collectPointerRef(pointerRefs, "target", inv.target); + collectPointerRef(pointerRefs, "gas", inv.gas); + collectPointerRef(pointerRefs, "value", inv.value); + collectPointerRef(pointerRefs, "input", inv.input); + } else if ("create" in inv) { + callType = "create"; + collectPointerRef(pointerRefs, "value", inv.value); + collectPointerRef(pointerRefs, "salt", inv.salt); + collectPointerRef(pointerRefs, "input", inv.input); + } + + return { + kind: "invoke", + identifier: inv.identifier as string | undefined, + callType, + pointerRefs, + }; + } + + if ("return" in ctx) { + const ret = ctx.return as Record; + const pointerRefs: CallInfo["pointerRefs"] = []; + collectPointerRef(pointerRefs, "data", ret.data); + collectPointerRef(pointerRefs, "success", ret.success); + + return { + kind: "return", + identifier: ret.identifier as string | undefined, + pointerRefs, + }; + } + + if ("revert" in ctx) { + const rev = ctx.revert as Record; + const pointerRefs: CallInfo["pointerRefs"] = []; + collectPointerRef(pointerRefs, "reason", rev.reason); + + return { + kind: "revert", + identifier: rev.identifier as string | undefined, + panic: rev.panic as number | undefined, + pointerRefs, + }; + } + + // Walk gather/pick to find call info + if ("gather" in ctx && Array.isArray(ctx.gather)) { + for (const sub of ctx.gather as Program.Context[]) { + const info = extractCallInfoFromContext(sub); + if (info) { + return info; + } + } + } + + if ("pick" in ctx && Array.isArray(ctx.pick)) { + for (const sub of ctx.pick as Program.Context[]) { + const info = extractCallInfoFromContext(sub); + if (info) { + return info; + } + } + } + + return undefined; +} + +function collectPointerRef( + refs: CallInfo["pointerRefs"], + label: string, + value: unknown, +): void { + if (value && typeof value === "object" && "pointer" in value) { + refs.push({ label, pointer: (value as { pointer: unknown }).pointer }); + } +} + +/** + * A frame in the call stack. + */ +export interface CallFrame { + /** Function name */ + identifier?: string; + /** The step index where this call was invoked */ + stepIndex: number; + /** The call type */ + callType?: "internal" | "external" | "create"; +} + +/** + * Build a call stack by scanning instructions from + * step 0 to the given step index. + */ +export function buildCallStack( + trace: TraceStep[], + pcToInstruction: Map, + upToStep: number, +): CallFrame[] { + const stack: CallFrame[] = []; + + for (let i = 0; i <= upToStep && i < trace.length; i++) { + const step = trace[i]; + const instruction = pcToInstruction.get(step.pc); + if (!instruction) { + continue; + } + + const callInfo = extractCallInfoFromInstruction(instruction); + if (!callInfo) { + continue; + } + + if (callInfo.kind === "invoke") { + // The compiler emits invoke on both the caller JUMP and + // callee entry JUMPDEST. Skip if the top frame already + // matches this call. + const top = stack[stack.length - 1]; + if ( + !top || + top.identifier !== callInfo.identifier || + top.callType !== callInfo.callType + ) { + stack.push({ + identifier: callInfo.identifier, + stepIndex: i, + callType: callInfo.callType, + }); + } + } else if (callInfo.kind === "return" || callInfo.kind === "revert") { + // Pop the matching frame + if (stack.length > 0) { + stack.pop(); + } + } + } + + return stack; +} + /** * Build a map of PC to instruction for quick lookup. */ diff --git a/packages/web/src/theme/ProgramExample/CallInfoPanel.css b/packages/web/src/theme/ProgramExample/CallInfoPanel.css new file mode 100644 index 000000000..1adfe5dcc --- /dev/null +++ b/packages/web/src/theme/ProgramExample/CallInfoPanel.css @@ -0,0 +1,85 @@ +.call-info-panel { + font-size: 0.9em; +} + +.call-info-banner { + padding: 6px 10px; + border-radius: 4px; + font-weight: 500; + margin-bottom: 6px; +} + +.call-info-banner-invoke { + background: var(--programs-invoke-bg, #e8f4fd); + color: var(--programs-invoke-text, #0969da); + border-left: 3px solid var(--programs-invoke-accent, #0969da); +} + +.call-info-banner-return { + background: var(--programs-return-bg, #dafbe1); + color: var(--programs-return-text, #1a7f37); + border-left: 3px solid var(--programs-return-accent, #1a7f37); +} + +.call-info-banner-revert { + background: var(--programs-revert-bg, #ffebe9); + color: var(--programs-revert-text, #cf222e); + border-left: 3px solid var(--programs-revert-accent, #cf222e); +} + +.call-info-refs { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; +} + +.call-info-ref { + display: flex; + align-items: baseline; + gap: 6px; + padding: 2px 0; +} + +.call-info-ref-label { + font-weight: 500; + color: var(--programs-text-muted, #888); + min-width: 80px; + text-align: right; + flex-shrink: 0; +} + +.call-info-ref-resolved { + font-family: monospace; + font-size: 0.9em; + word-break: break-all; +} + +.call-info-ref-error { + color: var(--programs-error, #cf222e); + font-style: italic; +} + +.call-info-ref-pending { + color: var(--programs-text-muted, #888); + font-style: italic; +} + +.call-info-ref-pointer { + margin-top: 2px; +} + +.call-info-ref-pointer summary { + cursor: pointer; + color: var(--programs-text-muted, #888); + font-size: 0.85em; +} + +.call-info-ref-pointer-json { + font-size: 0.8em; + padding: 4px 8px; + background: var(--programs-bg-code, #f6f8fa); + border-radius: 3px; + overflow-x: auto; + max-height: 200px; +} diff --git a/packages/web/src/theme/ProgramExample/CallStackDisplay.css b/packages/web/src/theme/ProgramExample/CallStackDisplay.css new file mode 100644 index 000000000..5d928c292 --- /dev/null +++ b/packages/web/src/theme/ProgramExample/CallStackDisplay.css @@ -0,0 +1,46 @@ +.call-stack { + font-size: 0.85em; + padding: 4px 8px; +} + +.call-stack-empty { + color: var(--programs-text-muted, #888); +} + +.call-stack-breadcrumb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 2px; +} + +.call-stack-separator { + color: var(--programs-text-muted, #888); + user-select: none; +} + +.call-stack-frame { + display: inline-flex; + align-items: center; + background: none; + border: 1px solid transparent; + border-radius: 3px; + padding: 1px 4px; + cursor: pointer; + font-family: inherit; + font-size: inherit; + color: var(--programs-link, #0366d6); +} + +.call-stack-frame:hover { + background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05)); + border-color: var(--programs-border, #ddd); +} + +.call-stack-name { + font-weight: 500; +} + +.call-stack-parens { + color: var(--programs-text-muted, #888); +} diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 4da55e7be..9f36d24c0 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -127,6 +127,83 @@ text-align: center; } +/* Call stack breadcrumb */ +.call-stack-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 2px; + padding: 4px 12px; + font-size: 12px; + background: var(--ifm-background-surface-color); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + flex-shrink: 0; +} + +.call-stack-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ifm-color-content-secondary); + margin-right: 4px; +} + +.call-stack-toplevel { + color: var(--ifm-color-content-secondary); + font-style: italic; +} + +.call-stack-sep { + color: var(--ifm-color-content-secondary); + padding: 0 2px; + user-select: none; +} + +.call-stack-frame-btn { + background: none; + border: 1px solid transparent; + border-radius: 3px; + padding: 1px 4px; + cursor: pointer; + font-family: var(--ifm-font-family-monospace); + font-size: 12px; + font-weight: 500; + color: var(--ifm-color-primary); +} + +.call-stack-frame-btn:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-300); +} + +/* Call info banner */ +.call-info-bar { + padding: 4px 12px; + font-size: 12px; + font-weight: 500; + flex-shrink: 0; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.call-info-invoke { + background: var(--ifm-color-info-contrast-background); + color: var(--ifm-color-info-darkest); + border-left: 3px solid var(--ifm-color-info); +} + +.call-info-return { + background: var(--ifm-color-success-contrast-background); + color: var(--ifm-color-success-darkest); + border-left: 3px solid var(--ifm-color-success); +} + +.call-info-revert { + background: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-darkest); + border-left: 3px solid var(--ifm-color-danger); +} + /* Trace panels */ .trace-panels { display: grid; diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 72c69418b..0cae58678 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -85,6 +85,62 @@ function TraceDrawerContent(): JSX.Element { return extractVariables(instruction.debug.context); }, [trace, currentStep, pcToInstruction]); + // Extract call info from current instruction context + const currentCallInfo = useMemo(() => { + if (trace.length === 0 || currentStep >= trace.length) { + return undefined; + } + + const step = trace[currentStep]; + const instruction = pcToInstruction.get(step.pc); + if (!instruction?.debug?.context) return undefined; + + return extractCallInfo(instruction.debug.context); + }, [trace, currentStep, pcToInstruction]); + + // Build call stack by scanning invoke/return/revert up to + // current step + const callStack = useMemo(() => { + const frames: Array<{ + identifier?: string; + stepIndex: number; + callType?: string; + }> = []; + + for (let i = 0; i <= currentStep && i < trace.length; i++) { + const step = trace[i]; + const instruction = pcToInstruction.get(step.pc); + if (!instruction?.debug?.context) continue; + + const info = extractCallInfo(instruction.debug.context); + if (!info) continue; + + if (info.kind === "invoke") { + // The compiler emits invoke on both the caller + // JUMP and callee entry JUMPDEST. Skip if the + // top frame already matches this call. + const top = frames[frames.length - 1]; + if ( + !top || + top.identifier !== info.identifier || + top.callType !== info.callType + ) { + frames.push({ + identifier: info.identifier, + stepIndex: i, + callType: info.callType, + }); + } + } else if (info.kind === "return" || info.kind === "revert") { + if (frames.length > 0) { + frames.pop(); + } + } + } + + return frames; + }, [trace, currentStep, pcToInstruction]); + // Compile source and run trace in one shot. // Takes source directly to avoid stale-state issues. const compileAndTrace = useCallback(async (sourceCode: string) => { @@ -298,6 +354,36 @@ function TraceDrawerContent(): JSX.Element { +
+ Call Stack: + {callStack.length === 0 ? ( + (top level) + ) : ( + callStack.map((frame, i) => ( + + {i > 0 && ( + + )} + + + )) + )} +
+ + {currentCallInfo && ( +
+ {formatCallBanner(currentCallInfo)} +
+ )} +
Instructions
@@ -468,6 +554,90 @@ function VariablesDisplay({ variables }: VariablesDisplayProps): JSX.Element { ); } +/** + * Info about a call context (invoke/return/revert). + */ +interface CallInfoResult { + kind: "invoke" | "return" | "revert"; + identifier?: string; + callType?: string; +} + +/** + * Extract call info from an ethdebug format context object. + */ +function extractCallInfo(context: unknown): CallInfoResult | undefined { + if (!context || typeof context !== "object") { + return undefined; + } + + const ctx = context as Record; + + if ("invoke" in ctx && ctx.invoke) { + const inv = ctx.invoke as Record; + let callType: string | undefined; + if ("jump" in inv) callType = "internal"; + else if ("message" in inv) callType = "external"; + else if ("create" in inv) callType = "create"; + + return { + kind: "invoke", + identifier: inv.identifier as string | undefined, + callType, + }; + } + + if ("return" in ctx && ctx.return) { + const ret = ctx.return as Record; + return { + kind: "return", + identifier: ret.identifier as string | undefined, + }; + } + + if ("revert" in ctx && ctx.revert) { + const rev = ctx.revert as Record; + return { + kind: "revert", + identifier: rev.identifier as string | undefined, + }; + } + + // Walk gather/pick + if ("gather" in ctx && Array.isArray(ctx.gather)) { + for (const sub of ctx.gather) { + const info = extractCallInfo(sub); + if (info) return info; + } + } + + if ("pick" in ctx && Array.isArray(ctx.pick)) { + for (const sub of ctx.pick) { + const info = extractCallInfo(sub); + if (info) return info; + } + } + + return undefined; +} + +/** + * Format a call info banner string. + */ +function formatCallBanner(info: CallInfoResult): string { + const name = info.identifier || "(anonymous)"; + switch (info.kind) { + case "invoke": { + const prefix = info.callType === "create" ? "Creating" : "Calling"; + return `${prefix} ${name}()`; + } + case "return": + return `Returned from ${name}()`; + case "revert": + return `Reverted in ${name}()`; + } +} + /** * Extract variables from an ethdebug format context object. */ diff --git a/packages/web/src/theme/ProgramExample/TraceViewer.tsx b/packages/web/src/theme/ProgramExample/TraceViewer.tsx index 59ee577f0..c2348beb7 100644 --- a/packages/web/src/theme/ProgramExample/TraceViewer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceViewer.tsx @@ -12,6 +12,8 @@ import { TraceProgress, VariableInspector, StackInspector, + CallStackDisplay, + CallInfoPanel, useTraceContext, type TraceStep, } from "@ethdebug/programs-react"; @@ -20,6 +22,8 @@ import { import "./TraceViewer.css"; import "./TraceControls.css"; import "./VariableInspector.css"; +import "./CallStackDisplay.css"; +import "./CallInfoPanel.css"; export interface TraceViewerProps { /** The execution trace */ @@ -97,6 +101,7 @@ function TraceViewerContent({
+
@@ -118,6 +123,8 @@ function TraceViewerContent({
+ + {showVariables && (

Variables

diff --git a/packages/web/src/theme/ProgramExample/index.ts b/packages/web/src/theme/ProgramExample/index.ts index 47a0e1113..c930d1b4d 100644 --- a/packages/web/src/theme/ProgramExample/index.ts +++ b/packages/web/src/theme/ProgramExample/index.ts @@ -17,13 +17,19 @@ export { TraceProgress, VariableInspector, StackInspector, + CallStackDisplay, + CallInfoPanel, type TraceState, type TraceProviderProps, type ResolvedVariable, + type ResolvedCallInfo, + type ResolvedPointerRef, type TraceControlsProps, type TraceProgressProps, type VariableInspectorProps, type StackInspectorProps, + type CallStackDisplayProps, + type CallInfoPanelProps, } from "@ethdebug/programs-react"; // Also re-export utilities for convenience @@ -33,12 +39,16 @@ export { createMockTrace, findInstructionAtPc, extractVariablesFromInstruction, + extractCallInfoFromInstruction, buildPcToInstructionMap, + buildCallStack, type DynamicInstruction, type DynamicContext, type ContextThunk, type TraceStep, type MockTraceSpec, + type CallInfo, + type CallFrame, } from "@ethdebug/programs-react"; // Local Docusaurus-specific components