-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #90 from ethdebug/pointers
Implement @ethdebug/pointers TypeScript package to serve as debugger-side reference implementation of ethdebug/format pointers
- Loading branch information
Showing
57 changed files
with
6,766 additions
and
1,255 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
}; | ||
} | ||
}; | ||
} |
Oops, something went wrong.