@informalsystems/quint
Version:
Core tool for the Quint specification language
307 lines (231 loc) • 11 kB
text/typescript
import { describe, it } from 'mocha'
import { assert } from 'chai'
import { buildModuleWithDecls } from '../builders/ir'
import { effectSchemeToString } from '../../src/effects/printing'
import { errorTreeToString } from '../../src/errorTree'
import { EffectInferenceResult, EffectInferrer } from '../../src/effects/inferrer'
import { parseMockedModule } from '../util'
import { EffectScheme } from '../../src/effects/base'
import { isDef } from '../../src/ir/quintIr'
describe('inferEffects', () => {
const baseDefs = ['const N: int', 'const S: Set[int]', 'var x: int']
function inferEffectsForDefs(defs: string[]): EffectInferenceResult {
const text = `module wrapper { ${baseDefs.concat(defs).join('\n')} }`
const { modules, table } = parseMockedModule(text)
const inferrer = new EffectInferrer(table)
return inferrer.inferEffects(modules[0].declarations)
}
function effectForDef(defs: string[], effects: Map<bigint, EffectScheme>, defName: string) {
const module = buildModuleWithDecls(baseDefs.concat(defs))
const result = module.declarations.find(decl => isDef(decl) && decl.name === defName)
if (!result) {
throw new Error(`Could not find def with name ${defName}`)
}
return effectSchemeToString(effects.get(result.id)!)
}
it('infers simple operator effect', () => {
const defs = [`def a(p) = x' = p`]
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "∀ v0 . (Read[v0]) => Read[v0] & Update['x']"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('infers application of multiple arity opertors', () => {
const defs = ['def a(p) = and(p, x)', 'def b(p) = and(p, 1, 2)']
const [errors, effects] = inferEffectsForDefs(defs)
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(
effectForDef(defs, effects, 'a'),
"∀ v0, v1 . (Read[v0] & Temporal[v1]) => Read[v0, 'x'] & Temporal[v1]"
)
assert.deepEqual(
effectForDef(defs, effects, 'b'),
'∀ v0, v1 . (Read[v0] & Temporal[v1]) => Read[v0] & Temporal[v1]'
)
})
it('infers references to operators', () => {
const defs = ['def a(p) = foldl(x, p, iadd)']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "∀ v0, v1 . (Read[v0] & Temporal[v1]) => Read[v0, 'x'] & Temporal[v1]"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('infers references to user-defined operators', () => {
const defs = ['def a(p) = def my_add = iadd { foldl(x, p, my_add) }']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "∀ v0, v1 . (Read[v0] & Temporal[v1]) => Read[v0, 'x'] & Temporal[v1]"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('infers effects for operators defined with let-in', () => {
const defs = ['val b = val m = x { m }']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "Read['x']"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'b'), expectedEffect)
})
it('infers pure effect for literals and value constants', () => {
const defs = ['val b = N + 1']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = 'Pure'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'b'), expectedEffect)
})
it('infers arrow effect for operator constants', () => {
const defs = ['const MyOp: int => int', 'val b = MyOp(x)']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "Read['x']"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'b'), expectedEffect)
})
it('handles underscore', () => {
const defs = ['val b = N.map(_ => 1)']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = 'Pure'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'b'), expectedEffect)
})
it('infers polymorphic high order operators', () => {
const defs = ['def a(g, p) = g(p)']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = '∀ e0, e1 . ((e0) => e1, e0) => e1'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('infers monomorphic high order operators', () => {
const defs = ['def a(g, p) = g(p) + g(not(p))']
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect =
'∀ v0, v1, v2, v3 . ((Read[v0] & Temporal[v1]) => Read[v2] & Temporal[v3], Read[v0] & Temporal[v1]) => Read[v2] & Temporal[v3]'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('keeps track of substitutions with nested defs', () => {
const defs = [
'pure def a(p) = and{' +
' val b = p + 1' +
' p + b > 0,' +
' val c = p + 2' +
' p + c > 0,' +
' p > 0,' +
'}',
]
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = '∀ v0, v1 . (Read[v0] & Temporal[v1]) => Read[v0] & Temporal[v1]'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'a'), expectedEffect)
})
it('keeps track of substitutions with lambdas and applications', () => {
const defs = [
`pure def MinBy(__set: Set[a], __f: a => int, __i: a): a = {
__set.fold(
__i,
(__m, __e) => if(__f(__m) < __f(__e)) {__m } else {__e}
)
}`,
]
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = '∀ v0, v1 . (Read[v0], (Read[v0]) => Read[v1], Read[v0]) => Read[v0, v1]'
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'MinBy'), expectedEffect)
})
it('regression on #1091', () => {
const defs = [
'var channel: int',
`action CoolAction(boolean: bool): bool =
any {
all {
boolean,
channel' = channel
},
all {
not(boolean),
channel' = channel
}
}`,
]
const [errors, effects] = inferEffectsForDefs(defs)
const expectedEffect = "∀ v0 . (Read[v0]) => Read[v0, 'channel'] & Update['channel']"
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
assert.deepEqual(effectForDef(defs, effects, 'CoolAction'), expectedEffect)
})
it('avoids invalid cyclical binding error (regression on #1356)', () => {
const defs = [
`pure def foo(s: int, g: int => int): int = {
val r = if (true) s else g(s)
g(r)
}`,
]
const [errors, _] = inferEffectsForDefs(defs)
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
})
it('returns error when operator signature is not unifiable with args', () => {
const defs = [`def a = S.map(p => x' = p)`]
const [errors] = inferEffectsForDefs(defs)
errors.forEach(v =>
assert.deepEqual(v, {
children: [
{
children: [
{
children: [
{
children: [
{
children: [],
location: "Trying to unify entities ['x'] and []",
message: 'Expected [x] and [] to be the same',
},
],
location: "Trying to unify Read[_v4] & Temporal[_v5] and Update['x']",
},
],
location:
"Trying to unify (Pure) => Read[_v4] & Temporal[_v5] and (Read[_v1]) => Read[_v1] & Update['x']",
},
],
location:
"Trying to unify (Read[_v2] & Temporal[_v3], (Read[_v2] & Temporal[_v3]) => Read[_v4] & Temporal[_v5]) => Read[_v2, _v4] & Temporal[_v3, _v5] and (Pure, (Read[_v1]) => Read[_v1] & Update['x']) => _e1",
},
],
location: 'Trying to infer effect for operator application in map(S, ((p) => assign(x, p)))',
})
)
})
it('returns error when lambda returns an operator', () => {
const defs = ['pure def f(p) = p', 'pure def myOp = (_) => f']
const [errors] = inferEffectsForDefs(defs)
assert.deepEqual([...errors.values()][0], {
children: [],
location: 'Inferring effect for f',
message: 'Result cannot be an opperator',
})
})
it('returns error when `match` branches update different variables', () => {
const defs = ['type Result = | Some(int) | None', "val foo = match Some(1) { | Some(n) => x' = n | None => true }"]
const [errors] = inferEffectsForDefs(defs)
assert.deepEqual([...errors.values()][0].children[0].children[0].children[0].children[0], {
children: [],
location: "Trying to unify entities ['x'] and []",
message: 'Expected [x] and [] to be the same',
})
})
it('differentiates variables from different instances', () => {
const baseDefs = ['const N: int', 'const S: Set[int]', 'var x: int']
const text = `
module base { ${baseDefs.join('\n')} }
module wrapper {
import base(N=1) as B1
import base(N=2) as B2
val a = B1::x + B2::x
}`
const { modules, table } = parseMockedModule(text)
const inferrer = new EffectInferrer(table)
inferrer.inferEffects(modules[0].declarations)
const [errors, effects] = inferrer.inferEffects(modules[1].declarations)
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
const def = modules[1].declarations.find(decl => isDef(decl) && decl.name === 'a')!
const expectedEffect = "Read['wrapper::B1::x', 'wrapper::B2::x']"
assert.deepEqual(effectSchemeToString(effects.get(def.id)!), expectedEffect)
})
})