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

feat(store,world): enable namespaces input #2968

Merged
merged 18 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
34 changes: 18 additions & 16 deletions examples/multiple-namespaces/mud.config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { defineWorld } from "@latticexyz/world";

export default defineWorld({
namespace: "game",
codegen: { namespaceDirectories: true },
tables: {
Health: {
schema: {
player: "address",
value: "uint32",
namespaces: {
game: {
tables: {
Health: {
schema: {
player: "address",
value: "uint32",
},
key: ["player"],
},
Position: {
schema: {
player: "address",
x: "int32",
y: "int32",
},
key: ["player"],
},
},
key: ["player"],
},
Position: {
schema: {
player: "address",
x: "int32",
y: "int32",
},
key: ["player"],
},
},
});
7 changes: 0 additions & 7 deletions examples/multiple-namespaces/src/codegen/index.sol

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { Health } from "../codegen/game/tables/Health.sol";
import { Health } from "./codegen/tables/Health.sol";

contract HealSystem is System {
function heal(address player) public {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { Position } from "../codegen/game/tables/Position.sol";
import { Position } from "./codegen/tables/Position.sol";

contract MoveSystem is System {
function move(address player, int32 x, int32 y) public {
Expand Down

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

2 changes: 1 addition & 1 deletion examples/multiple-namespaces/test/HealthTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "forge-std/Test.sol";
import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";
import { Health } from "../src/codegen/game/tables/Health.sol";
import { Health } from "../src/namespaces/game/codegen/tables/Health.sol";

contract HealthTest is MudTest {
function testHealth(address player) public {
Expand Down
2 changes: 1 addition & 1 deletion examples/multiple-namespaces/test/PositionTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "forge-std/Test.sol";
import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";
import { Position, PositionData } from "../src/codegen/game/tables/Position.sol";
import { Position, PositionData } from "../src/namespaces/game/codegen/tables/Position.sol";

contract PositionTest is MudTest {
function testPosition(address player, int32 x, int32 y) public {
Expand Down
12 changes: 6 additions & 6 deletions packages/store/ts/codegen/tablegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export async function tablegen({ rootDir, config }: TablegenOptions) {

await Promise.all(
Object.values(config.namespaces).map(async (namespace) => {
// TODO: get this value from config once multiple namespaces are supported
const multipleNamespaces = false;
const sourceDir = multipleNamespaces
const sourceDir = config.multipleNamespaces
? path.join(config.sourceDirectory, "namespaces", namespace.label)
: config.sourceDirectory;
const codegenDir = path.join(sourceDir, config.codegen.outputDirectory);
Expand Down Expand Up @@ -60,9 +58,11 @@ export async function tablegen({ rootDir, config }: TablegenOptions) {
}),
);

const codegenIndexPath = path.join(rootDir, codegenDir, config.codegen.indexFilename);
const source = renderTableIndex(codegenIndexPath, tableOptions);
await formatAndWriteSolidity(source, codegenIndexPath, "Generated table index");
if (config.codegen.indexFilename !== false && tableOptions.length > 0) {
const codegenIndexPath = path.join(rootDir, codegenDir, config.codegen.indexFilename);
const source = renderTableIndex(codegenIndexPath, tableOptions);
await formatAndWriteSolidity(source, codegenIndexPath, "Generated table index");
}
}),
);
}
2 changes: 0 additions & 2 deletions packages/store/ts/config/v2/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ export const CODEGEN_DEFAULTS = {
storeImportPath: "@latticexyz/store/src",
userTypesFilename: "common.sol",
outputDirectory: "codegen",
// TODO: default to true if using top-level `namespaces` key (once its migrated to store)
namespaceDirectories: false,
indexFilename: "index.sol",
} as const satisfies CodegenInput;

Expand Down
41 changes: 41 additions & 0 deletions packages/store/ts/config/v2/flattenNamespacedTables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { show } from "@arktype/util";
import { Namespaces } from "./output";

type flattenNamespacedTableKeys<config> = config extends {
readonly namespaces: infer namespaces;
}
? {
[namespaceLabel in keyof namespaces]: namespaces[namespaceLabel] extends { readonly tables: infer tables }
? namespaceLabel extends ""
? keyof tables
: `${namespaceLabel & string}__${keyof tables & string}`
: never;
}[keyof namespaces]
: never;

/**
* @internal Only kept for backwards compatibility
*/
export type flattenNamespacedTables<config> = config extends { readonly namespaces: Namespaces }
? {
readonly [key in flattenNamespacedTableKeys<config>]: key extends `${infer namespaceLabel}__${infer tableLabel}`
? config["namespaces"][namespaceLabel]["tables"][tableLabel]
: config["namespaces"][""]["tables"][key];
}
: never;

/**
* @internal Only kept for backwards compatibility
*/
export function flattenNamespacedTables<config extends { readonly namespaces: Namespaces }>(
config: config,
): show<flattenNamespacedTables<config>> {
return Object.fromEntries(
Object.entries(config.namespaces).flatMap(([namespaceLabel, namespace]) =>
Object.entries(namespace.tables).map(([tableLabel, table]) => [
namespaceLabel === "" ? tableLabel : `${namespaceLabel}__${tableLabel}`,
table,
]),
),
) as never;
}
3 changes: 2 additions & 1 deletion packages/store/ts/config/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export * from "./defaults";
export * from "./codegen";
export * from "./enums";
export * from "./userTypes";
export * from "./flattenNamespacedTables";
export * from "./namespace";
export * from "./namespacedTables";
export * from "./namespaces";
7 changes: 6 additions & 1 deletion packages/store/ts/config/v2/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type TableInput = {
export type TableShorthandInput = SchemaInput | string;

export type TablesInput = {
// remove label and namespace from table input as these are set contextually
// remove label and namespace as these are set contextually
// and allow defining a table using shorthand
readonly [label: string]: Omit<TableInput, "label" | "namespace"> | TableShorthandInput;
};
Expand All @@ -68,7 +68,12 @@ export type NamespaceInput = {
readonly tables?: TablesInput;
};

export type NamespacesInput = {
readonly [label: string]: Omit<NamespaceInput, "label">;
};

export type StoreInput = Omit<NamespaceInput, "label"> & {
readonly namespaces?: NamespacesInput;
/**
* Directory of Solidity source relative to the MUD config.
* This is used to resolve other paths in the config, like codegen and user types.
Expand Down
24 changes: 0 additions & 24 deletions packages/store/ts/config/v2/namespacedTables.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/store/ts/config/v2/namespaces.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it } from "vitest";
import { attest } from "@arktype/attest";
import { defineNamespaces } from "./namespaces";

describe("defineNamespaces", () => {
it("should throw on duplicates", () => {
attest(() =>
defineNamespaces({
First: { namespace: "app" },
Second: { namespace: "app" },
}),
).throws("Found namespaces defined more than once in config: app");
});
});
58 changes: 58 additions & 0 deletions packages/store/ts/config/v2/namespaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { show, flatMorph } from "@arktype/util";
import { isObject, mergeIfUndefined } from "./generics";
import { NamespacesInput } from "./input";
import { AbiTypeScope, Scope } from "./scope";
import { validateNamespace, resolveNamespace } from "./namespace";
import { groupBy } from "@latticexyz/common/utils";

export type validateNamespaces<namespaces, scope extends Scope = AbiTypeScope> = {
[label in keyof namespaces]: validateNamespace<namespaces[label], scope>;
};

export function validateNamespaces<scope extends Scope = AbiTypeScope>(
namespaces: unknown,
scope: scope,
): asserts namespaces is NamespacesInput {
if (!isObject(namespaces)) {
throw new Error(`Expected namespaces, received ${JSON.stringify(namespaces)}`);
}
for (const namespace of Object.values(namespaces)) {
validateNamespace(namespace, scope);
}
}

export type resolveNamespaces<namespaces, scope extends Scope = AbiTypeScope> = {
readonly [label in keyof namespaces]: resolveNamespace<mergeIfUndefined<namespaces[label], { label: label }>, scope>;
};

export function resolveNamespaces<input extends NamespacesInput, scope extends Scope = AbiTypeScope>(
input: input,
scope: scope,
): show<resolveNamespaces<input, scope>> {
if (!isObject(input)) {
throw new Error(`Expected namespaces config, received ${JSON.stringify(input)}`);
}

const namespaces = flatMorph(input as NamespacesInput, (label, namespace) => [
label,
resolveNamespace(mergeIfUndefined(namespace, { label }), scope),
]);

// This should probably be in `validate`, but `namespace` gets set during the resolve step above, so it's easier to validate here.
const duplicates = Array.from(groupBy(Object.values(namespaces), (namespace) => namespace.namespace).entries())
.filter(([, entries]) => entries.length > 1)
.map(([namespace]) => namespace);
if (duplicates.length > 0) {
throw new Error(`Found namespaces defined more than once in config: ${duplicates.join(", ")}`);
}

return namespaces as never;
}

export function defineNamespaces<input, scope extends Scope = AbiTypeScope>(
input: validateNamespaces<input, scope>,
scope: scope = AbiTypeScope as never,
): show<resolveNamespaces<input, scope>> {
validateNamespaces(input, scope);
return resolveNamespaces(input, scope) as never;
}
34 changes: 26 additions & 8 deletions packages/store/ts/config/v2/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,13 @@ export type Codegen = {
*/
readonly outputDirectory: string;
/**
* Whether or not to organize codegen output (table libraries, etc.) into directories by namespace.
* Tables index filename.
*
* For example, a `Counter` table in the `app` namespace will have codegen at `codegen/app/tables/Counter.sol`.
* Defaults to `"index.sol"` when in single-namespace mode, and `false` for multi-namespace mode.
*
* Defaults to `true` when using top-level `namespaces` key, `false` otherwise.
* @deprecated We recommend importing directly from table libraries rather than from the index for better compile times and deterministic deploys.
Copy link
Member

Choose a reason for hiding this comment

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

ooc do we know that it impacts compile times?

Copy link
Member Author

@holic holic Jul 29, 2024

Choose a reason for hiding this comment

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

I haven't measured it but it's very likely given 1) more files need to be compiled, even if not used and 2) reports from @0xhank and others on more files = worse compile times (which may be fixable upstream in forge/foundry/solc)

The main motivation to deprecate this here is that any change to tables impacts bytecode of all things import index.sol, even if they aren't importing the table that changed. Which means systems become less consistent in terms of bytecode.

*/
// TODO: move `namespaces` key handling into store so we can conditionally turn this on/off
readonly namespaceDirectories: boolean;
readonly indexFilename: string;
readonly indexFilename: string | false;
};

export type Namespace = {
Expand All @@ -90,7 +88,28 @@ export type Namespaces = {
readonly [label: string]: Namespace;
};

export type Store = Omit<Namespace, "label"> & {
export type Store = {
/**
* @internal
* Whether this project is using multiple namespaces or not, dictated by using `namespaces` config key.
*
* If using multiple namespaces, systems must be organized in `namespaces/{namespaceLabel}` directories.
* Similarly, table libraries will be generated into these namespace directories.
*/
readonly multipleNamespaces: boolean;
/**
* When in single-namespace mode, this is set to the config's base `namespace`.
* When in multi-namespace mode, this is `null`.
*/
readonly namespace: string | null;
readonly namespaces: Namespaces;
/**
* Flattened set of tables, where each key is `{namespaceLabel}__{tableLabel}`.
* For namespace labels using an empty string, no double-underscore is used, so the key is `{tableLabel}`.
* This is kept for backwards compatibility.
* It's recommended that you use `config.namespaces[namespaceLabel].tables[tableLabel]` instead.
*/
readonly tables: Namespace["tables"];
/**
* Directory of Solidity source relative to the MUD config.
* This is used to resolve other paths in the config, like codegen and user types.
Expand All @@ -102,5 +121,4 @@ export type Store = Omit<Namespace, "label"> & {
readonly enums: Enums;
readonly enumValues: EnumValues;
readonly codegen: Codegen;
readonly namespaces: Namespaces;
};
Loading
Loading