@informalsystems/quint
Version:
Core tool for the Quint specification language
161 lines (143 loc) • 6.06 kB
text/typescript
import { assert, expect } from 'chai'
import { describe } from 'mocha'
import { dedent } from '../textUtils'
import { newIdGenerator } from '../../src/idGenerator'
import {
ParserPhase3,
parsePhase1fromText,
parsePhase2sourceResolution,
parsePhase3importAndNameResolution,
} from '../../src/parsing/quintParserFrontend'
import { SourceLookupPath, fileSourceResolver } from '../../src/parsing/sourceResolver'
import { LookupTable, QuintDeclaration, QuintExport, QuintImport, QuintInstance, QuintModule } from '../../src'
import { CallGraphVisitor, mkCallGraphContext } from '../../src/static/callgraph'
import { walkModule } from '../../src/ir/IRVisitor'
import { flow } from 'lodash'
describe('compute call graph', () => {
// Parse Quint code without, stopping after name resolution
function parse3phases(code: string): [QuintModule[], LookupTable] {
const idGen = newIdGenerator()
const fakePath: SourceLookupPath = {
normalizedPath: 'fake_path',
toSourceName: () => 'fake_path',
}
// we are calling parse phases directly instead of `parse`,
// as the call graph will be computed at parse phase 4
const resolver = fileSourceResolver()
const { modules, table, errors }: ParserPhase3 = flow([
() => parsePhase1fromText(idGen, code, fakePath.normalizedPath),
phase1Data => parsePhase2sourceResolution(idGen, resolver, fakePath, phase1Data),
parsePhase3importAndNameResolution,
])()
assert.isEmpty(errors)
return [modules, table]
}
function findDef(module: QuintModule, name: string): QuintDeclaration {
const d = module.declarations.find(
d => (d.kind === 'def' || d.kind === 'const' || d.kind === 'var' || d.kind === 'typedef') && d.name === name
)
assert(d, `Definition ${name} not found`)
return d
}
function findImport(module: QuintModule, pred: (imp: QuintImport) => boolean): QuintDeclaration {
const d = module.declarations.find(d => d.kind === 'import' && pred(d))
assert(d, `Import not found in ${module.name}`)
return d
}
function findInstance(module: QuintModule, pred: (imp: QuintInstance) => boolean): QuintDeclaration {
const d = module.declarations.find(d => d.kind === 'instance' && pred(d))
assert(d, `Instance not found in ${module.name}`)
return d
}
function findExport(module: QuintModule, pred: (imp: QuintExport) => boolean): QuintDeclaration {
const d = module.declarations.find(d => d.kind === 'export' && pred(d))
assert(d, `Export not found in ${module.name}`)
return d
}
it('computes a call graph of const, var, and operator definitions', () => {
const code = dedent(
`module main {
| const N: int
| var w: int
| pure def plus(x, y) = x + y
| pure def double(x) = plus(x, x)
| pure def triple(x) = plus(x, double(x, x))
| val getW = w + N
|}`
)
const [modules, table] = parse3phases(code)
const main = modules.find(m => m.name === 'main')!
const visitor = new CallGraphVisitor(table, mkCallGraphContext(modules))
walkModule(visitor, main)
const graph = visitor.graph
const plus = findDef(main, 'plus')
const double = findDef(main, 'double')
const triple = findDef(main, 'triple')
const w = findDef(main, 'w')
const N = findDef(main, 'N')
const getW = findDef(main, 'getW')
expect(graph.get(plus.id)?.size).to.equal(2)
expect(graph.get(double.id)?.toArray()).to.include.members([plus.id])
expect(graph.get(triple.id)?.toArray()).to.include.members([plus.id, double.id])
expect(graph.get(getW.id)?.toArray()).to.include.members([w.id, N.id])
})
it('computes a "uses" graph of typedefs', () => {
const code = dedent(
`module main {
| type BagOfApples = Set[int]
| type BoxOfApples = Set[BagOfApples]
| var x: BoxOfApples
|}`
)
const [modules, table] = parse3phases(code)
const main = modules.find(m => m.name === 'main')!
const visitor = new CallGraphVisitor(table, mkCallGraphContext(modules))
walkModule(visitor, main)
const graph = visitor.graph
const bagOfApples = findDef(main, 'BagOfApples')
const boxOfApples = findDef(main, 'BoxOfApples')
const x = findDef(main, 'x')
expect(graph.get(bagOfApples.id)).to.be.undefined
expect(graph.get(boxOfApples.id)?.toArray()).to.eql([bagOfApples.id])
expect(graph.get(x.id)?.toArray()).to.eql([boxOfApples.id])
})
it('computes a "uses" graph of imports and defs', () => {
// the following definitions should always come in this order:
const code = dedent(
`module A {
| pure def sqr(x) = x * x
|}
|module B {
| const M: int
| pure val doubleM = M + M
|}
|module main {
| import A.*
| pure val myM = sqr(3)
| import B(M = myM) as B1
| pure val quadM = 2 * B1::doubleM
| export B1.*
|}`
)
const [modules, table] = parse3phases(code)
const findModule = (name: string) => modules.find(m => m.name === name)!
const [A, B, main] = ['A', 'B', 'main'].map(findModule)
const visitor = new CallGraphVisitor(table, mkCallGraphContext(modules))
walkModule(visitor, main)
const graph = visitor.graph
// uncomment to debug the graph structure
//visitor.print(console.log)
const sqr = findDef(A, 'sqr')
const importA = findImport(main, imp => imp.protoName === 'A')
const myM = findDef(main, 'myM')
const importB = findInstance(main, imp => imp.protoName === 'B')
const quadM = findDef(main, 'quadM')
const doubleM = findDef(B, 'doubleM')
const exportB1 = findExport(main, exp => exp.protoName === 'B1')
expect(graph.get(importA.id)?.toArray()).eql([A.id])
expect(graph.get(myM.id)?.toArray()).to.eql([sqr.id, importA.id])
expect(graph.get(importB.id)?.toArray()).to.eql([B.id, myM.id])
expect(graph.get(quadM.id)?.toArray()).to.eql([doubleM.id, importB.id])
expect(graph.get(exportB1.id)?.toArray()).to.eql([importB.id])
})
})