Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions .changeset/small-horses-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/world": patch
---

Support systems without the System suffix by adding them to the MUD config.
3 changes: 2 additions & 1 deletion packages/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ type BuildOptions = {
};

export async function build({ rootDir, config, foundryProfile }: BuildOptions): Promise<void> {
await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]);
await tablegen({ rootDir, config });
await worldgen({ rootDir, config });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to be done serially now that we're parsing all *.sol source files in worldgen to determine if a source file is a system, otherwise we get a bunch of "file does not exist" errors from tablegen deleting/creating files

await printCommand(
execa("forge", ["build"], {
stdio: "inherit",
Expand Down
70 changes: 8 additions & 62 deletions packages/common/src/codegen/utils/contractToInterface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { parse, visit } from "@solidity-parser/parser";
import type {
ContractDefinition,
SourceUnit,
TypeName,
VariableDeclaration,
} from "@solidity-parser/parser/dist/src/ast-types";
import type { SourceUnit, TypeName, VariableDeclaration } from "@solidity-parser/parser/dist/src/ast-types";
import { MUDError } from "../../errors";
import { findContractNode } from "./findContractNode";
import { SymbolImport, findSymbolImport } from "./findSymbolImport";

export interface ContractInterfaceFunction {
name: string;
Expand All @@ -19,11 +16,6 @@ export interface ContractInterfaceError {
parameters: string[];
}

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

/**
* Parse the contract data to get the functions necessary to generate an interface,
* and symbols to import from the original contract.
Expand Down Expand Up @@ -106,20 +98,6 @@ export function contractToInterface(
};
}

export function findContractNode(ast: SourceUnit, contractName: string): ContractDefinition | undefined {
let contract: ContractDefinition | undefined = undefined;

visit(ast, {
ContractDefinition(node) {
if (node.name === contractName) {
contract = node;
}
},
});

return contract;
}

function parseParameter({ name, typeName, storageLocation }: VariableDeclaration): string {
let typedNameWithLocation = "";

Expand Down Expand Up @@ -197,42 +175,10 @@ function typeNameToSymbols(typeName: TypeName | null): string[] {
}
}

// Get imports for given symbols.
// To avoid circular dependencies of interfaces on their implementations,
// symbols used for args/returns must always be imported from an auxiliary file.
// To avoid parsing the entire project to build dependencies,
// symbols must be imported with an explicit `import { symbol } from ...`
function symbolsToImports(ast: SourceUnit, symbols: string[]): SymbolImport[] {
const imports: SymbolImport[] = [];

for (const symbol of symbols) {
let symbolImport: SymbolImport | undefined;

visit(ast, {
ImportDirective({ path, symbolAliases }) {
if (symbolAliases) {
for (const symbolAndAlias of symbolAliases) {
// either check the alias, or the original symbol if there's no alias
const symbolAlias = symbolAndAlias[1] || symbolAndAlias[0];
if (symbol === symbolAlias) {
symbolImport = {
// always use the original symbol for interface imports
symbol: symbolAndAlias[0],
path,
};
return;
}
}
}
},
});

if (symbolImport) {
imports.push(symbolImport);
} else {
throw new MUDError(`Symbol "${symbol}" has no explicit import`);
}
}

return imports;
return symbols.map((symbol) => {
const symbolImport = findSymbolImport(ast, symbol);
if (!symbolImport) throw new MUDError(`Symbol "${symbol}" has no explicit import`);
return symbolImport;
});
}
16 changes: 16 additions & 0 deletions packages/common/src/codegen/utils/findContractNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { visit } from "@solidity-parser/parser";
import type { ContractDefinition, SourceUnit } from "@solidity-parser/parser/dist/src/ast-types";

export function findContractNode(ast: SourceUnit, contractName: string): ContractDefinition | undefined {
let contract: ContractDefinition | undefined = undefined;

visit(ast, {
ContractDefinition(node) {
if (node.name === contractName) {
contract = node;
}
},
});

return contract;
}
40 changes: 40 additions & 0 deletions packages/common/src/codegen/utils/findSymbolImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { visit } from "@solidity-parser/parser";
import type { SourceUnit } from "@solidity-parser/parser/dist/src/ast-types";

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

/**
* Get import for given symbol.
*
* To avoid circular dependencies of interfaces on their implementations,
* symbols used for args/returns must always be imported from an auxiliary file.
* To avoid parsing the entire project to build dependencies,
* symbols must be imported with an explicit `import { symbol } from ...`
*/
export function findSymbolImport(ast: SourceUnit, symbol: string): SymbolImport | undefined {
let symbolImport: SymbolImport | undefined;

visit(ast, {
ImportDirective({ path, symbolAliases }) {
if (symbolAliases) {
for (const symbolAndAlias of symbolAliases) {
// either check the alias, or the original symbol if there's no alias
const symbolAlias = symbolAndAlias[1] ?? symbolAndAlias[0];
if (symbol === symbolAlias) {
symbolImport = {
// always use the original symbol for interface imports
symbol: symbolAndAlias[0],
path,
};
return;
}
}
}
},
});

return symbolImport;
}
1 change: 1 addition & 0 deletions packages/common/src/codegen/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./contractToInterface";
export * from "./format";
export * from "./formatAndWrite";
export * from "./parseSystem";
40 changes: 40 additions & 0 deletions packages/common/src/codegen/utils/parseSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { parse, visit } from "@solidity-parser/parser";

import { findContractNode } from "./findContractNode";
import { findSymbolImport } from "./findSymbolImport";

const baseSystemName = "System";
const baseSystemPath = "@latticexyz/world/src/System.sol";

export function parseSystem(
source: string,
contractName: string,
): undefined | { contractType: "contract" | "abstract" } {
const ast = parse(source);
const contractNode = findContractNode(ast, contractName);
if (!contractNode) return;

const contractType = contractNode.kind;
// skip libraries and interfaces
if (contractType !== "contract" && contractType !== "abstract") return;

const isSystem = ((): boolean => {
// if using the System suffix, assume its a system
if (contractName.endsWith("System") && contractName !== baseSystemName) return true;

// otherwise check if we're inheriting from the base system
let extendsBaseSystem = false;
visit(contractNode, {
InheritanceSpecifier(node) {
if (node.baseName.namePath === baseSystemName) {
extendsBaseSystem = true;
}
},
});
return extendsBaseSystem && findSymbolImport(ast, baseSystemName)?.path === baseSystemPath;
})();

if (isSystem) {
return { contractType };
}
}
4 changes: 3 additions & 1 deletion packages/world-module-callwithsignature/ts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ const configPath = "../mud.config";

const { default: config } = await import(configPath);
const rootDir = path.dirname(path.join(__dirname, configPath));
await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]);

await tablegen({ rootDir, config });
await worldgen({ rootDir, config });
4 changes: 3 additions & 1 deletion packages/world-module-metadata/ts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ const configPath = "../mud.config";

const { default: config } = await import(configPath);
const rootDir = path.dirname(path.join(__dirname, configPath));
await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]);

await tablegen({ rootDir, config });
await worldgen({ rootDir, config });
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ contract StandardDelegationsModuleTest is Test, GasReporter {

function testRegisterDelegationRevertInterfaceNotSupported() public {
// Register a system that is not a delegation control system
System noDelegationControlSystem = new System();
System noDelegationControlSystem = new WorldTestSystem();
ResourceId noDelegationControlId = WorldResourceIdLib.encode({
typeId: RESOURCE_SYSTEM,
namespace: "namespace",
Expand Down
2 changes: 1 addition & 1 deletion packages/world/src/System.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import { WorldContextConsumer } from "./WorldContext.sol";
* @dev The System contract currently acts as an alias for `WorldContextConsumer`.
* This structure is chosen for potential extensions in the future, where default functionality might be added to the System.
*/
contract System is WorldContextConsumer {
abstract contract System is WorldContextConsumer {
// Currently, no additional functionality is added. Future enhancements can be introduced here.
}
14 changes: 7 additions & 7 deletions packages/world/test/World.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ contract WorldTest is Test, GasReporter {
}

function testRegisterSystem() public {
System system = new System();
System system = new WorldTestSystem();
bytes14 namespace = "";
ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
bytes16 name = "testSystem";
Expand Down Expand Up @@ -564,7 +564,7 @@ contract WorldTest is Test, GasReporter {
assertTrue(ResourceAccess.get({ resourceId: namespaceId, caller: address(system) }));

// Expect the registration to fail if the namespace does not exist yet
System newSystem = new System();
System newSystem = new WorldTestSystem();
ResourceId invalidNamespaceSystemId = WorldResourceIdLib.encode({
typeId: RESOURCE_SYSTEM,
namespace: "newNamespace",
Expand Down Expand Up @@ -612,7 +612,7 @@ contract WorldTest is Test, GasReporter {
world.registerSystem(tableId, newSystem, true);

// Expect an error when registering a system in a namespace that is not owned by the caller
System yetAnotherSystem = new System();
System yetAnotherSystem = new WorldTestSystem();
_expectAccessDenied(address(0x01), "", "", RESOURCE_NAMESPACE);
world.registerSystem(
WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "rootSystem" }),
Expand Down Expand Up @@ -658,11 +658,11 @@ contract WorldTest is Test, GasReporter {
world.registerNamespace(systemId.getNamespaceId());

// Register a system
System oldSystem = new System();
System oldSystem = new WorldTestSystem();
world.registerSystem(systemId, oldSystem, true);

// Upgrade the system and set public access to false
System newSystem = new System();
System newSystem = new WorldTestSystem();
world.registerSystem(systemId, newSystem, false);

// Expect the system address and public access to be updated in the System table
Expand Down Expand Up @@ -712,7 +712,7 @@ contract WorldTest is Test, GasReporter {
);

// Deploy a new system
System system = new System();
System system = new WorldTestSystem();

// Expect an error when trying to register a system at the same ID
vm.expectRevert(
Expand All @@ -727,7 +727,7 @@ contract WorldTest is Test, GasReporter {

// Register a new system
ResourceId systemId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "namespace2", name: "name" });
world.registerSystem(systemId, new System(), false);
world.registerSystem(systemId, new WorldTestSystem(), false);

// Expect an error when trying to register a table at the same ID
vm.expectRevert(
Expand Down
33 changes: 21 additions & 12 deletions packages/world/ts/node/getSystemContracts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import path from "node:path";
import fs from "node:fs/promises";
import { World } from "../config/v2/output";
import { findSolidityFiles } from "./findSolidityFiles";
import { parseSystem } from "@latticexyz/common/codegen";
import { isDefined } from "@latticexyz/common/utils";

export type SystemContract = {
readonly sourcePath: string;
Expand All @@ -17,21 +20,26 @@ export async function getSystemContracts({
rootDir,
config,
}: GetSystemContractsOptions): Promise<readonly SystemContract[]> {
const solidityFiles = await findSolidityFiles({
const filePaths = await findSolidityFiles({
cwd: rootDir,
pattern: path.join(config.sourceDirectory, "**"),
});

return solidityFiles
.filter(
(file) =>
file.basename.endsWith("System") &&
// exclude the base System contract
file.basename !== "System" &&
// exclude interfaces
!/^I[A-Z]/.test(file.basename),
)
const files = await Promise.all(
filePaths.map(async (file) => {
console.log("reading source for", file.filename);
const source = await fs.readFile(path.join(rootDir, file.filename), "utf-8");
return { ...file, source };
}),
);

console.log("got files", filePaths);

return files
.map((file) => {
const parsedSystem = parseSystem(file.source, file.basename);
if (!parsedSystem) return;

const namespaceLabel = (() => {
// TODO: remove `config.namespace` null check once this narrows properly
if (!config.multipleNamespaces && config.namespace != null) return config.namespace;
Expand All @@ -52,6 +60,7 @@ export async function getSystemContracts({
sourcePath: file.filename,
namespaceLabel,
systemLabel: file.basename,
};
});
} satisfies SystemContract;
})
.filter(isDefined);
}
12 changes: 5 additions & 7 deletions packages/world/ts/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import config, { systemsConfig } from "../../mud.config";
const configPath = await resolveConfigPath();
const rootDir = path.dirname(configPath);

await Promise.all([
tablegen({ rootDir, config }),
// until we get finer-grained control of for namespaces (source path, codegen)
// or being able to merge configs with strong types, we need to use a separate
// config for systems to maintain source locations
worldgen({ rootDir, config: systemsConfig }),
]);
await tablegen({ rootDir, config });
// until we get finer-grained control of for namespaces (source path, codegen)
// or being able to merge configs with strong types, we need to use a separate
// config for systems to maintain source locations
await worldgen({ rootDir, config: systemsConfig });