Skip to content

Commit 3866cec

Browse files
committed
Add $concat operator to pointer expression schema
- Define result to preserve padding of variadic list of operand expressions - Document operator on website - Add TypeScript refence implementation types to @ethdebug/format - Implement operator in @ethdebug/pointers reference implementation
1 parent d67fe49 commit 3866cec

File tree

7 files changed

+144
-0
lines changed

7 files changed

+144
-0
lines changed

packages/format/src/types/pointer/pointer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export namespace Pointer {
225225
| Expression.Lookup
226226
| Expression.Read
227227
| Expression.Keccak256
228+
| Expression.Concat
228229
| Expression.Resize;
229230

230231
export const isExpression = (value: unknown): value is Expression =>
@@ -236,6 +237,7 @@ export namespace Pointer {
236237
Expression.isLookup,
237238
Expression.isRead,
238239
Expression.isKeccak256,
240+
Expression.isConcat,
239241
Expression.isResize
240242
].some(guard => guard(value));
241243

@@ -394,6 +396,12 @@ export namespace Pointer {
394396
export const isKeccak256 =
395397
makeIsOperation<"$keccak256", Keccak256>("$keccak256", isOperands);
396398

399+
export interface Concat {
400+
$concat: Expression[];
401+
}
402+
export const isConcat =
403+
makeIsOperation<"$concat", Concat>("$concat", isOperands);
404+
397405
export type Resize<N extends number = number> =
398406
| Resize.ToNumber<N>
399407
| Resize.ToWordsize;

packages/pointers/src/evaluate.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,69 @@ describe("evaluate", () => {
127127
.toEqual(Data.fromUint(42n % 0x1fn));
128128
});
129129

130+
describe("evaluates concat expressions", () => {
131+
it("concatenates hex literals", async () => {
132+
const expression: Pointer.Expression = {
133+
$concat: ["0x00", "0x00"]
134+
};
135+
expect(await evaluate(expression, options))
136+
.toEqual(Data.fromHex("0x0000"));
137+
});
138+
139+
it("concatenates multiple values preserving byte widths", async () => {
140+
const expression: Pointer.Expression = {
141+
$concat: ["0xdead", "0xbeef"]
142+
};
143+
expect(await evaluate(expression, options))
144+
.toEqual(Data.fromHex("0xdeadbeef"));
145+
});
146+
147+
it("returns empty data for empty operand list", async () => {
148+
const expression: Pointer.Expression = {
149+
$concat: []
150+
};
151+
expect(await evaluate(expression, options))
152+
.toEqual(Data.zero());
153+
});
154+
155+
it("preserves single operand unchanged", async () => {
156+
const expression: Pointer.Expression = {
157+
$concat: ["0xabcdef"]
158+
};
159+
expect(await evaluate(expression, options))
160+
.toEqual(Data.fromHex("0xabcdef"));
161+
});
162+
163+
it("concatenates variables", async () => {
164+
const expression: Pointer.Expression = {
165+
$concat: ["foo", "bar"]
166+
};
167+
// foo = 0x2a (42), bar = 0x1f
168+
expect(await evaluate(expression, options))
169+
.toEqual(Data.fromHex("0x2a1f"));
170+
});
171+
172+
it("concatenates nested expressions", async () => {
173+
const expression: Pointer.Expression = {
174+
$concat: [
175+
{ $sum: [1, 2] }, // 3 = 0x03
176+
"0xff"
177+
]
178+
};
179+
expect(await evaluate(expression, options))
180+
.toEqual(Data.fromHex("0x03ff"));
181+
});
182+
183+
it("preserves leading zeros in hex literals", async () => {
184+
const expression: Pointer.Expression = {
185+
$concat: ["0x0001", "0x0002"]
186+
};
187+
const result = await evaluate(expression, options);
188+
expect(result).toEqual(Data.fromHex("0x00010002"));
189+
expect(result.length).toBe(4);
190+
});
191+
});
192+
130193
// skipped because test does not perform proper padding
131194
it.skip("evaluates keccak256 expressions", async () => {
132195
const expression: Pointer.Expression = {

packages/pointers/src/evaluate.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export async function evaluate(
5858
return evaluateKeccak256(expression, options);
5959
}
6060

61+
if (Pointer.Expression.isConcat(expression)) {
62+
return evaluateConcat(expression, options);
63+
}
64+
6165
if (Pointer.Expression.isResize(expression)) {
6266
return evaluateResize(expression, options);
6367
}
@@ -219,6 +223,17 @@ async function evaluateKeccak256(
219223
return hash;
220224
}
221225

226+
async function evaluateConcat(
227+
expression: Pointer.Expression.Concat,
228+
options: EvaluateOptions
229+
): Promise<Data> {
230+
const operands = await Promise.all(expression.$concat.map(
231+
async expression => await evaluate(expression, options)
232+
));
233+
234+
return Data.zero().concat(...operands);
235+
}
236+
222237
async function evaluateResize(
223238
expression: Pointer.Expression.Resize,
224239
options: EvaluateOptions

packages/web/docs/implementation-guides/pointers/evaluating-expressions.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ use of hashing to allocate persistent data.
158158
sourceFile => sourceFile.getFunction("evaluateKeccak256")
159159
} />
160160

161+
## Evaluating concatenation
162+
163+
Byte concatenation is straightforward: recursively evaluate the operands and
164+
join them together, preserving the byte width of each operand.
165+
166+
<CodeListing
167+
packageName="@ethdebug/pointers"
168+
sourcePath="src/evaluate.ts"
169+
extract={
170+
sourceFile => sourceFile.getFunction("evaluateConcat")
171+
} />
172+
161173
## Evaluating property lookups
162174

163175
Pointer expressions can compose values taken from the properties of other,

packages/web/spec/pointer/expression.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ hash of the concatenation of bytes specified by the list.
9292
pointer="#/$defs/Keccak256"
9393
/>
9494

95+
## Bytes concatenation
96+
97+
An expression can be an object of form `{ "$concat": [...] }`, indicating
98+
that the value of the expression is the concatenation of bytes from each
99+
value in the list. The byte width of each operand is preserved; no padding
100+
is added or removed between operands.
101+
102+
This is useful for building composite byte sequences, for example when
103+
constructing storage slot keys or preparing data for hashing.
104+
105+
<SchemaViewer
106+
schema={{ id: "schema:ethdebug/format/pointer/expression" }}
107+
pointer="#/$defs/Concat"
108+
/>
109+
95110
## Resize operations
96111

97112
In certain situations, e.g. keccak256 hashes, it's crucially important to be

packages/web/src/schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ const pointerSchemaIndex: SchemaIndex = {
147147
title: "Keccak256 hash expression schema",
148148
anchor: "#keccak256-hashes"
149149
},
150+
Concat: {
151+
title: "Bytes concatenation schema",
152+
anchor: "#bytes-concatenation"
153+
},
150154
Resize: {
151155
title: "Resize operation schema",
152156
anchor: "#resize-operations"

schemas/pointer/expression.schema.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ oneOf:
1313
- $ref: "#/$defs/Lookup"
1414
- $ref: "#/$defs/Read"
1515
- $ref: "#/$defs/Keccak256"
16+
- $ref: "#/$defs/Concat"
1617
- $ref: "#/$defs/Resize"
1718

1819
$defs:
@@ -188,6 +189,32 @@ $defs:
188189
- 0
189190
- "0x00"
190191

192+
Concat:
193+
title: Concatenate values
194+
description: |
195+
An object of the form `{ "$concat": [...values] }`, indicating that this
196+
expression evaluates to the concatenation of bytes from each value.
197+
The byte width of each operand is preserved; no padding is added or
198+
removed between operands.
199+
type: object
200+
properties:
201+
$concat:
202+
title: Array of values to concatenate
203+
type: array
204+
items:
205+
$ref: "schema:ethdebug/format/pointer/expression"
206+
additionalProperties: false
207+
required:
208+
- $concat
209+
examples:
210+
- $concat:
211+
- "0x00"
212+
- "0x00"
213+
- $concat:
214+
- "0xdead"
215+
- "0xbeef"
216+
- $concat: []
217+
191218
Resize:
192219
title: Resize data
193220
description: |

0 commit comments

Comments
 (0)