UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

414 lines (352 loc) 14.2 kB
import { describe, it } from 'mocha' import { assert } from 'chai' import { Effect, unify } from '../../src/effects/base' import { parseEffectOrThrow } from '../../src/effects/parser' import { substitutionsToString } from '../../src' describe('unify', () => { describe('simple effects', () => { it('unifies temporal effects', () => { const e1 = parseEffectOrThrow('Temporal[t1]') const e2 = parseEffectOrThrow("Temporal['x']") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [ { kind: 'entity', name: 't1', value: { kind: 'concrete', stateVariables: [{ name: 'x', reference: 1n }] } }, ]) ) }) it('returns error unifying temporal and update effects', () => { const e1 = parseEffectOrThrow("Update['x']") const e2 = parseEffectOrThrow("Temporal['y']") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify Update['x'] and Temporal['y']", children: [ { location: "Trying to unify entities ['x'] and []", message: 'Expected [x] and [] to be the same', children: [], }, ], }) ) }) it('returns error unifying temporal and pure effects', () => { const e1 = parseEffectOrThrow('Pure') const e2 = parseEffectOrThrow("Temporal['y']") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify Pure and Temporal['y']", children: [ { location: "Trying to unify entities ['y'] and []", message: 'Expected [y] and [] to be the same', children: [], }, ], }) ) }) it('unifies entities with different orders', () => { const e1 = parseEffectOrThrow("Read['x', 'y']") const e2 = parseEffectOrThrow("Read['y', 'x']") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [])) }) it('flattens any nested unions', () => { const e1 = parseEffectOrThrow('E') const e2: Effect = { kind: 'concrete', components: [ { kind: 'read', entity: { kind: 'union', entities: [ { kind: 'union', entities: [ { kind: 'variable', name: 'r1' }, { kind: 'variable', name: 'r2' }, ], }, { kind: 'concrete', stateVariables: [ { name: 'x', reference: 1n }, { name: 'y', reference: 2n }, ], }, ], }, }, ], } const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [ { kind: 'effect', name: 'E', value: parseEffectOrThrow("Read[r1, r2, 'x', 'y']"), }, ]) ) }) it('unifies unions when they are the exact same for temporal', () => { const e = parseEffectOrThrow('Temporal[v1, v2]') const result = unify(e, e) result.map(r => assert.deepEqual(r, [])) assert.isTrue(result.isRight()) }) it('unifies unions when they are the exact same for updates', () => { const e = parseEffectOrThrow('Update[v1, v2]') const result = unify(e, e) result.map(r => assert.deepEqual(r, [])) assert.isTrue(result.isRight()) }) }) describe('simple arrow effects', () => { it('unifies effects with parameters', () => { const e1 = parseEffectOrThrow('(Read[v]) => Update[v]') const e2 = parseEffectOrThrow("(Read['x']) => E") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [ { kind: 'entity', name: 'v', value: { kind: 'concrete', stateVariables: [{ name: 'x', reference: 1n }] } }, { kind: 'effect', name: 'E', value: parseEffectOrThrow("Update['x']") }, ]) ) }) it('results in the same effect regardless of unpacked projection', () => { const e1 = parseEffectOrThrow('(Read[r1], Read[r2]) => Read[r1]') const e2 = parseEffectOrThrow("(Read['x', 'y']) => E") const e3 = parseEffectOrThrow('(Read[r1], Read[r2]) => Read[r2]') const e4 = parseEffectOrThrow("(Read['x', 'y']) => E") const result1 = unify(e1, e2) const result2 = unify(e3, e4) result1.map(r1 => result2.map(r2 => assert.deepEqual(substitutionsToString(r1), substitutionsToString(r2)))) }) it('returns error when there are not enough parameters', () => { const e1 = parseEffectOrThrow('(Read[v]) => Update[v]') const e2 = parseEffectOrThrow("() => Update['y']") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify (Read[v]) => Update[v] and () => Update['y']", message: 'Expected 1 arguments, got 0', children: [], }) ) }) it('returns error when trying to unify with non-arrow effect', () => { const e1 = parseEffectOrThrow('(Read[v]) => Update[v]') const e2 = parseEffectOrThrow("Update['y']") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify (Read[v]) => Update[v] and Update['y']", message: "Can't unify different types of effects", children: [], }) ) }) }) describe('nested arrow effects', () => { it('unifies effects with parameters', () => { const e1 = parseEffectOrThrow('((Pure) => E) => E') const e2 = parseEffectOrThrow("((Pure) => Read['x']) => Read['x']") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [{ kind: 'effect', name: 'E', value: parseEffectOrThrow("Read['x']") }]) ) }) }) describe('effects with multiple variable entities', () => { it('unfies with concrete and unifiable effect', () => { const e1 = parseEffectOrThrow('(Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u]') const e2 = parseEffectOrThrow( "(Read['x'] & Update['x'], Read['y', 'z'] & Update['x']) => Read['x', 'y', 'z'] & Update['x']" ) const result = unify(e1, e2) const reversedResult = unify(e2, e1) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [ { kind: 'entity', name: 'r1', value: { kind: 'concrete', stateVariables: [{ name: 'x', reference: 1n }] } }, { kind: 'entity', name: 'r2', value: { kind: 'concrete', stateVariables: [ { name: 'y', reference: 3n }, { name: 'z', reference: 4n }, ], }, }, { kind: 'entity', name: 'u', value: { kind: 'concrete', stateVariables: [{ name: 'x', reference: 2n }] } }, ]) ) assert.deepEqual(result, reversedResult, 'Result should be the same regardless of the effect order in parameters') }) it('returns error with incompatible effect', () => { const e1 = parseEffectOrThrow('(Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u]') const e2 = parseEffectOrThrow("(Read['x'] & Update['x'], Read['y'] & Update['y']) => E") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify (Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u] and (Read['x'] & Update['x'], Read['y'] & Update['y']) => E", children: [ { location: "Trying to unify Read[r2] & Update['x'] and Read['y'] & Update['y']", children: [ { location: "Trying to unify entities ['x'] and ['y']", message: 'Expected [x] and [y] to be the same', children: [], }, ], }, ], }) ) }) it('returns partial bindings when unifying with another variable effect', () => { const e1 = parseEffectOrThrow('(Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u]') const e2 = parseEffectOrThrow("(Read['x'] & Update[v], Read['y'] & Update[v]) => Read['x', 'y'] & Update[v]") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [ { kind: 'entity', name: 'r1', value: { kind: 'concrete', stateVariables: [{ name: 'x', reference: 1n }] } }, { kind: 'entity', name: 'r2', value: { kind: 'concrete', stateVariables: [{ name: 'y', reference: 2n }] } }, { kind: 'entity', name: 'u', value: { kind: 'variable', name: 'v' } }, ]) ) }) it('simplifies unions of entities before giving up on unifying them', () => { const e1 = parseEffectOrThrow("Read[r1, r2, 'x']") const e2 = parseEffectOrThrow("Read[r1, 'x']") const result = unify(e1, e2) assert.isTrue(result.isRight()) result.map(r => assert.sameDeepMembers(r, [{ kind: 'entity', name: 'r2', value: { kind: 'concrete', stateVariables: [] } }]) ) }) it('returns error with effect with incompatible entity variables', () => { const e1 = parseEffectOrThrow('(Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u]') const e2 = parseEffectOrThrow("(Read['y'] & Update['x'], Read['z'] & Update['x']) => Read['y'] & Update[u]") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify (Read[r1] & Update[u], Read[r2] & Update[u]) => Read[r1, r2] & Update[u] and (Read['y'] & Update['x'], Read['z'] & Update['x']) => Read['y'] & Update[u]", children: [ { location: "Trying to unify Read['y', 'z'] & Update['x'] and Read['y'] & Update['x']", children: [ { location: "Trying to unify entities ['y', 'z'] and ['y']", message: 'Expected [y,z] and [y] to be the same', children: [], }, ], }, ], }) ) }) it('returns error when unifying union with another union', () => { const e1 = parseEffectOrThrow('Read[r1, r2]') const e2 = parseEffectOrThrow("Read[r, 'x', 'y']") const result = unify(e1, e2) assert.isTrue(result.isLeft()) result.mapLeft(r => assert.deepEqual(r, { location: "Trying to unify Read[r1, r2] and Read[r, 'x', 'y']", children: [ { location: "Trying to unify entities [r1, r2] and [r, 'x', 'y']", message: 'Unification for unions of entities is not implemented', children: [], }, ], }) ) }) it('returs error when effect names are cyclical', () => { const e1 = parseEffectOrThrow('e1') const e2 = parseEffectOrThrow('() => e1') const result = unify(e1, e2) result.mapLeft(e => assert.deepEqual(e, { location: 'Trying to unify e1 and () => e1', message: "Can't bind e1 to () => e1: cyclical binding", children: [], }) ) assert.isTrue(result.isLeft()) }) it('returs error when effect names are cyclical in other way', () => { const e1 = parseEffectOrThrow('() => e1') const e2 = parseEffectOrThrow('e1') const result = unify(e1, e2) result.mapLeft(e => assert.deepEqual(e, { location: 'Trying to unify () => e1 and e1', message: "Can't bind e1 to () => e1: cyclical binding", children: [], }) ) assert.isTrue(result.isLeft()) }) it('can unify entities when a single variable in the lhs appears in a union on the rhs', () => { // E.g., given the unification problem // // v1 =.= v1 ∪ v2 // // We should be able to form a valid substitution iff v1 =.= v2, since // this then simplifies to // // v1 =.= v1 =.= v2 // // NOTE: This test was inverted after an incorrect occurs check was // causing erroneous effect checking failures, as reported in // https://github.com/informalsystems/quint/issues/1356 // // Occurs checks are called for to prevent the attempt to unify a free // variable with a term that includes that variable as a subterm. E.g., `X // =.= foo(a, X)`, which can never be resolved into a ground term. // However, despite appearances, the unification of so called "entity // unions", as in the example above is not such a case. Each "entity // variable" stands for a set of possible state variables. As such, the // unification problem above can be expanded to // // v1 ∪ {} =.= v1 ∪ v2 ∪ {} // // Which helps make clear why the unification succeeds iff v1 =.= v2. const read1 = parseEffectOrThrow('Read[v1]') const read2 = parseEffectOrThrow('Read[v1, v2]') assert.isTrue(unify(read1, read2).isRight()) // Check the symmetrical case. const temporal1 = parseEffectOrThrow('Temporal[v1, v2]') const temporal2 = parseEffectOrThrow('Temporal[v1]') assert.isTrue(unify(temporal1, temporal2).isRight()) }) }) })