Skip to content

Commit a457ae5

Browse files
committed
fix: support syntax node as the first arg in local query
1 parent e8ae238 commit a457ae5

File tree

4 files changed

+131
-94
lines changed

4 files changed

+131
-94
lines changed

packages/dbml-parse/__tests__/examples/compiler/syncDiagramView.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ describe('syncDiagramView - entity name quoting', () => {
996996
},
997997
]);
998998
const compiler = new Compiler();
999-
compiler.setSource(newDbml);
999+
compiler.setSource(DEFAULT_ENTRY, newDbml);
10001000
expect(compiler.parse.errors()).toHaveLength(0);
10011001
});
10021002

packages/dbml-parse/__tests__/snapshots/lexer/lexer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('[snapshot] lexer', () => {
3636
const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8');
3737

3838
const compiler = new Compiler();
39-
compiler.setSource(program);
39+
compiler.setSource(DEFAULT_ENTRY, program);
4040

4141
const lexer = new Lexer(program, DEFAULT_ENTRY);
4242

packages/dbml-parse/__tests__/snapshots/nan/nan.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
scanTestNames, toSnapshot,
1010
} from '@tests/utils';
1111
import Compiler from '@/compiler';
12+
import { DEFAULT_ENTRY } from '@/constants';
1213
import type { Database } from '@/core/types/schemaJson';
1314
import type Report from '@/core/types/report';
1415

@@ -30,7 +31,7 @@ describe('[snapshot] nan', () => {
3031
const program = readFileSync(path.resolve(__dirname, `./input/${testName}.in.dbml`), 'utf-8');
3132

3233
const compiler = new Compiler();
33-
compiler.setSource(program);
34+
compiler.setSource(DEFAULT_ENTRY, program);
3435
const report = compiler.parse._();
3536

3637
it(testName, () => expect(serializeInterpreterResult(compiler, report)).toMatchFileSnapshot(path.resolve(__dirname, `./output/${testName}.out.json`)));

packages/dbml-parse/src/compiler/index.ts

Lines changed: 127 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
type Internable, type Primitive, intern,
2020
} from '@/core/types/internable';
2121
import {
22-
SyntaxNodeIdGenerator,
22+
SyntaxNode, SyntaxNodeIdGenerator,
2323
} from '@/core/types/nodes';
2424
import {
2525
NodeSymbolIdGenerator, SymbolFactory,
@@ -94,14 +94,6 @@ const COMPUTING = Symbol('COMPUTING');
9494
type QuerySymbol = symbol;
9595

9696
export default class Compiler {
97-
// A global cache mapping from:
98-
// (QuerySymbol, interned argument string) -> Result
99-
private globalCache = new Map<QuerySymbol, Map<string, any>>();
100-
101-
// A local cache mapping from:
102-
// (Filepath, QuerySymbol, interned argument string) -> Result
103-
private localCache = new Map<FilepathId, Map<QuerySymbol, Map<string, any>>>();
104-
10597
// Interners
10698
nodeIdGenerator = new SyntaxNodeIdGenerator();
10799

@@ -132,30 +124,45 @@ export default class Compiler {
132124
this.globalCache.clear();
133125
}
134126

127+
// A global cache mapping from:
128+
// (QuerySymbol, interned argument string) -> Result
129+
private globalCache = new Map<QuerySymbol, Map<string, any>>();
130+
131+
// Turn a normal function into a Compiler's global query
132+
// Input: A function that only accepts internable types | primitive types
133+
// Output: A global query wrapping the function
135134
private globalQuery<Args extends (Primitive | Primitive[] | Internable<Primitive>)[], Return> (
136135
fn: (this: Compiler, ...args: Args) => Return,
137136
): (...args: Args) => Return {
137+
// 1. Generates a unique query symbol for the query
138138
const queryKey = Symbol();
139+
139140
return ((...args: Args): Return => {
141+
// Ensure there's a cache entry for this Query
142+
if (!this.globalCache.has(queryKey)) {
143+
this.globalCache.set(queryKey, new Map());
144+
}
145+
146+
// 2. Intern all arguments and join into a single string
140147
const argKey = args.map((a) => intern(a)).join('\0');
141-
let subCache = this.globalCache.get(queryKey);
142-
if (subCache instanceof Map) {
143-
if (subCache.has(argKey)) {
144-
const cached = subCache.get(argKey);
145-
if (cached === COMPUTING) {
146-
throw new Error(`Cycle detected in query: ${fn.name}(${argKey})`);
147-
}
148-
return cached;
148+
149+
// 3. Check the cache using (QuerySymbol, interned argument string)
150+
const subCache = this.globalCache.get(queryKey)!;
151+
if (subCache.has(argKey)) {
152+
const cached = subCache.get(argKey);
153+
// 4. If the cache entry is being computed -> Throw query cycle error
154+
if (cached === COMPUTING) {
155+
throw new Error(`Cycle detected in query: ${fn.name}(${argKey})`);
149156
}
157+
// 5. Cache hit, return
158+
return cached;
150159
}
151160

152-
if (!(subCache instanceof Map)) {
153-
subCache = new Map();
154-
this.globalCache.set(queryKey, subCache);
155-
}
161+
// 6. Flag the current entry as computing
156162
subCache.set(argKey, COMPUTING);
157163

158164
try {
165+
// 7. Cache miss, recompute & cache
159166
const result = fn.apply(this, args);
160167
subCache.set(argKey, result);
161168
return result;
@@ -166,24 +173,45 @@ export default class Compiler {
166173
}) as (...args: Args) => Return;
167174
}
168175

169-
private localQuery<Args extends [Filepath, ...(Primitive | Primitive[] | Internable<Primitive>)[]], Return> (
176+
// A local cache mapping from:
177+
// (Filepath, QuerySymbol, interned argument string) -> Result
178+
private localCache = new Map<FilepathId, Map<QuerySymbol, Map<string, any>>>();
179+
180+
// Turn a normal function into a Compiler's local query
181+
// Input: A function whose first arg is a Filepath or SyntaxNode (from which we extract .filepath)
182+
// & accepts internable types | primitive types for the rest
183+
// Output: A local query wrapping the function, cached per-file
184+
private localQuery<Args extends [
185+
Filepath | SyntaxNode, // filepath or node as first arg
186+
...(Primitive | Primitive[] | Internable<Primitive>)[],
187+
], Return> (
170188
fn: (this: Compiler, ...args: Args) => Return,
171189
): (...args: Args) => Return {
190+
// 1. Generates a unique query symbol for the query
172191
const queryKey = Symbol();
192+
173193
return ((...args: Args): Return => {
194+
// 2. Retrieve the filepath (from Filepath directly or from SyntaxNode.filepath)
174195
const [
175-
filepath,
196+
firstArg,
176197
...rest
177198
] = args;
199+
const filepath = firstArg instanceof SyntaxNode ? firstArg.filepath : firstArg;
178200
const filepathId = filepath.intern();
179-
const argKey = rest.map((a) => intern(a)).join('\0');
180201

202+
// 3. Intern all arguments (including first) and join into a single string
203+
// The first arg is interned too so different nodes from the same file
204+
// don't collide in the cache.
205+
const argKey = args.map((a) => intern(a)).join('\0');
206+
207+
// Ensure that the local cache for the filepath exists
181208
let filepathCache = this.localCache.get(filepathId);
182209
if (!filepathCache) {
183210
filepathCache = new Map();
184211
this.localCache.set(filepathId, filepathCache);
185212
}
186213

214+
// Ensure that the local subcache for the (filepath, QuerySymbol) exists
187215
let subCache = filepathCache.get(queryKey);
188216
if (!subCache) {
189217
subCache = new Map();
@@ -192,14 +220,18 @@ export default class Compiler {
192220

193221
if (subCache.has(argKey)) {
194222
const cached = subCache.get(argKey);
223+
// 4. Query cycle, throw
195224
if (cached === COMPUTING) {
196225
throw new Error(`Cycle detected in query: ${fn.name}(${filepathId}, ${argKey})`);
197226
}
227+
// 5. Cache hit, return
198228
return cached;
199229
}
200230

231+
// 6. Flag the current query as being computed
201232
subCache.set(argKey, COMPUTING);
202233
try {
234+
// 7. Recompute the query result & cache
203235
const result = fn.apply(this, args);
204236
subCache.set(argKey, result);
205237
return result;
@@ -210,102 +242,105 @@ export default class Compiler {
210242
}) as (...args: Args) => Return;
211243
}
212244

213-
// A local query.
214-
// Lex + parse a single file into an AST. Related: parseProject.
215-
// (filepath: Filepath) => Report<FileParseIndex>
245+
// A local query
246+
// Lex + parse a single file into an AST
247+
// Signature: (filepath: Filepath) => Report<FileParseIndex>
216248
parseFile = this.localQuery(parseFile);
217-
// A global query.
218-
// Lex + parse all entry-point files. Related: parseFile.
219-
// () => Map<string, Report<FileParseIndex>>
249+
// A global query
250+
// Lex + parse all entry-point files
251+
// Returns a map from the file's absolute path to FileParseIndex
252+
// Signature: () => Map<string, Report<FileParseIndex>>
220253
parseProject = this.globalQuery(parseProject);
221254

222-
// A global query.
223-
// Run the binder on a single AST node (dispatches to global modules). Related: bindFile, bindProject.
224-
// (node: SyntaxNode) => Report<void> | Report<Unhandled>
255+
// A local query
256+
// Validate an AST node and return any compile errors
257+
// Signature: (node: SyntaxNode) => Report<void> | Report<Unhandled>
258+
validateNode = this.localQuery(validateNode);
259+
// A local query
260+
// Return the fully-qualified name segments of an AST node (e.g. ['public', 'users'])
261+
// Signature: (node: SyntaxNode) => Report<string[] | undefined> | Report<Unhandled>
262+
nodeFullname = this.localQuery(fullname);
263+
// A local query
264+
// Return the alias string of an AST node if one is declared
265+
// Signature: (node: SyntaxNode) => Report<string | undefined> | Report<Unhandled>
266+
nodeAlias = this.localQuery(nodeAlias);
267+
// A local query
268+
// Return the settings/options map of an AST node
269+
// Signature: (node: SyntaxNode) => Report<Settings> | Report<Unhandled>
270+
nodeSettings = this.localQuery(nodeSettings);
271+
272+
// A global query
273+
// Run the binder on a single AST node
274+
// Signature: (node: SyntaxNode) => Report<void> | Report<Unhandled>
225275
bindNode = this.globalQuery(bindNode);
226-
// A global query.
227-
// Bind a single file's AST (calls bindNode on the root). Related: bindNode, bindProject.
228-
// (filepath: Filepath) => Report<void>
276+
// A global query
277+
// Bind a single file's AST (calls bindNode on the root)
278+
// Signature: (filepath: Filepath) => Report<void>
229279
bindFile = this.globalQuery(bindFile);
230-
// A global query.
231-
// Bind all entry-point files in dependency order. Related: bindFile.
232-
// () => Map<string, Report<void>>
280+
// A global query
281+
// Bind all entry-point files in dependency order
282+
// Returns a map from the file's absolute path to bind errors
283+
// Signature: () => Map<string, Report<void>>
233284
bindProject = this.globalQuery(bindProject);
234285

235-
// A global query.
236-
// Interpret a single AST node into a SchemaElement (dispatches to global modules). Related: interpretFile, interpretProject.
237-
// (node: SyntaxNode) => Report<SchemaElement | SchemaElement[] | undefined> | Report<Unhandled>
286+
// A global query
287+
// Interpret a single AST node into a SchemaElement
288+
// Signature: (node: SyntaxNode) => Report<SchemaElement | SchemaElement[] | undefined> | Report<Unhandled>
238289
interpretNode = this.globalQuery(interpretNode);
239-
// A global query.
240-
// Interpret a single file's AST into a raw Database. Related: interpretNode, interpretProject.
241-
// (filepath: Filepath) => Report<Readonly<Database> | undefined>
290+
// A global query
291+
// Interpret a single file's AST into a raw Database
292+
// Signature: (filepath: Filepath) => Report<Readonly<Database> | undefined>
242293
interpretFile = this.globalQuery(interpretFile);
243-
// A global query.
244-
// Interpret all reachable files and merge into a MasterDatabase. Related: interpretFile.
245-
// () => Report<MasterDatabase>
294+
// A global query
295+
// Interpret all reachable files and merge into a MasterDatabase (A database but for multifile)
296+
// Signature: () => Report<MasterDatabase>
246297
interpretProject = this.globalQuery(interpretProject);
247-
// A global query.
248-
// Export a reconciled, alias-resolved Database for a single file (calls interpretProject internally). Related: interpretProject.
249-
// (filepath: Filepath) => Report<Readonly<Database> | undefined>
298+
// A global query
299+
// Start from an entrypoint file, collects all imports and merges into the entrypoint
300+
// Export a Database representing this merged view of the MasterDatabase
301+
// Signature: (filepath: Filepath) => Report<Readonly<Database> | undefined>
250302
exportSchemaJson = this.globalQuery(exportSchemaJson);
251303

252-
// A global query.
253-
// Resolve the NodeSymbol declared by a given AST node. Related: symbolMembers, nodeReferee.
254-
// (node: SyntaxNode) => Report<NodeSymbol> | Report<Unhandled>
304+
// A global query
305+
// Return the NodeSymbol for a single SyntaxNode
306+
// Signature: (node: SyntaxNode) => Report<NodeSymbol> | Report<Unhandled>
255307
nodeSymbol = this.globalQuery(nodeSymbol);
256-
// A global query.
257-
// Return all direct child symbols of a symbol. Related: nodeSymbol, lookupMembers.
258-
// (symbol: NodeSymbol) => Report<NodeSymbol[]> | Report<Unhandled>
308+
// A global query
309+
// Return all direct child symbols of a symbol
310+
// Signature: (symbol: NodeSymbol) => Report<NodeSymbol[]> | Report<Unhandled>
259311
symbolMembers = this.globalQuery(symbolMembers);
260-
// A global query.
261-
// Look up a member by kind and name inside a symbol or node scope. Related: symbolMembers.
262-
// (symbolOrNode: NodeSymbol | SyntaxNode, targetKind: SymbolKind, targetName: string) => Report<NodeSymbol | undefined>
312+
// A global query
313+
// Look up a member by kind and name inside a symbol or node scope
314+
// Signature: (symbolOrNode: NodeSymbol | SyntaxNode, targetKind: SymbolKind, targetName: string) => Report<NodeSymbol | undefined>
263315
lookupMembers = this.globalQuery(lookupMembers);
264-
// A global query.
316+
// A global query
265317
// Resolve what symbol a reference node points to. Related: nodeSymbol, symbolReferences.
266-
// (node: SyntaxNode) => Report<NodeSymbol | undefined> | Report<Unhandled>
318+
// Signature: (node: SyntaxNode) => Report<NodeSymbol | undefined> | Report<Unhandled>
267319
nodeReferee = this.globalQuery(nodeReferee);
268-
// A global query.
320+
// A global query
269321
// Find all reference nodes that point to a given symbol. Related: nodeReferee.
270-
// (symbol: NodeSymbol) => Report<SyntaxNode[]>
322+
// Signature: (symbol: NodeSymbol) => Report<SyntaxNode[]>
271323
symbolReferences = this.globalQuery(symbolReferences);
272-
// A global query.
324+
// A global query
273325
// Return all AliasSymbols across the project whose originalSymbol is the given symbol (transitive).
274-
// (symbol: NodeSymbol) => Report<AliasSymbol[]>
326+
// Signature: (symbol: NodeSymbol) => Report<AliasSymbol[]>
275327
symbolAliases = this.globalQuery(symbolAliases);
276-
// A global query.
328+
// A global query
277329
// Return all UseSymbols across the project whose originalSymbol is the given symbol (transitive).
278-
// (symbol: NodeSymbol) => Report<UseSymbol[]>
330+
// Signature: (symbol: NodeSymbol) => Report<UseSymbol[]>
279331
symbolUses = this.globalQuery(symbolUses);
280332

281-
// A global query.
282-
// Validate an AST node and return any compile errors.
283-
// (node: SyntaxNode) => Report<void> | Report<Unhandled>
284-
validateNode = this.globalQuery(validateNode);
285-
// A global query.
286-
// Return the fully-qualified name segments of an AST node (e.g. ['public', 'users']).
287-
// (node: SyntaxNode) => Report<string[] | undefined> | Report<Unhandled>
288-
nodeFullname = this.globalQuery(fullname);
289-
// A global query.
290-
// Return the alias string of an AST node if one is declared. Related: nodeFullname.
291-
// (node: SyntaxNode) => Report<string | undefined> | Report<Unhandled>
292-
nodeAlias = this.globalQuery(nodeAlias);
293-
// A global query.
294-
// Return the settings/options map of an AST node.
295-
// (node: SyntaxNode) => Report<Settings> | Report<Unhandled>
296-
nodeSettings = this.globalQuery(nodeSettings);
297-
298-
// A local query.
333+
// A local query
299334
// Return the direct import filepath IDs declared by use statements in a file. Related: reachableFiles.
300-
// (filepath: Filepath) => Set<Filepath>
335+
// Signature: (filepath: Filepath) => Filepath[]
301336
fileDependencies = this.localQuery(fileDependencies);
302-
// A local query.
337+
// A local query
303338
// BFS-traverse imports from an entry filepath and return all reachable files. Related: fileDependencies.
304-
// (entry: Filepath) => Set<Filepath>
339+
// Signature: (entry: Filepath) => Filepath[]
305340
reachableFiles = this.localQuery(reachableFiles);
306-
// A global query.
341+
// A global query
307342
// Return the importable members (non-schema, schema, reuses, uses) of a schema symbol or file. Related: topLevelSchemaMembers.
308-
// (symbolOrFilepath: SchemaSymbol | Filepath) => Report<{ nonSchemaMembers, schemaMembers, reuses, uses }>
343+
// Signature: (symbolOrFilepath: SchemaSymbol | Filepath) => Report<{ nonSchemaMembers, schemaMembers, reuses, uses }>
309344
usableMembers = this.globalQuery(usableMembers);
310345

311346
// transform queries
@@ -343,6 +378,7 @@ export default class Compiler {
343378
return {
344379
definitionProvider: new DBMLDefinitionProvider(this),
345380
referenceProvider: new DBMLReferencesProvider(this),
381+
// Trigger completion on these characters
346382
autocompletionProvider: new DBMLCompletionItemProvider(this, [
347383
'.',
348384
',',

0 commit comments

Comments
 (0)