diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index bf7b8d701..7833d9262 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -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 @@ -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 diff --git a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts index 70a42d5bb..1f1f9c9d8 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts @@ -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, @@ -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 diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts index deb18cc35..5d0e42ba1 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.ts @@ -58,6 +58,14 @@ type StackLocal = NonNullable[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 @@ -65,18 +73,28 @@ type DefSite = Ir.Utils.DefSite; * 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 ]. @@ -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 }; } /** @@ -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` }, }; } @@ -436,10 +463,7 @@ export function withStackLocals( 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 { @@ -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; @@ -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 } : {}), }); } diff --git a/packages/bugc/src/ir/spec/instruction.ts b/packages/bugc/src/ir/spec/instruction.ts index 0ae29c1db..e15a3dcd7 100644 --- a/packages/bugc/src/ir/spec/instruction.ts +++ b/packages/bugc/src/ir/spec/instruction.ts @@ -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[]; }>; diff --git a/packages/bugc/test/evm/locals.ts b/packages/bugc/test/evm/locals.ts index 237886f4d..2e8d87670 100644 --- a/packages/bugc/test/evm/locals.ts +++ b/packages/bugc/test/evm/locals.ts @@ -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, @@ -390,15 +390,16 @@ async function readReference( index: number, ): Promise { 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, diff --git a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx index eddd3fe19..c848d9692 100644 --- a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx +++ b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx @@ -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" }`: {`{ "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" } } ] }`}