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

New flattening for imports and exports #1109

Merged
merged 17 commits into from
Aug 21, 2023
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
174 changes: 174 additions & 0 deletions quint/src/flattening/flattener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* ----------------------------------------------------------------------------------
* Copyright (c) Informal Systems 2023. All rights reserved.
* Licensed under the Apache 2.0.
* See License.txt in the project root for license information.
* --------------------------------------------------------------------------------- */

/**
* Flattening for modules, replacing instances, imports and exports with definitions refered by the module.
*
* @author Gabriela Moreira
*
* @module
*/

import { LookupDefinition, LookupTable, builtinNames } from '../names/base'
import {
QuintApp,
QuintDef,
QuintExport,
QuintImport,
QuintInstance,
QuintModule,
QuintName,
qualifier,
} from '../ir/quintIr'

import { IRVisitor, walkDefinition, walkModule } from '../ir/IRVisitor'
import { addNamespaceToDefinition } from '../ir/namespacer'

/**
* Flatten a module, replacing instances, imports and exports with definitions refered by the module.
*
* @param quintModule - The module to be flattened
* @param modulesByName - A map of referred modules by name
* @param lookupTable - The lookup table for the module and all its references
*
* @returns The flattened module
*/
export function flattenModule(
quintModule: QuintModule,
modulesByName: Map<string, QuintModule>,
lookupTable: LookupTable
): QuintModule {
const moduleCopy: QuintModule = { ...quintModule, declarations: [...quintModule.declarations] }
const flattener = new Flattener(modulesByName, lookupTable)
walkModule(flattener, moduleCopy)
return moduleCopy
}

class Flattener implements IRVisitor {
private modulesByName: Map<string, QuintModule>
private lookupTable: LookupTable
// Buffer of definitions to add to the module. We can try to make this ordered in the future.
// For now, we rely on toposorting defs after flattening.
private defsToAdd: Map<string, QuintDef> = new Map()
// Store a namespace to use when recursing into nested definitions
private namespaceForNested?: string

constructor(modulesByName: Map<string, QuintModule>, lookupTable: LookupTable) {
this.modulesByName = modulesByName
this.lookupTable = lookupTable
}

enterModule(_quintModule: QuintModule) {
// Reset `defsToAdd`
this.defsToAdd = new Map()
}

exitModule(quintModule: QuintModule) {
// Get rid of imports and exports, and add the definitions we collected
quintModule.declarations = quintModule.declarations.filter(d => d.kind !== 'import' && d.kind !== 'export')
quintModule.declarations.push(...this.defsToAdd.values())
}

enterName(name: QuintName) {
this.flattenName(name.id)
}

enterApp(app: QuintApp) {
this.flattenName(app.id)
}

enterExport(decl: QuintExport) {
// Find all top-level definitions in the exported module that are used somewhere
// (not necessarily in the module itself)
const ids = this.modulesByName.get(decl.protoName)!.declarations.map(d => d.id)
const definitions = [...this.lookupTable.values()].filter(d => ids.includes(d.id))

definitions.forEach(def => {
if (def.kind === 'param') {
throw new Error(
`Impossible: intersection of top-level declarations with lookup table entries should never be a param. Found ${def}`
)
}

if (this.defsToAdd.has(def.name)) {
// Already added
return
}
bugarela marked this conversation as resolved.
Show resolved Hide resolved

const namespace = this.namespaceForNested ?? qualifier(decl)
const newDef =
namespace && !def.name.startsWith(namespace)
? addNamespaceToDefinition(def, namespace, new Set(builtinNames))
: def

this.walkNested(namespace, newDef)
this.defsToAdd.set(newDef.name, newDef)
})
}

enterInstance(decl: QuintInstance) {
if (decl.qualifiedName) {
this.modulesByName.set(decl.qualifiedName, this.modulesByName.get(decl.protoName)!)
}
}

enterImport(decl: QuintImport) {
if (decl.qualifiedName) {
this.modulesByName.set(decl.qualifiedName, this.modulesByName.get(decl.protoName)!)
}
}

private flattenName(id: bigint) {
const def = this.lookupTable.get(id)!
if (!def) {
// skip bulit-in names
return
}

if (def.kind === 'def' && def.depth && def.depth > 0) {
// skip non-top level definitions
return
}

if (def.kind === 'param') {
// skip lambda parameters
return
}

if (def.importedFrom?.kind === 'instance') {
// Don't do anything for definitions that come from instances. Those have to be instantiated first.
return
}

const namespace = this.namespaceForNested ?? getNamespaceForDef(def)
const newDef =
namespace && !def.name.startsWith(namespace)
? addNamespaceToDefinition(def, namespace, new Set(builtinNames))
: def

this.walkNested(namespace, newDef)
this.defsToAdd.set(newDef.name, newDef)
}

private walkNested(namespace: string | undefined, def: QuintDef) {
// Save previous value to be restored
const nestedBefore = this.namespaceForNested

this.namespaceForNested = namespace
walkDefinition(this, def)

// Restore previous value
this.namespaceForNested = nestedBefore
}
}

function getNamespaceForDef(def: LookupDefinition): string | undefined {
if (!def.namespaces) {
return
}

return [...def.namespaces].reverse().join('::')
}
8 changes: 8 additions & 0 deletions quint/src/ir/quintIr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,11 @@ export interface FlatModule extends WithId {
export function isDef(decl: QuintDeclaration): decl is QuintDef {
return decl.kind !== 'instance' && decl.kind !== 'import' && decl.kind !== 'export'
}

export function qualifier(decl: QuintImport | QuintInstance | QuintExport): string | undefined {
if (decl.kind === 'instance') {
return decl.qualifiedName
}

return decl.defName ? undefined : decl.qualifiedName ?? decl.protoName
}
29 changes: 29 additions & 0 deletions quint/src/names/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type LookupDefinition = (QuintDef | ({ kind: 'param' } & QuintLambdaParam
* Some types in `QuintDef` already have a `typeAnnotation` field. This
* ensures that this field is always accessible */
typeAnnotation?: QuintType
/** optional depth of the definition, 0 if top-level. Only for `QuintOpDef`. */
depth?: number
}

/**
Expand Down Expand Up @@ -93,6 +95,33 @@ export function copyNames(
return table
}

/**
* Add namespaces to a definition's `namespaces` field, if it doesn't already
* have them on the last position or in the beginning of its name.
*
* @param def - The definition to add the namespaces to
* @param namespaces - The namespaces to be added
*
* @returns The definition with the namespaces added
*/
export function addNamespacesToDef(def: LookupDefinition, namespaces: string[]): LookupDefinition {
// FIXME(#1111): This doesn't take care of some corner cases.
return namespaces.reduce((def, namespace) => {
if (def.namespaces && def.namespaces[def.namespaces?.length - 1] === namespace) {
// If this is already the last namespace, don't add it again
return def
}

if (def.name.startsWith(`${namespace}::`)) {
// If the namespace is already in the beginning of the name, don't add it again
return def
}

const namespaces = namespace ? def.namespaces?.concat([namespace]) ?? [namespace] : []
bugarela marked this conversation as resolved.
Show resolved Hide resolved
return { ...def, namespaces }
}, def)
}

/**
* Built-in name definitions that are always resolved and generate conflicts if collected.
*/
Expand Down
Loading
Loading