Skip to main content

Pointer types

Types and type guards for all kinds of pointers

This package provides the root Pointer type and accompanying Pointer namespace, which contains TypeScript type definitions and type guards for working with ethdebug/format/pointer objects.

The Pointer namespace is organized itself into namespaces in a nested manner, roughly to correspond to the JSON-Schema organization itself.

Types and type guards are available for all pointer schemas, i.e., for every different kind of region and collection.

A full source listing follows below, but see example usage to get a sense for how these types are organized:

Usage example
import { Pointer, isPointer } from "@ethdebug/pointers";

const region: Pointer.Region = { location: "stack", slot: 0 };
const group: Pointer.Collection.Group = { group: [region] };

isPointer(region); // true
Pointer.isRegion(region); // true
Pointer.isRegion(group); // false
Pointer.isCollection(group); // true
Pointer.Collection.isGroup(group); // true

Pointer.Expression

The Pointer namespace also contains the Pointer.Expression type and accompanying Pointer.Expression namespace. This namespace is similarly nested, also roughly to correspond to the root JSON-Schema.

See these quick examples to get a sense for this part of the type hierarchy:

Usage example
import { Pointer } from "@ethdebug/pointers";

const expression: Pointer.Expression = {
$sum: [0, 1]
}

Pointer.Expression.isKeccak256(expression); // false
Pointer.Expression.isArithmetic(expression); // true
Pointer.Expression.Arithmetic.isSum(expression); // true

Code listing

src/pointer.ts
export type Pointer =
| Pointer.Region
| Pointer.Collection;

export const isPointer = (value: unknown): value is Pointer =>
[
Pointer.isRegion,
Pointer.isCollection
].some(guard => guard(value));

export namespace Pointer {
export type Identifier = string;
export const isIdentifier = (value: unknown): value is Identifier =>
typeof value === "string" && /^[a-zA-Z_\\-]+[a-zA-Z0-9$_\\-]*$/.test(value);

export type Region =
| Region.Stack
| Region.Memory
| Region.Storage
| Region.Calldata
| Region.Returndata
| Region.Transient
| Region.Code;

export const isRegion = (value: unknown): value is Region =>
[
Region.isStack,
Region.isMemory,
Region.isStorage,
Region.isCalldata,
Region.isReturndata,
Region.isTransient,
Region.isCode
].some(guard => guard(value));

export namespace Region {
export interface Base {
name?: string;
location: string;
}
export const isBase = (value: unknown): value is Base =>
!!value &&
typeof value === "object" &&
(!("name" in value) || typeof value.name === "string") &&
"location" in value &&
typeof value.location === "string";

export type Name = Base["name"];

export type Stack =
& Base
& Scheme.Segment
& { location: "stack" };
export const isStack = (value: unknown): value is Stack =>
isBase(value) && Scheme.isSegment(value) && value.location === "stack";

export type Memory =
& Base
& Scheme.Slice
& { location: "memory" };
export const isMemory = (value: unknown): value is Memory =>
isBase(value) && Scheme.isSlice(value) && value.location === "memory";

export type Storage =
& Base
& Scheme.Segment
& { location: "storage" };
export const isStorage = (value: unknown): value is Storage =>
isBase(value) && Scheme.isSegment(value) && value.location === "storage";

export type Calldata =
& Base
& Scheme.Slice
& { location: "calldata" };
export const isCalldata = (value: unknown): value is Calldata =>
isBase(value) && Scheme.isSlice(value) && value.location === "calldata";

export type Returndata =
& Base
& Scheme.Slice
& { location: "returndata" };
export const isReturndata = (value: unknown): value is Returndata =>
isBase(value) && Scheme.isSlice(value) && value.location === "returndata";

export type Transient =
& Base
& Scheme.Segment
& { location: "transient" };
export const isTransient = (value: unknown): value is Transient =>
isBase(value) && Scheme.isSegment(value) && value.location === "transient";

export type Code =
& Base
& Scheme.Slice
& { location: "code" };
export const isCode = (value: unknown): value is Code =>
isBase(value) && Scheme.isSlice(value) && value.location === "code";
}

export namespace Scheme {
export interface Segment {
slot: Expression;
offset?: Expression;
length?: Expression;
}
export const isSegment = (value: unknown): value is Segment =>
!!value &&
typeof value === "object" &&
"slot" in value &&
isExpression(value.slot) &&
(!("offset" in value) || isExpression(value.offset)) &&
(!("length" in value) || isExpression(value.length));

export interface Slice {
offset: Expression;
length: Expression;
}

export const isSlice = (value: unknown): value is Slice =>
!!value &&
typeof value === "object" &&
"offset" in value &&
isExpression(value.offset) &&
"length" in value &&
isExpression(value.length);
}

export type Collection =
| Collection.Group
| Collection.List
| Collection.Conditional
| Collection.Scope;
export const isCollection = (value: unknown): value is Collection =>
[
Collection.isGroup,
Collection.isList,
Collection.isConditional,
Collection.isScope
].some(guard => guard(value));

export namespace Collection {
export interface Group {
group: Pointer[];
}
export const isGroup = (value: unknown): value is Group =>
!!value &&
typeof value === "object" &&
Object.keys(value).length === 1 &&
"group" in value &&
value.group instanceof Array &&
value.group.length >= 1 &&
value.group.every(isPointer);

export interface List {
list: {
count: Expression;
each: Identifier;
is: Pointer;
}
}
export const isList = (value: unknown): value is List =>
!!value &&
typeof value === "object" &&
Object.keys(value).length === 1 &&
"list" in value &&
!!value.list &&
typeof value.list === "object" &&
Object.keys(value.list).length === 3 &&
"count" in value.list &&
isExpression(value.list.count) &&
"each" in value.list &&
isIdentifier(value.list.each) &&
"is" in value.list &&
isPointer(value.list.is);

export interface Conditional {
if: Expression;
then: Pointer;
else?: Pointer;
}
export const isConditional = (value: unknown): value is Conditional =>
!!value &&
typeof value === "object" &&
"if" in value &&
isExpression(value.if) &&
"then" in value &&
isPointer(value.then) &&
(!("else" in value) || isPointer(value.else));

export interface Scope {
define: {
[identifier: string]: Expression;
}
in: Pointer;
}

export const isScope = (value: unknown): value is Scope =>
!!value &&
typeof value === "object" &&
"define" in value &&
typeof value.define === "object" && !!value.define &&
Object.keys(value.define).every(key => isIdentifier(key)) &&
"in" in value &&
isPointer(value.in);
}

export type Expression =
| Expression.Literal
| Expression.Constant
| Expression.Variable
| Expression.Arithmetic
| Expression.Lookup
| Expression.Read
| Expression.Keccak256
| Expression.Resize;

export const isExpression = (value: unknown): value is Expression =>
[
Expression.isLiteral,
Expression.isConstant,
Expression.isVariable,
Expression.isArithmetic,
Expression.isLookup,
Expression.isRead,
Expression.isKeccak256,
Expression.isResize
].some(guard => guard(value));

export namespace Expression {
export type Literal = number | `0x${string}`;
export const isLiteral = (value: unknown): value is Literal =>
typeof value === "number" ||
typeof value === "string" && /^0x[0-9a-fA-F]+$/.test(value);

export type Constant =
| "$wordsize";
export const isConstant = (value: unknown): value is Constant =>
typeof value === "string" && ["$wordsize"].includes(value);

export type Variable = string;
export const isVariable = (value: unknown): value is Variable =>
isIdentifier(value);

export type Arithmetic =
| Arithmetic.Sum
| Arithmetic.Difference
| Arithmetic.Product
| Arithmetic.Quotient
| Arithmetic.Remainder;

export const isArithmetic = (value: unknown): value is Arithmetic =>
[
Arithmetic.isSum,
Arithmetic.isDifference,
Arithmetic.isProduct,
Arithmetic.isQuotient,
Arithmetic.isRemainder
].some(guard => guard(value));

const makeIsOperation = <
O extends string,
T extends { [K in O]: any; }
>(
operation: O,
checkOperands: (operands: unknown) => operands is T[O]
) => (value: unknown): value is T =>
!!value &&
typeof value === "object" &&
Object.keys(value).length === 1 &&
operation in value &&
checkOperands(value[operation as keyof typeof value]);

export type Operands = Expression[];
export const isOperands =
(value: unknown): value is Expression[] =>
value instanceof Array && value.every(isExpression);

export namespace Arithmetic {
export type Operation =
| keyof Sum
| keyof Difference
| keyof Product
| keyof Quotient
| keyof Remainder;

export const isTwoOperands =
(value: unknown): value is [Expression, Expression] =>
isOperands(value) && value.length === 2;

export interface Sum {
$sum: Expression[];
}
export const isSum =
makeIsOperation<"$sum", Sum>("$sum", isOperands);

export interface Difference {
$difference: [Expression, Expression];
}
export const isDifference =
makeIsOperation<"$difference", Difference>("$difference", isTwoOperands);

export interface Product {
$product: Expression[];
}
export const isProduct =
makeIsOperation<"$product", Product>("$product", isOperands);

export interface Quotient {
$quotient: [Expression, Expression];
}
export const isQuotient =
makeIsOperation<"$quotient", Quotient>("$quotient", isTwoOperands);

export interface Remainder {
$remainder: [Expression, Expression];
}
export const isRemainder =
makeIsOperation<"$remainder", Remainder>("$remainder", isTwoOperands);
}

export type Reference =
| Identifier
| "$this";
export const isReference = (value: unknown): value is Reference =>
isIdentifier(value) || value === "$this";

export type Lookup =
| Lookup.Offset
| Lookup.Length
| Lookup.Slot;
export const isLookup = (value: unknown): value is Lookup =>
[
Lookup.isOffset,
Lookup.isLength,
Lookup.isSlot
].some(guard => guard(value));

export namespace Lookup {
export type Operation =
| keyof Offset
| keyof Length
| keyof Slot;

export type ForOperation<O extends Operation> =
& Lookup
& { [K in O]: any };

export const propertyFrom = <O extends Operation>(
operation: O
): "slot" | "offset" | "length" => {
return operation.slice(1) as "slot" | "offset" | "length";
}

export interface Offset {
".offset": Reference;
}
export const isOffset =
makeIsOperation<".offset", Offset>(".offset", isReference);

export interface Length {
".length": Reference;
}
export const isLength =
makeIsOperation<".length", Length>(".length", isReference);

export interface Slot {
".slot": Reference;
}
export const isSlot =
makeIsOperation<".slot", Slot>(".slot", isReference);
}

export interface Read {
$read: Reference
}
export const isRead = makeIsOperation<"$read", Read>("$read", isReference);

export interface Keccak256 {
$keccak256: Expression[];
}
export const isKeccak256 =
makeIsOperation<"$keccak256", Keccak256>("$keccak256", isOperands);

export type Resize<N extends number = number> =
| Resize.ToNumber<N>
| Resize.ToWordsize;
export const isResize = <N extends number>(
value: unknown
): value is Resize<N> =>
[
Resize.isToWordsize,
Resize.isToNumber,
].some(guard => guard(value));

export namespace Resize {
export type ToNumber<N extends number> = {
[K in `$sized${N}`]: Expression;
};
export const isToNumber = <N extends number>(
value: unknown
): value is ToNumber<N> => {
if (
!value ||
typeof value !== "object" ||
Object.keys(value).length !== 1
) {
return false;
}
const [key] = Object.keys(value);

return typeof key === "string" && /^\$sized([1-9]+[0-9]*)$/.test(key);
};

export type ToWordsize = {
$wordsized: Expression;
}
export const isToWordsize = (value: unknown): value is ToWordsize =>
!!value &&
typeof value === "object" &&
Object.keys(value).length === 1 &&
"$wordsized" in value &&
typeof value.$wordsized !== "undefined" &&
isExpression(value.$wordsized);


}
}
}