Skip to content

Commit cdf353d

Browse files
author
Shon Feder
authored
Merge pull request #1359 from informalsystems/1356/effect-cyclical-binding-bug
Remove erroneous occurs check from "effect entity" unfication
2 parents 42cb5f9 + f009af2 commit cdf353d

File tree

6 files changed

+88
-60
lines changed

6 files changed

+88
-60
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Deprecated
1313
### Removed
1414
### Fixed
15+
16+
- Erroneous effect checking failure resulting from invalid occurs check. This
17+
error prevented some valid specs from being simulated or verified (#1359).
18+
1519
### Security
1620

1721
## v0.18.2 -- 2024-01-26

quint/src/cliCommands.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,12 @@ export function mkErrorMessage(sourceMap: SourceMap): (_: QuintError) => ErrorMe
255255
*/
256256
export async function typecheck(parsed: ParsedStage): Promise<CLIProcedure<TypecheckedStage>> {
257257
const { table, modules, sourceMap } = parsed
258-
const typechecking = { ...parsed, stage: 'typechecking' as stage }
259258

260259
const [errorMap, result] = analyzeModules(table, modules)
261260

261+
const typechecking = { ...parsed, ...result, stage: 'typechecking' as stage }
262262
if (errorMap.length === 0) {
263-
return right({ ...typechecking, ...result })
263+
return right(typechecking)
264264
} else {
265265
const errors = errorMap.map(mkErrorMessage(sourceMap))
266266
return cliErr('typechecking failed', { ...typechecking, errors })

quint/src/effects/base.ts

+35-10
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ export interface StateVariable {
8181
* entity variable or a combination of other entities.
8282
*/
8383
export type Entity =
84-
/* A list of state variables */
84+
/* A set of state variables, represented as a list */
8585
| { kind: 'concrete'; stateVariables: StateVariable[] }
86-
/* A variable representing some entity */
86+
/* A variable representing some set of entities */
8787
| { kind: 'variable'; name: string; reference?: bigint }
88-
/* A combination of entities */
88+
/* The union of sets of entities, represented as a list */
8989
| { kind: 'union'; entities: Entity[] }
9090

9191
/*
@@ -177,11 +177,36 @@ function bindEffect(name: string, effect: Effect): Either<string, Substitutions>
177177
}
178178
}
179179

180-
function bindEntity(name: string, entity: Entity): Either<string, Substitutions> {
181-
if (entityNames(entity).includes(name)) {
182-
return left(`Can't bind ${name} to ${entityToString(entity)}: cyclical binding`)
183-
} else {
184-
return right([{ kind: 'entity', name, value: entity }])
180+
function bindEntity(name: string, entity: Entity): Substitutions {
181+
switch (entity.kind) {
182+
case 'concrete':
183+
case 'variable':
184+
return [{ kind: 'entity', name, value: entity }]
185+
case 'union':
186+
if (entityNames(entity).includes(name)) {
187+
// An entity variable (which always stands for a set of state variables)
188+
// unifies with the union of n sets of entities that may include itself,
189+
// iff it unifies with each set.
190+
//
191+
// I.e.:
192+
//
193+
// (v1 =.= v1 ∪ ... ∪ vn) <=> (v1 =.= ... =.= vn)
194+
//
195+
// We call this function recursively because, in the general case,
196+
// occurrences of `v1` may be nested, as in:
197+
//
198+
// v1 =.= v1 ∪ (v2 ∪ (v3 ∪ v1)) ∪ v4
199+
//
200+
// In practice, we are flattening unions before we call this function,
201+
// but solving the general case ensures we preserve correct behavior
202+
// even if this function is used on its own, without incurring any
203+
// notable overhead when `entities` is already flat.
204+
return entity.entities.flatMap(e => bindEntity(name, e))
205+
} else {
206+
// Otherwise, the variable may be bound to the union of the entities
207+
// without qualification.
208+
return [{ kind: 'entity', name, value: entity }]
209+
}
185210
}
186211
}
187212

@@ -321,9 +346,9 @@ export function unifyEntities(va: Entity, vb: Entity): Either<ErrorTree, Substit
321346
} else if (v1.kind === 'variable' && v2.kind === 'variable' && v1.name === v2.name) {
322347
return right([])
323348
} else if (v1.kind === 'variable') {
324-
return bindEntity(v1.name, v2).mapLeft(msg => buildErrorLeaf(location, msg))
349+
return right(bindEntity(v1.name, v2))
325350
} else if (v2.kind === 'variable') {
326-
return bindEntity(v2.name, v1).mapLeft(msg => buildErrorLeaf(location, msg))
351+
return right(bindEntity(v2.name, v1))
327352
} else if (isEqual(v1, v2)) {
328353
return right([])
329354
} else if (v1.kind === 'union' && v2.kind === 'concrete') {

quint/src/effects/inferrer.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { Error, ErrorTree, buildErrorLeaf, buildErrorTree, errorTreeToString } f
3737
import { getSignatures, standardPropagation } from './builtinSignatures'
3838
import { FreshVarGenerator } from '../FreshVarGenerator'
3939
import { effectToString } from './printing'
40-
import { zip } from 'lodash'
40+
import { zip } from '../util'
4141
import { addNamespaces } from './namespaces'
4242

4343
export type EffectInferenceResult = [Map<bigint, ErrorTree>, Map<bigint, EffectScheme>]
@@ -185,11 +185,6 @@ export class EffectInferrer implements IRVisitor {
185185
effects,
186186
expr.args.map(a => a.id)
187187
).forEach(([effect, id]) => {
188-
if (!effect || !id) {
189-
// Impossible: effects and expr.args are the same length
190-
throw new Error(`Expected ${expr.args.length} effects, but got ${effects.length}`)
191-
}
192-
193188
const r = applySubstitution(s, effect).map(toScheme)
194189
this.addToResults(id, r)
195190
})

quint/test/effects/base.test.ts

+33-42
Original file line numberDiff line numberDiff line change
@@ -375,48 +375,39 @@ describe('unify', () => {
375375
assert.isTrue(result.isLeft())
376376
})
377377

378-
it('returs error when entity names are cyclical', () => {
379-
const e1 = parseEffectOrThrow('Read[v1]')
380-
const e2 = parseEffectOrThrow('Read[v1, v2]')
381-
382-
const result = unify(e1, e2)
383-
384-
result.mapLeft(e =>
385-
assert.deepEqual(e, {
386-
location: 'Trying to unify Read[v1] and Read[v1, v2]',
387-
children: [
388-
{
389-
location: 'Trying to unify entities [v1] and [v1, v2]',
390-
message: "Can't bind v1 to v1, v2: cyclical binding",
391-
children: [],
392-
},
393-
],
394-
})
395-
)
396-
397-
assert.isTrue(result.isLeft())
398-
})
399-
400-
it('returs error when entity names are cyclical in other way', () => {
401-
const e1 = parseEffectOrThrow('Temporal[v1, v2]')
402-
const e2 = parseEffectOrThrow('Temporal[v1]')
403-
404-
const result = unify(e1, e2)
405-
406-
result.mapLeft(e =>
407-
assert.deepEqual(e, {
408-
location: 'Trying to unify Temporal[v1, v2] and Temporal[v1]',
409-
children: [
410-
{
411-
location: 'Trying to unify entities [v1, v2] and [v1]',
412-
message: "Can't bind v1 to v1, v2: cyclical binding",
413-
children: [],
414-
},
415-
],
416-
})
417-
)
418-
419-
assert.isTrue(result.isLeft())
378+
it('can unify entities when a single variable in the lhs appears in a union on the rhs', () => {
379+
// E.g., given the unification problem
380+
//
381+
// v1 =.= v1 ∪ v2
382+
//
383+
// We should be able to form a valid substitution iff v1 =.= v2, since
384+
// this then simplifies to
385+
//
386+
// v1 =.= v1 =.= v2
387+
//
388+
// NOTE: This test was inverted after an incorrect occurs check was
389+
// causing erroneous effect checking failures, as reported in
390+
// https://github.com/informalsystems/quint/issues/1356
391+
//
392+
// Occurs checks are called for to prevent the attempt to unify a free
393+
// variable with a term that includes that variable as a subterm. E.g., `X
394+
// =.= foo(a, X)`, which can never be resolved into a ground term.
395+
// However, despite appearances, the unification of so called "entity
396+
// unions", as in the example above is not such a case. Each "entity
397+
// variable" stands for a set of possible state variables. As such, the
398+
// unification problem above can be expanded to
399+
//
400+
// v1 ∪ {} =.= v1 ∪ v2 ∪ {}
401+
//
402+
// Which helps make clear why the unification succeeds iff v1 =.= v2.
403+
const read1 = parseEffectOrThrow('Read[v1]')
404+
const read2 = parseEffectOrThrow('Read[v1, v2]')
405+
assert.isTrue(unify(read1, read2).isRight())
406+
407+
// Check the symmetrical case.
408+
const temporal1 = parseEffectOrThrow('Temporal[v1, v2]')
409+
const temporal2 = parseEffectOrThrow('Temporal[v1]')
410+
assert.isTrue(unify(temporal1, temporal2).isRight())
420411
})
421412
})
422413
})

quint/test/effects/inferrer.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,19 @@ describe('inferEffects', () => {
207207
assert.deepEqual(effectForDef(defs, effects, 'CoolAction'), expectedEffect)
208208
})
209209

210+
it('avoids invalid cyclical binding error (regression on #1356)', () => {
211+
const defs = [
212+
`pure def foo(s: int, g: int => int): int = {
213+
val r = if (true) s else g(s)
214+
g(r)
215+
}`,
216+
]
217+
218+
const [errors, _] = inferEffectsForDefs(defs)
219+
220+
assert.isEmpty(errors, `Should find no errors, found: ${[...errors.values()].map(errorTreeToString)}`)
221+
})
222+
210223
it('returns error when operator signature is not unifiable with args', () => {
211224
const defs = [`def a = S.map(p => x' = p)`]
212225

0 commit comments

Comments
 (0)