diff --git a/.changeset/small-horses-own.md b/.changeset/small-horses-own.md new file mode 100644 index 0000000000..467bf4445f --- /dev/null +++ b/.changeset/small-horses-own.md @@ -0,0 +1,16 @@ +--- +"@latticexyz/world": patch +"@latticexyz/cli": patch +--- + +`mud` CLI commands will now recognize systems if they inherit directly from the base `System` imported from `@latticexyz/world/src/System.sol`, allowing you to write systems without a `System` suffix. + +```solidity +import {System} from "@latticexyz/world/src/System.sol"; + +contract EntityProgram is System { + ... +} +``` + +If you have contracts that inherit from the base `System` that aren't meant to be deployed, you can mark them as `abstract contract` or [disable the system's deploy via config](https://mud.dev/config/reference). diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 325e11ed56..4561606350 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -16,7 +16,8 @@ type BuildOptions = { }; export async function build({ rootDir, config, foundryProfile }: BuildOptions): Promise { - await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]); + await tablegen({ rootDir, config }); + await worldgen({ rootDir, config }); await printCommand( execa("forge", ["build"], { stdio: "inherit", diff --git a/packages/common/src/codegen/utils/contractToInterface.ts b/packages/common/src/codegen/utils/contractToInterface.ts index a896404fac..914860b188 100644 --- a/packages/common/src/codegen/utils/contractToInterface.ts +++ b/packages/common/src/codegen/utils/contractToInterface.ts @@ -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; @@ -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. @@ -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 = ""; @@ -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; + }); } diff --git a/packages/common/src/codegen/utils/findContractNode.ts b/packages/common/src/codegen/utils/findContractNode.ts new file mode 100644 index 0000000000..cccc8822f0 --- /dev/null +++ b/packages/common/src/codegen/utils/findContractNode.ts @@ -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; +} diff --git a/packages/common/src/codegen/utils/findSymbolImport.ts b/packages/common/src/codegen/utils/findSymbolImport.ts new file mode 100644 index 0000000000..2b3d5ac9b1 --- /dev/null +++ b/packages/common/src/codegen/utils/findSymbolImport.ts @@ -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; +} diff --git a/packages/common/src/codegen/utils/index.ts b/packages/common/src/codegen/utils/index.ts index b037143393..2e80179c69 100644 --- a/packages/common/src/codegen/utils/index.ts +++ b/packages/common/src/codegen/utils/index.ts @@ -1,3 +1,4 @@ export * from "./contractToInterface"; export * from "./format"; export * from "./formatAndWrite"; +export * from "./parseSystem"; diff --git a/packages/common/src/codegen/utils/parseSystem.ts b/packages/common/src/codegen/utils/parseSystem.ts new file mode 100644 index 0000000000..656a42fbd0 --- /dev/null +++ b/packages/common/src/codegen/utils/parseSystem.ts @@ -0,0 +1,41 @@ +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 + // we allow abstract systems here so that we can create system libraries from them but without deploying them + 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 }; + } +} diff --git a/packages/store/ts/flattenStoreLogs.test.ts b/packages/store/ts/flattenStoreLogs.test.ts index 6dcc0d87f0..ea25fea7b0 100644 --- a/packages/store/ts/flattenStoreLogs.test.ts +++ b/packages/store/ts/flattenStoreLogs.test.ts @@ -134,8 +134,8 @@ describe("flattenStoreLogs", async () => { "Store_SetRecord store__ResourceIds (0x746200000000000000000000000000005465727261696e000000000000000000)", "Store_SetRecord store__ResourceIds (0x737900000000000000000000000000004d6f766553797374656d000000000000)", "Store_SetRecord world__Systems (0x737900000000000000000000000000004d6f766553797374656d000000000000)", - "Store_SetRecord world__SystemRegistry (0x000000000000000000000000cbcdc66f9301ccf30b6b46efba8a3015d332dc13)", - "Store_SetRecord world__ResourceAccess (0x6e73000000000000000000000000000000000000000000000000000000000000,0x000000000000000000000000cbcdc66f9301ccf30b6b46efba8a3015d332dc13)", + "Store_SetRecord world__SystemRegistry (0x00000000000000000000000040d21680e49a1f969a53760ff488a9d1ad01ca89)", + "Store_SetRecord world__ResourceAccess (0x6e73000000000000000000000000000000000000000000000000000000000000,0x00000000000000000000000040d21680e49a1f969a53760ff488a9d1ad01ca89)", "Store_SetRecord world__FunctionSelector (0xb591186e00000000000000000000000000000000000000000000000000000000)", "Store_SetRecord world__FunctionSignatur (0xb591186e00000000000000000000000000000000000000000000000000000000)", "Store_SetRecord store__Tables (0x7462000000000000000000000000000043616c6c576974685369676e61747572)", diff --git a/packages/store/ts/getStoreLogs.test.ts b/packages/store/ts/getStoreLogs.test.ts index 88608021ca..6cecdbb192 100644 --- a/packages/store/ts/getStoreLogs.test.ts +++ b/packages/store/ts/getStoreLogs.test.ts @@ -157,8 +157,8 @@ describe("getStoreLogs", async () => { "Store_SpliceStaticData store__ResourceIds (0x746200000000000000000000000000005465727261696e000000000000000000)", "Store_SpliceStaticData store__ResourceIds (0x737900000000000000000000000000004d6f766553797374656d000000000000)", "Store_SetRecord world__Systems (0x737900000000000000000000000000004d6f766553797374656d000000000000)", - "Store_SpliceStaticData world__SystemRegistry (0x000000000000000000000000cbcdc66f9301ccf30b6b46efba8a3015d332dc13)", - "Store_SpliceStaticData world__ResourceAccess (0x6e73000000000000000000000000000000000000000000000000000000000000,0x000000000000000000000000cbcdc66f9301ccf30b6b46efba8a3015d332dc13)", + "Store_SpliceStaticData world__SystemRegistry (0x00000000000000000000000040d21680e49a1f969a53760ff488a9d1ad01ca89)", + "Store_SpliceStaticData world__ResourceAccess (0x6e73000000000000000000000000000000000000000000000000000000000000,0x00000000000000000000000040d21680e49a1f969a53760ff488a9d1ad01ca89)", "Store_SetRecord world__FunctionSelector (0xb591186e00000000000000000000000000000000000000000000000000000000)", "Store_SetRecord world__FunctionSignatur (0xb591186e00000000000000000000000000000000000000000000000000000000)", "Store_SetRecord world__FunctionSignatur (0xb591186e00000000000000000000000000000000000000000000000000000000)", diff --git a/packages/world-module-callwithsignature/ts/build.ts b/packages/world-module-callwithsignature/ts/build.ts index 9f24536f54..267e5bfdb3 100644 --- a/packages/world-module-callwithsignature/ts/build.ts +++ b/packages/world-module-callwithsignature/ts/build.ts @@ -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 }); diff --git a/packages/world-module-metadata/ts/build.ts b/packages/world-module-metadata/ts/build.ts index 9f24536f54..267e5bfdb3 100644 --- a/packages/world-module-metadata/ts/build.ts +++ b/packages/world-module-metadata/ts/build.ts @@ -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 }); diff --git a/packages/world-modules/test/StandardDelegationsModule.t.sol b/packages/world-modules/test/StandardDelegationsModule.t.sol index 537fcced39..7b978ee023 100644 --- a/packages/world-modules/test/StandardDelegationsModule.t.sol +++ b/packages/world-modules/test/StandardDelegationsModule.t.sol @@ -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", diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index fabd65da55..0954e44484 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -123,7 +123,7 @@ "file": "test/World.t.sol:WorldTest", "test": "testRegisterSystem", "name": "register a system", - "gasUsed": 185032 + "gasUsed": 185170 }, { "file": "test/World.t.sol:WorldTest", @@ -243,7 +243,7 @@ "file": "test/WorldProxy.t.sol:WorldProxyTest", "test": "testRegisterSystem", "name": "register a system", - "gasUsed": 189934 + "gasUsed": 190072 }, { "file": "test/WorldProxy.t.sol:WorldProxyTest", diff --git a/packages/world/src/System.sol b/packages/world/src/System.sol index f1add8cd23..74206bd060 100644 --- a/packages/world/src/System.sol +++ b/packages/world/src/System.sol @@ -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. } diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 9d74439f56..5522abe3cd 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -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"; @@ -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", @@ -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" }), @@ -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 @@ -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( @@ -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( diff --git a/packages/world/ts/node/getSystemContracts.ts b/packages/world/ts/node/getSystemContracts.ts index 76aff24a4d..0efd41da86 100644 --- a/packages/world/ts/node/getSystemContracts.ts +++ b/packages/world/ts/node/getSystemContracts.ts @@ -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; @@ -17,21 +20,24 @@ export async function getSystemContracts({ rootDir, config, }: GetSystemContractsOptions): Promise { - 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 }; + }), + ); + + 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; @@ -52,6 +58,7 @@ export async function getSystemContracts({ sourcePath: file.filename, namespaceLabel, systemLabel: file.basename, - }; - }); + } satisfies SystemContract; + }) + .filter(isDefined); } diff --git a/packages/world/ts/scripts/build.ts b/packages/world/ts/scripts/build.ts index ef89cdcffc..47c871c685 100644 --- a/packages/world/ts/scripts/build.ts +++ b/packages/world/ts/scripts/build.ts @@ -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 });