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
8 changes: 8 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ support. Changes to the specification itself are tracked in the root
- The `advanced/token-registry.bug` example is no longer marked work
in progress. It compiles, and runs the same, at every optimization
level ([#395]).
- The pointer of a local of bytes or a string in calldata names each
half of the local's word as its own region: `x-offset`, the word's
high 16 bytes, and `x-length`, its low 16 bytes. The `calldata`
region `x-data` reads its offset and length from those regions.
Before, the pointer named the whole word `x`, and `x-data` took its
offset and length as `~quotient` and `~remainder` of that word by
2^128. The bytes the pointer reads are the same ([#398]).

### Fixed

Expand Down Expand Up @@ -588,3 +595,4 @@ First publication.
[#391]: https://github.com/ethdebug/format/pull/391
[#395]: https://github.com/ethdebug/format/pull/395
[#397]: https://github.com/ethdebug/format/pull/397
[#398]: https://github.com/ethdebug/format/pull/398
53 changes: 53 additions & 0 deletions packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import { describe, it, expect } from "vitest";

import { Program } from "@ethdebug/format";
import type * as Format from "@ethdebug/format";
import { dereference } from "@ethdebug/pointers";
import { executeProgram } from "#test/evm/behavioral";
import {
type Level,
Expand Down Expand Up @@ -1646,6 +1648,57 @@ code {
});
});

describe.each(levels)("a calldata reference's word at O%i", (level) => {
// Its word holds the bytes' offset in its high 16 bytes and their
// length in its low 16 bytes: each half is a region of its own, which
// the bytes' region reads, with no arithmetic on the whole word
it.each([
setMotdProgram(""),
namedProgram(motd),
parameterProgram(motd, "record"),
// On the stack, at every level
parameterProgram(motd, "record", "bytes"),
parameterProgram(motd, "join"),
])("$name", async ({ source, calldata }) => {
const run = await traceLocals(source, level, calldata);
let checked = 0;
for (let k = 0; k + 1 < run.steps.length; k++) {
const context = run.instructionAt(run.steps[k])?.context;
for (const entry of localsOf(context)) {
const pointer = entry.pointer as Format.Pointer | undefined;
const json = JSON.stringify(pointer);
if (!pointer || !json.includes('"calldata"')) continue;
expect(json).not.toMatch(/~quotient|~remainder/);

const name = entry.identifier as string;
const state = run.stateAt(k + 1);
const view = await (await dereference(pointer, { state })).view(state);
const [offset, length] = ["offset", "length"].map((part) => {
const regions = view.regions.named(`${name}-${part}`);
expect(regions).toHaveLength(1);
return regions[0];
});
expect(length.location).toEqual(offset.location);
expect(offset.length?.asUint()).toEqual(16n);
expect(length.length?.asUint()).toEqual(16n);
if (offset.location === "stack" && length.location === "stack") {
expect(length.slot.asUint()).toEqual(offset.slot.asUint());
expect(offset.offset?.asUint()).toEqual(0n);
expect(length.offset?.asUint()).toEqual(16n);
} else {
expect(offset.location).toEqual("memory");
expect(length.offset!.asUint()).toEqual(
offset.offset!.asUint() + 16n,
);
}
expect(view.regions.named(name)).toHaveLength(0);
checked++;
}
}
expect(checked).toBeGreaterThan(0);
});
});

describe("variables before a function's first statement", () => {
// Contexts are postconditions: the step after an instruction shows
// its context. Every step lists the storage variable; from the
Expand Down
95 changes: 60 additions & 35 deletions packages/bugc/src/evmgen/debug/local-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,43 @@ type StackLocal = NonNullable<Ir.Instruction.Debug["stackLocals"]>[number];

type DefSite = Ir.Utils.DefSite;

/** A region in a local's word (see `memoryPointer`) */
type Part = StackLocal["parts"][number];

/** A scalar of `size` bytes, or a whole word, as one part */
function wholePart(name: string, size = 32): Part[] {
return [{ name, offset: 32 - size, length: size }];
}

/**
* A value is homed by a full-word `MSTORE` of its stack word, which
* writes the word big-endian. bugc keeps every scalar right-aligned on
* the stack (`bytesN` included), so a scalar of `size` bytes is the
* word's low-order `size` bytes: `32 - size` bytes into the word,
* `size` bytes long. The same holds for a stack slot (segment offsets
* count from the word's most significant byte).
*
* A word may hold more than one part (bytes in calldata: their offset
* and length); each part is a region of its own.
*/
function memoryPointer(
identifier: string,
size: number,
parts: Part[],
allocation: Memory.Allocation,
frameSize: number | undefined,
): Format.Pointer {
const offset = Number(allocation.offset) + 32 - size;
const word = Number(allocation.offset);

if (frameSize === undefined) {
// No call frame (main/create): static memory offset.
return { name: identifier, location: "memory", offset, length: size };
const regions = parts.map(
({ name, offset, length }): Format.Pointer => ({
name,
location: "memory",
offset: word + offset,
length,
}),
);
return regions.length === 1 ? regions[0] : { group: regions };
}

// Frame-relative: mem[ mem[FRAME_POINTER] + delta ].
Expand All @@ -88,27 +106,28 @@ function memoryPointer(
offset: Memory.regions.FRAME_POINTER,
length: 32,
},
{
name: identifier,
location: "memory",
offset: { "~sum": [{ "~read": FRAME }, offset] },
length: size,
},
...parts.map(
({ name, offset, length }): Format.Pointer => ({
name,
location: "memory",
offset: { "~sum": [{ "~read": FRAME }, word + offset] },
length,
}),
),
],
};
}

function stackPointer(
identifier: string,
slot: number,
size: number,
): Format.Pointer {
return {
name: identifier,
location: "stack",
slot,
...(size < 32 ? { offset: 32 - size, length: size } : {}),
};
function stackPointer(parts: Part[], slot: number): Format.Pointer {
const regions = parts.map(
({ name, offset, length }): Format.Pointer => ({
name,
location: "stack",
slot,
...(length < 32 ? { offset, length } : {}),
}),
);
return regions.length === 1 ? regions[0] : { group: regions };
}

/**
Expand Down Expand Up @@ -240,17 +259,25 @@ function dataPointers(
}

/**
* A pointer to the bytes in calldata a local's word refers to: the
* word holds their offset in its high 128 bits and their length in its
* low 128 bits. The region is named `x-data`.
* The parts of a local's word that refers to bytes in calldata: their
* offset in its high 16 bytes, their length in its low 16 bytes. The
* regions are named `x-offset` and `x-length`.
*/
function calldataParts(name: string): Part[] {
return [
{ name: `${name}-offset`, offset: 0, length: 16 },
{ name: `${name}-length`, offset: 16, length: 16 },
];
}

/** A pointer to the bytes in calldata, named `x-data`, that the parts
* of a local's word (`calldataParts`) give */
function calldataPointer(name: string): Format.Pointer {
const half = `0x1${"0".repeat(32)}`; // 2^128
return {
name: `${name}-data`,
location: "calldata",
offset: { "~quotient": [{ "~read": name }, half] },
length: { "~remainder": [{ "~read": name }, half] },
offset: { "~read": `${name}-offset` },
length: { "~read": `${name}-length` },
};
}

Expand Down Expand Up @@ -436,10 +463,7 @@ export function withStackLocals<I extends { debug?: Ir.Instruction.Debug }>(
if (slot < 0) return entry;
return {
...entry,
pointer: withData(
stackPointer(local.identifier, slot, local.size),
local.data,
),
pointer: withData(stackPointer(local.parts, slot), local.data),
};
});
return {
Expand Down Expand Up @@ -691,11 +715,12 @@ function snapshotAt(
// A scalar's word holds its value; a reference's word holds the
// address of its data.
const { name } = declaration;
let size = 32;
let parts = wholePart(name);
let data: Format.Pointer[] | undefined;
if (Ir.Type.isScalar(type)) {
size = type.size;
parts = wholePart(name, type.size);
} else if (type.kind === "ref" && type.location === "calldata") {
parts = calldataParts(name);
data = [calldataPointer(name)];
} else {
if (type.kind !== "ref" || type.location !== "memory") continue;
Expand All @@ -705,14 +730,14 @@ function snapshotAt(
const allocation = info.allocations[located.temp];
if (allocation) {
entry.pointer = withData(
memoryPointer(name, size, allocation, info.frameSize),
memoryPointer(parts, allocation, info.frameSize),
data,
);
} else {
stackLocals.push({
identifier: name,
temp: located.temp,
size,
parts,
...(data ? { data } : {}),
});
}
Expand Down
4 changes: 3 additions & 1 deletion packages/bugc/src/ir/spec/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export namespace Instruction {
stackLocals?: Array<{
identifier: string;
temp: string;
size: number;
/** The regions in the temp's word: each `length` bytes, `offset`
* bytes from its most significant byte */
parts: Array<{ name: string; offset: number; length: number }>;
/** For a reference, the pointers to the data it refers to */
data?: Format.Pointer[];
}>;
Expand Down
11 changes: 6 additions & 5 deletions packages/bugc/test/evm/locals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ async function readScalar(
* follows the length word, as long as the length says. A struct has no
* length word: each field is in its word from the base. Bytes in
* calldata are the calldata from the offset in the word's high 16
* bytes, as long as its low 16 bytes say.
* bytes, as long as its low 16 bytes say: each half a region.
*/
async function readReference(
pointer: unknown,
Expand All @@ -390,15 +390,16 @@ async function readReference(
index: number,
): Promise<RefValue> {
const regions = await regionsAt(pointer, run, index);
regions.seek(name);

if (shape.kind === "calldata") {
const word = (await regions.read(name)).asUint();
const offset = word >> 128n;
const length = word & ((1n << 128n) - 1n);
regions.seek(`${name}-offset`);
const offset = (await regions.read(`${name}-offset`)).asUint();
const length = (await regions.read(`${name}-length`)).asUint();
return (await regions.read(`${name}-data`, offset, length)).toHex();
}

regions.seek(name);

/** A scalar's value, or a reference's, whose word is at `word` */
const decodeWord = async (
shape: Shape | Scalar,
Expand Down
39 changes: 22 additions & 17 deletions packages/web/docs/implementation-guides/compiler/case-study-bug.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,12 @@ join(name); // passes the word; copies nothing

A value of bytes in calldata is one word: the offset of the first byte
in the high 128 bits, and the length in the low 128 bits. A local's
pointer names that word and then the bytes in calldata. The region
`text-data` reads exactly the bytes, even when, as above, the offset
and length are computed while the program runs. A `string calldata`
local has the same pointer, and the type `{ "kind": "string" }`:
pointer names each half of that word as its own region (`text-offset`,
the word's first 16 bytes, and `text-length`, its last 16), and then
the bytes in calldata. The region `text-data` reads those two regions,
and so reads exactly the bytes, even when, as above, the offset and
length are computed while the program runs. A `string calldata` local
has the same pointer, and the type `{ "kind": "string" }`:

<SchemaExample
schema="pointer/collection/group"
Expand All @@ -222,22 +224,25 @@ local has the same pointer, and the type `{ "kind": "string" }`:
>
{`{
"group": [
{ "name": "text", "location": "stack", "slot": 2 },
{
"name": "text-offset",
"location": "stack",
"slot": 2,
"offset": 0,
"length": 16
},
{
"name": "text-length",
"location": "stack",
"slot": 2,
"offset": 16,
"length": 16
},
{
"name": "text-data",
"location": "calldata",
"offset": {
"~quotient": [
{ "~read": "text" },
"0x100000000000000000000000000000000"
]
},
"length": {
"~remainder": [
{ "~read": "text" },
"0x100000000000000000000000000000000"
]
}
"offset": { "~read": "text-offset" },
"length": { "~read": "text-length" }
}
]
}`}
Expand Down
Loading