Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions .changeset/small-horses-own.md
Original file line number Diff line number Diff line change
@@ -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).
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
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";
41 changes: 41 additions & 0 deletions packages/common/src/codegen/utils/parseSystem.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
4 changes: 2 additions & 2 deletions packages/store/ts/flattenStoreLogs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
4 changes: 2 additions & 2 deletions packages/store/ts/getStoreLogs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
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
4 changes: 2 additions & 2 deletions packages/world/gas-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
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
Loading
Loading