Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1e6d5ca
bugc: flatten TCO back-edge JUMP context (#214)
gnidan Jun 18, 2026
3518f49
format: make invoke.target optional for internal calls (#213)
gnidan Jun 18, 2026
5210e38
format: add transform context for compiler optimizations (#212)
gnidan Jul 2, 2026
314b42c
bugc: emit tailcall transform context on TCO back-edge (#217)
gnidan Jul 2, 2026
3d5b461
programs-react: render tailcall transform + fix TCO call stack (#218)
gnidan Jul 2, 2026
185e831
docs: add tail-call optimization tracing example (#219)
gnidan Jul 2, 2026
25dd2ca
docs: link first frame mention to its spec page (#220)
gnidan Jul 2, 2026
13a5843
bugc: preserve function loc/sourceId through optimizer (#223)
gnidan Jul 2, 2026
9bb47f8
web: tracer drawer — opt-level selector, tailcall render, right-colum…
gnidan Jul 2, 2026
3f2fa5e
docs: match TCO example prose to the Opt selector (O0-O3) (#221)
gnidan Jul 2, 2026
e6fb3f1
bugc: emit fold transform on constant folding (#225)
gnidan Jul 2, 2026
4a46330
bugc: emit coalesce transform on read-write merging (#228)
gnidan Jul 2, 2026
f5f66a9
format: spec the inlined-call virtual activation contract (#229)
gnidan Jul 2, 2026
22bf0c8
bugc: add function inlining pass with inline transform (L2) (#230)
gnidan Jul 2, 2026
c03679c
format: clarify inline activation reconstruction (push/pop vs members…
gnidan Jul 3, 2026
410362d
web/programs-react: reconstruct inline virtual activations in tracer …
gnidan Jul 3, 2026
ea80a41
bugc: bracket inlined invoke/return to boundary ops in evmgen (#235)
gnidan Jul 3, 2026
1e90935
format: define context `name` as a referenceable identifier (activati…
gnidan Jul 3, 2026
20a7654
programs-react: adopt locked inline-activation contract (#237)
gnidan Jul 3, 2026
0ca6be7
programs-react: lock inline reconstruction against bracketed + smeare…
gnidan Jul 3, 2026
b2d9e38
format: add Name context type and guard to TS types (#238)
gnidan Jul 3, 2026
ee6c1ee
format: correct activation correlation — `activation` id inside invok…
gnidan Jul 3, 2026
f31be26
format: add activation correlation id to invoke/return/revert types (…
gnidan Jul 3, 2026
a30fd19
web/bugc-react: add curated example selector to the docs BugPlaygroun…
gnidan Jul 3, 2026
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
44 changes: 44 additions & 0 deletions packages/bugc-react/src/examples.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* The curated example set must stay small, clean, and — above all —
* compilable: these strings are shipped verbatim into the docs
* playground editor, so a typo would surface as a broken example.
*/
import { describe, it, expect } from "vitest";
import { compile } from "@ethdebug/bugc";
import { bugExamples } from "./examples.js";

describe("bugExamples", () => {
it("is the curated trio (counter, functions, arrays)", () => {
expect(bugExamples.map((e) => e.name)).toEqual([
"counter",
"functions",
"arrays",
]);
});

it("gives every example a display name and non-empty source", () => {
for (const ex of bugExamples) {
expect(ex.displayName.trim().length).toBeGreaterThan(0);
expect(ex.code.trim().length).toBeGreaterThan(0);
}
});

it("carries no leftover @test annotation blocks", () => {
for (const ex of bugExamples) {
expect(ex.code).not.toContain("@test");
}
});

// Each curated source must compile cleanly to bytecode — this is
// the guard that matters, since these ship straight to the editor.
for (const ex of bugExamples) {
it(`compiles ${ex.name} to bytecode without errors`, async () => {
const result = await compile({
to: "bytecode",
source: ex.code,
optimizer: { level: 0 },
});
expect(result.success).toBe(true);
});
}
});
102 changes: 102 additions & 0 deletions packages/bugc-react/src/examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* A small, curated set of BUG programs for the docs playground's
* example selector. Kept intentionally short and clean — these
* strings are shown verbatim in the editor, so they carry no
* `/*@test*\/`-style behavioral annotations (unlike the raw
* `packages/bugc/examples` sources they're distilled from).
*
* The set is validated by examples.test.ts: every entry must
* compile to bytecode without errors.
*/

/** A named, display-labelled BUG source for the example selector. */
export interface BugExample {
/** Stable identifier (used as the <option> value). */
name: string;
/** Human-readable label shown in the dropdown. */
displayName: string;
/** The BUG source. */
code: string;
}

const counter = `name Counter;

storage {
[0] count: uint256;
[1] threshold: uint256;
}

create {
count = 0;
threshold = 100;
}

code {
// Increment the counter, wrapping when it hits the threshold
count = count + 1;
if (count >= threshold) {
count = 0;
}
}
`;

const functions = `name Functions;

define {
// A leaf helper
function add(a: uint256, b: uint256) -> uint256 {
return a + b;
};

// Calls add() twice — a nested function call
function addThree(x: uint256, y: uint256, z: uint256) -> uint256 {
let partial = add(x, y);
return add(partial, z);
};
}

storage {
[0] result: uint256;
}

code {
result = addThree(10, 20, 30);
}
`;

const arrays = `name Arrays;

storage {
[0] numbers: array<uint256, 5>;
[1] sum: uint256;
[2] max: uint256;
}

code {
// Fill the array with squares: 0, 1, 4, 9, 16
for (let i = 0; i < 5; i = i + 1) {
numbers[i] = i * i;
}

// Sum every element
sum = 0;
for (let i = 0; i < 5; i = i + 1) {
sum = sum + numbers[i];
}

// Track the largest element
max = numbers[0];
for (let i = 1; i < 5; i = i + 1) {
if (numbers[i] > max) {
max = numbers[i];
}
}
}
`;

/** The curated example set, in display order. */
export const bugExamples: BugExample[] = [
{ name: "counter", displayName: "Counter", code: counter },
{ name: "functions", displayName: "Function calls", code: functions },
{ name: "arrays", displayName: "Arrays & loops", code: arrays },
];
3 changes: 3 additions & 0 deletions packages/bugc-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export {
type EditorSourceRange,
} from "#components/Editor";

// Curated example programs (for playground selectors)
export { bugExamples, type BugExample } from "./examples.js";

// Utilities
export {
// Debug utilities
Expand Down
13 changes: 13 additions & 0 deletions packages/bugc-react/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true,
environment: "jsdom",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "dist/", "**/*.test.ts", "**/*.test.tsx"],
},
},
});
6 changes: 4 additions & 2 deletions packages/bugc/src/evmgen/call-contexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ code {
expect(typeof invoke.declaration!.range!.length).toBe("number");

// Target should be a code pointer (not stack)
expect(Pointer.Region.isCode(call.target.pointer)).toBe(true);
expect(call.target).toBeDefined();
expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true);

// Caller JUMP should NOT have argument pointers
// (args live on the callee JUMPDEST invoke context)
Expand Down Expand Up @@ -156,7 +157,8 @@ code {
expect(call.identifier).toBe("add");

// Target should be a code pointer
expect(Pointer.Region.isCode(call.target.pointer)).toBe(true);
expect(call.target).toBeDefined();
expect(Pointer.Region.isCode(call.target!.pointer)).toBe(true);

// Should have argument pointers matching
// function parameters
Expand Down
27 changes: 25 additions & 2 deletions packages/bugc/src/evmgen/generation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Memory } from "#evmgen/analysis";
import { calculateSize } from "#evmgen/serialize";

import * as Instruction from "./instruction.js";
import { bracketActivation, carriesActivation } from "./bracket-activation.js";
import { loadValue } from "./values/index.js";
import {
generateTerminator,
Expand Down Expand Up @@ -161,9 +162,31 @@ export function generate<S extends Stack>(
// the runtime predecessor differs from the layout-order
// predecessor.

// Process regular instructions
// Process regular instructions. Invoke/return activation
// discriminators must be bracketed to the first/last emitted op
// of the instruction (see bracket-activation.ts); everything else
// (source mapping, variables, transform markers) rides all ops.
for (const inst of block.instructions) {
result = result.then(Instruction.generate(inst));
const gen = Instruction.generate(inst);
const operationCtx = inst.operationDebug?.context;
if (
!carriesActivation(operationCtx, "invoke") &&
!carriesActivation(operationCtx, "return")
) {
result = result.then(gen);
continue;
}
result = result.peek((state, builder) => {
const start = state.instructions.length;
return builder.then(gen).then((s) => ({
...s,
instructions: bracketActivation(
s.instructions,
start,
operationCtx,
),
}));
});
}

// Emit phi copies for successor blocks before the
Expand Down
164 changes: 164 additions & 0 deletions packages/bugc/src/evmgen/generation/bracket-activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* Bracket invoke/return activation discriminators onto the boundary
* ops of an IR instruction's emitted op-run.
*
* A single IR instruction lowers to N EVM micro-ops, and the generic
* lowering attaches that instruction's whole `operationDebug` (source
* mapping, variables, transform markers, AND any invoke/return
* discriminators) to every one of those ops. That is correct for
* source/variable/transform context — a debugger wants all N ops
* mapped to the instruction — but WRONG for invoke/return: those are
* positional activation boundaries. An `invoke` marks a single push
* point; a `return` a single pop point. Broadcasting them across the
* whole op-run makes a push/pop reconstruction see every op as both a
* push and a pop.
*
* This module de-smears: for the ops emitted by one instruction, the
* `invoke` discriminator is kept on only the FIRST op, `return` on only
* the LAST op, and stripped from the interior. The `transform`
* membership markers (and source/variables) stay on every op.
*
* It is a general evmgen invariant, not inline-specific: it is a no-op
* for real calls (whose invoke/return already ride single-op JUMP /
* JUMPDEST terminators) and fires only when invoke/return happen to
* ride a multi-op instruction — which today is inlined virtual
* activations.
*/
import type * as Format from "@ethdebug/format";
import type * as Evm from "#evm";

type Ctx = Format.Program.Context;
type Activation = "invoke" | "return";

function isPick(ctx: Ctx): ctx is Ctx & { pick: Ctx[] } {
return (
typeof ctx === "object" &&
ctx !== null &&
"pick" in ctx &&
Array.isArray((ctx as { pick: unknown }).pick)
);
}

function isGather(ctx: Ctx): ctx is Ctx & { gather: Ctx[] } {
return (
typeof ctx === "object" &&
ctx !== null &&
"gather" in ctx &&
Array.isArray((ctx as { gather: unknown }).gather)
);
}

/** Whether ctx carries the given activation key anywhere, reaching
* into pick/gather composites. */
export function carriesActivation(
ctx: Ctx | undefined,
key: Activation,
): boolean {
if (!ctx || typeof ctx !== "object") return false;
if (isPick(ctx)) return ctx.pick.some((c) => carriesActivation(c, key));
if (isGather(ctx)) return ctx.gather.some((c) => carriesActivation(c, key));
return key in ctx;
}

/** The first activation value found for the given key, reaching into
* pick/gather composites. */
function findActivation(ctx: Ctx | undefined, key: Activation): unknown {
if (!ctx || typeof ctx !== "object") return undefined;
if (isPick(ctx)) {
for (const c of ctx.pick) {
const v = findActivation(c, key);
if (v !== undefined) return v;
}
return undefined;
}
if (isGather(ctx)) {
for (const c of ctx.gather) {
const v = findActivation(c, key);
if (v !== undefined) return v;
}
return undefined;
}
return (ctx as Record<string, unknown>)[key];
}

/** Remove invoke and return discriminators anywhere in ctx, reaching
* into pick/gather composites. Returns undefined if nothing remains. */
export function stripActivation(ctx: Ctx | undefined): Ctx | undefined {
if (!ctx || typeof ctx !== "object") return ctx;
if (isPick(ctx)) {
const kids = ctx.pick
.map(stripActivation)
.filter((c): c is Ctx => c !== undefined);
if (kids.length === 0) return undefined;
if (kids.length === 1) return kids[0];
return { pick: kids } as Ctx;
}
if (isGather(ctx)) {
const kids = ctx.gather
.map(stripActivation)
.filter((c): c is Ctx => c !== undefined);
if (kids.length === 0) return undefined;
if (kids.length === 1) return kids[0];
return { gather: kids } as Ctx;
}
const rest = { ...(ctx as Record<string, unknown>) };
delete rest.invoke;
delete rest.return;
return Object.keys(rest).length > 0 ? (rest as Ctx) : undefined;
}

/** Attach an activation discriminator, composing it as a flat sibling
* key on a leaf context (per the flat-composition convention), or
* appending it to a pick/gather composite. */
function attachActivation(
ctx: Ctx | undefined,
key: Activation,
value: unknown,
): Ctx {
const marker = { [key]: value } as Ctx;
if (!ctx || typeof ctx !== "object") return marker;
if (isPick(ctx)) return { pick: [...ctx.pick, marker] } as Ctx;
if (isGather(ctx)) return { gather: [...ctx.gather, marker] } as Ctx;
return { ...(ctx as Record<string, unknown>), [key]: value } as Ctx;
}

/**
* Rewrite the ops emitted by one IR instruction (the tail slice
* `instructions[start..]`) so invoke rides only the first op and
* return only the last op, using the discriminators found on the
* instruction's `operationDebug` context. No-op unless that context
* carries invoke and/or return, so it never touches ordinary code.
*/
export function bracketActivation(
instructions: Evm.Instruction[],
start: number,
operationCtx: Ctx | undefined,
): Evm.Instruction[] {
const end = instructions.length; // exclusive
if (end <= start) return instructions;

const hasInvoke = carriesActivation(operationCtx, "invoke");
const hasReturn = carriesActivation(operationCtx, "return");
if (!hasInvoke && !hasReturn) return instructions;

const invokeValue = hasInvoke
? findActivation(operationCtx, "invoke")
: undefined;
const returnValue = hasReturn
? findActivation(operationCtx, "return")
: undefined;

const out = instructions.slice();
for (let i = start; i < end; i++) {
const op = out[i];
let ctx = stripActivation(op.debug?.context);
if (hasInvoke && i === start) {
ctx = attachActivation(ctx, "invoke", invokeValue);
}
if (hasReturn && i === end - 1) {
ctx = attachActivation(ctx, "return", returnValue);
}
out[i] = { ...op, debug: { ...op.debug, context: ctx } };
}
return out;
}
Loading
Loading