Skip to content

Commit 58091ca

Browse files
committed
Merge remote-tracking branch 'origin/main' into compiler-locals-emission
2 parents 928c824 + 7ee27fc commit 58091ca

8 files changed

Lines changed: 245 additions & 36 deletions

File tree

‎packages/bugc/src/evmgen/generation/block.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function generate<S extends Stack>(
6767
// Check if this is a call continuation
6868
let isContinuation = false;
6969
let calledFunction = "";
70+
let callSiteCode: Format.Program.Context.Code["code"] | undefined;
7071
if (func && predecessor) {
7172
const predBlock = func.blocks.get(predecessor);
7273
if (
@@ -75,6 +76,15 @@ export function generate<S extends Stack>(
7576
) {
7677
isContinuation = true;
7778
calledFunction = predBlock.terminator.function;
79+
// The continuation resumes at the call expression, so
80+
// carry the call site's source range onto the return
81+
// context (disjoint keys — flat composition).
82+
const ctx = predBlock.terminator.operationDebug?.context as
83+
| Record<string, unknown>
84+
| undefined;
85+
if (ctx && "code" in ctx) {
86+
callSiteCode = ctx.code as Format.Program.Context.Code["code"];
87+
}
7888
}
7989
}
8090

@@ -105,7 +115,10 @@ export function generate<S extends Stack>(
105115
},
106116
};
107117
const continuationDebug = {
108-
context: returnCtx as Format.Program.Context,
118+
context: {
119+
...returnCtx,
120+
...(callSiteCode ? { code: callSiteCode } : {}),
121+
} as Format.Program.Context,
109122
};
110123
result = result.then(JUMPDEST({ debug: continuationDebug }));
111124
} else {

‎packages/bugc/src/evmgen/generation/control-flow/terminator.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ import { type Transition, operations, pipe } from "#evmgen/operations";
1010

1111
import { valueId, loadValue } from "../values/index.js";
1212

13+
/**
14+
* Extract the `code` source-range context from an instruction or
15+
* terminator debug, if present.
16+
*/
17+
function codeContext(
18+
debug: { context?: Format.Program.Context } | undefined,
19+
): Format.Program.Context.Code["code"] | undefined {
20+
const ctx = debug?.context as Record<string, unknown> | undefined;
21+
if (ctx && "code" in ctx) {
22+
return ctx.code as Format.Program.Context.Code["code"];
23+
}
24+
return undefined;
25+
}
26+
1327
/**
1428
* Generate code for a block terminator
1529
*/
@@ -249,8 +263,15 @@ export function generateCallTerminator<S extends Stack>(
249263
},
250264
},
251265
};
266+
// Compose the call-site source range (from the call op's debug)
267+
// flat alongside the invoke, so the caller JUMP maps back to the
268+
// call expression. invoke and code are disjoint keys.
269+
const callSiteCode = codeContext(debug);
252270
const invokeContext = {
253-
context: invoke as Format.Program.Context,
271+
context: {
272+
...invoke,
273+
...(callSiteCode ? { code: callSiteCode } : {}),
274+
} as Format.Program.Context,
254275
};
255276

256277
currentState = {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* The call-site source range is carried on the invoke/return
3+
* contexts of a function call, so a debugger stepping onto the
4+
* caller JUMP or the continuation JUMPDEST maps back to the call
5+
* expression (not just the callee's definition).
6+
*/
7+
import { describe, it, expect } from "vitest";
8+
9+
import { compile } from "#compiler";
10+
import { executeProgram } from "#test/evm/behavioral";
11+
import { Program } from "@ethdebug/format";
12+
import type * as Format from "@ethdebug/format";
13+
14+
const { Context } = Program;
15+
16+
const source = `name Adder;
17+
define {
18+
function add(a: uint256, b: uint256) -> uint256 { return a + b; };
19+
}
20+
storage { [0] r: uint256; }
21+
create { r = 0; }
22+
code { r = add(3, 4); }`;
23+
24+
async function runtimeProgram(): Promise<Format.Program> {
25+
const result = await compile({
26+
to: "bytecode",
27+
source,
28+
optimizer: { level: 0 },
29+
});
30+
if (!result.success) throw new Error("compile failed");
31+
return result.value.bytecode.runtimeProgram;
32+
}
33+
34+
function hasCodeRange(ctx: Record<string, unknown>): boolean {
35+
const code = ctx.code as { range?: { offset?: unknown; length?: unknown } };
36+
return (
37+
!!code &&
38+
typeof code.range?.offset === "number" &&
39+
typeof code.range?.length === "number"
40+
);
41+
}
42+
43+
describe("call-site source range on invoke/return contexts", () => {
44+
it("the caller JUMP invoke context carries a call-site code range", async () => {
45+
const program = await runtimeProgram();
46+
const invokeJump = program.instructions.find(
47+
(i) =>
48+
i.operation?.mnemonic === "JUMP" &&
49+
i.context !== undefined &&
50+
Context.isInvoke(i.context),
51+
);
52+
expect(invokeJump, "invoke JUMP").toBeDefined();
53+
expect(hasCodeRange(invokeJump!.context as Record<string, unknown>)).toBe(
54+
true,
55+
);
56+
});
57+
58+
it("the continuation JUMPDEST return context carries a call-site code range", async () => {
59+
const program = await runtimeProgram();
60+
const contJumpdest = program.instructions.find(
61+
(i) =>
62+
i.operation?.mnemonic === "JUMPDEST" &&
63+
i.context !== undefined &&
64+
Context.isReturn(i.context),
65+
);
66+
expect(contJumpdest, "continuation JUMPDEST").toBeDefined();
67+
expect(hasCodeRange(contJumpdest!.context as Record<string, unknown>)).toBe(
68+
true,
69+
);
70+
});
71+
72+
it("still identifies the invoke by name and keeps behavior", async () => {
73+
const program = await runtimeProgram();
74+
const invokeJump = program.instructions.find(
75+
(i) =>
76+
i.operation?.mnemonic === "JUMP" &&
77+
i.context !== undefined &&
78+
Context.isInvoke(i.context),
79+
);
80+
const ctx = invokeJump!.context as Format.Program.Context.Invoke;
81+
expect(ctx.invoke.identifier).toBe("add");
82+
83+
const res = await executeProgram(source, {
84+
calldata: "",
85+
optimizationLevel: 0,
86+
});
87+
expect(res.callSuccess).toBe(true);
88+
expect(await res.getStorage(0n)).toBe(7n);
89+
});
90+
});

‎packages/bugc/src/ir/utils/debug.ts‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type * as Ir from "#ir";
22
import type * as Format from "@ethdebug/format";
33

44
/**
5-
* Combine multiple debug contexts into a single context.
6-
* If multiple contexts have source information, creates a pick context.
5+
* Combine multiple debug contexts into a single context that asserts
6+
* all of them. Contexts with disjoint keys compose flat as siblings;
7+
* contexts with a colliding key (e.g. two `code` ranges) are gathered.
78
* Filters out empty contexts.
89
*/
910
export function combineDebugContexts(
@@ -19,10 +20,14 @@ export function combineDebugContexts(
1920
return {};
2021
}
2122

22-
// Flatten pick contexts - if a context has a pick, extract its children
23+
// Flatten enclosing gather (and legacy pick) wrappers so their
24+
// children re-compose here rather than nesting. `gather` children
25+
// all apply simultaneously, which is exactly what combining means.
2326
const flattenedContexts: Format.Program.Context[] = [];
2427
for (const context of contexts) {
25-
if ("pick" in context && Array.isArray(context.pick)) {
28+
if ("gather" in context && Array.isArray(context.gather)) {
29+
flattenedContexts.push(...context.gather);
30+
} else if ("pick" in context && Array.isArray(context.pick)) {
2631
flattenedContexts.push(...context.pick);
2732
} else {
2833
flattenedContexts.push(context);
@@ -66,10 +71,23 @@ export function combineDebugContexts(
6671
return { context: uniqueContexts[0] };
6772
}
6873

69-
// Multiple unique contexts - create a pick context
74+
// Multiple distinct contexts that all apply simultaneously. If
75+
// their discriminator keys are disjoint, compose them flat as
76+
// sibling keys on one object; if any key collides (e.g. two
77+
// `code` ranges), they cannot be siblings, so gather them. Both
78+
// mean "all apply" — pick would wrongly mean "choose one".
79+
const keys = uniqueContexts.flatMap((context) => Object.keys(context));
80+
const hasCollision = new Set(keys).size !== keys.length;
81+
82+
if (!hasCollision) {
83+
return {
84+
context: Object.assign({}, ...uniqueContexts) as Format.Program.Context,
85+
};
86+
}
87+
7088
return {
7189
context: {
72-
pick: uniqueContexts,
90+
gather: uniqueContexts,
7391
} as Format.Program.Context,
7492
};
7593
}
@@ -145,7 +163,8 @@ export function extractContexts(
145163
* - The operation's debug context
146164
* - Debug contexts from all operands/fields
147165
*
148-
* Uses pick contexts to preserve all distinct debug information.
166+
* Distinct contexts are gathered (or composed flat when their keys
167+
* are disjoint) so all applicable debug information is preserved.
149168
*/
150169
export function combineSubInstructionContexts(
151170
operationDebug: Ir.Instruction.Debug | Ir.Block.Debug | undefined,

‎packages/web/docs/core-schemas/programs/tracing-examples.ts‎

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
import { exampleSources, stripTestAnnotations } from "@ethdebug/bugc/examples";
2+
3+
/**
4+
* SimpleFunctions, referenced from the canonical bugc examples — the same
5+
* `basic/functions.bug` the BUG playground dropdown sources — with bugc's
6+
* inline test annotations stripped for display. Kept DRY rather than
7+
* copying the `.bug` source here.
8+
*/
9+
export const simpleFunctions = stripTestAnnotations(
10+
exampleSources["basic/functions.bug"],
11+
);
12+
113
export const counterIncrement = `name Counter;
214
315
storage {
@@ -52,26 +64,6 @@ code {
5264
b = b + 1;
5365
}`;
5466

55-
export const functionCallAndReturn = `name Adder;
56-
57-
define {
58-
function add(a: uint256, b: uint256) -> uint256 {
59-
return a + b;
60-
};
61-
}
62-
63-
storage {
64-
[0] result: uint256;
65-
}
66-
67-
create {
68-
result = 0;
69-
}
70-
71-
code {
72-
result = add(3, 4);
73-
}`;
74-
7567
export const mutualRecursion = `name EvenOdd;
7668
7769
define {

‎packages/web/docs/explore/trace-playground.mdx‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ sidebar_label: "Step through an execution trace"
77
import { TracePlayground, TraceExample } from "@theme/ProgramExample";
88
import {
99
counterIncrement,
10-
functionCallAndReturn,
10+
simpleFunctions,
1111
tailRecursiveFactorial,
1212
inlineDemo,
1313
} from "../core-schemas/programs/tracing-examples";
@@ -39,14 +39,16 @@ the context attached to each instruction.
3939

4040
## A function call
4141

42-
Real programs call functions, and the debugger follows them. Watch the
43-
call stack push a frame on the JUMP into `add` (an **invoke** context)
44-
and pop it on the JUMP back (a **return** context).
42+
Real programs call functions, and the debugger follows them.
43+
`SimpleFunctions` calls an `add` helper directly, then calls `addThree` —
44+
which itself calls `add` twice — so the call stack nests two frames deep.
45+
Each JUMP into a function pushes a frame (an **invoke** context); each
46+
JUMP back pops it (a **return** context).
4547

4648
<TraceExample
47-
title="Function call and return"
48-
description="Calls an internal add function and stores the result"
49-
source={functionCallAndReturn}
49+
title="Nested function calls"
50+
description="Calls add directly, then addThree — which calls add twice — storing 60"
51+
source={simpleFunctions}
5052
/>
5153

5254
For the exact shape of invoke, return, and revert contexts, see the

‎packages/web/src/theme/ProgramExample/TraceDrawer.css‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,29 @@
526526
font-style: italic;
527527
}
528528

529+
/* Function-call boundary badges: same pill shape as the transform tags,
530+
but in the tracer's invoke=info / return=success colors (matching the
531+
call-info banner and bytecode-view context badges). */
532+
.opcode-call-tag {
533+
margin-left: auto;
534+
padding: 0 5px;
535+
border-radius: 8px;
536+
font-size: 10px;
537+
font-weight: 600;
538+
white-space: nowrap;
539+
color: var(--ifm-color-content);
540+
}
541+
542+
.opcode-call-invoke {
543+
background: var(--ifm-color-info-contrast-background);
544+
border: 1px solid var(--ifm-color-info);
545+
}
546+
547+
.opcode-call-return {
548+
background: var(--ifm-color-success-contrast-background);
549+
border: 1px solid var(--ifm-color-success);
550+
}
551+
529552
/* Instruction object footer - user-resizable, scrolls internally */
530553
.instruction-object-panel {
531554
position: relative;

0 commit comments

Comments
 (0)