diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index 22614550ae..6999866af2 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -109,6 +109,24 @@ support. Changes to the specification itself are tracked in the root function whose `return` is only inside a loop needs one after it. Before, such a function compiled, and the compiler ended it with a `return` that gave no value ([#352]). +- A new type, `bytes calldata`, is bytes in calldata, as in Solidity. + `msg.data` and a slice of it have this type, and a slice of calldata + refers to the calldata instead of copying it, as in + `let text: bytes calldata = msg.data[68:68 + n];` or + `let text = msg.data[68:68 + n];`. A `let` typed `bytes` copies the + bytes to memory. `bytes calldata` can only be the type of a `let` or + of a cast. Such a value is one word, with the offset in its high 128 + bits and the length in its low 128 bits. `.length`, `text[i]` (a + `CALLDATALOAD`), slicing and a cast to `bytesN` read the calldata. An + assignment to a `bytes` local or to storage, a cast to `bytes` or + `string`, a function argument or return value, and the argument of + `keccak256` copy the bytes to memory. A local's pointer names its word + and then a `calldata` region whose offset and length are `$quotient` + and `$remainder` of that word by 2^128, so it reads exactly the local's + bytes, even when its bounds are computed. Writing to bytes in + calldata, or assigning or casting memory bytes to `bytes calldata`, is + a type error. Before, every slice was copied to memory, and a local's + pointer read the copy ([#367]). ### Fixed @@ -404,3 +422,4 @@ First publication. [#364]: https://github.com/ethdebug/format/pull/364 [#365]: https://github.com/ethdebug/format/pull/365 [#366]: https://github.com/ethdebug/format/pull/366 +[#367]: https://github.com/ethdebug/format/pull/367 diff --git a/packages/bugc/examples/advanced/token-registry.bug b/packages/bugc/examples/advanced/token-registry.bug index a788fb36bb..a128ff052b 100644 --- a/packages/bugc/examples/advanced/token-registry.bug +++ b/packages/bugc/examples/advanced/token-registry.bug @@ -128,7 +128,7 @@ code { } // Extract 4-byte function selector from calldata - let selector = msg.data[0:4]; + let selector: bytes = msg.data[0:4]; // Compute function selectors from signatures (optimizer should evaluate these as constants) // These keccak256 computations should be optimized out to constants diff --git a/packages/bugc/src/ast/spec.ts b/packages/bugc/src/ast/spec.ts index 0967273886..a0ef836af8 100644 --- a/packages/bugc/src/ast/spec.ts +++ b/packages/bugc/src/ast/spec.ts @@ -464,16 +464,29 @@ export namespace Type { return { id, kind: "type:elementary:bool", loc: loc ?? null }; } + /** `bytesN`, `bytes`, or `bytes calldata` (`location`) */ export interface Bytes extends Elementary.Base { kind: "type:elementary:bytes"; size?: number; + location?: "calldata"; } export const isBytes = (type: Type.Base): type is Bytes => type.kind === "type:elementary:bytes"; - export function bytes(id: Id, size?: number, loc?: SourceLocation): Bytes { - return { id, kind: "type:elementary:bytes", size, loc: loc ?? null }; + export function bytes( + id: Id, + size?: number, + loc?: SourceLocation, + location?: "calldata", + ): Bytes { + return { + id, + kind: "type:elementary:bytes", + size, + ...(location ? { location } : {}), + loc: loc ?? null, + }; } export interface String extends Elementary.Base { diff --git a/packages/bugc/src/evmgen/bounds.test.ts b/packages/bugc/src/evmgen/bounds.test.ts index 7218e3dd72..ddc38e8e1d 100644 --- a/packages/bugc/src/evmgen/bounds.test.ts +++ b/packages/bugc/src/evmgen/bounds.test.ts @@ -257,7 +257,7 @@ describe("memory bytes bounds", () => { "read and write the first and last bytes": [ `storage { [0] r0: uint256; [1] r2: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; b[0] = b[0] + 1 as uint8; b[2] = b[2] + 1 as uint8; r0 = b[0]; @@ -272,7 +272,7 @@ code { "read at the length": [ `storage { [0] r: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; r = b[3]; }`, "0x0a0b0c", @@ -280,7 +280,7 @@ code { "write at the length": [ `storage { [0] r: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; r = 1; b[3] = 1 as uint8; }`, diff --git a/packages/bugc/src/evmgen/calldata-slices.test.ts b/packages/bugc/src/evmgen/calldata-slices.test.ts new file mode 100644 index 0000000000..5679a62beb --- /dev/null +++ b/packages/bugc/src/evmgen/calldata-slices.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect } from "vitest"; +import { keccak256 } from "ethereum-cryptography/keccak"; +import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils"; + +import { executeProgram } from "#test/evm/behavioral"; + +const levels = [0, 1, 2, 3] as const; + +const word = (n: bigint) => n.toString(16).padStart(64, "0"); + +/** Calldata for `setMotd(string)`, ABI-encoded, without the 0x */ +function setMotd(text: string): string { + const data = Buffer.from(text).toString("hex"); + const padded = data.padEnd(Math.ceil(data.length / 64) * 64, "0"); + return "deadbeef" + word(32n) + word(BigInt(text.length)) + padded; +} + +/** The slot of `out[i]`, with `[3] out: mapping` */ +const outSlot = (i: number) => + BigInt("0x" + bytesToHex(keccak256(hexToBytes(word(BigInt(i)) + word(3n))))); + +/** A string's slot as Solidity stores it, and its data slots */ +function stored(text: string, slot: bigint): Map { + const bytes = Buffer.from(text); + const slots = new Map(); + if (bytes.length < 32) { + const data = Buffer.alloc(32); + bytes.copy(data); + data[31] = bytes.length * 2; + slots.set(slot, BigInt("0x" + data.toString("hex"))); + return slots; + } + slots.set(slot, BigInt(bytes.length * 2 + 1)); + const base = BigInt("0x" + bytesToHex(keccak256(hexToBytes(word(slot))))); + for (let i = 0; i * 32 < bytes.length; i++) { + const data = Buffer.alloc(32); + bytes.copy(data, 0, i * 32, i * 32 + 32); + slots.set(base + BigInt(i), BigInt("0x" + data.toString("hex"))); + } + return slots; +} + +/** + * Declare `text`, with an annotation (`: bytes calldata` or `: bytes`) + * or none. Its bounds come from its ABI offset and length words. + */ +const decodeAs = (annotation: string) => ` + let offset = msg.data[4:36] as bytes32 as uint256; + let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256; + let text${annotation} = msg.data[36 + offset:36 + offset + n];`; + +const decode = decodeAs(""); + +// A calldata reference untyped or typed so, and a copy in memory +const annotations = ["", ": bytes calldata", ": bytes"]; + +const texts = [ + "", + "hello", + "a".repeat(31), + "b".repeat(32), + "the quick brown fox jumps over the lazy dog, twice", +]; + +describe.each(levels)("a slice of calldata at O%i", (level) => { + for (const [text, annotation] of texts.flatMap((text) => + annotations.map((annotation) => [text, annotation] as const), + )) { + const label = `${text.length} bytes, \`let text${annotation}\``; + const decode = decodeAs(annotation); + + it(`reads its length and bytes from calldata, ${label}`, async () => { + const result = await executeProgram( + `name Read; +storage { + [0] motd: string; [1] len: uint256; [3] out: mapping; +} +code {${decode} + len = text.length; + for (let i = 0; i < text.length; i = i + 1) { + out[i] = text[i]; + } +}`, + { calldata: setMotd(text), optimizationLevel: level }, + ); + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(1n)).toBe(BigInt(text.length)); + for (let i = 0; i < text.length; i++) { + expect(await result.getStorage(outSlot(i)), `byte ${i}`).toBe( + BigInt(text.charCodeAt(i)), + ); + } + }); + + it(`stores its bytes to storage, ${label}`, async () => { + const result = await executeProgram( + `name Store; +storage { [0] motd: string; [1] raw: bytes; } +code {${decode} + motd = text as string; + raw = text; +}`, + { calldata: setMotd(text), optimizationLevel: level }, + ); + expect(result.callSuccess).toBe(true); + for (const slot of [0n, 1n]) { + for (const [at, value] of stored(text, slot)) { + expect(await result.getStorage(at), `slot ${at}`).toBe(value); + } + } + }); + } + + it("copies to a memory local, a cast and a parameter", async () => { + const text = "the quick brown fox jumps over the lazy dog, twice"; + const result = await executeProgram( + `name Copy; +define { + function sum(b: bytes) -> uint256 { + let total = 0; + for (let i = 0; i < b.length; i = i + 1) { + total = total + b[i]; + } + return total; + }; +} +storage { + [0] motd: string; [1] len: uint256; [2] total: uint256; + [3] out: mapping; +} +code {${decode} + let m: bytes = text; + m[0] = 88; + len = m.length; + let c = text as bytes; + c[1] = 89; + out[0] = m[0]; + out[1] = text[0]; + out[2] = c[1]; + out[3] = text[1]; + total = sum(text); +}`, + { calldata: setMotd(text), optimizationLevel: level }, + ); + expect(result.callSuccess).toBe(true); + expect(await result.getStorage(1n)).toBe(BigInt(text.length)); + expect(await result.getStorage(outSlot(0))).toBe(0x58n); + expect(await result.getStorage(outSlot(1))).toBe( + BigInt(text.charCodeAt(0)), + ); + expect(await result.getStorage(outSlot(2))).toBe(89n); + expect(await result.getStorage(outSlot(3))).toBe( + BigInt(text.charCodeAt(1)), + ); + expect(await result.getStorage(2n)).toBe( + [...Buffer.from(text)].reduce((a, b) => a + BigInt(b), 0n), + ); + }); + + it("slices a slice, and casts one to bytesN", async () => { + const result = await executeProgram( + `name SliceCast; +storage { + [0] a: bytes32; [1] b: bytes4; [2] c: uint256; + [3] out: mapping; +} +code { + let all = msg.data[2:msg.data.length]; + let part = all[1:4]; + a = part as bytes32; + b = msg.data[0:4] as bytes4; + c = part.length; + out[0] = part[0]; + out[1] = part[2]; +}`, + { calldata: "0102030405060708", optimizationLevel: level }, + ); + expect(result.callSuccess).toBe(true); + // Bytes past the slice's length are zero + expect(await result.getStorage(0n)).toBe(0x040506n << 232n); + expect(await result.getStorage(1n)).toBe(0x01020304n); + expect(await result.getStorage(2n)).toBe(3n); + expect(await result.getStorage(outSlot(0))).toBe(4n); + expect(await result.getStorage(outSlot(1))).toBe(6n); + }); + + it("reverts on an index or slice past the end", async () => { + for (const access of ["text[5]", "text[2:6].length", "text[3:2].length"]) { + const result = await executeProgram( + `name Bounds; +storage { [1] len: uint256; } +code {${decode} + len = ${access}; +}`, + { calldata: setMotd("hello"), optimizationLevel: level }, + ); + expect(result.callSuccess, access).toBe(false); + } + }); +}); 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 3943454557..039ab2239d 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts @@ -408,6 +408,56 @@ code { r = g(${arg}, 0); }`, * Programs and the values their locals take, in execution order. A * value with `after` may be read only once that statement has run. */ +const motd = "hello world, this is longer than thirty-two bytes"; + +/** Calldata for `setMotd(string)`, ABI-encoded (hex, no 0x) */ +function setMotd(text: string): string { + const word = (n: number) => n.toString(16).padStart(64, "0"); + const data = Buffer.from(text).toString("hex"); + return ( + "deadbeef" + + word(32) + + word(text.length) + + data.padEnd(Math.ceil(data.length / 64) * 64, "0") + ); +} + +// `text` refers to the calldata, untyped or typed so, or is a copy in +// memory +const annotations = ["", ": bytes calldata", ": bytes"]; + +/** + * A slice of calldata refers to the calldata. Its bounds here are + * computed from the ABI offset and length words of a string argument, + * and its pointer reads exactly its bytes (or, typed `bytes`, the + * copy's). + */ +function setMotdProgram(annotation: string): LocalsProgram { + return { + name: `a calldata slice with computed bounds, \`let text${annotation}\``, + source: `name SetMotd; +storage { [0] motd: string; [1] r: uint256; } +create { r = 1; } +code { + let offset = msg.data[4:36] as bytes32 as uint256; + let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256; + let text${annotation} = msg.data[36 + offset:36 + offset + n]; + motd = text as string; + if (r > 0) { r = text.length + text[1]; } +}`, + calldata: setMotd(motd), + locals: { + offset: { values: [32n] }, + n: { values: [BigInt(motd.length)] }, + text: { + shape: { kind: annotation === ": bytes" ? "bytes" : "calldata" }, + values: [textBytes(motd)], + everyLevel: true, + }, + }, + }; +} + const programs: LocalsProgram[] = [ { name: "straight line", @@ -1038,8 +1088,9 @@ code { r = r + b.length; }`, // Called with no calldata, so a longer slice would revert - locals: { b: { shape: { kind: "bytes" }, values: ["0x"] } }, + locals: { b: { shape: { kind: "calldata" }, values: ["0x"] } }, }, + ...annotations.map(setMotdProgram), { // A slice longer than a word: its pointer reads every byte name: "a long bytes slice", diff --git a/packages/bugc/src/evmgen/debug/local-variables.ts b/packages/bugc/src/evmgen/debug/local-variables.ts index 845135db09..5b8f612021 100644 --- a/packages/bugc/src/evmgen/debug/local-variables.ts +++ b/packages/bugc/src/evmgen/debug/local-variables.ts @@ -30,7 +30,9 @@ * * A reference-typed local (memory array, string, dynamic bytes) is * held as an address word; its pointer names that word, then the data - * it refers to (`dataPointers`). + * it refers to (`dataPointers`). A local of bytes in calldata is held + * as a word with their offset and length; its pointer names that + * word, then those bytes (`calldataPointer`). * * The memory pointer encodes bugc's frame convention: the frame base * lives in memory at `FRAME_POINTER` (0x80); a frame-homed local is at @@ -199,6 +201,21 @@ 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`. + */ +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] }, + }; +} + /** A pointer to a local's word plus, for a reference, its data. */ function withData( word: Format.Pointer, @@ -640,6 +657,8 @@ function snapshotAt( let data: Format.Pointer[] | undefined; if (Ir.Type.isScalar(type)) { size = type.size; + } else if (type.kind === "ref" && type.location === "calldata") { + data = [calldataPointer(name)]; } else { if (type.kind !== "ref" || type.location !== "memory") continue; data = dataPointers({ $read: name }, type.origin, name); diff --git a/packages/bugc/src/evmgen/generation/instructions/cast.ts b/packages/bugc/src/evmgen/generation/instructions/cast.ts index 6f4d035ef7..6f92053734 100644 --- a/packages/bugc/src/evmgen/generation/instructions/cast.ts +++ b/packages/bugc/src/evmgen/generation/instructions/cast.ts @@ -5,8 +5,19 @@ import { type Transition, operations, pipe, rebrand } from "#evmgen/operations"; import { loadValue, storeValueIfNeeded } from "../values/index.js"; -const { ADD, AND, DUP1, MLOAD, NOT, PUSHn, SHL, SHR, SIGNEXTEND, SWAP1 } = - operations; +const { + ADD, + AND, + CALLDATALOAD, + DUP1, + MLOAD, + NOT, + PUSHn, + SHL, + SHR, + SIGNEXTEND, + SWAP1, +} = operations; /** * Generate code for cast instructions: truncate, sign-extend or shift @@ -71,7 +82,9 @@ function generateStep( .then(SHL({ debug }), { as: "value" }) .done(); case "load": - return generateLoad(debug); + return step.from === "calldata" + ? generateCalldataLoad(debug) + : generateLoad(debug); } } @@ -111,3 +124,41 @@ function generateLoad( .done() ); } + +/** + * Replace a reference to bytes in calldata (offset in the high 128 + * bits, length in the low 128) with their first 32 bytes, zeroing the + * bytes past their length: + * `calldataload(word >> 128) & ~(~0 >> (8 * (word & (2^128 - 1))))` + */ +function generateCalldataLoad( + debug: Ir.Instruction.Debug | undefined, +): Transition { + return ( + pipe() + // The first 32 bytes + .then(DUP1({ debug })) + .then(PUSHn(128n, { debug }), { as: "shift" }) + .then(SHR({ debug }), { as: "i" }) + .then(CALLDATALOAD({ debug }), { as: "data" }) + + // The length, in bits + .then(SWAP1({ debug })) + .then(rebrand<"value", "b">({ 1: "b" })) + .then(PUSHn((1n << 128n) - 1n, { debug }), { as: "a" }) + .then(AND({ debug }), { as: "value" }) + .then(PUSHn(3n, { debug }), { as: "shift" }) + .then(SHL({ debug }), { as: "shift" }) + + // The mask that keeps `length` leading bytes + .then(PUSHn(0n, { debug }), { as: "a" }) + .then(NOT({ debug }), { as: "value" }) + .then(SWAP1({ debug })) + .then(SHR({ debug }), { as: "a" }) + .then(NOT({ debug }), { as: "a" }) + + .then(rebrand<"a", "a", "data", "b">({ 1: "a", 2: "b" })) + .then(AND({ debug }), { as: "value" }) + .done() + ); +} diff --git a/packages/bugc/src/evmgen/memory-arrays.test.ts b/packages/bugc/src/evmgen/memory-arrays.test.ts index f3ffe31daf..8b2696e9ed 100644 --- a/packages/bugc/src/evmgen/memory-arrays.test.ts +++ b/packages/bugc/src/evmgen/memory-arrays.test.ts @@ -179,12 +179,12 @@ code { }); describe("memory bytes", () => { - // `msg.data` sliced into memory + // `msg.data` sliced and copied into memory run({ "read bytes": [ `storage { [0] r0: uint256; [1] r2: uint256; [2] n: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; r0 = b[0]; r2 = b[2]; n = b.length; @@ -197,7 +197,7 @@ code { [0] r0: uint256; [1] r1: uint256; [2] r2: uint256; [3] n: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; b[1] = 200 as uint8; r0 = b[0]; r1 = b[1]; @@ -212,7 +212,7 @@ code { [0] r0: uint256; [1] r1: uint256; [2] r2: uint256; [3] n: uint256; } code { - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; for (let i = 0 as uint8; i < 3 as uint8; i = i + 1 as uint8) { b[i] = b[i] + 1 as uint8; } diff --git a/packages/bugc/src/ir/utils/cast.ts b/packages/bugc/src/ir/utils/cast.ts index 8e56830276..d335742bbb 100644 --- a/packages/bugc/src/ir/utils/cast.ts +++ b/packages/bugc/src/ir/utils/cast.ts @@ -7,14 +7,15 @@ import { Type } from "../spec/type.js"; * word, `bytesN` included: a value of `n` bytes is the word's low * `n` bytes, and the first byte of a `bytesN` is the highest of them. * - * - `load`: replace a reference to dynamic `bytes` in memory with its - * first 32 bytes, as a `bytes32` (bytes past the length are zero) + * - `load`: replace a reference to dynamic `bytes` in memory or + * calldata (`from`) with its first 32 bytes, as a `bytes32` (bytes + * past the length are zero) * - `and`: keep the low `bytes` bytes * - `signextend`: sign-extend from the low `bytes` bytes * - `shr` / `shl`: shift by `bytes` bytes */ export type CastStep = - | { op: "load" } + | { op: "load"; from: "memory" | "calldata" } | { op: "and" | "signextend" | "shr" | "shl"; bytes: number }; type Kind = "unsigned" | "signed" | "bytes"; @@ -57,8 +58,9 @@ export function castSteps(from: Type, to: Type): CastStep[] { if (!Type.isScalar(from)) { const loaded = Type.scalar(32, BugType.Elementary.bytes(32)); - return Type.isRef(from) && from.location === "memory" - ? [{ op: "load" }, ...castSteps(loaded, to)] + return Type.isRef(from) && + (from.location === "memory" || from.location === "calldata") + ? [{ op: "load", from: from.location }, ...castSteps(loaded, to)] : []; } @@ -98,7 +100,7 @@ const word = 2n ** 256n; /** * Apply a cast's steps to a constant. Returns undefined for a cast - * that loads from memory. + * that loads from memory or calldata. */ export function foldCast( value: bigint, diff --git a/packages/bugc/src/irgen/generate/calldata.ts b/packages/bugc/src/irgen/generate/calldata.ts new file mode 100644 index 0000000000..cd9785896e --- /dev/null +++ b/packages/bugc/src/irgen/generate/calldata.ts @@ -0,0 +1,136 @@ +/** + * Bytes in calldata: `msg.data`, its slices, and the locals they + * initialize refer to the calldata and are not copied. Such a value is + * one word: the offset of its first byte in the calldata in the high + * 128 bits, and its length in the low 128 bits. `msg.data` is offset + * 0 and length CALLDATASIZE, so its word is the calldata size. + */ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Type } from "#types"; + +import { Process } from "./process.js"; + +const uint256 = Ir.Type.Scalar.uint256; + +/** The bits below the offset: the length */ +export const LENGTH_BITS = 128n; + +const constant = (value: bigint) => Ir.Value.constant(value, uint256); + +/** The IR type of bytes in calldata */ +export const calldataType = (): Ir.Type => + Ir.Type.ref("calldata", Type.Elementary.calldataBytes()); + +export const isCalldata = (value: Ir.Value): boolean => + Ir.Type.isRef(value.type) && value.type.location === "calldata"; + +/** The offset and length of bytes in calldata */ +export interface Bounds { + offset: Ir.Value; + length: Ir.Value; +} + +function* emitBinary( + op: Ir.Instruction.BinaryOp["op"], + left: Ir.Value, + right: Ir.Value, + node: Ast.Node, +): Process { + const dest = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op, + left, + right, + dest, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.BinaryOp); + return Ir.Value.temp(dest, uint256); +} + +/** `left + right`, or `right` when `left` is the constant 0 */ +export function* emitOffsetSum( + left: Ir.Value, + right: Ir.Value, + node: Ast.Node, +): Process { + if (left.kind === "const" && left.value === 0n) return right; + return yield* emitBinary("add", left, right, node); +} + +/** The offset and length of the bytes in calldata a word refers to */ +export function* emitBounds(word: Ir.Value, node: Ast.Node): Process { + return { + offset: yield* emitBinary("shr", word, constant(LENGTH_BITS), node), + length: yield* emitBinary( + "and", + word, + constant((1n << LENGTH_BITS) - 1n), + node, + ), + }; +} + +/** The word that refers to `length` bytes of calldata at `offset` */ +export function* emitWord( + { offset, length }: Bounds, + node: Ast.Node, +): Process { + const high = yield* emitBinary("shl", offset, constant(LENGTH_BITS), node); + const word = yield* emitBinary("or", high, length, node); + return { ...word, type: calldataType() }; +} + +/** + * Copy bytes in calldata to new memory, as a length word and then the + * data. The copy has the type `origin` (`bytes` unless given). + */ +export function* emitCopyToMemory( + word: Ir.Value, + node: Ast.Node, + origin: Type = Type.Elementary.bytes(), +): Process { + const { offset, length } = yield* emitBounds(word, node); + const debug = yield* Process.Debug.forAstNode(node); + + const size = yield* emitBinary("add", length, constant(32n), node); + const base = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "allocate", + location: "memory", + size, + dest: base, + operationDebug: debug, + } as Ir.Instruction.Allocate); + + const baseValue = Ir.Value.temp(base, uint256); + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: baseValue, + length: constant(32n), + value: length, + operationDebug: debug, + } as Ir.Instruction.Write); + + const data = yield* emitBinary("add", baseValue, constant(32n), node); + yield* Process.Instructions.emit({ + kind: "copy", + location: "calldata", + source: offset, + offset: data, + length, + operationDebug: debug, + } as Ir.Instruction.Copy); + + return Ir.Value.temp(base, Ir.Type.ref("memory", origin)); +} + +/** A value, or, if it is bytes in calldata, a copy of them in memory */ +export function* emitInMemory( + value: Ir.Value, + node: Ast.Node, +): Process { + return isCalldata(value) ? yield* emitCopyToMemory(value, node) : value; +} diff --git a/packages/bugc/src/irgen/generate/expressions/access.ts b/packages/bugc/src/irgen/generate/expressions/access.ts index c89f7aa9e2..7db343fba1 100644 --- a/packages/bugc/src/irgen/generate/expressions/access.ts +++ b/packages/bugc/src/irgen/generate/expressions/access.ts @@ -19,6 +19,8 @@ import { emitMemoryElementOffset, emitMemoryByteOffset, } from "../memory.js"; +import * as Calldata from "../calldata.js"; +import { buildMsgDataEnv } from "./special.js"; /** * Build an access expression (array/member access) @@ -94,6 +96,17 @@ const makeBuildMemberAccess = ( (Type.Elementary.isBytes(objectType) || Type.Elementary.isString(objectType)))) ) { + // Bytes in calldata: the length is in their word + if ( + Type.Elementary.Bytes.isCalldata(objectType) && + !Ast.Expression.Special.isMsgData(expr.object) + ) { + const word = yield* buildExpression(expr.object, { + kind: "rvalue", + }); + return (yield* Calldata.emitBounds(word, expr)).length; + } + const resultType: Ir.Type = Ir.Type.Scalar.uint256; const tempId = yield* Process.Variables.newTemp(); @@ -119,7 +132,9 @@ const makeBuildMemberAccess = ( } // For dynamic arrays/bytes/strings, emit length instruction - const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + const object = Ast.Expression.Special.isMsgData(expr.object) + ? yield* buildMsgDataEnv(expr.object) + : yield* buildExpression(expr.object, { kind: "rvalue" }); yield* Process.Instructions.emit({ kind: "length", object, @@ -193,6 +208,37 @@ const makeBuildMemberAccess = ( ); }; +/** + * Bytes in calldata: msg.data's env value, or another value's word + */ +type CalldataObject = { msgData: Ir.Value } | { word: Ir.Value }; + +function* buildCalldataObject( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, + object: Ast.Expression, +): Process { + return Ast.Expression.Special.isMsgData(object) + ? { msgData: yield* buildMsgDataEnv(object) } + : { word: yield* buildExpression(object, { kind: "rvalue" }) }; +} + +/** The bounds of bytes in calldata; msg.data is at offset 0 */ +function* emitCalldataBounds( + object: CalldataObject, + node: Ast.Expression, +): Process { + if ("msgData" in object) { + return { + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: yield* emitLength(object.msgData, node), + }; + } + return yield* Calldata.emitBounds(object.word, node); +} + const makeBuildSliceAccess = ( buildExpression: ( node: Ast.Expression, @@ -205,6 +251,37 @@ const makeBuildSliceAccess = ( ): Process { // Slice access - start:end const objectType = yield* Process.Types.nodeType(expr.object); + + // A slice of bytes in calldata refers to the calldata + if (objectType && Type.Elementary.Bytes.isCalldata(objectType)) { + const object = yield* buildCalldataObject(buildExpression, expr.object); + const start = yield* buildExpression(expr.start, { kind: "rvalue" }); + const end = yield* buildExpression(expr.end, { kind: "rvalue" }); + const { offset, length } = yield* emitCalldataBounds(object, expr); + + // Revert unless start <= end <= length + yield* emitBoundsCheck("le", start, end, expr); + yield* emitBoundsCheck("le", end, length, expr); + + const lengthTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "sub", + left: end, + right: start, + dest: lengthTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.BinaryOp); + + return yield* Calldata.emitWord( + { + offset: yield* Calldata.emitOffsetSum(offset, start, expr), + length: Ir.Value.temp(lengthTemp, Ir.Type.Scalar.uint256), + }, + expr, + ); + } + if ( objectType && Type.isElementary(objectType) && @@ -277,32 +354,28 @@ const makeBuildSliceAccess = ( operationDebug: yield* Process.Debug.forAstNode(expr), } as Ir.Instruction.Write); - // Copy the slice's bytes from the source: `msg.data` is calldata - // from offset 0; other bytes are in memory after a length word - const isCalldata = Ast.Expression.Special.isMsgData(expr.object); - let source = start; - if (!isCalldata) { - const sourceOffsetTemp = yield* Process.Variables.newTemp(); - yield* Process.Instructions.emit({ - kind: "binary", - op: "add", - left: object, - right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), - dest: sourceOffsetTemp, - operationDebug: yield* Process.Debug.forAstNode(expr), - } as Ir.Instruction); + // Copy the slice's bytes from the source, in memory after a + // length word + const sourceOffsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: object, + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: sourceOffsetTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); - const adjustedSourceTemp = yield* Process.Variables.newTemp(); - yield* Process.Instructions.emit({ - kind: "binary", - op: "add", - left: Ir.Value.temp(sourceOffsetTemp, Ir.Type.Scalar.uint256), - right: start, - dest: adjustedSourceTemp, - operationDebug: yield* Process.Debug.forAstNode(expr), - } as Ir.Instruction); - source = Ir.Value.temp(adjustedSourceTemp, Ir.Type.Scalar.uint256); - } + const adjustedSourceTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(sourceOffsetTemp, Ir.Type.Scalar.uint256), + right: start, + dest: adjustedSourceTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + const source = Ir.Value.temp(adjustedSourceTemp, Ir.Type.Scalar.uint256); // Calculate destination offset (skip length prefix) const destDataOffsetTemp = yield* Process.Variables.newTemp(); @@ -317,7 +390,7 @@ const makeBuildSliceAccess = ( yield* Process.Instructions.emit({ kind: "copy", - location: isCalldata ? "calldata" : "memory", + location: "memory", source, offset: Ir.Value.temp(destDataOffsetTemp, Ir.Type.Scalar.uint256), length, @@ -445,6 +518,29 @@ const makeBuildIndexAccess = ( // First check if we're indexing into bytes (not part of storage chain) const nodeType = yield* Process.Types.nodeType(expr); const objectType = yield* Process.Types.nodeType(expr.object); + + // A byte of bytes in calldata + if (objectType && Type.Elementary.Bytes.isCalldata(objectType)) { + const object = yield* buildCalldataObject(buildExpression, expr.object); + const index = yield* buildExpression(expr.index, { kind: "rvalue" }); + const { offset, length } = yield* emitCalldataBounds(object, expr); + yield* emitBoundsCheck("lt", index, length, expr); + + const elementType: Ir.Type = Ir.Type.scalar(1, "synthetic"); + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "calldata", + offset: yield* Calldata.emitOffsetSum(offset, index, expr), + length: Ir.Value.constant(1n, Ir.Type.Scalar.uint256), + type: elementType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, elementType); + } + if ( objectType && Type.isElementary(objectType) && diff --git a/packages/bugc/src/irgen/generate/expressions/call.ts b/packages/bugc/src/irgen/generate/expressions/call.ts index 7ce1bfc88d..83f9886cf5 100644 --- a/packages/bugc/src/irgen/generate/expressions/call.ts +++ b/packages/bugc/src/irgen/generate/expressions/call.ts @@ -13,6 +13,7 @@ import { emitStorageChainLoad, emitStorageChainStore, } from "../storage.js"; +import { emitInMemory } from "../calldata.js"; /** * Build a call expression @@ -53,10 +54,16 @@ export const makeBuildCall = ( return Ir.Value.constant(0n, Ir.Type.Scalar.bytes32); } - // Evaluate the arguments, in order + // Evaluate the arguments, in order (in memory: the hash reads + // memory) const values: Ir.Value[] = []; for (const argument of expr.arguments) { - values.push(yield* buildExpression(argument, { kind: "rvalue" })); + values.push( + yield* emitInMemory( + yield* buildExpression(argument, { kind: "rvalue" }), + argument, + ), + ); } // Generate hash instruction @@ -91,10 +98,15 @@ export const makeBuildCall = ( return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); } - // Evaluate arguments + // Evaluate arguments; a parameter's bytes are in memory const argValues: Ir.Value[] = []; for (const arg of expr.arguments) { - argValues.push(yield* buildExpression(arg, { kind: "rvalue" })); + argValues.push( + yield* emitInMemory( + yield* buildExpression(arg, { kind: "rvalue" }), + arg, + ), + ); } // A void function's call has the type checker's "void diff --git a/packages/bugc/src/irgen/generate/expressions/cast.ts b/packages/bugc/src/irgen/generate/expressions/cast.ts index 388f30e86b..fa299f5d72 100644 --- a/packages/bugc/src/irgen/generate/expressions/cast.ts +++ b/packages/bugc/src/irgen/generate/expressions/cast.ts @@ -7,6 +7,7 @@ import { Error as IrgenError } from "#irgen/errors"; import { fromBugType } from "#irgen/type"; import { Process } from "../process.js"; +import { emitCopyToMemory, isCalldata } from "../calldata.js"; import type { Context } from "./context.js"; /** @@ -71,7 +72,17 @@ export const makeBuildCast = ( } const targetIrType = fromBugType(targetType); - const value = yield* buildOperand(expr.expression, targetIrType); + let value = yield* buildOperand(expr.expression, targetIrType); + + // A cast of bytes in calldata to a type in memory (`string`) copies + // them there + if ( + Ir.Type.isRef(targetIrType) && + targetIrType.location === "memory" && + isCalldata(value) + ) { + value = yield* emitCopyToMemory(value, expr, targetType); + } const resultTemp = yield* Process.Variables.newTemp(); yield* Process.Instructions.emit({ diff --git a/packages/bugc/src/irgen/generate/expressions/special.ts b/packages/bugc/src/irgen/generate/expressions/special.ts index 2ba03b05fb..b36c867960 100644 --- a/packages/bugc/src/irgen/generate/expressions/special.ts +++ b/packages/bugc/src/irgen/generate/expressions/special.ts @@ -5,10 +5,37 @@ import { Severity } from "#result"; import { Error as IrgenError, assertExhausted } from "#irgen/errors"; import { fromBugType } from "#irgen/type"; import { Process } from "../process.js"; +import { emitLength } from "../memory.js"; +import { calldataType } from "../calldata.js"; + +/** + * The `msg_data` env value, whose `length` code generation reads as + * the calldata size + */ +export function* buildMsgDataEnv( + msgData: Ast.Expression.Special, +): Process { + const temp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "env", + op: "msg_data", + dest: temp, + operationDebug: yield* Process.Debug.forAstNode(msgData), + } as Ir.Instruction.Env); + return Ir.Value.temp(temp, calldataType()); +} + /** * Build a special expression (msg.sender, block.number, etc.) */ export function* buildSpecial(expr: Ast.Expression.Special): Process { + // msg.data refers to all of the calldata: offset 0, so its word is + // its length (see `calldata.ts`) + if (expr.kind === "expression:special:msg.data") { + const size = yield* emitLength(yield* buildMsgDataEnv(expr), expr); + return { ...size, type: calldataType() }; + } + // Get the type from the type checker const nodeType = yield* Process.Types.nodeType(expr); @@ -35,9 +62,6 @@ export function* buildSpecial(expr: Ast.Expression.Special): Process { case "expression:special:msg.value": op = "msg_value"; break; - case "expression:special:msg.data": - op = "msg_data"; - break; case "expression:special:block.timestamp": op = "block_timestamp"; break; diff --git a/packages/bugc/src/irgen/generate/statements/assign.ts b/packages/bugc/src/irgen/generate/statements/assign.ts index 6350b1054c..82b5859751 100644 --- a/packages/bugc/src/irgen/generate/statements/assign.ts +++ b/packages/bugc/src/irgen/generate/statements/assign.ts @@ -13,6 +13,7 @@ import { emitStorageVariableStore, } from "../storage.js"; import { emitMemoryElementOffset, emitMemoryByteOffset } from "../memory.js"; +import { emitInMemory } from "../calldata.js"; /** * Build an assignment statement @@ -61,8 +62,13 @@ function* buildLValue( } } - // Evaluate the value expression with the appropriate context - const value = yield* buildExpression(valueExpr, context); + // Evaluate the value expression with the appropriate context. Bytes + // in calldata copy to memory, unless the target is in calldata too. + let value = yield* buildExpression(valueExpr, context); + const targetType = yield* Process.Types.nodeType(target); + if (!targetType || !Type.Elementary.Bytes.isCalldata(targetType)) { + value = yield* emitInMemory(value, valueExpr); + } // For storage array assignments, the array expression will have already // expanded to storage writes, so we don't need to do anything else diff --git a/packages/bugc/src/irgen/generate/statements/control-flow.ts b/packages/bugc/src/irgen/generate/statements/control-flow.ts index aa3c78397f..d5ebc5b22b 100644 --- a/packages/bugc/src/irgen/generate/statements/control-flow.ts +++ b/packages/bugc/src/irgen/generate/statements/control-flow.ts @@ -7,6 +7,7 @@ import { buildExpression } from "../expressions/index.js"; import { makeBuildBlock } from "./block.js"; import { Process } from "../process.js"; +import { emitInMemory } from "../calldata.js"; import type { State } from "../state.js"; /** @@ -295,8 +296,12 @@ export const makeBuildForStatement = ( function* buildReturnStatement( stmt: Ast.Statement.ControlFlow.Return, ): Process { + // A returned value's bytes are in memory const value = stmt.value - ? yield* buildExpression(stmt.value, { kind: "rvalue" }) + ? yield* emitInMemory( + yield* buildExpression(stmt.value, { kind: "rvalue" }), + stmt.value, + ) : undefined; yield* Process.Blocks.terminate({ diff --git a/packages/bugc/src/irgen/generate/statements/declare.ts b/packages/bugc/src/irgen/generate/statements/declare.ts index 4680a16a4f..44f1eb27a0 100644 --- a/packages/bugc/src/irgen/generate/statements/declare.ts +++ b/packages/bugc/src/irgen/generate/statements/declare.ts @@ -7,6 +7,7 @@ import { fromBugType } from "#irgen/type"; import { buildExpression } from "../expressions/index.js"; import { Process } from "../process.js"; +import { emitInMemory } from "../calldata.js"; /** * Build a declaration statement @@ -53,7 +54,9 @@ function* buildVariableDeclaration( const irType = type ? fromBugType(type) : Ir.Type.Scalar.uint256; // Check if this is a reference type that needs memory allocation - const needsMemoryAllocation = irType.kind === "ref"; + // (bytes in calldata need none: the local holds their word) + const needsMemoryAllocation = + irType.kind === "ref" && irType.location !== "calldata"; if (needsMemoryAllocation) { // For types that need memory allocation @@ -84,11 +87,13 @@ function* buildVariableDeclaration( { placeholder: builtLater }, ); - // If there's an initializer, store the value in memory + // If there's an initializer, store the value in memory (bytes in + // calldata copy there) if (decl.initializer) { - const value = yield* buildExpression(decl.initializer, { - kind: "rvalue", - }); + const value = yield* emitInMemory( + yield* buildExpression(decl.initializer, { kind: "rvalue" }), + decl.initializer, + ); // The value is already a reference to memory (a literal, an // array or a slice): the local refers to it diff --git a/packages/bugc/src/irgen/length.test.ts b/packages/bugc/src/irgen/length.test.ts index 3bb1d36b9b..df74695bd7 100644 --- a/packages/bugc/src/irgen/length.test.ts +++ b/packages/bugc/src/irgen/length.test.ts @@ -84,7 +84,7 @@ describe("IR Builder - Length Instructions", () => { if (lengthInst && lengthInst.kind === "length") { expect(lengthInst.object.type?.kind).toBe("ref"); if (lengthInst.object.type?.kind === "ref") { - expect(lengthInst.object.type.location).toBe("memory"); + expect(lengthInst.object.type.location).toBe("calldata"); } } } diff --git a/packages/bugc/src/irgen/slice.test.ts b/packages/bugc/src/irgen/slice.test.ts index 5a49ce7556..fca27e873f 100644 --- a/packages/bugc/src/irgen/slice.test.ts +++ b/packages/bugc/src/irgen/slice.test.ts @@ -6,7 +6,7 @@ import { Severity } from "#result"; import "#test/matchers"; describe("IR slice generation", () => { - test("generates slice IR for msg.data", () => { + test("generates a reference for a slice of msg.data", () => { const result = parse(` name Test; code { @@ -28,25 +28,18 @@ describe("IR slice generation", () => { const ir = irResult.value; expect(ir.main).toBeDefined(); - // Find the decomposed slice operations + // A slice of calldata refers to it: its word is the offset + // shifted left 128 bits, or the length. Nothing is copied. const mainBlocks = Array.from(ir.main.blocks.values()); const allInsts = mainBlocks.flatMap((block) => block.instructions); - // Should have operations for: sub (length calc), add (size calc), - // allocate, write (length), add (dest offset), copy (data) - const subInsts = allInsts.filter( - (inst) => inst.kind === "binary" && inst.op === "sub", - ); - const allocInsts = allInsts.filter((inst) => inst.kind === "allocate"); - const copyInsts = allInsts.filter((inst) => inst.kind === "copy"); - const writeInsts = allInsts.filter((inst) => inst.kind === "write"); - - // Should have decomposed the slice into multiple operations - expect(subInsts.length).toBeGreaterThan(0); // Length calculation - expect(allocInsts.length).toBeGreaterThan(0); // Memory allocation(s) - may allocate for result - expect(writeInsts.length).toBeGreaterThan(0); // Write length - // Copy the data from calldata - expect(copyInsts).toMatchObject([{ location: "calldata" }]); + expect(allInsts.filter((inst) => inst.kind === "allocate")).toEqual([]); + expect(allInsts.filter((inst) => inst.kind === "copy")).toEqual([]); + expect( + allInsts.filter( + (inst) => inst.kind === "binary" && ["shl", "or"].includes(inst.op), + ), + ).toMatchObject([{ op: "shl", right: { value: 128n } }, { op: "or" }]); } } }); diff --git a/packages/bugc/src/irgen/type.ts b/packages/bugc/src/irgen/type.ts index d506e7ac95..1f01e4c25c 100644 --- a/packages/bugc/src/irgen/type.ts +++ b/packages/bugc/src/irgen/type.ts @@ -53,8 +53,8 @@ export function fromBugType(type: BugType): Ir.Type { // Fixed-size bytes are scalars return Ir.Type.scalar(type.size as Ir.Type.Scalar.Size, type); } else { - // Dynamic bytes are memory references - return Ir.Type.ref("memory", type); + // Dynamic bytes are memory references, or calldata ones + return Ir.Type.ref(type.location ?? "memory", type); } case "string": // Strings are always memory references diff --git a/packages/bugc/src/parser/parser.ts b/packages/bugc/src/parser/parser.ts index 2f6357a49c..2b86b0b8c6 100644 --- a/packages/bugc/src/parser/parser.ts +++ b/packages/bugc/src/parser/parser.ts @@ -376,33 +376,47 @@ let statement: P.Parser; * Type Parsers */ -// Elementary types with location +// Elementary types with location. Dynamic bytes may be followed by +// `calldata`: bytes in calldata const elementaryType = located( - Lang.elementaryType.map((name: string) => { - // Parse the type name to extract kind and bits - if (name.startsWith("uint")) { - const bits = parseInt(name.substring(4), 10); - return Ast.Type.Elementary.uint(PENDING_ID, bits); - } else if (name.startsWith("int")) { - const bits = parseInt(name.substring(3), 10); - return Ast.Type.Elementary.int(PENDING_ID, bits); - } else if (name.startsWith("bytes") && name !== "bytes") { - const size = parseInt(name.substring(5), 10); - return Ast.Type.Elementary.bytes(PENDING_ID, size); - } else if (name === "address") { - return Ast.Type.Elementary.address(PENDING_ID); - } else if (name === "bool") { - return Ast.Type.Elementary.bool(PENDING_ID); - } else if (name === "string") { - return Ast.Type.Elementary.string(PENDING_ID); - } else if (name === "bytes") { - return Ast.Type.Elementary.bytes(PENDING_ID); - } - // This should never happen as elementaryTypeName parser ensures valid names - throw new Error(`Unknown elementary type: ${name}`); - }), + Lang.elementaryType.chain((name: string) => + name === "bytes" + ? Lang.keyword("calldata") + .atMost(1) + .map((calldata) => + Ast.Type.Elementary.bytes( + PENDING_ID, + undefined, + undefined, + calldata.length > 0 ? "calldata" : undefined, + ), + ) + : P.succeed(elementaryTypeOf(name)), + ), ); +function elementaryTypeOf(name: string): Ast.Type.Elementary { + // Parse the type name to extract kind and bits + if (name.startsWith("uint")) { + const bits = parseInt(name.substring(4), 10); + return Ast.Type.Elementary.uint(PENDING_ID, bits); + } else if (name.startsWith("int")) { + const bits = parseInt(name.substring(3), 10); + return Ast.Type.Elementary.int(PENDING_ID, bits); + } else if (name.startsWith("bytes") && name !== "bytes") { + const size = parseInt(name.substring(5), 10); + return Ast.Type.Elementary.bytes(PENDING_ID, size); + } else if (name === "address") { + return Ast.Type.Elementary.address(PENDING_ID); + } else if (name === "bool") { + return Ast.Type.Elementary.bool(PENDING_ID); + } else if (name === "string") { + return Ast.Type.Elementary.string(PENDING_ID); + } + // This should never happen as elementaryTypeName parser ensures valid names + throw new Error(`Unknown elementary type: ${name}`); +} + // Reference type (identifier in type position) const referenceType = located( Lang.identifier.map((name: string) => Ast.Type.reference(PENDING_ID, name)), diff --git a/packages/bugc/src/parser/slice.test.ts b/packages/bugc/src/parser/slice.test.ts index f51c38ddee..e160684af8 100644 --- a/packages/bugc/src/parser/slice.test.ts +++ b/packages/bugc/src/parser/slice.test.ts @@ -92,4 +92,32 @@ describe("Slice expressions", () => { expect(sliced.kind).toBe("expression:access:slice"); } }); + + test("parses `bytes calldata` as a type", () => { + const result = parse(` + name Test; + code { + let t: bytes calldata = msg.data[0:4]; + let u = t as bytes calldata; + let m: bytes = t; + let calldata = 1; + } + `); + if (!result.success) throw new Error("Parse failed"); + + const types = (result.value.body?.items ?? []).map((item) => { + const decl = (item as Ast.Statement.Declare).declaration; + const { initializer, type } = decl as Ast.Declaration.Variable; + return Ast.Expression.isCast(initializer!) + ? initializer.targetType + : type; + }); + expect(types).toMatchObject([ + { kind: "type:elementary:bytes", location: "calldata" }, + { kind: "type:elementary:bytes", location: "calldata" }, + { kind: "type:elementary:bytes" }, + undefined, + ]); + expect(types[2]).not.toHaveProperty("location"); + }); }); diff --git a/packages/bugc/src/typechecker/assignable.ts b/packages/bugc/src/typechecker/assignable.ts index eae8d57316..37e670022f 100644 --- a/packages/bugc/src/typechecker/assignable.ts +++ b/packages/bugc/src/typechecker/assignable.ts @@ -12,6 +12,16 @@ export function isAssignable(target: Type, value: Type): boolean { return true; } + // Bytes in calldata copy to dynamic bytes in memory or storage + if ( + Type.Elementary.Bytes.isCalldata(value) && + Type.isElementary(target) && + Type.Elementary.isBytes(target) && + target.size === undefined + ) { + return true; + } + // Numeric types can be implicitly converted (with range checks) if ( Type.isElementary(target) && diff --git a/packages/bugc/src/typechecker/blocks.ts b/packages/bugc/src/typechecker/blocks.ts index 3ee5c274cf..2bb520010e 100644 --- a/packages/bugc/src/typechecker/blocks.ts +++ b/packages/bugc/src/typechecker/blocks.ts @@ -5,7 +5,11 @@ import type { Symbol as BugSymbol } from "./symbols.js"; import type { Context, Report } from "./context.js"; import { enterFunctionScope } from "./symbols.js"; import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; -import { resolveTypeWithBindings } from "./declarations.js"; +import { + calldataTypeError, + calldataTypeNode, + resolveTypeWithBindings, +} from "./declarations.js"; import { isAssignable } from "./assignable.js"; import { alwaysReturns } from "./returns.js"; @@ -320,6 +324,8 @@ export const blockChecker: Pick< // Also process the type node to record bindings for type references if (node.type) { + const calldata = calldataTypeNode(node.type); + if (calldata) errors.push(calldataTypeError(calldata)); const typeResult = resolveTypeWithBindings( node.type, context.structs, @@ -374,6 +380,12 @@ export const blockChecker: Pick< // Determine the variable's type let type: Type; if (node.type) { + // `bytes calldata` is only the whole type + const calldata = calldataTypeNode(node.type); + if (calldata && calldata !== node.type) { + errors.push(calldataTypeError(calldata)); + } + // If a type is explicitly declared, use it and record bindings const typeResult = resolveTypeWithBindings( node.type, diff --git a/packages/bugc/src/typechecker/checker.test.ts b/packages/bugc/src/typechecker/checker.test.ts index ed7b6eff74..55dcd5707a 100644 --- a/packages/bugc/src/typechecker/checker.test.ts +++ b/packages/bugc/src/typechecker/checker.test.ts @@ -353,7 +353,7 @@ describe("checkProgram", () => { name Test; code { let a: array = [1, 2, 3]; - let b = msg.data[0:3]; + let b: bytes = msg.data[0:3]; let i: int256 = 1 as int256; ${statement} } diff --git a/packages/bugc/src/typechecker/declarations.ts b/packages/bugc/src/typechecker/declarations.ts index 60f13b73c9..447e67e921 100644 --- a/packages/bugc/src/typechecker/declarations.ts +++ b/packages/bugc/src/typechecker/declarations.ts @@ -82,6 +82,7 @@ function buildStructType( for (const field of decl.fields) { if (Ast.Declaration.isField(field) && field.type) { + checkNoCalldata(field.type); const fieldType = resolveType(field.type, existingStructs); fields.set(field.name, fieldType); } @@ -103,11 +104,13 @@ function buildFunctionSignature( // Resolve parameter types const parameterTypes: Type[] = []; for (const param of decl.parameters) { + checkNoCalldata(param.type); const paramType = resolveType(param.type, structTypes); parameterTypes.push(paramType); } // Resolve return type (null for void functions) + if (decl.returnType) checkNoCalldata(decl.returnType); const returnType = decl.returnType ? resolveType(decl.returnType, structTypes) : null; @@ -115,6 +118,51 @@ function buildFunctionSignature( return Type.function_(parameterTypes, returnType, decl.name); } +/** The `bytes calldata` in a type node, if any */ +export function calldataTypeNode(typeNode: Ast.Type): Ast.Type | undefined { + if (Ast.Type.isElementary(typeNode)) { + return Ast.Type.Elementary.isBytes(typeNode) && + typeNode.location === "calldata" + ? typeNode + : undefined; + } + if (Ast.Type.isComplex(typeNode)) { + if (Ast.Type.Complex.isArray(typeNode)) { + return calldataTypeNode(typeNode.element); + } + if (Ast.Type.Complex.isMapping(typeNode)) { + return calldataTypeNode(typeNode.key) ?? calldataTypeNode(typeNode.value); + } + } + return undefined; +} + +/** + * The error for `bytes calldata` where it cannot be: it is only the + * whole type of a `let` or of a cast + */ +export function calldataTypeError(typeNode: Ast.Type): TypeError { + return new TypeError( + "`bytes calldata` can only be the type of a `let` or a cast", + typeNode.loc || undefined, + undefined, + undefined, + ErrorCode.CALLDATA_TYPE, + ); +} + +/** Throw unless a type node has no `bytes calldata` */ +function checkNoCalldata(typeNode: Ast.Type): void { + const found = calldataTypeNode(typeNode); + if (found) throw calldataTypeError(found); +} + +/** Dynamic `bytes`, or `bytes calldata` */ +export const dynamicBytes = (typeNode: Ast.Type.Elementary.Bytes): Type => + typeNode.location === "calldata" + ? Type.Elementary.calldataBytes() + : Type.Elementary.bytes(); + /** * Resolves an AST type node to a Type object and records bindings */ @@ -161,7 +209,7 @@ export function resolveTypeWithBindings( if (Ast.Type.Elementary.isBytes(typeNode)) { if (!typeNode.size) { - return { type: Type.Elementary.bytes(), bindings }; // Dynamic bytes + return { type: dynamicBytes(typeNode), bindings }; } // typeNode.bits now contains the byte size directly (e.g., 32 for bytes32) const validSizes = [4, 8, 16, 32]; @@ -287,7 +335,7 @@ export function resolveType( if (Ast.Type.Elementary.isBytes(typeNode)) { if (!typeNode.size) { - return Type.Elementary.bytes(); // Dynamic bytes + return dynamicBytes(typeNode); } // typeNode.bits now contains the byte size directly (e.g., 32 for bytes32) const validSizes = [4, 8, 16, 32]; diff --git a/packages/bugc/src/typechecker/errors.ts b/packages/bugc/src/typechecker/errors.ts index f35cdc5ddb..2b7ef10a17 100644 --- a/packages/bugc/src/typechecker/errors.ts +++ b/packages/bugc/src/typechecker/errors.ts @@ -25,6 +25,7 @@ export enum ErrorCode { INVALID_TYPE_CAST = "TYPE013", INTERNAL_ERROR = "TYPE014", MISSING_RETURN = "TYPE015", + CALLDATA_TYPE = "TYPE016", GENERAL = "TYPE_ERROR", // Legacy support } diff --git a/packages/bugc/src/typechecker/expressions.ts b/packages/bugc/src/typechecker/expressions.ts index 6fb6317828..5ce19aed73 100644 --- a/packages/bugc/src/typechecker/expressions.ts +++ b/packages/bugc/src/typechecker/expressions.ts @@ -9,6 +9,7 @@ import { assertExhausted, } from "./errors.js"; import { isAssignable, commonType } from "./assignable.js"; +import { calldataTypeError, calldataTypeNode } from "./declarations.js"; import { fits, integerLiteral, isInteger } from "./literals.js"; /** @@ -453,8 +454,11 @@ export const expressionChecker: Pick, "expression"> = { ); errors.push(error); } - // Slicing bytes returns dynamic bytes - resultType = Type.Elementary.bytes(); + // Slicing bytes returns dynamic bytes, in calldata for a + // slice of calldata + resultType = Type.Elementary.Bytes.isCalldata(objectType) + ? Type.Elementary.calldataBytes() + : Type.Elementary.bytes(); } else { const error = new TypeError( `Cannot slice ${Type.format(objectType)} - only bytes types can be sliced`, @@ -802,6 +806,29 @@ export const expressionChecker: Pick, "expression"> = { return { symbols, nodeTypes, bindings, errors }; } + // `bytes calldata` is only the whole target type, and only bytes + // in calldata cast to it + const calldata = calldataTypeNode(node.targetType); + if (calldata && calldata !== node.targetType) { + errors.push(calldataTypeError(calldata)); + return { symbols, nodeTypes, bindings, errors }; + } + if ( + Type.Elementary.Bytes.isCalldata(targetTypeResult.type) && + !Type.Elementary.Bytes.isCalldata(exprResult.type) + ) { + errors.push( + new TypeError( + `Cannot cast from ${Type.format(exprResult.type)} to bytes calldata`, + node.loc || undefined, + "bytes calldata", + Type.format(exprResult.type), + ErrorCode.INVALID_TYPE_CAST, + ), + ); + return { symbols, nodeTypes, bindings, errors }; + } + // Dynamic bytes cast to an integer or address only via bytesN, // as in Solidity if ( @@ -1053,7 +1080,7 @@ export const expressionChecker: Pick, "expression"> = { type = Type.Elementary.uint(256); break; case "expression:special:msg.data": - type = Type.Elementary.bytes(); + type = Type.Elementary.calldataBytes(); break; case "expression:special:block.timestamp": type = Type.Elementary.uint(256); diff --git a/packages/bugc/src/typechecker/slice.test.ts b/packages/bugc/src/typechecker/slice.test.ts index 66dc3fedcd..ebd44e53fb 100644 --- a/packages/bugc/src/typechecker/slice.test.ts +++ b/packages/bugc/src/typechecker/slice.test.ts @@ -37,11 +37,100 @@ describe("Slice type checking", () => { if (!sliceType) { throw new Error("Unexpected missing slice type"); } - expect(Type.format(sliceType)).toBe("bytes"); + expect(Type.format(sliceType)).toBe("bytes calldata"); } } }); + /** The type of each `let` in a program's `code` block, by name */ + function letTypes(source: string): Record { + const result = parse(source); + if (!result.success) throw new Error("Parse failed"); + const typeResult = checkProgram(result.value); + if (!typeResult.success) throw new Error("Type check failed"); + const { types } = typeResult.value; + const lets: Record = {}; + for (const item of result.value.body?.items ?? []) { + if (Ast.isStatement(item) && Ast.Statement.isDeclare(item)) { + const decl = item.declaration; + lets[decl.name] = Type.format(types.get(decl.id)!); + } + } + return lets; + } + + test("keeps a slice of calldata in calldata, unless typed bytes", () => { + expect( + letTypes(` + name Test; + code { + let a = msg.data[4:36]; + let b: bytes = msg.data[4:36]; + let c = b[1:3]; + let m: bytes = 0x${"12".repeat(33)}; + let d = m[0:1]; + let e = b as bytes; + let f: bytes calldata = msg.data[4:36]; + let g: bytes calldata = f[1:3]; + let h = a as bytes calldata; + let i = a as bytes; + } + `), + ).toEqual({ + a: "bytes calldata", + b: "bytes", + c: "bytes", + m: "bytes", + d: "bytes", + e: "bytes", + f: "bytes calldata", + g: "bytes calldata", + h: "bytes calldata", + i: "bytes", + }); + }); + + test("allows bytes calldata only as the type of a let or a cast", () => { + const placement = + "`bytes calldata` can only be the type of a `let` or a cast"; + const errors = (source: string) => { + const result = parse(source); + if (!result.success) throw new Error("Parse failed"); + const typeResult = checkProgram(result.value); + expect(typeResult.success).toBe(false); + return (typeResult.messages[Severity.Error] ?? []).map((m) => m.message); + }; + + expect( + errors(` + name Test; + define { + function f(b: bytes calldata) -> uint256 { return b.length; }; + struct S { b: bytes calldata; }; + } + code {} + `), + ).toEqual([placement, placement]); + + expect( + errors(` + name Test; + storage { [0] s: bytes calldata; } + code { + let a: array = [msg.data]; + let m: bytes = 0x${"12".repeat(33)}; + let c: bytes calldata = m; + let d = m as bytes calldata; + } + `), + ).toEqual([ + placement, + placement, + "Type mismatch: expected bytes calldata, got bytes", + "Cannot cast from bytes to bytes calldata", + ]); + }); + test("rejects slice of non-bytes type", () => { const result = parse(` name Test; diff --git a/packages/bugc/src/typechecker/statements.ts b/packages/bugc/src/typechecker/statements.ts index 7cefdf16be..7b53687e3c 100644 --- a/packages/bugc/src/typechecker/statements.ts +++ b/packages/bugc/src/typechecker/statements.ts @@ -59,6 +59,24 @@ export const statementChecker: Pick, "statement"> = { bindings = targetResult.bindings; errors.push(...targetResult.errors); + // Calldata is read-only + const target = node.target as Ast.Expression.Access.Index; + const objectType = + target.kind === "expression:access:index" && + nodeTypes.get(target.object.id); + if (objectType && Type.Elementary.Bytes.isCalldata(objectType)) { + errors.push( + new TypeError( + "Cannot assign to bytes in calldata", + node.target.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_ASSIGNMENT, + ), + ); + return { symbols, nodeTypes, bindings, errors }; + } + // Type check value const valueContext: Context = { ...context, diff --git a/packages/bugc/src/typechecker/type-nodes.ts b/packages/bugc/src/typechecker/type-nodes.ts index 00ed7f9858..27015b75f9 100644 --- a/packages/bugc/src/typechecker/type-nodes.ts +++ b/packages/bugc/src/typechecker/type-nodes.ts @@ -3,6 +3,7 @@ import { Type } from "#types"; import type { Visitor } from "#ast"; import type { Context, Report } from "./context.js"; import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; +import { dynamicBytes } from "./declarations.js"; /** * Type checker for type AST nodes. @@ -43,7 +44,7 @@ export const typeNodeChecker: Pick, "type"> = { Type.failure(`Unknown int size: ${node.bits}`); } else if (Ast.Type.Elementary.isBytes(node)) { if (!node.size) { - type = Type.Elementary.bytes(); // Dynamic bytes + type = dynamicBytes(node); } else { type = Type.Elementary.bytes(node.size); } diff --git a/packages/bugc/src/types/spec.ts b/packages/bugc/src/types/spec.ts index 44d013e1a6..81d4539cd6 100644 --- a/packages/bugc/src/types/spec.ts +++ b/packages/bugc/src/types/spec.ts @@ -202,9 +202,16 @@ export namespace Type { export const format = (_type: Type.Elementary.Bool): string => `bool`; } + /** + * `bytesN` (with a `size`), or dynamic `bytes`. Dynamic bytes are + * in memory, or, with `location: "calldata"`, are a slice of the + * calldata (`msg.data` and its slices, and a `let` they + * initialize) + */ export interface Bytes { kind: "bytes"; size?: number; + location?: "calldata"; } export const bytes = (size?: number): Type.Elementary.Bytes => ({ @@ -212,23 +219,34 @@ export namespace Type { size, }); + /** Dynamic bytes in calldata */ + export const calldataBytes = (): Type.Elementary.Bytes => ({ + kind: "bytes", + location: "calldata", + }); + export namespace Bytes { export const isDynamic = ( type: Type.Elementary.Bytes, ): type is Type.Elementary.Bytes & { size?: undefined } => !("size" in type) || type.size === undefined; + export const isCalldata = (type: Type): boolean => + Type.isElementary(type) && + Type.Elementary.isBytes(type) && + type.location === "calldata"; + export const equals = ( a: Type.Elementary.Bytes, b: Type.Elementary.Bytes, - ): boolean => a.size == b.size; + ): boolean => a.size == b.size && a.location === b.location; export const format = (type: Type.Elementary.Bytes) => `bytes${ "size" in type && typeof type.size === "number" ? type.size.toString() : "" - }`; + }${type.location === "calldata" ? " calldata" : ""}`; } export interface String { diff --git a/packages/bugc/test/evm/locals.ts b/packages/bugc/test/evm/locals.ts index 4eac4997fc..751267ae6d 100644 --- a/packages/bugc/test/evm/locals.ts +++ b/packages/bugc/test/evm/locals.ts @@ -7,7 +7,7 @@ import { expect } from "vitest"; import { compile } from "#compiler"; import { Executor, createMachineState, type TraceStep } from "@ethdebug/evm"; -import { dereference } from "@ethdebug/pointers"; +import { Data, dereference, type Machine } from "@ethdebug/pointers"; import { bytesToHex } from "ethereum-cryptography/utils"; import type * as Format from "@ethdebug/format"; @@ -17,14 +17,18 @@ export interface LocalsTrace { program: Format.Program; executor: Executor; steps: TraceStep[]; + /** The transaction's calldata */ + calldata: Uint8Array; /** The program's instruction at a step's pc */ instructionAt(step: TraceStep): Format.Program.Instruction | undefined; } -/** Compile (at O0 by default) and run once, keeping every step. */ +/** Compile (at O0 by default) and run once, with `calldata` (hex, no + * 0x), keeping every step. */ export async function traceLocals( source: string, level: Level = 0, + calldata: string = "", ): Promise { const result = await compile({ to: "bytecode", @@ -42,7 +46,7 @@ export async function traceLocals( ), ); const steps: TraceStep[] = []; - await executor.execute({ data: "" }, (step) => steps.push(step)); + await executor.execute({ data: calldata }, (step) => steps.push(step)); const program = bytecode.runtimeProgram; const byOffset = new Map( @@ -52,6 +56,7 @@ export async function traceLocals( program, executor, steps, + calldata: Buffer.from(calldata, "hex"), instructionAt: (step) => byOffset.get(step.pc), }; } @@ -68,6 +73,7 @@ export function localsOf(context: unknown): Array> { /** The layout of a reference-typed local, for reading its pointer */ export type Shape = | { kind: "bytes" } + | { kind: "calldata" } | { kind: "array"; element: Shape | { kind: "scalar"; size: number } }; /** A reference's value as its regions read it: hex for bytes and @@ -101,6 +107,8 @@ export interface LocalsProgram { locals: Record; /** The levels to check at, if not all */ levels?: Level[]; + /** The calldata to run with (hex, no 0x), if any */ + calldata?: string; } /** @@ -114,11 +122,11 @@ export interface LocalsProgram { * some step. */ export async function check( - { source, locals }: LocalsProgram, + { source, locals, calldata }: LocalsProgram, level: Level, ): Promise { - const run = await traceLocals(source, level); - const { steps, executor } = run; + const run = await traceLocals(source, level, calldata); + const { steps } = run; // The declaration each key names: an offset in the source const keys = Object.keys(locals).map((key) => { @@ -206,14 +214,8 @@ export async function check( const local = locals[key!]; const read = local.shape - ? await readReference( - pointer, - name, - local.shape, - executor, - steps[k + 1], - ) - : await readScalar(pointer, name, executor, steps[k + 1]); + ? await readReference(pointer, name, local.shape, run, steps[k + 1]) + : await readScalar(pointer, name, run, steps[k + 1]); // Never an earlier value than the last one read, nor than the // latest one whose statement has finished (that would be stale); @@ -289,14 +291,24 @@ function stringify(value: unknown): string { /** * Dereference a pointer at a step and read its regions in order, - * checking each against the step's memory. + * checking each against the step's memory or the calldata. */ async function regionsAt( pointer: unknown, - executor: Executor, + { executor, calldata }: LocalsTrace, step: TraceStep, ) { - const state = createMachineState(executor, { traceStep: step }); + const state: Machine.State = { + ...createMachineState(executor, { traceStep: step }), + calldata: { + length: Promise.resolve(BigInt(calldata.length)), + read: async ({ slice: { offset, length } }) => { + const data = new Uint8Array(Number(length)); + data.set(calldata.slice(Number(offset), Number(offset + length))); + return Data.fromBytes(data); + }, + }, + }; const cursor = await dereference(pointer as Format.Pointer, { state }); const view = await cursor.view(state); const memory = step.memory ?? new Uint8Array(); @@ -309,14 +321,16 @@ async function regionsAt( if (next < 0) throw new Error(`no region ${name}`); }, /** Read the next region, which must be named `name` (and, in - * memory, at `offset` and `size` bytes long, if given) */ + * memory or calldata, at `offset` and `size` bytes long, if + * given) */ async read(name: string, offset?: bigint, size?: bigint) { const region = regions[next++]; if (!region || region.name !== name) { throw new Error(`expected region ${name}, got ${region?.name}`); } const data = await view.read(region); - if (region.location === "memory") { + if (region.location === "memory" || region.location === "calldata") { + const bytes = region.location === "memory" ? memory : calldata; const start = region.offset!.asUint(); const length = region.length!.asUint(); if (offset !== undefined && start !== offset) { @@ -326,9 +340,11 @@ async function regionsAt( throw new Error(`${name} is ${length} bytes, expected ${size}`); } const actual = new Uint8Array(Number(length)); - actual.set(memory.slice(Number(start), Number(start + length))); + actual.set(bytes.slice(Number(start), Number(start + length))); if (data.toHex() !== "0x" + Buffer.from(actual).toString("hex")) { - throw new Error(`${name} does not read memory at ${start}`); + throw new Error( + `${name} does not read ${region.location} at ${start}`, + ); } } return data; @@ -340,10 +356,10 @@ async function regionsAt( async function readScalar( pointer: unknown, name: string, - executor: Executor, + run: LocalsTrace, step: TraceStep, ): Promise { - const regions = await regionsAt(pointer, executor, step); + const regions = await regionsAt(pointer, run, step); regions.seek(name); return (await regions.read(name)).toHex(); } @@ -352,25 +368,34 @@ async function readScalar( * A reference-typed local's value at a step, by its layout: the * local's word holds the base address, the length word is at the base, * each element is in its word (a scalar right-aligned), and the data - * follows the length word, as long as the length says. + * follows the length word, as long as the length says. Bytes in + * calldata are the calldata from the offset in the word's high 16 + * bytes, as long as its low 16 bytes say. */ async function readReference( pointer: unknown, name: string, shape: Shape, - executor: Executor, + run: LocalsTrace, step: TraceStep, ): Promise { - const regions = await regionsAt(pointer, executor, step); + const regions = await regionsAt(pointer, run, step); regions.seek(name); + if (shape.kind === "calldata") { + const word = (await regions.read(name)).asUint(); + const offset = word >> 128n; + const length = word & ((1n << 128n) - 1n); + return (await regions.read(`${name}-data`, offset, length)).toHex(); + } + const decode = async ( shape: Shape, prefix: string, base: bigint, ): Promise => { const length = (await regions.read(`${prefix}-length`, base, 32n)).asUint(); - if (shape.kind === "bytes") { + if (shape.kind !== "array") { return (await regions.read(`${prefix}-data`, base + 32n, length)).toHex(); } const element = shape.element; 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 67cfcfdb63..d25d5cca85 100644 --- a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx +++ b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx @@ -130,6 +130,70 @@ history.push(7); // history: array in storage history[history.length] = 8; // reverts with Panic(0x32) ``` +### Bytes in calldata + +`msg.data` is `bytes calldata`, and so is a slice of it. A slice of +calldata refers to the calldata; nothing is copied. A `let` typed +`bytes calldata`, or without a type, refers to the calldata too: + +```bug +// setMotd(string): the text's ABI offset word, then its length word +let offset = msg.data[4:36] as bytes32 as uint256; +let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256; +let text: bytes calldata = msg.data[36 + offset:36 + offset + n]; +text.length; // n +text[0]; // CALLDATALOAD, then the first byte +text[1:3]; // bytes calldata, too +text as bytes4; // its first 4 bytes +let copy: bytes = text; // a copy in memory +``` + +The type says where the bytes are: `bytes` is always in memory, as in +a parameter, a return type or a struct field, and `bytes calldata` can +only be the type of a `let` or of a cast. Calldata is read only: +`text[0] = 1` is a type error, and so is assigning bytes in memory to a +`bytes calldata` local or casting them to `bytes calldata`. Bytes in +calldata are copied to memory (with `CALLDATACOPY`) wherever a value +must be in memory: in a `let` typed `bytes`, an assignment to a `bytes` +local, a cast to `bytes` or `string`, a function argument or return +value, and the argument of `keccak256`. Assigning to a `bytes` or +`string` storage variable stores the bytes as Solidity does, through +such a copy. + +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: + + + {`{ + "group": [ + { "name": "text", "location": "stack", "slot": 2 }, + { + "name": "text-data", + "location": "calldata", + "offset": { + "$quotient": [ + { "$read": "text" }, + "0x100000000000000000000000000000000" + ] + }, + "length": { + "$remainder": [ + { "$read": "text" }, + "0x100000000000000000000000000000000" + ] + } + } + ] +}`} + + ## Compilation pipeline BUG uses a multi-stage compilation pipeline: