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

fix: support overloaded system functions in system libraries #3397

Open
wants to merge 11 commits into
base: vdrg/fix-library-imports
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/real-pigs-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/world": patch
---

Fix relative system imports in system libraries.
27 changes: 27 additions & 0 deletions packages/common/src/codegen/utils/contractToInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,38 @@ export interface ContractInterfaceError {
parameters: string[];
}

export interface ContractInterfaceStruct {
name: string;
members: { name: string; type: string }[];
}

interface SymbolImport {
symbol: string;
path: string;
}

export function extractStructs(source: string): ContractInterfaceStruct[] {
const ast = parse(source);
const structs: ContractInterfaceStruct[] = [];

visit(ast, {
StructDefinition({ name, members }) {
structs.push({
name,
members: members.map((member) => {
const [type, name] = parseParameter(member).split(" ");
return {
name,
type,
};
}),
});
},
});

return structs;
}

/**
* Parse the contract data to get the functions necessary to generate an interface,
* and symbols to import from the original contract.
Expand Down
54 changes: 47 additions & 7 deletions packages/world/ts/node/render-solidity/renderSystemLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
renderedSolidityHeader,
renderImports,
ContractInterfaceFunction,
ContractInterfaceStruct,
} from "@latticexyz/common/codegen";
import { RenderSystemLibraryOptions } from "./types";
import { ContractInterfaceError } from "@latticexyz/common/codegen";
Expand All @@ -20,6 +21,7 @@ export function renderSystemLibrary(options: RenderSystemLibraryOptions) {
errors: systemErrors,
worldImportPath,
storeImportPath,
symbolDefinitions,
} = options;

// Add required imports, if they are already included they will get removed in renderImports
Expand Down Expand Up @@ -87,9 +89,9 @@ export function renderSystemLibrary(options: RenderSystemLibraryOptions) {

${renderList(functions, (contractFunction) => renderUserTypeFunction(contractFunction, userTypeName))}

${renderList(functions, (contractFunction) => renderCallWrapperFunction(contractFunction, systemLabel, callingFromRootSystemErrorName))}
${renderList(functions, (contractFunction) => renderCallWrapperFunction(contractFunction, systemLabel, callingFromRootSystemErrorName, symbolDefinitions))}

${renderList(functions, (contractFunction) => renderRootCallWrapperFunction(contractFunction, systemLabel, namespace))}
${renderList(functions, (contractFunction) => renderRootCallWrapperFunction(contractFunction, systemLabel, namespace, symbolDefinitions))}

function callFrom(${userTypeName} self, address from) internal pure returns (CallWrapper memory) {
return CallWrapper(self.toResourceId(), from);
Expand Down Expand Up @@ -156,6 +158,7 @@ function renderCallWrapperFunction(
contractFunction: ContractInterfaceFunction,
systemLabel: string,
callingFromRootSystemErrorName: string,
symbolDefinitions: Record<string, ContractInterfaceStruct>,
) {
const { name, parameters, stateMutability, returnParameters } = contractFunction;

Expand All @@ -174,7 +177,7 @@ function renderCallWrapperFunction(
if (address(_world()) == address(this)) revert ${callingFromRootSystemErrorName}();
`;

const encodedSystemCall = renderEncodeSystemCall(systemLabel, name, parameters);
const encodedSystemCall = renderEncodeSystemCall(name, parameters, symbolDefinitions);

if (stateMutability === "") {
return `
Expand Down Expand Up @@ -207,6 +210,7 @@ function renderRootCallWrapperFunction(
contractFunction: ContractInterfaceFunction,
systemLabel: string,
namespace: string,
symbolDefinitions: Record<string, ContractInterfaceStruct>,
) {
const { name, parameters, stateMutability, returnParameters } = contractFunction;

Expand All @@ -226,7 +230,7 @@ function renderRootCallWrapperFunction(
${renderReturnParameters(returnParameters)}
`;

const encodedSystemCall = renderEncodeSystemCall(systemLabel, name, parameters);
const encodedSystemCall = renderEncodeSystemCall(name, parameters, symbolDefinitions);

if (stateMutability === "") {
return `
Expand All @@ -248,9 +252,45 @@ function renderRootCallWrapperFunction(
}
}

function renderEncodeSystemCall(interfaceName: string, functionName: string, parameters: string[]) {
const paramNames = parameters.map((param) => param.split(" ").slice(-1)[0]).join(", ");
return `abi.encodeCall(${interfaceName}.${functionName}, (${paramNames}))`;
function renderEncodeSystemCall(
functionName: string,
parameters: string[],
symbolDefinitions: Record<string, ContractInterfaceStruct>,
) {
const { paramNames, paramTypes } = parameters.reduce(
(res, param) => {
const paramArray = param.split(" ");
let name = paramArray.slice(-1)[0];
let type = paramArray[0];

const symbolDefinition = symbolDefinitions[type];
if (symbolDefinition) {
const typeList = symbolDefinition.members.map((member) => member.type).join(",");
type = `(${typeList})`;

const nameList = symbolDefinition.members
.map((member) => {
return `${name}.${member.name}`;
})
.join(",");
name = `${nameList}`;
}

res.paramNames.push(name);
res.paramTypes.push(type);

return res;
},
{
paramNames: [] as string[],
paramTypes: [] as string[],
},
);

const selector = `bytes4(keccak256("${functionName}(${paramTypes})"))`;
const systemCall = `abi.encodeWithSelector(${selector}${paramNames.length > 0 ? `, ${paramNames.join(", ")}` : ""})`;

return systemCall;
}

function renderAbiDecode(expression: string, returnParameters: string[]) {
Expand Down
9 changes: 8 additions & 1 deletion packages/world/ts/node/render-solidity/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { ImportDatum, ContractInterfaceFunction, ContractInterfaceError } from "@latticexyz/common/codegen";
import type {
ImportDatum,
ContractInterfaceFunction,
ContractInterfaceError,
ContractInterfaceStruct,
} from "@latticexyz/common/codegen";

export interface RenderSystemInterfaceOptions {
/** List of symbols to import, and their file paths */
Expand All @@ -25,4 +30,6 @@ export interface RenderSystemLibraryOptions {
storeImportPath: string;
/** Path for world package imports */
worldImportPath: string;

symbolDefinitions: Record<string, ContractInterfaceStruct>;
}
59 changes: 50 additions & 9 deletions packages/world/ts/node/render-solidity/worldgen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import fs from "node:fs/promises";
import path from "node:path";
import { formatAndWriteSolidity, contractToInterface, type ImportDatum } from "@latticexyz/common/codegen";
import {
formatAndWriteSolidity,
contractToInterface,
type ImportDatum,
extractStructs,
ContractInterfaceStruct,
} from "@latticexyz/common/codegen";
import { renderSystemInterface } from "./renderSystemInterface";
import { renderWorldInterface } from "./renderWorldInterface";
import { renderSystemLibrary } from "./renderSystemLibrary";
Expand Down Expand Up @@ -80,30 +86,64 @@ export async function worldgen({
const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8");
// get external functions from a contract
const { functions, errors, symbolImports } = contractToInterface(source, system.label);
const imports = symbolImports.map(

const symbolDefinitions: Record<string, ContractInterfaceStruct> = {};
const uniqueSymbolImportPaths = new Set(
symbolImports.map(({ path }) => path).filter((path) => !path.includes("codegen")),
);

for (const symbolImportPath of uniqueSymbolImportPaths) {
// TODO: import structs from non-relative paths
if (!symbolImportPath.startsWith(".")) {
continue;
}

const symbolPath = path.resolve(rootDir, path.dirname(system.sourcePath), symbolImportPath);
const symbolImportSource = await fs.readFile(symbolPath, "utf8");
const structs = extractStructs(symbolImportSource);
for (const struct of structs) {
symbolDefinitions[struct.name] = struct;
}
}

const interfaceImports = symbolImports.map(
({ symbol, path: importPath }): ImportDatum => ({
symbol,
path: importPath.startsWith(".")
? "./" + path.relative(worldgenOutDir, path.join(rootDir, path.dirname(system.sourcePath), importPath))
: importPath,
}),
);

const systemInterface = renderSystemInterface({
name: system.interfaceName,
functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`,
functions,
errors,
imports,
imports: interfaceImports,
});
// write to file
await formatAndWriteSolidity(systemInterface, system.interfacePath, "Generated system interface");

const systemImport = {
symbol: system.label,
path: "./" + path.relative(path.dirname(system.libraryPath), system.sourcePath),
};

if (config.codegen.generateSystemLibraries) {
const systemImport = {
symbol: system.label,
path: "./" + path.relative(path.dirname(system.libraryPath), system.sourcePath),
};

const libraryImports = symbolImports.map(
({ symbol, path: importPath }): ImportDatum => ({
symbol,
path: importPath.startsWith(".")
? "./" +
path.relative(
path.dirname(system.libraryPath),
path.join(rootDir, path.dirname(system.sourcePath), importPath),
)
: importPath,
}),
);

const systemLibrary = renderSystemLibrary({
libraryName: system.libraryName,
interfaceName: system.interfaceName,
Expand All @@ -113,9 +153,10 @@ export async function worldgen({
resourceId: resourceToHex({ type: "system", namespace: system.namespace, name: system.name }),
functions,
errors,
imports: [systemImport, ...imports],
imports: [systemImport, ...libraryImports],
storeImportPath,
worldImportPath,
symbolDefinitions,
});
// write to file
await formatAndWriteSolidity(systemLibrary, system.libraryPath, "Generated system library");
Expand Down
6 changes: 6 additions & 0 deletions test/system-libraries/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export default defineWorld({
},
key: [],
},
NameValue: {
schema: {
value: "string",
},
key: [],
},
},
},
b: {},
Expand Down
6 changes: 6 additions & 0 deletions test/system-libraries/src/codegen/world/IASystem.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion test/system-libraries/src/codegen/world/IBSystem.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion test/system-libraries/src/codegen/world/IRootSystem.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions test/system-libraries/src/namespaces/a/ASystem.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ pragma solidity >=0.8.28;
import { System } from "@latticexyz/world/src/System.sol";
import { Value } from "./codegen/tables/Value.sol";
import { AddressValue } from "./codegen/tables/AddressValue.sol";
import { NameValue } from "./codegen/tables/NameValue.sol";
import { ASystemThing, ASystemThing2 } from "./ASystemTypes.sol";

contract ASystem is System {
function setComplexValue(ASystemThing2 memory value, string memory name) external {
Value.set(value.a);
NameValue.set(name);
}

function setValue(ASystemThing memory value) external {
Value.set(value.a);
}

function setValue(uint256 value) external {
Value.set(value);
}
Expand Down
12 changes: 12 additions & 0 deletions test/system-libraries/src/namespaces/a/ASystemTypes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.28;

struct ASystemThing {
uint256 a;
}

struct ASystemThing2 {
uint256 a;
uint256 b;
uint256 c;
}
1 change: 1 addition & 0 deletions test/system-libraries/src/namespaces/a/codegen/index.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading