Skip to content

Commit

Permalink
docs(common,store,world): comment codegen [L-02] (#2185)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
  • Loading branch information
dk1a and holic committed Jan 25, 2024
1 parent 0f48474 commit bcf22f5
Show file tree
Hide file tree
Showing 18 changed files with 273 additions and 17 deletions.
76 changes: 64 additions & 12 deletions packages/common/src/codegen/render-solidity/common.ts
Expand Up @@ -9,6 +9,9 @@ import {
} from "./types";
import { posixPath } from "../utils";

/**
* Common header for all codegenerated solidity files
*/
export const renderedSolidityHeader = `// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
Expand All @@ -29,17 +32,28 @@ export function renderArguments(args: (string | undefined)[]): string {
return internalRenderList(",", filteredArgs, (arg) => arg);
}

interface RenderedCommonData {
/** `_tableId` variable prefixed with its type (empty string if absent) */
_typedTableId: string;
/** Comma-separated table key names prefixed with their types (empty string if 0 keys) */
_typedKeyArgs: string;
/** Definition and initialization of the dynamic `_keyTuple` bytes32 array */
_keyTupleDefinition: string;
}

/**
* Renders some solidity statements commonly used within table libraries
* @param param0.staticResourceData static data about the table library
* @param param0.keyTuple key tuple of the table library
* @returns Rendered statement strings
*/
export function renderCommonData({
staticResourceData,
keyTuple,
}: {
staticResourceData?: StaticResourceData;
keyTuple: RenderKeyTuple[];
}): {
_typedTableId: string;
_typedKeyArgs: string;
_keyTupleDefinition: string;
} {
}): RenderedCommonData {
// static resource means static tableId as well, and no tableId arguments
const _typedTableId = staticResourceData ? "" : "ResourceId _tableId";
const _typedKeyArgs = renderArguments(keyTuple.map(({ name, typeWithLocation }) => `${typeWithLocation} ${name}`));
Expand Down Expand Up @@ -118,15 +132,28 @@ export function renderAbsoluteImports(imports: AbsoluteImportDatum[]): string {
return renderedImports.join("\n");
}

interface RenderWithStoreCallbackData {
/** `_store` variable prefixed with its type (undefined if library name) */
_typedStore: string | undefined;
/** `_store` variable (undefined if library name) */
_store: string;
/** Empty string if storeArgument is false, otherwise `" (using the specified store)"` */
_commentSuffix: string;
/** Prefix to differentiate different kinds of store usage within methods */
_methodNamePrefix: string;
/** Whether FieldLayout variable should be passed to store methods */
_useExplicitFieldLayout?: boolean;
}

/**
* Renders several versions of the callback's result, which access Store in different ways
* @param storeArgument whether to render a version with `IStore _store` as an argument
* @param callback renderer for a method which uses store
* @returns Concatenated results of all callback calls
*/
export function renderWithStore(
storeArgument: boolean,
callback: (data: {
_typedStore: string | undefined;
_store: string;
_commentSuffix: string;
_methodNamePrefix: string;
_useExplicitFieldLayout?: boolean;
}) => string
callback: (data: RenderWithStoreCallbackData) => string
): string {
let result = "";
result += callback({ _typedStore: undefined, _store: "StoreSwitch", _commentSuffix: "", _methodNamePrefix: "" });
Expand All @@ -152,6 +179,13 @@ export function renderWithStore(
return result;
}

/**
* Renders several versions of the callback's result, which have different method name suffixes
* @param withSuffixlessFieldMethods whether to render methods with an empty suffix
* @param fieldName name of the field which the methods access, used for a suffix
* @param callback renderer for a method to be suffixed
* @returns Concatenated results of all callback calls
*/
export function renderWithFieldSuffix(
withSuffixlessFieldMethods: boolean,
fieldName: string,
Expand All @@ -168,6 +202,10 @@ export function renderWithFieldSuffix(
return result;
}

/**
* Renders `_tableId` variable definition and initialization, and its alias which uses the provided `tableIdName`.
* @param param0 static resource data needed to construct the table id
*/
export function renderTableId({
namespace,
name,
Expand All @@ -188,6 +226,11 @@ export function renderTableId({
`;
}

/**
* Renders solidity typecasts to get from the given type to `bytes32`
* @param name variable name to be typecasted
* @param param1 type data
*/
export function renderValueTypeToBytes32(
name: string,
{ typeUnwrap, internalTypeId }: Pick<RenderType, "typeUnwrap" | "internalTypeId">
Expand All @@ -211,10 +254,16 @@ export function renderValueTypeToBytes32(
}
}

/**
* Whether the storage representation of the given solidity type is left aligned
*/
export function isLeftAligned(field: Pick<RenderType, "internalTypeId">): boolean {
return /^bytes\d{1,2}$/.test(field.internalTypeId);
}

/**
* The number of padding bits in the storage representation of a right-aligned solidity type
*/
export function getLeftPaddingBits(field: Pick<RenderType, "internalTypeId" | "staticByteLength">): number {
if (isLeftAligned(field)) {
return 0;
Expand All @@ -223,6 +272,9 @@ export function getLeftPaddingBits(field: Pick<RenderType, "internalTypeId" | "s
}
}

/**
* Internal helper to render `lineTerminator`-separated list of items mapped by `renderItem`
*/
function internalRenderList<T>(
lineTerminator: string,
list: T[],
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/codegen/render-solidity/renderEnums.ts
@@ -1,6 +1,9 @@
import { renderArguments, renderList, renderedSolidityHeader } from "./common";
import { RenderEnum } from "./types";

/**
* Render a list of enum data as solidity enum definitions
*/
export function renderEnums(enums: RenderEnum[]): string {
let result = renderedSolidityHeader;

Expand Down
19 changes: 19 additions & 0 deletions packages/common/src/codegen/render-solidity/renderTypeHelpers.ts
@@ -1,5 +1,8 @@
import { RenderField, RenderKeyTuple, RenderType } from "./types";

/**
* Renders the necessary helper functions to typecast to/from the types of given fields and keys
*/
export function renderTypeHelpers(options: { fields: RenderField[]; keyTuple: RenderKeyTuple[] }): string {
const { fields, keyTuple } = options;

Expand Down Expand Up @@ -59,6 +62,14 @@ function getWrappingHelpers(array: RenderType[]): string[] {
return [...wrappers.values(), ...unwrappers.values()];
}

/**
* Renders a function to cast a dynamic array to a static array.
* @param functionName name of the function to be rendered
* @param elementType type of the array's element
* @param staticLength length of the static array
* @param internalTypeId solidity type name of the dynamic array
* @returns
*/
function renderWrapperStaticArray(
functionName: string,
elementType: string,
Expand Down Expand Up @@ -96,6 +107,14 @@ function renderWrapperStaticArray(
`;
}

/**
* Renders a function to cast a static array to a dynamic array.
* @param functionName name of the function to be rendered
* @param elementType type of the array's element
* @param staticLength length of the static array
* @param internalTypeId solidity type name of the dynamic array
* @returns
*/
function renderUnwrapperStaticArray(
functionName: string,
elementType: string,
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/codegen/render-solidity/types.ts
Expand Up @@ -14,13 +14,18 @@ export type ImportDatum = AbsoluteImportDatum | RelativeImportDatum;
export interface StaticResourceData {
/** Name of the table id constant to render. */
tableIdName: string;
/** Table namespace string */
namespace: string;
/** Table name string */
name: string;
/** Whether the table is offchain only (does not use storage) */
offchainOnly: boolean;
}

export interface RenderType {
/** Fully-qualified name of the user-defined type (may include a library name as prefix) */
typeId: string;
/** Fully-qualified name of the user-defined type (may include a library name as prefix), followed by location (none/memory/storage) */
typeWithLocation: string;
/** The name of the enum element in SchemaType to use for schema registration (e.g. "UINT256_ARRAY") */
enumName: string;
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/codegen/utils/extractUserTypes.ts
Expand Up @@ -2,17 +2,24 @@ import { parse, visit } from "@solidity-parser/parser";
import { MUDError } from "../../errors";

export interface SolidityUserDefinedType {
/** Fully-qualified name of the user-defined type (may include a library name as prefix) */
typeId: string;
/** Name of the wrapped primitive type */
internalTypeId: string;
/** Symbol which must be imported to use the type (either the type name, or the library name where the type is defined) */
importSymbol: string;
/** Path to the solidity file which contains the user type */
fromPath: string;
/** Whether `fromPath` is relative */
isRelativePath: boolean;
}

/**
* Parse the solidity data to extract user-defined type information.
* @param data contents of a solidity file with the user types declarations
* @param userTypeNames names of the user types to extract
* @param fromPath path to the solidity file from which the user types are extracted
* @returns record of type names mapped to the extracted type information
*/
export function extractUserTypes(
data: string,
Expand Down
11 changes: 11 additions & 0 deletions packages/common/src/codegen/utils/format.ts
@@ -1,6 +1,12 @@
import prettier from "prettier";
import prettierPluginSolidity from "prettier-plugin-solidity";

/**
* Formats solidity code using prettier
* @param content solidity code
* @param prettierConfigPath optional path to a prettier config
* @returns formatted solidity code
*/
export async function formatSolidity(content: string, prettierConfigPath?: string): Promise<string> {
let config;
if (prettierConfigPath) {
Expand Down Expand Up @@ -31,6 +37,11 @@ export async function formatSolidity(content: string, prettierConfigPath?: strin
}
}

/**
* Formats typescript code using prettier
* @param content typescript code
* @returns formatted typescript code
*/
export async function formatTypescript(content: string): Promise<string> {
return prettier.format(content, {
parser: "typescript",
Expand Down
12 changes: 12 additions & 0 deletions packages/common/src/codegen/utils/formatAndWrite.ts
Expand Up @@ -3,6 +3,12 @@ import { dirname } from "path";
import { formatSolidity, formatTypescript } from "./format";
import { debug } from "../debug";

/**
* Formats solidity code using prettier and write it to a file
* @param output solidity code
* @param fullOutputPath full path to the output file
* @param logPrefix prefix for debug logs
*/
export async function formatAndWriteSolidity(output: string, fullOutputPath: string, logPrefix: string): Promise<void> {
const formattedOutput = await formatSolidity(output);

Expand All @@ -12,6 +18,12 @@ export async function formatAndWriteSolidity(output: string, fullOutputPath: str
debug(`${logPrefix}: ${fullOutputPath}`);
}

/**
* Formats typescript code using prettier and write it to a file
* @param output typescript code
* @param fullOutputPath full path to the output file
* @param logPrefix prefix for debug logs
*/
export async function formatAndWriteTypescript(
output: string,
fullOutputPath: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/codegen/utils/loadUserTypesFile.ts
Expand Up @@ -9,6 +9,13 @@ export type UserType = {
internalType: SchemaAbiType;
};

/**
* Load the user type files and extract type information from them.
* @param userTypes record of user type data mapped by type names
* @param outputBaseDirectory base path to the output directory
* @param remappings solc remappings
* @returns record of the user type information mapped by type names
*/
export function loadAndExtractUserTypes(
userTypes: Record<string, UserType>,
outputBaseDirectory: string,
Expand Down
50 changes: 47 additions & 3 deletions packages/store/ts/codegen/field.ts
Expand Up @@ -8,7 +8,12 @@ import {
} from "@latticexyz/common/codegen";
import { RenderTableOptions } from "./types";

export function renderFieldMethods(options: RenderTableOptions) {
/**
* Returns Solidity code for all the field-specific table methods (get, set, push, pop, etc.)
* @param options RenderTableOptions
* @returns string of Solidity code
*/
export function renderFieldMethods(options: RenderTableOptions): string {
const storeArgument = options.storeArgument;
const { _typedTableId, _typedKeyArgs, _keyTupleDefinition } = renderCommonData(options);

Expand Down Expand Up @@ -235,6 +240,11 @@ export function renderFieldMethods(options: RenderTableOptions) {
return result;
}

/**
* Returns Solidity code for how to encode a particular field into bytes before storing onchain
* @param field RenderField
* @returns string of Solidity code
*/
export function renderEncodeFieldSingle(field: RenderField) {
let func;
if (field.arrayElement) {
Expand All @@ -247,6 +257,12 @@ export function renderEncodeFieldSingle(field: RenderField) {
return `${func}(${field.typeUnwrap}(${field.name}))`;
}

/**
* Returns Solidity code for decoding a bytes value into its Solidity primitive type
* @param field description of field type
* @param offset byte-length offset of value in encoded bytes
* @returns string of Solidity code
*/
export function renderDecodeValueType(field: RenderType, offset: number) {
const { staticByteLength } = field;

Expand All @@ -255,6 +271,12 @@ export function renderDecodeValueType(field: RenderType, offset: number) {
return renderCastStaticBytesToType(field, innerSlice);
}

/**
* Returns Solidity code for how to cast a bytesN value to a particular type, which is assumed to have the same byte length
* @param field description of resulting field type
* @param staticBytes bytesN value
* @returns string of Solidity code
*/
function renderCastStaticBytesToType(field: RenderType, staticBytes: string) {
const { staticByteLength, internalTypeId } = field;
const bits = staticByteLength * 8;
Expand All @@ -274,8 +296,25 @@ function renderCastStaticBytesToType(field: RenderType, staticBytes: string) {
return `${field.typeWrap}(${result})`;
}

/** bytes/string are dynamic, but aren't really arrays */
function fieldPortionData(field: RenderField) {
interface FieldPortionData {
/** Fully-qualified name of the user-defined type (may include a library name as prefix), followed by location (none/memory/storage) */
typeWithLocation: string;
/** Name of the field portion variable */
name: string;
/** Solidity code which encodes the field portion variable into bytes (for storing onchain) */
encoded: string;
/** Solidity code which decodes `_blob` variable into the field portion variable's type */
decoded: string;
/** Description of the field portion kind ("an element" or "a slice") */
title: string;
/** Byte length of array elements for arrays, 1 otherwise */
elementLength: number;
}

/**
* Returns data to describe either an array element, or a bytes slice, depending on the provided field type
*/
function fieldPortionData(field: RenderField): FieldPortionData {
if (field.arrayElement) {
const name = "_element";
const elementFieldData = { ...field.arrayElement, arrayElement: undefined, name };
Expand All @@ -301,6 +340,11 @@ function fieldPortionData(field: RenderField) {
}
}

/**
* Returns Solidity code for how to decode `_blob` variable into the particular field type
* @param field RenderField
* @returns string of Solidity code
*/
function renderDecodeFieldSingle(field: RenderField) {
const { isDynamic, arrayElement } = field;
if (arrayElement) {
Expand Down

0 comments on commit bcf22f5

Please sign in to comment.