Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/programs-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ root [`CHANGELOG.md`](../../CHANGELOG.md).

## Unreleased

### Changed

- The trace shows the value of a local variable as one value decoded by its
type, read from the region named after the variable, instead of the hex of
all the regions of its pointer ([#270]).

## 0.1.0-preview.1 — 2026-10-03

Updated `@ethdebug/format` to `0.1.0-draft.1` and `@ethdebug/pointers` to
Expand Down Expand Up @@ -35,6 +41,7 @@ The version scheme changed: prerelease versions are now `preview.<n>`, and

First publication.

[#270]: https://github.com/ethdebug/format/pull/270
[#298]: https://github.com/ethdebug/format/pull/298
[#299]: https://github.com/ethdebug/format/pull/299
[#300]: https://github.com/ethdebug/format/pull/300
1 change: 1 addition & 0 deletions packages/programs-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@ethdebug/pointers": "^0.1.0-preview.1",
"@shikijs/langs": "^2.5.0",
"@shikijs/themes": "^2.5.0",
"ethereum-cryptography": "^2.2.1",
"shiki": "^2.5.0"
},
"devDependencies": {
Expand Down
29 changes: 25 additions & 4 deletions packages/programs-react/src/components/TraceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
buildCallStack,
} from "#utils/mockTrace";
import { traceStepToMachineState } from "#utils/traceState";
import { decodeValue } from "#utils/decodeValue";
import { effectiveContextForStep } from "#utils/effectiveContext";

/**
Expand Down Expand Up @@ -230,15 +231,32 @@ async function resolveVariableValue(
pointer: Pointer,
step: TraceStep,
templates: Pointer.Templates,
identifier?: string,
type?: unknown,
): Promise<string> {
const state = traceStepToMachineState(step);
const cursor = await dereference(pointer, {
state,
templates,
});
const view = await cursor.view(state);
const decode = (data: Data): string => decodeValue(data, type);

// Prefer the value region named after the variable. A memory-homed local's
// pointer is a group that also carries frame-scaffolding regions, so
// joining every region would surface the frame pointer alongside the value.
// `regions.lookup` gives the last concrete region generated with a given
// name — for a scalar that is exactly the value region. Decode it into a
// readable value (uint -> decimal, address -> checksummed, …) by type.
if (identifier) {
const region = view.regions.lookup[identifier];
if (region) {
return decode(await view.read(region));
}
}

// Collect values from all regions
// Fallback: no identifier-named region — read every region (previous
// behavior), covering pointers whose value region isn't identifier-named.
const values: Data[] = [];
for (const region of view.regions) {
const data = await view.read(region);
Expand All @@ -249,12 +267,12 @@ async function resolveVariableValue(
return "0x";
}

// Single region: return its hex value
// Single region: decode its value
if (values.length === 1) {
return values[0].toHex();
return decode(values[0]);
}

// Multiple regions: concatenate hex values
// Multiple regions (composite) — not a scalar; concatenate raw hex.
return values.map((d) => d.toHex()).join(", ");
}

Expand Down Expand Up @@ -357,6 +375,8 @@ export function TraceProvider({
v.pointer as Pointer,
currentStep,
templates,
v.identifier,
v.type,
);
if (!cancelled) {
resolved[index] = {
Expand Down Expand Up @@ -460,6 +480,7 @@ export function TraceProvider({
ptr as Pointer,
step,
templates,
names?.[i],
);
args[i] = { ...args[i], value };
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/programs-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export {
traceStepToMachineState,
effectiveContextForStep,
type EffectiveContextInput,
decodeValue,
type TraceStep,
type MockTraceSpec,
} from "#utils/index";
Expand Down
42 changes: 42 additions & 0 deletions packages/programs-react/src/utils/decodeValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect } from "vitest";
import { Data } from "@ethdebug/pointers";

import { decodeValue } from "./decodeValue.js";

const bytes = (hex: string) => Data.fromHex(hex);

describe("decodeValue", () => {
it("decodes uint to decimal", () => {
expect(decodeValue(bytes("0xc8"), { kind: "uint", bits: 8 })).toBe("200");
expect(
decodeValue(bytes(`0x${"ff".repeat(32)}`), { kind: "uint", bits: 256 }),
).toBe((2n ** 256n - 1n).toString(10));
});

it("decodes int as two's complement over the region width", () => {
const int8 = { kind: "int", bits: 8 };
expect(decodeValue(bytes("0x05"), int8)).toBe("5");
expect(decodeValue(bytes("0xfb"), int8)).toBe("-5");
expect(decodeValue(bytes("0x80"), int8)).toBe("-128");
});

it("decodes bool", () => {
expect(decodeValue(bytes("0x00"), { kind: "bool" })).toBe("false");
expect(decodeValue(bytes("0x01"), { kind: "bool" })).toBe("true");
});

it("decodes address with an EIP-55 checksum", () => {
expect(
decodeValue(bytes("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"), {
kind: "address",
}),
).toBe("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
});

it("shows other kinds, or no type, as hex", () => {
const data = bytes("0xabcdef12");
expect(decodeValue(data, { kind: "bytes", size: 4 })).toBe("0xabcdef12");
expect(decodeValue(data, { kind: "string" })).toBe("0xabcdef12");
expect(decodeValue(data, undefined)).toBe("0xabcdef12");
});
});
46 changes: 46 additions & 0 deletions packages/programs-react/src/utils/decodeValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Display-only value decoding for the trace views. It turns the bytes of
* a local's value region into a readable string, by the local's type.
*
* bugc's pointers name exactly a scalar's bytes (an address is 20 bytes,
* a bool is 1 byte), so this decodes the region as given. Kinds other
* than uint, int, bool and address show as hex.
*/
import { keccak256 } from "ethereum-cryptography/keccak";
import type { Data } from "@ethdebug/pointers";

export function decodeValue(data: Data, type: unknown): string {
const kind = (type as { kind?: unknown } | undefined)?.kind;
switch (kind) {
case "uint":
return data.asUint().toString(10);
case "int":
return decodeInt(data);
case "bool":
return data.asUint() === 0n ? "false" : "true";
case "address":
return checksum(data.resizeTo(20).toHex().slice(2));
default:
return data.toHex();
}
}

/** Two's complement over the region's byte width. */
function decodeInt(data: Data): string {
if (data.length === 0) return "0";
const bits = BigInt(data.length * 8);
const raw = data.asUint();
const value = raw >= 1n << (bits - 1n) ? raw - (1n << bits) : raw;
return value.toString(10);
}

/** EIP-55 checksum of a lowercase hex address (no `0x`). */
function checksum(lowerHex: string): string {
const hash = keccak256(new TextEncoder().encode(lowerHex));
let out = "0x";
for (let i = 0; i < lowerHex.length; i++) {
const nibble = i % 2 === 0 ? hash[i >> 1] >> 4 : hash[i >> 1] & 0x0f;
out += nibble >= 8 ? lowerHex[i].toUpperCase() : lowerHex[i];
}
return out;
}
2 changes: 2 additions & 0 deletions packages/programs-react/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export {
effectiveContextForStep,
type EffectiveContextInput,
} from "./effectiveContext.js";

export { decodeValue } from "./decodeValue.js";
18 changes: 18 additions & 0 deletions packages/web/docs/core-schemas/programs/tracing-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,21 @@ create {
code {
sumOfSquares = square(a) + square(b);
}`;

export const addressIdentity = `name Identity;

define {
function idn(owner: address) -> address {
return owner;
};
}

storage {
[0] stored: address;
}

create {}

code {
stored = idn(0x52908400098527886e0f7030069857d2e4169ee7);
}`;
15 changes: 15 additions & 0 deletions packages/web/docs/explore/trace-playground.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TracePlayground, TraceExample } from "@theme/ProgramExample";
import {
counterIncrement,
simpleFunctions,
addressIdentity,
tailRecursiveFactorial,
inlineDemo,
} from "../core-schemas/programs/tracing-examples";
Expand Down Expand Up @@ -55,6 +56,20 @@ For the exact shape of invoke, return, and revert contexts, see the
[function call spec](/spec/program/context/function) and the
[tracing reference](/docs/core-schemas/programs/tracing).

## Values, decoded by type

The drawer reads each in-scope variable from memory and decodes it by its
static type — a `uint256` reads as a decimal, an `address` as its
EIP-55 checksummed `0x…` form, not raw bytes. `Identity` passes an
address through a function; step into `idn` and watch `owner` render as a
checksummed address in the variables panel.

<TraceExample
title="Address round-trip"
description="Passes an address through idn(owner); owner decodes to its checksummed form"
source={addressIdentity}
/>

## Watching the optimizer

Compilers rewrite code as they optimize, and **transform** contexts
Expand Down
36 changes: 31 additions & 5 deletions packages/web/src/theme/ProgramExample/TraceDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Executor, createTraceCollector, type TraceStep } from "@ethdebug/evm";
import { dereference, Data, type Machine } from "@ethdebug/pointers";
import {
buildCallStack,
decodeValue,
effectiveContextForStep,
extractCallInfoFromInstruction,
extractTransformFromInstruction,
Expand Down Expand Up @@ -349,7 +350,7 @@ function TraceDrawerContent(): JSX.Element {

const resolvePromises = ptrs.map(async (ptr, i) => {
try {
const value = await resolvePointer(ptr, state);
const value = await resolvePointer(ptr, state, names?.[i]);
args[i] = { ...args[i], value };
} catch (err) {
args[i] = {
Expand Down Expand Up @@ -400,7 +401,10 @@ function TraceDrawerContent(): JSX.Element {
currentVariables.map(async (v) => {
if (!v.pointer) return;
try {
next.set(v.identifier, await resolvePointer(v.pointer, state));
next.set(
v.identifier,
await resolvePointer(v.pointer, state, v.identifier, v.rawType),
);
} catch {
// leave unresolved
}
Expand Down Expand Up @@ -1093,7 +1097,10 @@ function formatBigInt(value: bigint): string {
// Variable type extracted from debug context
interface Variable {
identifier: string;
/** Formatted type string, for display. */
type?: string;
/** Raw ethdebug type specifier, for value decoding. */
rawType?: unknown;
pointer?: unknown;
}

Expand All @@ -1114,7 +1121,7 @@ function VariablesDisplay({
<div key={i} className="variable-item">
<span className="variable-name">{variable.identifier}</span>
{value !== undefined && (
<code className="variable-value">{formatAsDecimal(value)}</code>
<code className="variable-value">{value}</code>
)}
{variable.type && (
<span className="variable-type">{variable.type}</span>
Expand Down Expand Up @@ -1223,6 +1230,7 @@ function extractVariables(context: unknown): Variable[] {
variables.push({
identifier: String(variable.identifier),
type: variable.type ? formatType(variable.type) : undefined,
rawType: variable.type,
pointer: variable.pointer,
});
}
Expand Down Expand Up @@ -1410,25 +1418,43 @@ function traceStepToState(
}

/**
* Resolve a single pointer against a machine state.
* Resolve a single pointer against a machine state, decoding the value
* region into a readable string when the variable's type is known.
*/
async function resolvePointer(
pointer: unknown,
state: Machine.State,
identifier?: string,
type?: unknown,
): Promise<string> {
const cursor = await dereference(
pointer as Parameters<typeof dereference>[0],
{ state, templates: {} },
);
const view = await cursor.view(state);
const decode = (data: Data): string => decodeValue(data, type);

// Prefer the value region named after the variable. A memory-homed local's
// pointer is a group that also carries frame-scaffolding regions, so
// joining every region would surface the frame pointer alongside the value.
if (identifier) {
const region = view.regions.lookup[identifier];
if (region) {
return decode(await view.read(region));
}
}

// Fallback: no identifier-named region — read every region (covers
// pointers whose value region isn't identifier-named, e.g. anonymous
// single-region storage locals).
const values: Data[] = [];
for (const region of view.regions) {
values.push(await view.read(region));
}

if (values.length === 0) return "0x";
if (values.length === 1) return values[0].toHex();
if (values.length === 1) return decode(values[0]);
// Composite (multiple regions) — not a scalar; show raw hex words.
return values.map((d) => d.toHex()).join(", ");
}

Expand Down
Loading