Skip to content
Merged
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
14 changes: 14 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ support. Changes to the specification itself are tracked in the root

- The `%` operator, with the precedence of `*` and `/`. It compiles to
the EVM's unsigned `MOD`, so `x % 0` is `0`, as `x / 0` is ([#321]).
- Each instruction has a `variables` context that lists the local
variables in scope (the innermost declaration of each name, which hides
a storage variable of that name), with a type, and a pointer to the
exact bytes that hold the value, in memory or on the stack, where the
value is there. For a dynamic array, `string` or `bytes` local, the
pointer reads the local's word and goes on to the length and the
elements or data it refers to. At optimization levels 1 to 3, a local
whose current value the optimizer folded to a constant or removed is
listed without a pointer, and an inlined function's locals are listed
in its inlined code ([#328]).

### Changed

Expand Down Expand Up @@ -108,6 +118,9 @@ support. Changes to the specification itself are tracked in the root
- At optimization level 3, two writes in a row to the same field of a
packed struct now leave the second value. Before, read/write merging
combined the two values with `or` ([#337]).
- A local initialized with a string literal (`let s = "hello";`) now
holds the string. Before, it held an empty allocation, so `s.length`
was `0` ([#328]).

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

Expand Down Expand Up @@ -149,6 +162,7 @@ First publication.
[#320]: https://github.com/ethdebug/format/pull/320
[#321]: https://github.com/ethdebug/format/pull/321
[#327]: https://github.com/ethdebug/format/pull/327
[#328]: https://github.com/ethdebug/format/pull/328
[#330]: https://github.com/ethdebug/format/pull/330
[#332]: https://github.com/ethdebug/format/pull/332
[#333]: https://github.com/ethdebug/format/pull/333
Expand Down
82 changes: 82 additions & 0 deletions packages/bugc/src/evmgen/debug/local-variables.bytecode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Debug info must not change the generated code: compiling every
* example at every level gives the same bytecode with local-variable
* tracking (recording origins, enriching instructions) as without it.
*/
import { it, expect, vi } from "vitest";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { bytesToHex } from "ethereum-cryptography/utils";

const examples = join(__dirname, "../../../examples");

function sources(dir = examples): Array<[string, string]> {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) return sources(path);
return entry.name.endsWith(".bug")
? [[path.slice(examples.length + 1), readFileSync(path, "utf8")]]
: [];
}) as Array<[string, string]>;
}

type Compile = (typeof import("#compiler"))["compile"];

async function bytecode(compile: Compile): Promise<Record<string, string>> {
const out: Record<string, string> = {};
for (const [name, source] of sources()) {
for (const level of [0, 1, 2, 3] as const) {
let result: string;
try {
const compiled = await compile({
to: "bytecode",
source,
optimizer: { level },
});
result = compiled.success
? bytesToHex(compiled.value.bytecode.runtime) +
"/" +
bytesToHex(compiled.value.bytecode.create ?? new Uint8Array())
: "fails";
} catch {
result = "throws";
}
out[`${name} O${level}`] = result;
}
}
return out;
}

it("local-variable tracking does not change any bytecode", async () => {
const tracked = await bytecode((await import("#compiler")).compile);

vi.resetModules();
vi.doMock(
"../../../dist/src/evmgen/debug/local-variables.js",
async (load) => ({
...(await load<object>()),
enrich: (module: unknown) => module,
}),
);
vi.doMock("../../../dist/src/optimizer/origins.js", () => ({
recordOrigins: () => {},
}));
const { compile } = await import("#compiler");

// Without tracking, no instruction lists a local
const probe = await compile({
to: "bytecode",
source: `name P; storage { [0] r: uint256; } create {}
code { let x = 7; r = x; }`,
optimizer: { level: 0 },
});
if (!probe.success) throw new Error("compile failed");
const listed = probe.value.bytecode.runtimeProgram.instructions.some((i) =>
(
i.context as { variables?: Array<{ identifier: string }> }
)?.variables?.some((v) => v.identifier === "x"),
);
expect(listed).toBe(false);

expect(await bytecode(compile)).toEqual(tracked);
}, 120_000);
Loading
Loading