Skip to content

Commit e6fb3f1

Browse files
authored
bugc: emit fold transform on constant folding (#225)
* bugc: emit fold transform on constant folding Marks constant-folded values with transform:["fold"] so debuggers can show that a value is a compile-time-evaluated constant rather than source the user wrote. Adds Ir.Utils.addTransform(debug, ...ids), which composes transform markers as a flat sibling key on a debug context (per the flat- composition convention) and appends to any existing transform array — so an instruction touched by multiple passes accumulates the multiset (e.g. ["fold","coalesce"]). ConstantFoldingStep applies it to folded binary and hash results. Although a folded const instruction is typically dissolved by constant propagation + DCE, ConstantPropagationStep already carries the folded const's operationDebug into the consuming instruction, so the marker survives to the emitted bytecode's runtimeInstructions (the tracer widget's path) at levels 1-3. Adds an end-to-end test asserting the fold marker reaches the bytecode at levels 1/2/3 and is absent at level 0. * bugc: route tailcall transform through addTransform helper Refactor buildTailCallJumpOptions to emit its transform:["tailcall"] marker via Ir.Utils.addTransform instead of a hand-written key, so all transform emission (fold/tailcall/coalesce/...) routes through the one helper. Behavior is identical — the back-edge JUMP still carries return + invoke + transform:["tailcall"] — but composition stays consistent (e.g. a folded-then-tailcall instruction would accumulate the multiset) and there's no divergent hand-rolled site. Existing #217 tailcall tests pass unchanged.
1 parent 3f2fa5e commit e6fb3f1

4 files changed

Lines changed: 142 additions & 7 deletions

File tree

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as Format from "@ethdebug/format";
22
import type * as Ir from "#ir";
3+
import { Utils as IrUtils } from "#ir";
34
import type * as Evm from "#evm";
45
import type { Stack } from "#evm";
56
import type { State } from "#evmgen/state";
@@ -440,7 +441,7 @@ function generateReturnEpilogue<S extends Stack>(
440441
* resolved later by patchInvokeTarget.
441442
*/
442443
function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
443-
debug: { context: Format.Program.Context };
444+
debug: Ir.Instruction.Debug;
444445
} {
445446
const declaration =
446447
tailCall.declarationLoc && tailCall.declarationSourceId
@@ -451,8 +452,7 @@ function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
451452
: undefined;
452453

453454
const combined: Format.Program.Context.Return &
454-
Format.Program.Context.Invoke &
455-
Format.Program.Context.Transform = {
455+
Format.Program.Context.Invoke = {
456456
return: {
457457
identifier: tailCall.function,
458458
...(declaration ? { declaration } : {}),
@@ -469,10 +469,18 @@ function buildTailCallJumpOptions(tailCall: Ir.Block.TailCall): {
469469
},
470470
},
471471
},
472-
transform: ["tailcall"],
473472
};
474473

475-
return { debug: { context: combined as Format.Program.Context } };
474+
// Route through the shared helper so all transform emission
475+
// (fold/tailcall/coalesce/...) composes consistently: the
476+
// `transform` marker becomes a flat sibling key appended to
477+
// any existing transform array.
478+
return {
479+
debug: IrUtils.addTransform(
480+
{ context: combined as Format.Program.Context },
481+
"tailcall",
482+
),
483+
};
476484
}
477485

478486
/** PUSH an integer as the smallest PUSHn. */
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Verifies that optimizer `transform` markers are emitted onto
3+
* the resulting bytecode's debug contexts, on the same
4+
* `runtimeInstructions` path the docs tracer widget consumes.
5+
*
6+
* Level 1 constant folding attaches `transform: ["fold"]` to the
7+
* folded value's instruction; a debugger can then show that a
8+
* PUSH is a compile-time-evaluated constant rather than source
9+
* the user wrote.
10+
*/
11+
import { describe, it, expect } from "vitest";
12+
13+
import { compile } from "#compiler";
14+
import type * as Format from "@ethdebug/format";
15+
import { Program } from "@ethdebug/format";
16+
17+
const { Context } = Program;
18+
19+
type OptLevel = 0 | 1 | 2 | 3;
20+
21+
async function compileBytecode(source: string, level: OptLevel) {
22+
const result = await compile({
23+
to: "bytecode",
24+
source,
25+
optimizer: { level },
26+
});
27+
if (!result.success) {
28+
const errors = result.messages.error ?? [];
29+
throw new Error(
30+
`Compilation failed at level ${level}:\n` +
31+
errors
32+
.map((e: { message?: string }) => e.message ?? String(e))
33+
.join("\n"),
34+
);
35+
}
36+
return result.value.bytecode;
37+
}
38+
39+
/** Flatten a context into leaves, unwrapping gather/pick. */
40+
function leaves(ctx: Format.Program.Context): Format.Program.Context[] {
41+
if (Context.isGather(ctx)) return ctx.gather.flatMap(leaves);
42+
if ("pick" in ctx && Array.isArray((ctx as { pick: unknown[] }).pick)) {
43+
return (ctx as { pick: Format.Program.Context[] }).pick.flatMap(leaves);
44+
}
45+
return [ctx];
46+
}
47+
48+
/**
49+
* Count instructions in the widget-path array whose context
50+
* (at top level or in any leaf) carries a transform containing
51+
* the given identifier.
52+
*/
53+
function countTransform(
54+
instructions: { debug?: { context?: Format.Program.Context } }[],
55+
id: string,
56+
): number {
57+
let count = 0;
58+
for (const instr of instructions) {
59+
const ctx = instr.debug?.context;
60+
if (!ctx) continue;
61+
const hit = [ctx, ...leaves(ctx)].some(
62+
(c) => Context.isTransform(c) && c.transform.includes(id),
63+
);
64+
if (hit) count += 1;
65+
}
66+
return count;
67+
}
68+
69+
describe("optimizer emits fold transform contexts", () => {
70+
// `2 + 3` and `4 * 5` fold to constants at level 1.
71+
const source = `name Fold;
72+
73+
storage { [0] r: uint256; }
74+
create { r = 0; }
75+
code { r = (2 + 3) * (4 * 5); }`;
76+
77+
it("emits no fold transform at level 0", async () => {
78+
const bc = await compileBytecode(source, 0);
79+
expect(countTransform(bc.runtimeInstructions, "fold")).toBe(0);
80+
});
81+
82+
for (const level of [1, 2, 3] as const) {
83+
it(`emits fold transform at level ${level}`, async () => {
84+
const bc = await compileBytecode(source, level);
85+
expect(countTransform(bc.runtimeInstructions, "fold")).toBeGreaterThan(0);
86+
});
87+
}
88+
});

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,36 @@ export function preserveSubInstructionDebug(
350350
...additionalContexts.map((c) => ({ context: c })),
351351
);
352352
}
353+
354+
/**
355+
* Add one or more `transform` optimization markers to a debug
356+
* context, composing them as a flat sibling key alongside any
357+
* existing context discriminators (per the flat-composition
358+
* convention: gather is only for same-key collisions).
359+
*
360+
* Markers are appended to any existing `transform` array on the
361+
* context, so an instruction touched by multiple passes
362+
* accumulates the multiset — e.g. a folded value later merged
363+
* yields `transform: ["fold", "coalesce"]`.
364+
*/
365+
export function addTransform(
366+
debug: Ir.Instruction.Debug | undefined,
367+
...ids: Format.Program.Context.Transform.Identifier[]
368+
): Ir.Instruction.Debug {
369+
const existing = debug?.context;
370+
371+
const prior: Format.Program.Context.Transform.Identifier[] =
372+
existing &&
373+
"transform" in existing &&
374+
Array.isArray((existing as Format.Program.Context.Transform).transform)
375+
? (existing as Format.Program.Context.Transform).transform
376+
: [];
377+
378+
const transform = [...prior, ...ids];
379+
380+
if (!existing) {
381+
return { context: { transform } };
382+
}
383+
384+
return { context: { ...existing, transform } };
385+
}

‎packages/bugc/src/optimizer/steps/constant-folding.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ export class ConstantFoldingStep extends BaseOptimizationStep {
134134
value: result,
135135
type: this.getResultType(inst.op, typeof result),
136136
dest: inst.dest,
137-
operationDebug: Ir.Utils.preserveDebug(inst),
137+
operationDebug: Ir.Utils.addTransform(
138+
Ir.Utils.preserveDebug(inst),
139+
"fold",
140+
),
138141
};
139142
}
140143

@@ -266,7 +269,10 @@ export class ConstantFoldingStep extends BaseOptimizationStep {
266269
value: hashValue,
267270
type: Ir.Type.Scalar.bytes32,
268271
dest: inst.dest,
269-
operationDebug: Ir.Utils.preserveDebug(inst),
272+
operationDebug: Ir.Utils.addTransform(
273+
Ir.Utils.preserveDebug(inst),
274+
"fold",
275+
),
270276
};
271277
}
272278

0 commit comments

Comments
 (0)