Skip to content

Commit 31085e1

Browse files
committed
Allow shadowing by keeping a list of definitions in the
`definitionsByName` map
1 parent df21b96 commit 31085e1

23 files changed

+153
-81
lines changed

quint/src/names/base.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @module
1313
*/
1414

15+
import { cloneDeep, compact } from 'lodash'
1516
import { QuintDef, QuintExport, QuintImport, QuintInstance, QuintLambdaParameter } from '../ir/quintIr'
1617
import { QuintType } from '../ir/quintTypes'
1718
import { QuintError } from '../quintError'
@@ -49,7 +50,7 @@ export type LookupDefinition = (QuintDef | ({ kind: 'param' } & QuintLambdaParam
4950
/**
5051
* A module's definitions, indexed by name.
5152
*/
52-
export type DefinitionsByName = Map<string, LookupDefinition & { hidden?: boolean }>
53+
export type DefinitionsByName = Map<string, (LookupDefinition & { hidden?: boolean })[]>
5354

5455
/**
5556
* Definitions for each module
@@ -83,6 +84,10 @@ export type NameResolutionResult = {
8384
errors: QuintError[]
8485
}
8586

87+
export function getTopLevelDef(defs: DefinitionsByName, name: string): LookupDefinition | undefined {
88+
return defs.get(name)?.at(0)
89+
}
90+
8691
/**
8792
* Copy the names of a definitions table to a new one, ignoring hidden
8893
* definitions, and optionally adding a namespace.
@@ -100,11 +105,15 @@ export function copyNames(
100105
): DefinitionsByName {
101106
const table = new Map()
102107

103-
originTable.forEach((def, identifier) => {
108+
originTable.forEach((defs, identifier) => {
104109
const name = namespace ? [namespace, identifier].join('::') : identifier
105-
if (!def.hidden || def.kind === 'const') {
106-
table.set(name, copyAsHidden ? { ...def, hidden: copyAsHidden } : def)
107-
}
110+
const newDefs = defs.map(def => {
111+
if (!def.hidden || def.kind === 'const') {
112+
return cloneDeep(copyAsHidden ? { ...def, hidden: copyAsHidden } : def)
113+
}
114+
})
115+
116+
table.set(name, compact(newDefs))
108117
})
109118

110119
return table

quint/src/names/collector.ts

+73-27
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
addNamespacesToDef,
3535
builtinNames,
3636
copyNames,
37+
getTopLevelDef,
3738
} from './base'
3839
import {
3940
moduleNotFoundError,
@@ -42,7 +43,7 @@ import {
4243
paramNotFoundError,
4344
selfReferenceError,
4445
} from './importErrors'
45-
import { compact } from 'lodash'
46+
import { cloneDeep, compact } from 'lodash'
4647

4748
/**
4849
* Collects all top-level definitions in Quint modules. Used internally by
@@ -118,7 +119,7 @@ export class NameCollector implements IRVisitor {
118119
return
119120
}
120121

121-
const instanceTable = new Map([...moduleTable.entries()])
122+
const instanceTable = cloneDeep(moduleTable)
122123
if (decl.qualifiedName) {
123124
// Add the qualifier to `definitionsMyModule` map with a copy of the
124125
// definitions, so if there is an export of that qualifier, we know which
@@ -129,7 +130,8 @@ export class NameCollector implements IRVisitor {
129130
// For each override, check if the name exists in the instantiated module and is a constant.
130131
// If so, update the value definition to point to the expression being overriden
131132
decl.overrides.forEach(([param, ex]) => {
132-
const constDef = instanceTable.get(param.name)
133+
// Constants are always top-level
134+
const constDef = getTopLevelDef(instanceTable, param.name)
133135

134136
if (!constDef) {
135137
this.errors.push(paramNotFoundError(decl, param))
@@ -142,13 +144,14 @@ export class NameCollector implements IRVisitor {
142144
}
143145

144146
// Update the definition to point to the expression being overriden
145-
instanceTable.set(param.name, { ...constDef, id: ex.id })
147+
constDef.id = ex.id
148+
constDef.hidden = false
146149
})
147150

148151
// All names from the instanced module should be acessible with the instance namespace
149152
// So, copy them to the current module's lookup table
150153
const newDefs = copyNames(instanceTable, decl.qualifiedName, true)
151-
this.collectDefinitions(newDefs, decl)
154+
this.collectTopLevelDefinitions(newDefs, decl)
152155
}
153156

154157
enterImport(decl: QuintImport): void {
@@ -158,7 +161,7 @@ export class NameCollector implements IRVisitor {
158161
return
159162
}
160163

161-
const moduleTable = this.definitionsByModule.get(decl.protoName)
164+
const moduleTable = cloneDeep(this.definitionsByModule.get(decl.protoName))
162165

163166
if (!moduleTable) {
164167
// Importing non-existing module
@@ -178,12 +181,12 @@ export class NameCollector implements IRVisitor {
178181

179182
if (!decl.defName || decl.defName === '*') {
180183
// Imports all definitions
181-
this.collectDefinitions(importableDefinitions, decl)
184+
this.collectTopLevelDefinitions(importableDefinitions, decl)
182185
return
183186
}
184187

185188
// Tries to find a specific definition, reporting an error if not found
186-
const newDef = importableDefinitions.get(decl.defName)
189+
const newDef = getTopLevelDef(importableDefinitions, decl.defName)
187190
if (!newDef) {
188191
this.errors.push(nameNotFoundError(decl))
189192
return
@@ -203,7 +206,7 @@ export class NameCollector implements IRVisitor {
203206
return
204207
}
205208

206-
const moduleTable = this.definitionsByModule.get(decl.protoName)
209+
const moduleTable = cloneDeep(this.definitionsByModule.get(decl.protoName))
207210
if (!moduleTable) {
208211
// Exporting non-existing module
209212
this.errors.push(moduleNotFoundError(decl))
@@ -214,12 +217,12 @@ export class NameCollector implements IRVisitor {
214217

215218
if (!decl.defName || decl.defName === '*') {
216219
// Export all definitions
217-
this.collectDefinitions(exportableDefinitions, decl)
220+
this.collectTopLevelDefinitions(exportableDefinitions, decl)
218221
return
219222
}
220223

221224
// Tries to find a specific definition, reporting an error if not found
222-
const newDef = exportableDefinitions.get(decl.defName)
225+
const newDef = getTopLevelDef(exportableDefinitions, decl.defName)
223226

224227
if (!newDef) {
225228
this.errors.push(nameNotFoundError(decl))
@@ -257,14 +260,36 @@ export class NameCollector implements IRVisitor {
257260
return
258261
}
259262

260-
if (this.definitionsByName.has(identifier) && this.definitionsByName.get(identifier)!.id != def.id) {
261-
// Conflict with a previous definition
262-
this.recordConflict(identifier, this.definitionsByName.get(identifier)!.id, source)
263+
def.depth ??= 0
264+
const namespaces = importedFrom ? this.namespaces(importedFrom) : []
265+
266+
if (!this.definitionsByName.has(identifier)) {
267+
// No existing defs with this name. Create an entry with a single def.
268+
this.definitionsByName.set(identifier, [{ ...addNamespacesToDef(def, namespaces), importedFrom }])
263269
return
264270
}
265271

266-
const namespaces = importedFrom ? this.namespaces(importedFrom) : []
267-
this.definitionsByName.set(identifier, { ...addNamespacesToDef(def, namespaces), importedFrom })
272+
// Else: There are exiting defs. We need to check for conflicts
273+
const existingEntries = this.definitionsByName.get(identifier)!
274+
// Entries conflict if they have different ids, but the same depth.
275+
// Entries with different depths are ok, because one is shadowing the
276+
// other.
277+
const conflictingEntries = existingEntries.filter(entry => entry.id !== def.id && entry.depth === def.depth)
278+
279+
// Record potential errors and move on
280+
conflictingEntries.forEach(existingEntry => {
281+
this.recordConflict(identifier, existingEntry.id, source)
282+
})
283+
284+
// Keep entries with different ids. DON'T keep the whole
285+
// `existingEntries` since those may contain the same exact defs, but
286+
// hidden.
287+
this.definitionsByName.set(
288+
identifier,
289+
existingEntries
290+
.filter(entry => entry.id !== def.id)
291+
.concat([{ ...addNamespacesToDef(def, namespaces), importedFrom }])
292+
)
268293
}
269294

270295
/**
@@ -273,19 +298,25 @@ export class NameCollector implements IRVisitor {
273298
* @param identifier - The identifier of the definition to delete.
274299
*/
275300
deleteDefinition(identifier: string): void {
276-
this.definitionsByName.delete(identifier)
301+
this.definitionsByName.get(identifier)?.pop()
302+
return
277303
}
278304

279305
/**
280-
* Obtains a collected definition.
306+
* Gets the definition with the given name, in the current (visiting) scope
281307
*
282308
* @param identifier - The identifier of the definition to retrieve.
283309
*
284310
* @returns The definition object for the given identifier, or undefined if a
285311
* definitions with that identifier was never collected.
286312
*/
287313
getDefinition(identifier: string): LookupDefinition | undefined {
288-
return this.definitionsByName.get(identifier)
314+
const defs = this.definitionsByName.get(identifier)
315+
if (defs === undefined || defs.length === 0) {
316+
return
317+
}
318+
319+
return defs[defs.length - 1]
289320
}
290321

291322
private namespaces(decl: QuintImport | QuintInstance | QuintExport): string[] {
@@ -297,18 +328,33 @@ export class NameCollector implements IRVisitor {
297328
return namespace ? [namespace] : []
298329
}
299330

300-
private collectDefinitions(
331+
private collectTopLevelDefinitions(
301332
newDefs: DefinitionsByName,
302333
importedFrom?: QuintImport | QuintExport | QuintInstance
303334
): void {
304335
const namespaces = importedFrom ? this.namespaces(importedFrom) : []
305-
const newEntries: [string, LookupDefinition][] = [...newDefs.entries()].map(([identifier, def]) => {
306-
const existingEntry = this.definitionsByName.get(identifier)
307-
if (existingEntry && existingEntry.id !== def.id) {
308-
this.recordConflict(identifier, existingEntry.id, def.id)
309-
}
310-
return [identifier, { ...addNamespacesToDef(def, namespaces), importedFrom }]
311-
})
336+
const newEntries: [string, LookupDefinition[]][] = compact(
337+
[...newDefs.keys()].map(identifier => {
338+
const def = getTopLevelDef(newDefs, identifier)
339+
if (!def) {
340+
return
341+
}
342+
343+
const existingEntries = this.definitionsByName.get(identifier)
344+
if (existingEntries) {
345+
const conflictingEntries = existingEntries.filter(entry => entry.id !== def.id)
346+
conflictingEntries.forEach(existingEntry => {
347+
this.recordConflict(identifier, existingEntry.id, def.id)
348+
})
349+
350+
// Keep conflicting entries and add the new one. DON'T keep the whole
351+
// `existingEntries` since those may contain the same exact defs, but
352+
// hidden.
353+
return [identifier, conflictingEntries.concat([{ ...addNamespacesToDef(def, namespaces), importedFrom }])]
354+
}
355+
return [identifier, [{ ...addNamespacesToDef(def, namespaces), importedFrom }]]
356+
})
357+
)
312358

313359
this.definitionsByName = new Map([...this.definitionsByName.entries(), ...newEntries])
314360
}

quint/src/names/resolver.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ class NameResolver implements IRVisitor {
5858
unusedDefinitions: UnusedDefinitions = moduleName => {
5959
const definitions: LookupDefinition[] = Array.from(
6060
this.collector.definitionsByModule.get(moduleName)?.values() || []
61-
)
62-
const usedDefinitions = [...this.table.values()]
61+
).flat()
62+
const usedDefinitions = [...this.table.values()].flat()
6363

6464
return new Set(difference(definitions, usedDefinitions))
6565
}

0 commit comments

Comments
 (0)