Skip to content

Commit

Permalink
Merge pull request #90 from ethdebug/pointers
Browse files Browse the repository at this point in the history
Implement @ethdebug/pointers TypeScript package to serve as debugger-side reference implementation of ethdebug/format pointers
  • Loading branch information
gnidan authored Jun 20, 2024
2 parents 60d7296 + 44ff687 commit 9c335cc
Show file tree
Hide file tree
Showing 57 changed files with 6,766 additions and 1,255 deletions.
3 changes: 2 additions & 1 deletion bin/start
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ else
fi

# Run the commands with concurrently
concurrently --names=format,web,jest \
concurrently --names=format,pointers,web,jest \
"cd ./packages/format && yarn watch" \
"cd ./packages/pointers && yarn watch" \
"cd ./packages/web && yarn start $NO_OPEN_FLAG" \
"yarn test --watchAll"

2 changes: 2 additions & 0 deletions packages/pointers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
7 changes: 7 additions & 0 deletions packages/pointers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @ethdebug/pointers

_This NPM package contains a reference implementation for dereferencing
**ethdebug/format** [pointers](https://ethdebug.github.io/format/spec/pointer/overview)._

:warning: This package is currently unpublished until ethdebug/format is more
complete.
21 changes: 21 additions & 0 deletions packages/pointers/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
extensionsToTreatAsEsm: [".ts"],
moduleFileExtensions: ["ts", "js"],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
modulePathIgnorePatterns: ["<rootDir>/dist/"],
transform: {
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
};
28 changes: 28 additions & 0 deletions packages/pointers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@ethdebug/pointers",
"version": "0.1.0-0",
"description": "Reference implementation for ethdebug/format pointers",
"main": "dist/src/index.js",
"type": "module",
"license": "MIT",
"scripts": {
"prepare": "tsc",
"watch": "yarn prepare --watch",
"test": "node --experimental-vm-modules $(yarn bin jest)"
},
"devDependencies": {
"@ethdebug/format": "^0.1.0-0",
"@jest/globals": "^29.7.0",
"chalk": "^5.3.0",
"cli-highlight": "^2.1.11",
"ganache": "7.9.x",
"jest": "^29.7.0",
"solc": "^0.8.26",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"ethereum-cryptography": "^2.1.3"
}
}
71 changes: 71 additions & 0 deletions packages/pointers/src/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Machine } from "./machine.js";
import type { Pointer } from "./pointer.js";
import type { Data } from "./data.js";

/**
* The result of dereferencing a pointer
*/
export interface Cursor {
view(state: Machine.State): Promise<Cursor.View>;
}

export namespace Cursor {
/**
* The result of viewing a Cursor with a given Machine.State
*/
export interface View {
/**
* A collection of concrete Cursor.Regions; this is a plain array of
* regions and also provides filtering/lookup of regions by name
* (according to the scoping rules outlined in the specification)
*/
regions: Cursor.Regions;

/**
* Read bytes from the machine state corresponding to the bytes range
* for a particular concrete Cursor.Region
*/
read(region: Cursor.Region): Promise<Data>;
}

/**
* A Pointer region where all dynamic expressions have been replaced with
* concrete bytes values.
*/
export type Region<R extends Pointer.Region = Pointer.Region> = {
[K in keyof R]: K extends "slot" | "offset" | "length"
? R[K] extends Pointer.Expression
? Data
: R[K] extends Pointer.Expression | undefined
? Data | undefined
: R[K]
: R[K];
}

/**
* A collection of concrete regions.
*
* This collection serves as a plain array of regions, for simple iteration
* and whatever filtering.
*
* It also provides a couple interfaces of its own for accessing regions by
* name.
*/
export type Regions =
& Cursor.Region[]
& {
/**
* Obtain an ordered list of all regions with a particular name.
*
* This is useful, e.g., when looking to concatenate a series of
* sequential regions that were generated by index from a list
* collection
*/
named(name: string): Cursor.Region[];

/**
* Retrieve the last concrete region generated with a particular name
*/
lookup: { [name: string]: Cursor.Region };
};
}
62 changes: 62 additions & 0 deletions packages/pointers/src/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, describe, it } from "@jest/globals";

import { Data } from "./data.js";

describe("Data", () => {
describe(".prototype.asUint()", () => {
it("correctly converts to integers (big endian)", () => {
const data = new Data([0x01, 0x00]);

expect(`${data.asUint()}`).toBe("256");
});
});

describe(".fromUint()", () => {
it("correctly creates Data instances from bigints", () => {
const data1 = Data.fromUint(0n);
expect(data1).toEqual(new Data([]));

const data2 = Data.fromUint(255n);
expect(data2).toEqual(new Data([0xff]));

const data3 = Data.fromUint(256n);
expect(data3).toEqual(new Data([0x01, 0x00]));

const data4 = Data.fromUint(1234567890n);
expect(data4).toEqual(new Data([0x49, 0x96, 0x02, 0xd2]));
});
});

describe(".fromNumber()", () => {
it("correctly creates Data instances from numbers", () => {
const data1 = Data.fromNumber(0);
expect(data1).toEqual(Data.zero());

const data2 = Data.fromNumber(255);
expect(data2).toEqual(new Data([0xff]));

const data3 = Data.fromNumber(256);
expect(data3).toEqual(new Data([0x01, 0x00]));
});
});

describe(".fromHex()", () => {
it("correctly creates Data instances from hex strings", () => {
const data1 = Data.fromHex("0x00");
expect(data1).toEqual(new Data([0x00]));

const data2 = Data.fromHex("0xff");
expect(data2).toEqual(new Data([0xff]));

const data3 = Data.fromHex("0x0100");
expect(data3).toEqual(new Data([0x01, 0x00]));

const data4 = Data.fromHex("0x499602d2");
expect(data4).toEqual(new Data([0x49, 0x96, 0x02, 0xd2]));
});

it("throws an error for invalid hex string format", () => {
expect(() => Data.fromHex("ff")).toThrow("Invalid hex string format. Expected \"0x\" prefix.");
});
});
});
61 changes: 61 additions & 0 deletions packages/pointers/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { toHex } from "ethereum-cryptography/utils";

export class Data extends Uint8Array {
static zero(): Data {
return new Data([]);
}

static fromUint(value: bigint): Data {
if (value === 0n) {
return this.zero();
}

const byteCount = Math.ceil(Number(value.toString(2).length) / 8);
const bytes = new Uint8Array(byteCount);
for (let i = byteCount - 1; i >= 0; i--) {
bytes[i] = Number(value & 0xffn);
value >>= 8n;
}
return new Data(bytes);
}

static fromNumber(value: number): Data {
const byteCount = Math.ceil(Math.log2(value + 1) / 8);
const bytes = new Uint8Array(byteCount);
for (let i = byteCount - 1; i >= 0; i--) {
bytes[i] = value & 0xff;
value >>= 8;
}
return new Data(bytes);
}

static fromHex(hex: string): Data {
if (!hex.startsWith('0x')) {
throw new Error('Invalid hex string format. Expected "0x" prefix.');
}
const bytes = new Uint8Array(hex.length / 2 - 1);
for (let i = 2; i < hex.length; i += 2) {
bytes[i / 2 - 1] = parseInt(hex.slice(i, i + 2), 16);
}
return new Data(bytes);
}

static fromBytes(bytes: Uint8Array): Data {
return new Data(bytes);
}

asUint(): bigint {
const bits = 8n;

let value = 0n;
for (const byte of this.values()) {
const byteValue = BigInt(byte)
value = (value << bits) + byteValue
}
return value;
}

toHex(): string {
return `0x${toHex(this)}`;
}
}
75 changes: 75 additions & 0 deletions packages/pointers/src/dereference/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Machine } from "../machine.js";
import type { Cursor } from "../cursor.js";
import { read } from "../read.js";

export function createCursor(
simpleCursor: (state: Machine.State) => AsyncIterable<Cursor.Region>
): Cursor {
return {
async view(state: Machine.State) {
const list = [];
for await (const region of simpleCursor(state)) {
list.push(region);
}

const named: { [name: string]: Cursor.Region[] } = {};
const current: { [name: string]: Cursor.Region } = {};

const propertyFlags = {
writable: false,
enumerable: false,
configurable: false
} as const;

const regions: Cursor.Regions = Object.create(Array.prototype, {
length: {
value: list.length,
...propertyFlags
}
});

for (const [index, region] of list.entries()) {
Object.defineProperty(regions, index, {
value: region,
...propertyFlags,
enumerable: true,
});

if (typeof region.name === "string") {
if (!(region.name in named)) {
named[region.name] = [];
}
named[region.name].push(region);
current[region.name] = region;
}
}

for (const [name, region] of Object.entries(current)) {
Object.defineProperty(regions, name, {
value: region,
...propertyFlags
});
}

Object.defineProperties(regions, {
named: {
value: (name: string) => named[name] || [],
...propertyFlags
},
lookup: {
value: {
...current
},
...propertyFlags
}
});

return {
regions,
async read(region: Cursor.Region) {
return await read(region, { state });
}
};
}
};
}
Loading

0 comments on commit 9c335cc

Please sign in to comment.