Skip to content

Commit 122945e

Browse files
authored
feat(worldgen): support inherited symbols in worldgen (#3790)
1 parent b84bcc6 commit 122945e

12 files changed

Lines changed: 402 additions & 23 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@latticexyz/common": patch
3+
"@latticexyz/world": patch
4+
---
5+
6+
Support using inherited symbols when generating System interfaces and libraries.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Apply type qualifiers to function parameters or return types
3+
* @param params Array of parameter strings (e.g., ["uint256 value", "SomeStruct memory data"])
4+
* @param typeQualifiers Map of type names to their qualified forms (e.g., "SomeStruct" -> "IParent.SomeStruct")
5+
* @returns Array of parameters with qualified types applied
6+
*/
7+
export function applyTypeQualifiers(params: string[], typeQualifiers?: Map<string, string>): string[] {
8+
if (!typeQualifiers || typeQualifiers.size === 0) {
9+
return params;
10+
}
11+
12+
return params.map((param) => {
13+
// Split parameter into parts (e.g., "SomeStruct memory myParam" -> ["SomeStruct", "memory", "myParam"])
14+
const parts = param.trim().split(/\s+/);
15+
if (parts.length === 0) return param;
16+
17+
const type = parts[0];
18+
19+
// Check if this is an array type
20+
const arrayMatch = type.match(/^(.+?)(\[\]|\[\d+\])$/);
21+
if (arrayMatch) {
22+
const baseType = arrayMatch[1];
23+
const arraySuffix = arrayMatch[2];
24+
25+
// Check if base type needs qualification
26+
if (typeQualifiers.has(baseType)) {
27+
const qualifiedType = typeQualifiers.get(baseType) + arraySuffix;
28+
parts[0] = qualifiedType;
29+
return parts.join(" ");
30+
}
31+
} else {
32+
// Check if type needs qualification
33+
if (typeQualifiers.has(type)) {
34+
parts[0] = typeQualifiers.get(type)!;
35+
return parts.join(" ");
36+
}
37+
}
38+
39+
return param;
40+
});
41+
}

packages/common/src/codegen/utils/contractToInterface.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export interface ContractInterfaceError {
1616
parameters: string[];
1717
}
1818

19+
export interface QualifiedSymbol {
20+
symbol: string;
21+
qualifier?: string; // e.g., "IParentContract" for IParentContract.SomeStruct
22+
sourcePath: string;
23+
}
24+
1925
/**
2026
* Parse the contract data to get the functions necessary to generate an interface,
2127
* and symbols to import from the original contract.
@@ -26,16 +32,24 @@ export interface ContractInterfaceError {
2632
export function contractToInterface(
2733
source: string,
2834
contractName: string,
35+
findInheritedSymbol?: (symbol: string) => QualifiedSymbol | undefined,
2936
): {
3037
functions: ContractInterfaceFunction[];
3138
errors: ContractInterfaceError[];
3239
symbolImports: SymbolImport[];
40+
qualifiedSymbols: Map<string, QualifiedSymbol>;
3341
} {
34-
const ast = parse(source);
42+
let ast: SourceUnit;
43+
try {
44+
ast = parse(source);
45+
} catch (error) {
46+
throw new MUDError(`Failed to parse contract ${contractName}: ${error}`);
47+
}
3548
const contractNode = findContractNode(ast, contractName);
3649
let symbolImports: SymbolImport[] = [];
3750
const functions: ContractInterfaceFunction[] = [];
3851
const errors: ContractInterfaceError[] = [];
52+
const qualifiedSymbols = new Map<string, QualifiedSymbol>();
3953

4054
if (!contractNode) {
4155
throw new MUDError(`Contract not found: ${contractName}`);
@@ -68,7 +82,7 @@ export function contractToInterface(
6882

6983
for (const { typeName } of parameters.concat(returnParameters ?? [])) {
7084
const symbols = typeNameToSymbols(typeName);
71-
symbolImports = symbolImports.concat(symbolsToImports(ast, symbols));
85+
symbolImports = symbolImports.concat(symbolsToImports(ast, symbols, findInheritedSymbol, qualifiedSymbols));
7286
}
7387
}
7488
} catch (error: unknown) {
@@ -86,7 +100,7 @@ export function contractToInterface(
86100

87101
for (const parameter of parameters) {
88102
const symbols = typeNameToSymbols(parameter.typeName);
89-
symbolImports = symbolImports.concat(symbolsToImports(ast, symbols));
103+
symbolImports = symbolImports.concat(symbolsToImports(ast, symbols, findInheritedSymbol, qualifiedSymbols));
90104
}
91105
},
92106
});
@@ -95,6 +109,7 @@ export function contractToInterface(
95109
functions,
96110
errors,
97111
symbolImports,
112+
qualifiedSymbols,
98113
};
99114
}
100115

@@ -175,10 +190,49 @@ function typeNameToSymbols(typeName: TypeName | null): string[] {
175190
}
176191
}
177192

178-
function symbolsToImports(ast: SourceUnit, symbols: string[]): SymbolImport[] {
179-
return symbols.map((symbol) => {
180-
const symbolImport = findSymbolImport(ast, symbol);
181-
if (!symbolImport) throw new MUDError(`Symbol "${symbol}" has no explicit import`);
182-
return symbolImport;
183-
});
193+
function symbolsToImports(
194+
ast: SourceUnit,
195+
symbols: string[],
196+
findInheritedSymbol?: (symbol: string) => QualifiedSymbol | undefined,
197+
qualifiedSymbols?: Map<string, QualifiedSymbol>,
198+
): SymbolImport[] {
199+
const imports: SymbolImport[] = [];
200+
201+
for (const symbol of symbols) {
202+
// First check explicit imports
203+
const explicitImport = findSymbolImport(ast, symbol);
204+
if (explicitImport) {
205+
imports.push(explicitImport);
206+
continue;
207+
}
208+
209+
// Then check inherited symbols
210+
if (findInheritedSymbol) {
211+
const inheritedSymbol = findInheritedSymbol(symbol);
212+
if (inheritedSymbol) {
213+
// Track qualified symbol
214+
if (qualifiedSymbols) {
215+
qualifiedSymbols.set(symbol, inheritedSymbol);
216+
}
217+
// Add import for the parent contract if it has a qualifier
218+
if (inheritedSymbol.qualifier) {
219+
imports.push({
220+
symbol: inheritedSymbol.qualifier,
221+
path: inheritedSymbol.sourcePath,
222+
});
223+
}
224+
}
225+
}
226+
}
227+
228+
// Deduplicate imports
229+
const uniqueImports = new Map<string, SymbolImport>();
230+
for (const imp of imports) {
231+
const key = `${imp.symbol}:${imp.path}`;
232+
if (!uniqueImports.has(key)) {
233+
uniqueImports.set(key, imp);
234+
}
235+
}
236+
237+
return Array.from(uniqueImports.values());
184238
}

packages/common/src/codegen/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from "./contractToInterface";
22
export * from "./format";
33
export * from "./formatAndWrite";
44
export * from "./parseSystem";
5+
export * from "./resolveInheritedSymbols";
6+
export * from "./applyTypeQualifiers";
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { parse, visit } from "@solidity-parser/parser";
2+
import type { SourceUnit, ContractDefinition } from "@solidity-parser/parser/dist/src/ast-types";
3+
import { readFile } from "node:fs/promises";
4+
import path from "node:path";
5+
import { findContractNode } from "./findContractNode";
6+
import { QualifiedSymbol } from "./contractToInterface";
7+
import { findSymbolImport } from "./findSymbolImport";
8+
9+
interface InheritanceInfo {
10+
baseContracts: string[];
11+
baseContractImports: Map<string, string>; // baseName -> import path
12+
symbols: Map<string, { type: "struct" | "enum" | "error"; contract: string }>;
13+
filePath?: string; // Track where this contract was defined
14+
}
15+
16+
/**
17+
* Creates a resolver function that can find symbols through contract inheritance chains
18+
* @param contractPath Path to the contract file
19+
* @param contractName Name of the contract
20+
* @returns A function that resolves symbols to their qualified forms
21+
*/
22+
export async function createInheritanceResolver(
23+
contractPath: string,
24+
contractName: string,
25+
): Promise<(symbol: string) => QualifiedSymbol | undefined> {
26+
const resolvedContracts = new Map<string, InheritanceInfo>();
27+
const visitedPaths = new Set<string>();
28+
29+
async function parseContract(filePath: string, targetContractName?: string): Promise<InheritanceInfo | undefined> {
30+
// Prevent infinite recursion
31+
const normalizedPath = path.resolve(filePath);
32+
if (visitedPaths.has(normalizedPath)) {
33+
return undefined;
34+
}
35+
visitedPaths.add(normalizedPath);
36+
37+
try {
38+
const source = await readFile(filePath, "utf8");
39+
40+
// Try to parse the source
41+
let ast: SourceUnit;
42+
try {
43+
ast = parse(source);
44+
} catch (parseError) {
45+
// If parsing fails, log the error and skip this file
46+
console.warn(`Warning: Failed to parse ${filePath} for inheritance resolution:`, parseError);
47+
return undefined;
48+
}
49+
50+
// If targetContractName is specified, find that specific contract
51+
// Otherwise, process all contracts in the file
52+
const contractsToProcess: ContractDefinition[] = [];
53+
54+
if (targetContractName) {
55+
const contractNode = findContractNode(ast, targetContractName);
56+
if (contractNode) {
57+
contractsToProcess.push(contractNode);
58+
}
59+
} else {
60+
visit(ast, {
61+
ContractDefinition(node) {
62+
contractsToProcess.push(node);
63+
},
64+
});
65+
}
66+
67+
// Process each contract
68+
for (const contractNode of contractsToProcess) {
69+
const info: InheritanceInfo = {
70+
baseContracts: [],
71+
baseContractImports: new Map(),
72+
symbols: new Map(),
73+
filePath: normalizedPath,
74+
};
75+
76+
// Extract base contracts
77+
if (contractNode.baseContracts) {
78+
for (const base of contractNode.baseContracts) {
79+
info.baseContracts.push(base.baseName.namePath);
80+
}
81+
}
82+
83+
// Extract symbols defined in this contract
84+
visit(contractNode, {
85+
StructDefinition(node) {
86+
if (node.name) {
87+
info.symbols.set(node.name, { type: "struct", contract: contractNode.name });
88+
}
89+
},
90+
EnumDefinition(node) {
91+
if (node.name) {
92+
info.symbols.set(node.name, { type: "enum", contract: contractNode.name });
93+
}
94+
},
95+
CustomErrorDefinition(node) {
96+
if (node.name) {
97+
info.symbols.set(node.name, { type: "error", contract: contractNode.name });
98+
}
99+
},
100+
});
101+
102+
resolvedContracts.set(contractNode.name, info);
103+
104+
// Recursively process base contracts
105+
for (const baseName of info.baseContracts) {
106+
// First check if it's imported
107+
const importInfo = findSymbolImport(ast, baseName);
108+
if (importInfo) {
109+
// Store the original import path
110+
info.baseContractImports.set(baseName, importInfo.path);
111+
112+
const importPath = importInfo.path.startsWith(".")
113+
? path.resolve(path.dirname(filePath), importInfo.path)
114+
: importInfo.path;
115+
116+
await parseContract(importPath, baseName);
117+
}
118+
}
119+
}
120+
121+
return targetContractName ? resolvedContracts.get(targetContractName) : undefined;
122+
} catch (error) {
123+
// Silently fail if we can't read/parse a file
124+
return undefined;
125+
}
126+
}
127+
128+
// Parse the main contract and its inheritance chain
129+
await parseContract(contractPath, contractName);
130+
131+
// Create the resolver function
132+
return (symbol: string): QualifiedSymbol | undefined => {
133+
// Check if symbol is defined in the main contract
134+
const mainInfo = resolvedContracts.get(contractName);
135+
if (mainInfo?.symbols.has(symbol)) {
136+
// Symbol is defined in main contract, no qualification needed
137+
return {
138+
symbol,
139+
sourcePath: contractPath,
140+
};
141+
}
142+
143+
// Search through inheritance chain
144+
function searchInheritance(currentContract: string, visited: Set<string> = new Set()): QualifiedSymbol | undefined {
145+
if (visited.has(currentContract)) {
146+
return undefined;
147+
}
148+
visited.add(currentContract);
149+
150+
const contractInfo = resolvedContracts.get(currentContract);
151+
if (!contractInfo) {
152+
return undefined;
153+
}
154+
155+
// Check each base contract
156+
for (const baseName of contractInfo.baseContracts) {
157+
const baseInfo = resolvedContracts.get(baseName);
158+
if (baseInfo?.symbols.has(symbol)) {
159+
// Found the symbol in a base contract
160+
// Get the import path from the main contract to this base contract
161+
const importPath = mainInfo?.baseContractImports.get(baseName) || `./${baseName}.sol`;
162+
163+
return {
164+
symbol,
165+
qualifier: baseName,
166+
sourcePath: importPath,
167+
};
168+
}
169+
170+
// Recursively search in base's bases
171+
const result = searchInheritance(baseName, visited);
172+
if (result) {
173+
return result;
174+
}
175+
}
176+
177+
return undefined;
178+
}
179+
180+
return searchInheritance(contractName);
181+
};
182+
}

0 commit comments

Comments
 (0)