Skip to content

Commit 79c5fb3

Browse files
committed
bugc: write bytes in calldata as bytes calldata
`let text: bytes calldata = msg.data[a:b]` is a calldata reference, as an untyped slice of msg.data is; `let text: bytes = ...` copies to memory. `bytes calldata` can only be the type of a let or of a cast.
1 parent e66759a commit 79c5fb3

13 files changed

Lines changed: 303 additions & 110 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,24 @@ support. Changes to the specification itself are tracked in the root
109109
function whose `return` is only inside a loop needs one after it.
110110
Before, such a function compiled, and the compiler ended it with a
111111
`return` that gave no value ([#352]).
112-
- `msg.data` and a slice of it are now `bytes` in calldata, and a slice
113-
of calldata refers to the calldata instead of copying it, as in
114-
`let text = msg.data[68:68 + n];`. Such a value is one word, with the
115-
offset in its high 128 bits and the length in its low 128 bits. `.length`,
116-
`text[i]` (a `CALLDATALOAD`), slicing and a cast to `bytesN` read the
117-
calldata. A `let` without a type keeps the calldata's type; a `let`
118-
typed `bytes`, an assignment to a `bytes` local in memory or to
119-
storage, a cast to `bytes` or `string`, a function argument or return
120-
value, and the argument of `keccak256` copy the bytes to memory. A
121-
local's pointer names its word and then a `calldata` region whose
122-
offset and length are `$quotient` and `$remainder` of that word by
123-
2^128, so it reads exactly the local's bytes, even when its bounds are
124-
computed. Writing to bytes in calldata, or assigning memory bytes to a
125-
local in calldata, is a type error. Before, every slice was copied to
126-
memory, and a local's pointer read the copy ([#367]).
112+
- A new type, `bytes calldata`, is bytes in calldata, as in Solidity.
113+
`msg.data` and a slice of it have this type, and a slice of calldata
114+
refers to the calldata instead of copying it, as in
115+
`let text: bytes calldata = msg.data[68:68 + n];` or
116+
`let text = msg.data[68:68 + n];`. A `let` typed `bytes` copies the
117+
bytes to memory. `bytes calldata` can only be the type of a `let` or
118+
of a cast. Such a value is one word, with the offset in its high 128
119+
bits and the length in its low 128 bits. `.length`, `text[i]` (a
120+
`CALLDATALOAD`), slicing and a cast to `bytesN` read the calldata. An
121+
assignment to a `bytes` local or to storage, a cast to `bytes` or
122+
`string`, a function argument or return value, and the argument of
123+
`keccak256` copy the bytes to memory. A local's pointer names its word
124+
and then a `calldata` region whose offset and length are `$quotient`
125+
and `$remainder` of that word by 2^128, so it reads exactly the local's
126+
bytes, even when its bounds are computed. Writing to bytes in
127+
calldata, or assigning or casting memory bytes to `bytes calldata`, is
128+
a type error. Before, every slice was copied to memory, and a local's
129+
pointer read the copy ([#367]).
127130

128131
### Fixed
129132

‎packages/bugc/src/ast/spec.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,16 +464,29 @@ export namespace Type {
464464
return { id, kind: "type:elementary:bool", loc: loc ?? null };
465465
}
466466

467+
/** `bytesN`, `bytes`, or `bytes calldata` (`location`) */
467468
export interface Bytes extends Elementary.Base {
468469
kind: "type:elementary:bytes";
469470
size?: number;
471+
location?: "calldata";
470472
}
471473

472474
export const isBytes = (type: Type.Base): type is Bytes =>
473475
type.kind === "type:elementary:bytes";
474476

475-
export function bytes(id: Id, size?: number, loc?: SourceLocation): Bytes {
476-
return { id, kind: "type:elementary:bytes", size, loc: loc ?? null };
477+
export function bytes(
478+
id: Id,
479+
size?: number,
480+
loc?: SourceLocation,
481+
location?: "calldata",
482+
): Bytes {
483+
return {
484+
id,
485+
kind: "type:elementary:bytes",
486+
size,
487+
...(location ? { location } : {}),
488+
loc: loc ?? null,
489+
};
477490
}
478491

479492
export interface String extends Elementary.Base {

‎packages/bugc/src/evmgen/calldata-slices.test.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,19 @@ function stored(text: string, slot: bigint): Map<bigint, bigint> {
4040
return slots;
4141
}
4242

43-
// The text's bounds come from its ABI offset and length words
44-
const decode = `
43+
/**
44+
* Declare `text`, with an annotation (`: bytes calldata` or `: bytes`)
45+
* or none. Its bounds come from its ABI offset and length words.
46+
*/
47+
const decodeAs = (annotation: string) => `
4548
let offset = msg.data[4:36] as bytes32 as uint256;
4649
let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256;
47-
let text = msg.data[36 + offset:36 + offset + n];`;
50+
let text${annotation} = msg.data[36 + offset:36 + offset + n];`;
51+
52+
const decode = decodeAs("");
53+
54+
// A calldata reference untyped or typed so, and a copy in memory
55+
const annotations = ["", ": bytes calldata", ": bytes"];
4856

4957
const texts = [
5058
"",
@@ -55,8 +63,11 @@ const texts = [
5563
];
5664

5765
describe.each(levels)("a slice of calldata at O%i", (level) => {
58-
for (const text of texts) {
59-
const label = `${text.length} bytes`;
66+
for (const [text, annotation] of texts.flatMap((text) =>
67+
annotations.map((annotation) => [text, annotation] as const),
68+
)) {
69+
const label = `${text.length} bytes, \`let text${annotation}\``;
70+
const decode = decodeAs(annotation);
6071

6172
it(`reads its length and bytes from calldata, ${label}`, async () => {
6273
const result = await executeProgram(

‎packages/bugc/src/evmgen/debug/local-variables.soundness.test.ts‎

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,42 @@ function setMotd(text: string): string {
422422
);
423423
}
424424

425+
// `text` refers to the calldata, untyped or typed so, or is a copy in
426+
// memory
427+
const annotations = ["", ": bytes calldata", ": bytes"];
428+
429+
/**
430+
* A slice of calldata refers to the calldata. Its bounds here are
431+
* computed from the ABI offset and length words of a string argument,
432+
* and its pointer reads exactly its bytes (or, typed `bytes`, the
433+
* copy's).
434+
*/
435+
function setMotdProgram(annotation: string): LocalsProgram {
436+
return {
437+
name: `a calldata slice with computed bounds, \`let text${annotation}\``,
438+
source: `name SetMotd;
439+
storage { [0] motd: string; [1] r: uint256; }
440+
create { r = 1; }
441+
code {
442+
let offset = msg.data[4:36] as bytes32 as uint256;
443+
let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256;
444+
let text${annotation} = msg.data[36 + offset:36 + offset + n];
445+
motd = text as string;
446+
if (r > 0) { r = text.length + text[1]; }
447+
}`,
448+
calldata: setMotd(motd),
449+
locals: {
450+
offset: { values: [32n] },
451+
n: { values: [BigInt(motd.length)] },
452+
text: {
453+
shape: { kind: annotation === ": bytes" ? "bytes" : "calldata" },
454+
values: [textBytes(motd)],
455+
everyLevel: true,
456+
},
457+
},
458+
};
459+
}
460+
425461
const programs: LocalsProgram[] = [
426462
{
427463
name: "straight line",
@@ -1054,32 +1090,7 @@ code {
10541090
// Called with no calldata, so a longer slice would revert
10551091
locals: { b: { shape: { kind: "calldata" }, values: ["0x"] } },
10561092
},
1057-
{
1058-
// A slice of calldata refers to the calldata. Its bounds here are
1059-
// computed from the ABI offset and length words of a string
1060-
// argument, and its pointer reads exactly its bytes.
1061-
name: "a calldata slice with computed bounds",
1062-
source: `name SetMotd;
1063-
storage { [0] motd: string; [1] r: uint256; }
1064-
create { r = 1; }
1065-
code {
1066-
let offset = msg.data[4:36] as bytes32 as uint256;
1067-
let n = msg.data[4 + offset:36 + offset] as bytes32 as uint256;
1068-
let text = msg.data[36 + offset:36 + offset + n];
1069-
motd = text as string;
1070-
if (r > 0) { r = text.length + text[1]; }
1071-
}`,
1072-
calldata: setMotd(motd),
1073-
locals: {
1074-
offset: { values: [32n] },
1075-
n: { values: [BigInt(motd.length)] },
1076-
text: {
1077-
shape: { kind: "calldata" },
1078-
values: [textBytes(motd)],
1079-
everyLevel: true,
1080-
},
1081-
},
1082-
},
1093+
...annotations.map(setMotdProgram),
10831094
{
10841095
// A slice longer than a word: its pointer reads every byte
10851096
name: "a long bytes slice",

‎packages/bugc/src/parser/parser.ts‎

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -376,33 +376,47 @@ let statement: P.Parser<Ast.Statement>;
376376
* Type Parsers
377377
*/
378378

379-
// Elementary types with location
379+
// Elementary types with location. Dynamic bytes may be followed by
380+
// `calldata`: bytes in calldata
380381
const elementaryType = located(
381-
Lang.elementaryType.map((name: string) => {
382-
// Parse the type name to extract kind and bits
383-
if (name.startsWith("uint")) {
384-
const bits = parseInt(name.substring(4), 10);
385-
return Ast.Type.Elementary.uint(PENDING_ID, bits);
386-
} else if (name.startsWith("int")) {
387-
const bits = parseInt(name.substring(3), 10);
388-
return Ast.Type.Elementary.int(PENDING_ID, bits);
389-
} else if (name.startsWith("bytes") && name !== "bytes") {
390-
const size = parseInt(name.substring(5), 10);
391-
return Ast.Type.Elementary.bytes(PENDING_ID, size);
392-
} else if (name === "address") {
393-
return Ast.Type.Elementary.address(PENDING_ID);
394-
} else if (name === "bool") {
395-
return Ast.Type.Elementary.bool(PENDING_ID);
396-
} else if (name === "string") {
397-
return Ast.Type.Elementary.string(PENDING_ID);
398-
} else if (name === "bytes") {
399-
return Ast.Type.Elementary.bytes(PENDING_ID);
400-
}
401-
// This should never happen as elementaryTypeName parser ensures valid names
402-
throw new Error(`Unknown elementary type: ${name}`);
403-
}),
382+
Lang.elementaryType.chain((name: string) =>
383+
name === "bytes"
384+
? Lang.keyword("calldata")
385+
.atMost(1)
386+
.map((calldata) =>
387+
Ast.Type.Elementary.bytes(
388+
PENDING_ID,
389+
undefined,
390+
undefined,
391+
calldata.length > 0 ? "calldata" : undefined,
392+
),
393+
)
394+
: P.succeed(elementaryTypeOf(name)),
395+
),
404396
);
405397

398+
function elementaryTypeOf(name: string): Ast.Type.Elementary {
399+
// Parse the type name to extract kind and bits
400+
if (name.startsWith("uint")) {
401+
const bits = parseInt(name.substring(4), 10);
402+
return Ast.Type.Elementary.uint(PENDING_ID, bits);
403+
} else if (name.startsWith("int")) {
404+
const bits = parseInt(name.substring(3), 10);
405+
return Ast.Type.Elementary.int(PENDING_ID, bits);
406+
} else if (name.startsWith("bytes") && name !== "bytes") {
407+
const size = parseInt(name.substring(5), 10);
408+
return Ast.Type.Elementary.bytes(PENDING_ID, size);
409+
} else if (name === "address") {
410+
return Ast.Type.Elementary.address(PENDING_ID);
411+
} else if (name === "bool") {
412+
return Ast.Type.Elementary.bool(PENDING_ID);
413+
} else if (name === "string") {
414+
return Ast.Type.Elementary.string(PENDING_ID);
415+
}
416+
// This should never happen as elementaryTypeName parser ensures valid names
417+
throw new Error(`Unknown elementary type: ${name}`);
418+
}
419+
406420
// Reference type (identifier in type position)
407421
const referenceType = located(
408422
Lang.identifier.map((name: string) => Ast.Type.reference(PENDING_ID, name)),

‎packages/bugc/src/parser/slice.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,32 @@ describe("Slice expressions", () => {
9292
expect(sliced.kind).toBe("expression:access:slice");
9393
}
9494
});
95+
96+
test("parses `bytes calldata` as a type", () => {
97+
const result = parse(`
98+
name Test;
99+
code {
100+
let t: bytes calldata = msg.data[0:4];
101+
let u = t as bytes calldata;
102+
let m: bytes = t;
103+
let calldata = 1;
104+
}
105+
`);
106+
if (!result.success) throw new Error("Parse failed");
107+
108+
const types = (result.value.body?.items ?? []).map((item) => {
109+
const decl = (item as Ast.Statement.Declare).declaration;
110+
const { initializer, type } = decl as Ast.Declaration.Variable;
111+
return Ast.Expression.isCast(initializer!)
112+
? initializer.targetType
113+
: type;
114+
});
115+
expect(types).toMatchObject([
116+
{ kind: "type:elementary:bytes", location: "calldata" },
117+
{ kind: "type:elementary:bytes", location: "calldata" },
118+
{ kind: "type:elementary:bytes" },
119+
undefined,
120+
]);
121+
expect(types[2]).not.toHaveProperty("location");
122+
});
95123
});

‎packages/bugc/src/typechecker/blocks.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { Symbol as BugSymbol } from "./symbols.js";
55
import type { Context, Report } from "./context.js";
66
import { enterFunctionScope } from "./symbols.js";
77
import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js";
8-
import { resolveTypeWithBindings } from "./declarations.js";
8+
import {
9+
calldataTypeError,
10+
calldataTypeNode,
11+
resolveTypeWithBindings,
12+
} from "./declarations.js";
913
import { isAssignable } from "./assignable.js";
1014
import { alwaysReturns } from "./returns.js";
1115

@@ -320,6 +324,8 @@ export const blockChecker: Pick<
320324

321325
// Also process the type node to record bindings for type references
322326
if (node.type) {
327+
const calldata = calldataTypeNode(node.type);
328+
if (calldata) errors.push(calldataTypeError(calldata));
323329
const typeResult = resolveTypeWithBindings(
324330
node.type,
325331
context.structs,
@@ -374,6 +380,12 @@ export const blockChecker: Pick<
374380
// Determine the variable's type
375381
let type: Type;
376382
if (node.type) {
383+
// `bytes calldata` is only the whole type
384+
const calldata = calldataTypeNode(node.type);
385+
if (calldata && calldata !== node.type) {
386+
errors.push(calldataTypeError(calldata));
387+
}
388+
377389
// If a type is explicitly declared, use it and record bindings
378390
const typeResult = resolveTypeWithBindings(
379391
node.type,

0 commit comments

Comments
 (0)