Skip to content
Open
43 changes: 43 additions & 0 deletions packages/bugc/src/irgen/debug/pointers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";

import { mappingAccess, arrayElementAccess } from "./pointers.js";

/**
* `$keccak256` operands must be width-bearing bytes: the EVM hashes
* key‖slot as two 32-byte words, so a bare integer slot operand would
* hash the wrong number of bytes.
*/
describe("mappingAccess", () => {
it("wordsizes a literal slot operand", () => {
expect(mappingAccess(0, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
});
});

it("wordsizes an arithmetic slot operand", () => {
expect(mappingAccess({ $sum: [3, 1] }, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: { $sum: [3, 1] } }],
});
});

it("does not re-wrap a nested keccak256 slot operand", () => {
const inner = mappingAccess(1, 0xaaaa);
expect(mappingAccess(inner, 0xbbbb)).toEqual({
$keccak256: [{ $wordsized: 0xbbbb }, inner],
});
});
});

describe("arrayElementAccess", () => {
it("wordsizes the base slot of a dynamic array", () => {
expect(arrayElementAccess(2, "i", true)).toEqual({
$sum: [{ $keccak256: [{ $wordsized: 2 }] }, "i"],
});
});

it("does not hash the base slot of a fixed array", () => {
expect(arrayElementAccess(2, "i", false)).toEqual({
$sum: [2, "i"],
});
});
});
35 changes: 25 additions & 10 deletions packages/bugc/src/irgen/debug/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,19 @@ export function translateComputeSlotChain(
const inst = step.instruction;

if (inst.slotKind === "mapping") {
// Mapping access: keccak256(wordsized(key), slot)
// Mapping access: keccak256(wordsized(key), wordsized(slot))
// Try to convert key to expression
const keyExpr = valueToExpression(step.key);
if (keyExpr !== null) {
expr = {
$keccak256: [{ $wordsized: keyExpr }, expr],
};
expr = mappingAccess(expr, keyExpr);
}
// If we can't convert the key, skip this step (use current expr)
} else if (inst.slotKind === "array") {
// Array base: keccak256(slot)
// Array base: keccak256(wordsized(slot))
// Note: actual element access is done with binary.add afterward
// which we don't see in the compute_slot chain
expr = {
$keccak256: [expr],
$keccak256: [wordsized(expr)],
};
} else if (inst.slotKind === "field") {
// Struct field: slot + fieldSlotOffset
Expand Down Expand Up @@ -182,17 +180,34 @@ function valueToExpression(
return null;
}

/**
* Give an expression a 32-byte width for use as a `$keccak256` operand.
*
* `$keccak256` operands must be width-bearing bytes; a bare integer
* (literal, `$sum`, ...) is invalid there. A `$keccak256` result is
* already 32 bytes wide, so it is passed through unwrapped.
*/
function wordsized(
expression: Format.Pointer.Expression,
): Format.Pointer.Expression {
if (typeof expression === "object" && "$keccak256" in expression) {
return expression;
}
return { $wordsized: expression };
}

/**
* Helper to create pointer expression for mapping access
*
* Generates: keccak256(concat(key, slot))
* Generates: keccak256(wordsized(key) ++ wordsized(slot)), matching the
* EVM's hash over key‖slot as two 32-byte words
*/
export function mappingAccess(
slot: number | Format.Pointer.Expression,
key: Format.Pointer.Expression,
): Format.Pointer.Expression {
return {
$keccak256: [{ $wordsized: key }, slot],
$keccak256: [{ $wordsized: key }, wordsized(slot)],
};
}

Expand All @@ -208,9 +223,9 @@ export function arrayElementAccess(
isDynamic: boolean,
): Format.Pointer.Expression {
if (isDynamic) {
// Dynamic array: keccak256(slot) + index
// Dynamic array: keccak256(wordsized(slot)) + index
return {
$sum: [{ $keccak256: [baseSlot] }, index],
$sum: [{ $keccak256: [wordsized(baseSlot)] }, index],
};
} else {
// Fixed array: slot + index
Expand Down
21 changes: 13 additions & 8 deletions packages/bugc/src/irgen/debug/storage-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,9 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(wordsized(0x1234), 0)
// Should be: keccak256(wordsized(0x1234), wordsized(0))
expect(pointer).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, 0],
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
});
});

Expand All @@ -446,12 +446,17 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(wordsized(0xbbbb), keccak256(wordsized(0xaaaa), 1))
// Should be:
// keccak256(
// wordsized(0xbbbb),
// keccak256(wordsized(0xaaaa), wordsized(1)),
// )
// The inner keccak256 is already 32-byte bytes, so it is not wrapped.
expect(pointer).toEqual({
$keccak256: [
{ $wordsized: 0xbbbb },
{
$keccak256: [{ $wordsized: 0xaaaa }, 1],
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 1 }],
},
],
});
Expand All @@ -470,9 +475,9 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(2)
// Should be: keccak256(wordsized(2))
expect(pointer).toEqual({
$keccak256: [2],
$keccak256: [{ $wordsized: 2 }],
});
});

Expand Down Expand Up @@ -509,11 +514,11 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: sum(keccak256(wordsized(0xaaaa), 4), 2)
// Should be: sum(keccak256(wordsized(0xaaaa), wordsized(4)), 2)
expect(pointer).toEqual({
$sum: [
{
$keccak256: [{ $wordsized: 0xaaaa }, 4],
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 4 }],
},
2,
],
Expand Down
4 changes: 2 additions & 2 deletions packages/pointers/src/dereference/generate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Pointer } from "@ethdebug/format";
import type { Machine } from "#machine";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import type { Value } from "#evaluate";

import { Memo } from "./memo.js";
import { processPointer, type ProcessOptions } from "./process.js";
Expand Down Expand Up @@ -129,7 +129,7 @@ async function initializeProcessOptions({
const stackLengthChange = currentStackLength - initialStackLength;

const regions: Record<string, Cursor.Region> = {};
const variables: Record<string, Data> = {};
const variables: Record<string, Value> = {};

return {
templates,
Expand Down
4 changes: 1 addition & 3 deletions packages/pointers/src/dereference/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,7 @@ describe("dereference", () => {
expect(region).toEqual({
name: "item",
location: "memory",
offset: Data.fromUint(
Data.fromNumber(index).asUint() * 32n,
).padUntilAtLeast(1),
offset: Data.fromUint(BigInt(index) * 32n),
length: Data.fromNumber(32),
});
}
Expand Down
6 changes: 3 additions & 3 deletions packages/pointers/src/dereference/memo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Pointer } from "@ethdebug/format";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import type { Value } from "#evaluate";

/**
* A single state transition for processing on a stack
Expand Down Expand Up @@ -62,14 +62,14 @@ export namespace Memo {
*/
export interface SaveVariables {
kind: "save-variables";
variables: Record<string, Data>;
variables: Record<string, Value>;
}

/**
* Initialize a SaveVariables memo
*/
export const saveVariables = (
variables: Record<string, Data>,
variables: Record<string, Value>,
): SaveVariables => ({
kind: "save-variables",
variables,
Expand Down
19 changes: 9 additions & 10 deletions packages/pointers/src/dereference/process.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Pointer } from "@ethdebug/format";
import type { Machine } from "#machine";
import type { Cursor } from "#cursor";
import { Data } from "#data";
import { evaluate } from "#evaluate";
import { evaluate, Value } from "#evaluate";

import { Memo } from "./memo.js";
import { adjustStackLength, evaluateRegion } from "./region.js";
Expand All @@ -15,7 +14,7 @@ export interface ProcessOptions {
state: Machine.State;
stackLengthChange: bigint;
regions: Record<string, Cursor.Region>;
variables: Record<string, Data>;
variables: Record<string, Value>;
}

/**
Expand Down Expand Up @@ -101,13 +100,13 @@ async function* processList(
const { list } = collection;
const { count: countExpression, each, is } = list;

const count = (await evaluate(countExpression, options)).asUint();
const count = Value.toInteger(await evaluate(countExpression, options));

const memos: Memo[] = [];
for (let index = 0n; index < count; index++) {
memos.push(
Memo.saveVariables({
[each]: Data.fromUint(index),
[each]: Value.integer(index),
}),
);

Expand All @@ -123,7 +122,7 @@ async function* processConditional(
): Process {
const { if: ifExpression, then: then_, else: else_ } = collection;

const if_ = (await evaluate(ifExpression, options)).asUint();
const if_ = Value.toInteger(await evaluate(ifExpression, options));

if (if_) {
return [Memo.dereferencePointer(then_)];
Expand All @@ -142,15 +141,15 @@ async function* processScope(
const allVariables = {
...options.variables,
};
const newVariables: { [identifier: string]: Data } = {};
const newVariables: { [identifier: string]: Value } = {};
for (const [identifier, expression] of Object.entries(variableExpressions)) {
const data = await evaluate(expression, {
const value = await evaluate(expression, {
...options,
variables: allVariables,
});

allVariables[identifier] = data;
newVariables[identifier] = data;
allVariables[identifier] = value;
newVariables[identifier] = value;
}

return [Memo.saveVariables(newVariables), Memo.dereferencePointer(in_)];
Expand Down
6 changes: 3 additions & 3 deletions packages/pointers/src/dereference/region.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Pointer } from "@ethdebug/format";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import { evaluate, type EvaluateOptions } from "#evaluate";
import { evaluate, Value, type EvaluateOptions } from "#evaluate";

/**
* Evaluate all Pointer.Expression-value properties on a given region
Expand Down Expand Up @@ -55,15 +55,15 @@ export async function evaluateRegion<R extends Pointer.Region>(
const [property, expression] = expressionQueue.shift()!;

try {
const data = await evaluate(expression, {
const value = await evaluate(expression, {
...options,
regions: {
...options.regions,
$this: partialRegion,
},
});

evaluatedProperties[property as keyof R] = data;
evaluatedProperties[property as keyof R] = Value.toData(value);
} catch (error) {
if (
error instanceof Error &&
Expand Down
Loading
Loading