Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement @ethdebug/pointers TypeScript package to serve as debugger-side reference implementation of ethdebug/format pointers #90

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading