@informalsystems/quint
Version:
Core tool for the Quint specification language
1,080 lines (905 loc) • 40.3 kB
text/typescript
import { describe, it } from 'mocha'
import { assert } from 'chai'
import { expressionToString } from '../../src/ir/IRprinting'
import { newTraceRecorder } from '../../src/runtime/trace'
import { dedent } from '../textUtils'
import { newIdGenerator } from '../../src/idGenerator'
import { newRng } from '../../src/rng'
import { fileSourceResolver } from '../../src/parsing/sourceResolver'
import { QuintEx, parseExpressionOrDeclaration, quintErrorToString, walkExpression } from '../../src'
import { parse } from '../../src/parsing/quintParserFrontend'
import { Evaluator } from '../../src/runtime/impl/evaluator'
import { Either, left } from '@sweet-monads/either'
// Use a global id generator, limited to this test suite.
const idGen = newIdGenerator()
// Compile an expression, evaluate it, convert to QuintEx, then to a string,
// compare the result. This is the easiest path to test the results.
//
// @param evalContext optional textual representation of context that may hold definitions which
// `input` depends on. This content will be wrapped in a module and imported unqualified
// before the input is evaluated. If not supplied, the context is empty.
function assertResultAsString(input: string, expected: string | undefined, evalContext: string = '') {
const [evaluator, expr] = prepareEvaluator(input, evalContext)
const newEval = evaluator.evaluate(expr)
newEval
.map(val => assert.deepEqual(expressionToString(val), expected, `Input: ${input}`))
.mapLeft(err => assert(expected === undefined, `Expected ${expected}, found error ${quintErrorToString(err)}`))
}
function prepareEvaluator(input: string, evalContext: string): [Evaluator, QuintEx] {
const mockLookupPath = fileSourceResolver(new Map()).lookupPath('/', './mock')
const { resolver, sourceMap } = parse(idGen, '<test>', mockLookupPath, `module contextM { ${evalContext} }`)
const parseResult = parseExpressionOrDeclaration(input, '<input>', idGen, sourceMap)
if (parseResult.kind !== 'expr') {
assert.fail(`Expected an expression, found ${parseResult.kind}`)
}
walkExpression(resolver, parseResult.expr)
if (resolver.errors.length > 0) {
assert.fail(`Resolver errors: ${resolver.errors.map(quintErrorToString).join(', ')}`)
}
const rng = newRng()
const evaluator = new Evaluator(resolver.table, newTraceRecorder(0, rng), rng)
return [evaluator, parseResult.expr]
}
// Compile a variable definition and check that the compiled value is defined.
function assertVarExists(name: string, context: string, input: string) {
const [evaluator, expr] = prepareEvaluator(input, context)
// Eval the name so we lookup and evaluate the var definition
evaluator.evaluate(expr)
assert.includeDeepMembers(
[...evaluator.ctx.varStorage.vars.values()].map(r => r.name),
[name],
`Input: ${input}`
)
}
// Scan the context for a callable. If found, evaluate it and return the value of the given var.
// Assumes the input has a single definition whose name is stored in `callee`.
function evalVarAfterRun(varName: string, callee: string, input: string): Either<string, string> {
const [evaluator, runExpr] = prepareEvaluator(callee, input)
const newEval = evaluator.evaluate(runExpr)
return newEval
.mapLeft(quintErrorToString)
.chain(runResult => {
if (!(runResult.kind == 'bool' && runResult.value === true)) {
return left(`Callable ${callee} was expected to evaluate to true, found: ${expressionToString(runResult)}`)
}
const registerValue = [...evaluator.ctx.varStorage.nextVars.values()].find(r => r.name === varName)?.value
if (!registerValue) {
return left(`Value of the variable ${varName} is undefined`)
}
return registerValue.mapLeft(quintErrorToString)
})
.map(res => expressionToString(res.toQuintEx(idGen)))
}
// Evaluate a run and return the result.
function evalRun(callee: string, input: string): Either<string, string> {
const [evaluator, runExpr] = prepareEvaluator(callee, input)
const newEval = evaluator.evaluate(runExpr)
return newEval.mapLeft(quintErrorToString).map(res => expressionToString(res))
}
function assertVarAfterCall(varName: string, expected: string, callee: string, input: string) {
evalVarAfterRun(varName, callee, input)
.mapLeft(m => assert.fail(m))
.mapRight(output => assert(expected === output, `Expected ${varName} == ${expected}, found ${output}`))
}
describe('compiling specs to runtime values', () => {
describe('compile over integers', () => {
it('computes integer literals', () => {
assertResultAsString('15', '15')
assertResultAsString('100_000_000', '100000000')
assertResultAsString('0xabcdef', '11259375')
assertResultAsString('0xab_cd_ef', '11259375')
assertResultAsString('0xAbCdEF', '11259375')
assertResultAsString('0xaB_cD_eF', '11259375')
})
it('computes addition', () => {
assertResultAsString('2 + 3', '5')
})
it('computes subtraction', () => {
assertResultAsString('2 - 3', '-1')
})
it('computes negation', () => {
assertResultAsString('-(2 + 3)', '-5')
})
it('computes multiplication', () => {
assertResultAsString('2 * 3', '6')
})
it('computes division', () => {
assertResultAsString('7 / 2', '3')
})
it('computes remainder', () => {
assertResultAsString('7 % 2', '1')
})
it('computes power', () => {
assertResultAsString('3^4', '81')
assertResultAsString('(-2)^3', '-8')
assertResultAsString('-2^3', '-8')
assertResultAsString('(-2)^4', '16')
assertResultAsString('-2^4', '-16')
assertResultAsString('0^(-1)', undefined)
assertResultAsString('0^0', undefined)
})
it('computes greater than', () => {
assertResultAsString('5 > 3', 'true')
assertResultAsString('5 > 5', 'false')
assertResultAsString('3 > 5', 'false')
})
it('computes less than', () => {
assertResultAsString('5 < 3', 'false')
assertResultAsString('5 < 5', 'false')
assertResultAsString('3 < 5', 'true')
})
it('computes greater than or equal', () => {
assertResultAsString('5 >= 4', 'true')
assertResultAsString('5 >= 5', 'true')
assertResultAsString('4 >= 5', 'false')
})
it('computes less than or equal', () => {
assertResultAsString('5 <= 4', 'false')
assertResultAsString('5 <= 5', 'true')
assertResultAsString('4 <= 5', 'true')
})
it('computes integer equality', () => {
assertResultAsString('5 == 4', 'false')
assertResultAsString('4 == 4', 'true')
})
it('computes integer inequality', () => {
assertResultAsString('5 != 4', 'true')
assertResultAsString('4 != 4', 'false')
})
})
describe('compile over Booleans', () => {
it('computes Boolean literals', () => {
assertResultAsString('false', 'false')
assertResultAsString('true', 'true')
})
it('computes not', () => {
assertResultAsString('not(false)', 'true')
assertResultAsString('not(true)', 'false')
})
it('computes and', () => {
assertResultAsString('false and false', 'false')
assertResultAsString('false and true', 'false')
assertResultAsString('true and false', 'false')
assertResultAsString('true and true', 'true')
assertResultAsString('and(true, true, false)', 'false')
assertResultAsString('and(true, true, true)', 'true')
})
it('computes "and" via short-circuit or fails', () => {
assertResultAsString('false and (1/0 == 0)', 'false')
assertResultAsString('true and (1/0 == 0)', undefined)
})
it('computes or', () => {
assertResultAsString('false or false', 'false')
assertResultAsString('false or true', 'true')
assertResultAsString('true or false', 'true')
assertResultAsString('true or true', 'true')
assertResultAsString('or(false, true, true)', 'true')
assertResultAsString('or(true, true, false)', 'true')
assertResultAsString('or(false, false, false)', 'false')
})
it('computes "or" via short-circuit or fails', () => {
assertResultAsString('false or (1/0 == 0)', undefined)
assertResultAsString('true or (1/0 == 0)', 'true')
})
it('computes "implies"', () => {
assertResultAsString('false implies false', 'true')
assertResultAsString('false implies true', 'true')
assertResultAsString('true implies false', 'false')
assertResultAsString('true implies true', 'true')
})
it('computes "implies" via short-circuit or fails', () => {
assertResultAsString('false implies (1/0 == 0)', 'true')
assertResultAsString('true implies (1/0 == 0)', undefined)
})
it('computes iff', () => {
assertResultAsString('false iff false', 'true')
assertResultAsString('false iff true', 'false')
assertResultAsString('true iff false', 'false')
assertResultAsString('true iff true', 'true')
})
it('computes Boolean equality', () => {
assertResultAsString('false == false', 'true')
assertResultAsString('true == true', 'true')
assertResultAsString('false == true', 'false')
assertResultAsString('true == false', 'false')
})
it('computes Boolean inequality', () => {
assertResultAsString('false != false', 'false')
assertResultAsString('true != true', 'false')
assertResultAsString('false != true', 'true')
assertResultAsString('true != false', 'true')
})
})
describe('compile over other operators', () => {
it('computes Boolean if-then-else', () => {
assertResultAsString('if (false) false else true', 'true')
assertResultAsString('if (true) false else true', 'false')
})
it('computes integer if-then-else', () => {
assertResultAsString('if (3 > 5) 1 else 2', '2')
assertResultAsString('if (5 > 3) 1 else 2', '1')
})
})
describe('compile over definitions', () => {
it('computes value definitions', () => {
const input = `val x = 3 + 4
val y = 2 * x
y - x`
assertResultAsString(input, '7')
})
it('computes multi-arg definitions', () => {
const input = `def mult(x, y) = (x * y)
mult(2, mult(3, 4))`
assertResultAsString(input, '24')
})
it('uses named def instead of lambda', () => {
const input = `def positive(x) = x > 0
(-3).to(3).filter(positive)`
assertResultAsString(input, 'Set(1, 2, 3)')
})
it('compile higher-order operators', () => {
const input = `def ho(lo, n) = lo(n)
def loImpl(x) = x * 2
ho(loImpl, 3)`
assertResultAsString(input, '6')
})
it('compile higher-order operators with lambda', () => {
const input = `def ho(lo, n) = lo(n)
ho(x => x * 2, 3)`
assertResultAsString(input, '6')
})
it('higher-order operators in folds', () => {
const input = `def plus(i, j) = i + j
2.to(6).fold(0, plus)`
assertResultAsString(input, '20')
})
})
describe('compile variables', () => {
it('variable definitions', () => {
const context = 'var x: int'
const input = "x' = 1"
assertVarExists('x', context, input)
})
})
describe('compile over sets', () => {
it('computes an interval', () => {
const input = '2.to(5)'
assertResultAsString(input, 'Set(2, 3, 4, 5)')
})
it('interval cardinality', () => {
const input = '2.to(5).size()'
assertResultAsString(input, '4')
})
it('interval isFinite', () => {
const input = '2.to(5).isFinite()'
assertResultAsString(input, 'true')
})
it('computes a flat set', () => {
const input = 'Set(1, 3 - 1, 3)'
assertResultAsString(input, 'Set(1, 2, 3)')
})
it('flat set cardinality', () => {
const input = 'Set(1, 4 - 1, 3).size()'
assertResultAsString(input, '2')
})
it('flat set isFinite', () => {
const input = 'Set(1, 4 - 1, 3).isFinite()'
assertResultAsString(input, 'true')
})
it('computes a flat set without duplicates', () => {
const input = 'Set(1, 2, 3 - 1, 3, 1)'
assertResultAsString(input, 'Set(1, 2, 3)')
})
it('computes a set of sets', () => {
const input = 'Set(Set(1, 2), Set(2, 3), Set(1, 3))'
assertResultAsString(input, 'Set(Set(1, 2), Set(1, 3), Set(2, 3))')
})
it('cardinality of a set of sets', () => {
const input = 'Set(Set(1, 2), Set(2, 3), Set(1, 3)).size()'
assertResultAsString(input, '3')
})
it('computes a set of intervals', () => {
const input = 'Set(1.to(3), 3.to(4))'
assertResultAsString(input, 'Set(Set(1, 2, 3), Set(3, 4))')
})
it('computes equality over sets', () => {
assertResultAsString('Set(1, 2) == Set(1, 3 - 1)', 'true')
assertResultAsString('Set(1, 2) == Set(1, 3 - 3)', 'false')
})
it('computes equality over intervals', () => {
assertResultAsString('1.to(3) == 1.to(4 - 1)', 'true')
assertResultAsString('1.to(3) == Set(1, 2, 3)', 'true')
assertResultAsString('Set(1, 2, 3) == 1.to(3)', 'true')
assertResultAsString('(-3).to(4) == Set(-3, -2, -1, 0, 1, 2, 3, 4)', 'true')
assertResultAsString('(-2).to(-4) == Set()', 'true')
assertResultAsString('3.to(-2) == Set()', 'true')
assertResultAsString('1.to(3) == 1.to(4)', 'false')
assertResultAsString('(-1).to(3) == 1.to(3)', 'false')
assertResultAsString('2.to(4) == 1.to(4)', 'false')
assertResultAsString('(-4).to(-2) == (-2).to(-4)', 'false')
assertResultAsString('3.to(0) == 4.to(-1)', 'true')
// See: https://github.com/informalsystems/quint/issues/578
//assertResultAsString('-1.to(2) == Set(-1, 0, 1, 2)', 'true')
})
it('computes inequality over sets', () => {
assertResultAsString('Set(1, 2) != Set(1, 3 - 1)', 'false')
assertResultAsString('Set(1, 2) != Set(1, 3 - 3)', 'true')
})
it('computes inequality over intervals', () => {
assertResultAsString('1.to(3) != 1.to(4 - 1)', 'false')
assertResultAsString('1.to(3) != Set(1, 2, 3)', 'false')
assertResultAsString('Set(1, 2, 3) != 1.to(3)', 'false')
assertResultAsString('1.to(3) != 1.to(4)', 'true')
assertResultAsString('2.to(4) != 1.to(4)', 'true')
})
it('computes a set of sets without duplicates', () => {
const input = 'Set(Set(1, 2), Set(2, 3), Set(1, 3), Set(2 - 1, 2 + 1))'
assertResultAsString(input, 'Set(Set(1, 2), Set(1, 3), Set(2, 3))')
})
it('computes contains', () => {
assertResultAsString('Set(1, 2, 3).contains(2)', 'true')
assertResultAsString('Set(1, 2, 3).contains(4)', 'false')
})
it('computes in', () => {
assertResultAsString('2.in(Set(1, 2, 3))', 'true')
assertResultAsString('4.in(Set(1, 2, 3))', 'false')
})
it('computes in an interval', () => {
assertResultAsString('2.in(1.to(3))', 'true')
assertResultAsString('4.in(1.to(3))', 'false')
assertResultAsString('1.to(3).in(Set(1.to(3), 2.to(4)))', 'true')
})
it('computes in over nested sets', () => {
assertResultAsString('Set(1, 2).in(Set(Set(1, 2), Set(2, 3)))', 'true')
assertResultAsString('Set(1, 3).in(Set(Set(1, 2), Set(2, 3)))', 'false')
})
it('computes subseteq', () => {
assertResultAsString('Set(1, 2).subseteq(Set(1, 2, 3))', 'true')
assertResultAsString('Set(1, 2, 4).subseteq(Set(1, 2, 3))', 'false')
})
it('computes subseteq over intervals', () => {
assertResultAsString('2.to(4).subseteq(1.to(10))', 'true')
assertResultAsString('2.to(0).subseteq(3.to(0))', 'true')
assertResultAsString('Set(2, 3, 4).subseteq(1.to(10))', 'true')
assertResultAsString('2.to(4).subseteq(1.to(3))', 'false')
assertResultAsString('2.to(4).subseteq(Set(1, 2, 3))', 'false')
})
it('computes union', () => {
assertResultAsString('Set(1, 2).union(Set(1, 3))', 'Set(1, 2, 3)')
assertResultAsString('1.to(3).union(2.to(4))', 'Set(1, 2, 3, 4)')
assertResultAsString('Set(1, 2, 3).union(2.to(4))', 'Set(1, 2, 3, 4)')
assertResultAsString('1.to(3).union(Set(2, 3, 4))', 'Set(1, 2, 3, 4)')
})
it('computes intersect', () => {
assertResultAsString('Set(1, 2).intersect(Set(1, 3))', 'Set(1)')
assertResultAsString('1.to(3).intersect(2.to(4))', 'Set(2, 3)')
assertResultAsString('Set(1, 2, 3).intersect(2.to(4))', 'Set(2, 3)')
assertResultAsString('1.to(3).intersect(Set(2, 3, 4))', 'Set(2, 3)')
})
it('computes exclude', () => {
assertResultAsString('Set(1, 2, 4).exclude(Set(1, 3))', 'Set(2, 4)')
assertResultAsString('1.to(3).exclude(2.to(4))', 'Set(1)')
assertResultAsString('Set(1, 2, 3).exclude(2.to(4))', 'Set(1)')
assertResultAsString('1.to(3).exclude(Set(2, 3, 4))', 'Set(1)')
})
it('computes flatten', () => {
assertResultAsString('Set(Set(1, 2), Set(2, 3), Set(3, 4)).flatten()', 'Set(1, 2, 3, 4)')
})
it('computes flatten on nested sets', () => {
assertResultAsString(
'Set(Set(Set(1, 2), Set(2, 3)), Set(Set(3, 4))).flatten()',
'Set(Set(1, 2), Set(2, 3), Set(3, 4))'
)
})
it('computes exists', () => {
assertResultAsString('Set(1, 2, 3).exists(x => true)', 'true')
assertResultAsString('Set(1, 2, 3).exists(x => false)', 'false')
assertResultAsString('Set(1, 2, 3).exists(x => x >= 2)', 'true')
assertResultAsString('Set(1, 2, 3).exists(x => x >= 5)', 'false')
})
it('unpacks tuples in exists', () => {
assertResultAsString('tuples(1.to(3), 4.to(6)).exists(((x, y)) => x + y == 7)', 'true')
})
it('computes exists over intervals', () => {
assertResultAsString('1.to(3).exists(x => true)', 'true')
assertResultAsString('1.to(3).exists(x => false)', 'false')
assertResultAsString('1.to(3).exists(x => x >= 2)', 'true')
assertResultAsString('1.to(3).exists(x => x >= 5)', 'false')
})
it('computes forall', () => {
assertResultAsString('Set(1, 2, 3).forall(x => true)', 'true')
assertResultAsString('Set(1, 2, 3).forall(x => false)', 'false')
assertResultAsString('Set(1, 2, 3).forall(x => x >= 2)', 'false')
assertResultAsString('Set(1, 2, 3).forall(x => x >= 0)', 'true')
})
it('unpacks tuples in forall', () => {
assertResultAsString('tuples(1.to(3), 4.to(6)).forall(((x, y)) => x + y <= 9)', 'true')
})
it('computes forall over nested sets', () => {
const input = 'Set(Set(1, 2), Set(2, 3)).forall(s => 2.in(s))'
assertResultAsString(input, 'true')
})
it('computes forall over intervals', () => {
assertResultAsString('1.to(3).forall(x => true)', 'true')
assertResultAsString('1.to(3).forall(x => false)', 'false')
assertResultAsString('1.to(3).forall(x => x >= 2)', 'false')
assertResultAsString('1.to(3).forall(x => x >= 0)', 'true')
})
it('computes map', () => {
// a bijection
assertResultAsString('Set(1, 2, 3).map(x => 2 * x)', 'Set(2, 4, 6)')
// not an injection: 2 and 3 are mapped to 1
assertResultAsString('Set(1, 2, 3).map(x => x / 2)', 'Set(0, 1)')
})
it('unpacks tuples in map', () => {
assertResultAsString('tuples(1.to(3), 4.to(6)).map(((x, y)) => x + y)', 'Set(5, 6, 7, 8, 9)')
})
it('computes map over intervals', () => {
// a bijection
assertResultAsString('1.to(3).map(x => 2 * x)', 'Set(2, 4, 6)')
// not an injection: 2 and 3 are mapped to 1
assertResultAsString('1.to(3).map(x => x / 2)', 'Set(0, 1)')
})
it('computes filter', () => {
assertResultAsString('Set(1, 2, 3, 4).filter(x => false)', 'Set()')
assertResultAsString('Set(1, 2, 3, 4).filter(x => true)', 'Set(1, 2, 3, 4)')
assertResultAsString('Set(1, 2, 3, 4).filter(x => x % 2 == 0)', 'Set(2, 4)')
})
it('unpacks tuples in filter', () => {
assertResultAsString('tuples(1.to(5), 2.to(3)).filter(((x, y)) => x < y)', 'Set(Tup(1, 2), Tup(1, 3), Tup(2, 3))')
})
it('computes filter over intervals', () => {
assertResultAsString('1.to(4).filter(x => false)', 'Set()')
assertResultAsString('1.to(4).filter(x => true)', 'Set(1, 2, 3, 4)')
assertResultAsString('1.to(4).filter(x => x % 2 == 0)', 'Set(2, 4)')
})
it('computes filter over sets of intervals', () => {
assertResultAsString('Set(1.to(4), 2.to(3)).filter(S => S.contains(1))', 'Set(Set(1, 2, 3, 4))')
assertResultAsString('Set(1.to(4), 2.to(3)).filter(S => S.contains(0))', 'Set()')
})
it('computes fold', () => {
// sum
assertResultAsString('Set(1, 2, 3).fold(10, (v, x) => v + x)', '16')
assertResultAsString('Set().fold(10, (v, x) => v + x)', '10')
// flatten
const input1 = dedent(
`Set(1.to(3), 4.to(5), 6.to(7))
| .fold(Set(0), (a, s) => a.union(s))`
)
assertResultAsString(input1, 'Set(0, 1, 2, 3, 4, 5, 6, 7)')
assertResultAsString('Set().fold(Set(), (a, s) => a.union(s))', 'Set()')
// product by using a definition
const input2 = dedent(
`def prod(x, y) = x * y;
|2.to(4).fold(1, prod)`
)
assertResultAsString(input2, '24')
})
})
describe('compile over powerset', () => {
it('computes a powerset', () => {
assertResultAsString(
'2.to(4).powerset()',
'Set(Set(), Set(2), Set(3), Set(2, 3), Set(4), Set(2, 4), Set(3, 4), Set(2, 3, 4))'
)
})
it('powerset equality', () => {
assertResultAsString('2.to(3).powerset() == Set(Set(), Set(2), Set(3), Set(2, 3))', 'true')
assertResultAsString('2.to(3).powerset() == Set(2, 3).powerset()', 'true')
assertResultAsString('2.to(4).powerset() == Set(2, 3).powerset()', 'false')
})
it('powerset contains', () => {
assertResultAsString('2.to(3).powerset().contains(Set(2))', 'true')
assertResultAsString('2.to(3).powerset().contains(Set(2, 4))', 'false')
})
it('powerset subseteq', () => {
assertResultAsString('2.to(4).powerset().subseteq(1.to(5).powerset())', 'true')
})
it('powerset cardinality', () => {
assertResultAsString('Set().powerset().size()', '1')
assertResultAsString('2.to(4).powerset().size()', '8')
assertResultAsString('2.to(5).powerset().size()', '16')
})
})
describe('compile over built-in sets', () => {
it('computes Bool', () => {
assertResultAsString('Bool', 'Set(false, true)')
})
it('computes Int', () => {
assertResultAsString('Int', 'Int')
})
it('computes Int.contains', () => {
assertResultAsString('Int.contains(123)', 'true')
assertResultAsString('Int.contains(0)', 'true')
assertResultAsString('Int.contains(-123)', 'true')
})
it('computes Nat', () => {
assertResultAsString('Nat', 'Nat')
})
it('computes Nat.contains', () => {
assertResultAsString('Nat.contains(123)', 'true')
assertResultAsString('Nat.contains(0)', 'true')
assertResultAsString('Nat.contains(-123)', 'false')
})
it('computes subseteq on Nat and Int', () => {
assertResultAsString('Nat.subseteq(Nat)', 'true')
assertResultAsString('Nat.subseteq(Int)', 'true')
assertResultAsString('Int.subseteq(Int)', 'true')
assertResultAsString('Int.subseteq(Nat)', 'false')
})
it('equality over Nat and Int', () => {
assertResultAsString('Nat == Nat', 'true')
assertResultAsString('Int == Int', 'true')
assertResultAsString('Nat == Int', 'false')
assertResultAsString('Int == Nat', 'false')
assertResultAsString('Int == Set(0, 1)', 'false')
})
})
describe('compile over tuples', () => {
it('tuple constructors', () => {
assertResultAsString('Tup(1, 2, 3)', 'Tup(1, 2, 3)')
assertResultAsString('(1, 2, 3)', 'Tup(1, 2, 3)')
assertResultAsString('(1, 2, 3,)', 'Tup(1, 2, 3)')
})
it('tuple access', () => {
assertResultAsString('Tup(4, 5, 6)._1', '4')
assertResultAsString('Tup(4, 5, 6)._2', '5')
assertResultAsString('Tup(4, 5, 6)._3', '6')
})
it('tuple equality', () => {
assertResultAsString('(4, 5, 6) == (5 - 1, 5, 6)', 'true')
assertResultAsString('(4, 5, 6) == (5, 5, 6)', 'false')
})
it('cross products', () => {
assertResultAsString('tuples(Set(), Set(), Set())', 'Set()')
assertResultAsString('tuples(Set(), 2.to(3))', 'Set()')
assertResultAsString('tuples(2.to(3), Set(), 3.to(5))', 'Set()')
assertResultAsString('tuples(1.to(2), 2.to(3))', 'Set(Tup(1, 2), Tup(2, 2), Tup(1, 3), Tup(2, 3))')
assertResultAsString('tuples(1.to(1), 1.to(1), 1.to(1))', 'Set(Tup(1, 1, 1))')
assertResultAsString('tuples(1.to(3), 2.to(4)) == tuples(1.to(3), 2.to(5 - 1))', 'true')
assertResultAsString('tuples(1.to(3), 2.to(4)) == tuples(1.to(3), 2.to(5 + 1))', 'false')
assertResultAsString('tuples(1.to(3), 2.to(4)).subseteq(tuples(1.to(3), 2.to(5 + 1)))', 'true')
assertResultAsString('tuples(1.to(4), 2.to(4)).subseteq(tuples(1.to(3), 2.to(5)))', 'false')
assertResultAsString('Set(tuples(1.to(2), 2.to(3)))', 'Set(Set(Tup(1, 2), Tup(1, 3), Tup(2, 2), Tup(2, 3)))')
})
it('cardinality of cross products', () => {
assertResultAsString('tuples(1.to(4), 2.to(4)).size()', '12')
assertResultAsString('tuples(Set(), 2.to(4)).size()', '0')
})
})
describe('compile over lists', () => {
it('list constructors', () => {
assertResultAsString('[4, 2, 3]', 'List(4, 2, 3)')
assertResultAsString('[4, 2, 3, ]', 'List(4, 2, 3)')
assertResultAsString('List(4, 2, 3)', 'List(4, 2, 3)')
})
it('list range', () => {
assertResultAsString('range(3, 7)', 'List(3, 4, 5, 6)')
assertResultAsString('range(4, 5)', 'List(4)')
assertResultAsString('range(3, 3)', 'List()')
})
it('list equality', () => {
assertResultAsString('[4, 5, 6] == [5 - 1, 5, 6]', 'true')
assertResultAsString('[4, 5, 6] == [5, 5, 6]', 'false')
// lists are not equal to tuples in the typed world
assertResultAsString('[4, 5, 6] == (4, 5, 6)', 'false')
})
it('list access', () => {
assertResultAsString('[4, 5, 6].nth(0)', '4')
assertResultAsString('[4, 5, 6].nth(2)', '6')
assertResultAsString('[4, 5, 6].nth(-1)', undefined)
assertResultAsString('[4, 5, 6].nth(3)', undefined)
})
it('list length', () => {
assertResultAsString('[4, 5, 6].length()', '3')
assertResultAsString('[].length()', '0')
})
it('list indices', () => {
assertResultAsString('[4, 5, 6].indices()', 'Set(0, 1, 2)')
assertResultAsString('[].indices()', 'Set()')
})
it('list append', () => {
assertResultAsString('[4, 2, 3].append(5)', 'List(4, 2, 3, 5)')
assertResultAsString('[].append(3)', 'List(3)')
})
it('list concat', () => {
assertResultAsString('[4, 2, 3].concat([5, 6])', 'List(4, 2, 3, 5, 6)')
assertResultAsString('[].concat([3, 4])', 'List(3, 4)')
assertResultAsString('[3, 4].concat([])', 'List(3, 4)')
assertResultAsString('[].concat([])', 'List()')
})
it('list head', () => {
assertResultAsString('[4, 5, 6].head()', '4')
assertResultAsString('[].head()', undefined)
})
it('list tail', () => {
assertResultAsString('[4, 5, 6].tail()', 'List(5, 6)')
assertResultAsString('[4].tail()', 'List()')
assertResultAsString('[].tail()', undefined)
})
it('list slice', () => {
assertResultAsString('[4, 5, 6, 7].slice(1, 3)', 'List(5, 6)')
assertResultAsString('[4, 5, 6, 7].slice(0, 4)', 'List(4, 5, 6, 7)')
assertResultAsString('[4, 5, 6, 7].slice(1, 7)', undefined)
assertResultAsString('[4, 5, 6, 7].slice(-1, 3)', undefined)
assertResultAsString('[1, 2].slice(1, 2)', 'List(2)')
assertResultAsString('[1, 2].slice(1, 1)', 'List()')
assertResultAsString('[1, 2].slice(2, 2)', 'List()')
assertResultAsString('[1, 2].slice(2, 1)', undefined)
assertResultAsString('[].slice(0, 0)', 'List()')
assertResultAsString('[].slice(1, 0)', undefined)
assertResultAsString('[].slice(1, 1)', undefined)
assertResultAsString('[].slice(0, -1)', undefined)
})
it('list replaceAt', () => {
assertResultAsString('[4, 5, 6].replaceAt(0, 10)', 'List(10, 5, 6)')
assertResultAsString('[4, 5, 6].replaceAt(2, 10)', 'List(4, 5, 10)')
assertResultAsString('[4, 5, 6].replaceAt(4, 10)', undefined)
assertResultAsString('[4, 5, 6].replaceAt(-1, 10)', undefined)
})
it('list foldl', () => {
assertResultAsString('[].foldl(3, (i, e) => i + e)', '3')
assertResultAsString('[4, 5, 6, 7].foldl(1, (i, e) => i + e)', '23')
assertResultAsString('[4, 5, 6, 7].foldl([], (l, e) => l.append(e))', 'List(4, 5, 6, 7)')
})
it('list foldr', () => {
assertResultAsString('[].foldr(3, (e, i) => e + i)', '3')
assertResultAsString('[4, 5, 6, 7].foldr(1, (e, i) => e + i)', '23')
assertResultAsString('[4, 5, 6, 7].foldr([], (e, l) => l.append(e))', 'List(7, 6, 5, 4)')
})
it('list select', () => {
assertResultAsString('[].select(e => e % 2 == 0)', 'List()')
assertResultAsString('[4, 5, 6].select(e => e % 2 == 0)', 'List(4, 6)')
})
it('allListsUpTo', () => {
assertResultAsString(
'Set(1, 2, 3).allListsUpTo(2)',
'Set(List(), List(1, 1), List(1, 2), List(1, 3), List(1), List(2, 1), List(2, 2), List(2, 3), List(2), List(3, 1), List(3, 2), List(3, 3), List(3))'
)
assertResultAsString('Set(1).allListsUpTo(3)', 'Set(List(), List(1, 1, 1), List(1, 1), List(1))')
assertResultAsString('Set().allListsUpTo(3)', 'Set(List())')
assertResultAsString('Set(1).allListsUpTo(0)', 'Set(List())')
})
it('getOnlyElement', () => {
assertResultAsString('Set(5).getOnlyElement()', '5')
assertResultAsString('Set().getOnlyElement()', undefined)
assertResultAsString('Set(1, 2).getOnlyElement()', undefined)
})
})
describe('compile over records', () => {
it('record constructors', () => {
assertResultAsString('Rec("a", 2, "b", true)', 'Rec("a", 2, "b", true)')
assertResultAsString('{ a: 2, b: true }', 'Rec("a", 2, "b", true)')
assertResultAsString('{ a: 2, b: true, }', 'Rec("a", 2, "b", true)')
})
it('record equality', () => {
assertResultAsString('{ a: 2 + 3, b: true } == { a: 5, b: true }', 'true')
assertResultAsString('{ a: 3, b: true } == { b: true, a: 3 }', 'true')
assertResultAsString('{ a: 2 + 3, b: true } == { a: 1, b: false }', 'false')
})
it('record field access', () => {
assertResultAsString('{ a: 2, b: true }.a', '2')
assertResultAsString('{ a: 2, b: true }.b', 'true')
})
it('record field names', () => {
assertResultAsString('{ a: 2, b: true }.fieldNames()', 'Set("a", "b")')
})
it('record field update', () => {
assertResultAsString('{ a: 2, b: true }.with("a", 3)', 'Rec("a", 3, "b", true)')
// The following query should not be possible, due to the type checker.
// Just in case, we check that the simulator returns 'undefined'.
assertResultAsString('{ a: 2, b: true }.with("c", 3)', undefined)
})
})
describe('compile over sum types', () => {
it('can compile construction of sum type variants', () => {
const context = 'type T = Some(int) | None'
assertResultAsString('Some(40 + 2)', 'variant("Some", 42)', context)
assertResultAsString('None', 'variant("None", Tup())', context)
})
it('can compile elimination of sum type variants via match', () => {
const context = 'type T = Some(int) | None'
assertResultAsString('match Some(40 + 2) { Some(x) => x | None => 0 }', '42', context)
assertResultAsString('match None { Some(x) => x | None => 0 }', '0', context)
})
it('can compile elimination of sum type variants via match using default', () => {
const context = 'type T = Some(int) | None'
// We can hit the fallback case
assertResultAsString('match None { Some(x) => x | _ => 3 }', '3', context)
})
})
describe('compile over maps', () => {
it('mapBy constructor', () => {
assertResultAsString('3.to(5).mapBy(i => 2 * i)', 'Map(Tup(3, 6), Tup(4, 8), Tup(5, 10))')
assertResultAsString('Set(2.to(4)).mapBy(s => s.size())', 'Map(Tup(Set(2, 3, 4), 3))')
})
it('setToMap constructor', () => {
assertResultAsString('setToMap(Set((3, 6), (4, 10 - 2), (5, 10)))', 'Map(Tup(3, 6), Tup(4, 8), Tup(5, 10))')
})
it('mapOf constructor', () => {
assertResultAsString('Map(3 -> 6, 4 -> 10 - 2, 5 -> 10)', 'Map(Tup(3, 6), Tup(4, 8), Tup(5, 10))')
})
it('map get', () => {
assertResultAsString('3.to(5).mapBy(i => 2 * i).get(4)', '8')
assertResultAsString('Set(2.to(4)).mapBy(s => s.size()).get(Set(2, 3, 4))', '3')
})
it('map update', () => {
assertResultAsString('3.to(5).mapBy(i => 2 * i).set(4, 20)', 'Map(Tup(3, 6), Tup(4, 20), Tup(5, 10))')
assertResultAsString('3.to(5).mapBy(i => 2 * i).set(7, 20)', undefined)
})
it('map setBy', () => {
assertResultAsString(
'3.to(5).mapBy(i => 2 * i).setBy(4, old => old + 1)',
'Map(Tup(3, 6), Tup(4, 9), Tup(5, 10))'
)
assertResultAsString('3.to(5).mapBy(i => 2 * i).setBy(7, old => old + 1)', undefined)
})
it('map put', () => {
assertResultAsString(
'3.to(5).mapBy(i => 2 * i).put(10, 11)',
'Map(Tup(10, 11), Tup(3, 6), Tup(4, 8), Tup(5, 10))'
)
})
it('map keys', () => {
assertResultAsString('Set(3, 5, 7).mapBy(i => 2 * i).keys()', 'Set(3, 5, 7)')
})
it('map equality', () => {
assertResultAsString('3.to(5).mapBy(i => 2 * i) == 3.to(5).mapBy(i => 3 * i - i)', 'true')
assertResultAsString('3.to(5).mapBy(i => 2 * i) == 3.to(6).mapBy(i => 2 * i)', 'false')
})
it('map setOfMaps', () => {
assertResultAsString(
'2.to(3).setOfMaps(5.to(6))',
'Set(Map(Tup(2, 5), Tup(3, 5)), Map(Tup(2, 6), Tup(3, 5)), Map(Tup(2, 5), Tup(3, 6)), Map(Tup(2, 6), Tup(3, 6)))'
)
assertResultAsString(
`2.to(3).setOfMaps(5.to(6)) ==
Set(Map(2 -> 5, 3 -> 5),
Map(2 -> 6, 3 -> 5),
Map(2 -> 5, 3 -> 6),
Map(2 -> 6, 3 -> 6))`,
'true'
)
assertResultAsString('Set().setOfMaps(Set(3, 5))', 'Set(Map())')
assertResultAsString('Set().setOfMaps(Set())', 'Set(Map())')
assertResultAsString('Set(1, 2).setOfMaps(Set())', 'Set()')
assertResultAsString('Set(2).setOfMaps(5.to(6))', 'Set(Map(Tup(2, 5)), Map(Tup(2, 6)))')
assertResultAsString('2.to(3).setOfMaps(Set(5))', 'Set(Map(Tup(2, 5), Tup(3, 5)))')
assertResultAsString('2.to(4).setOfMaps(5.to(8)).size()', '64')
assertResultAsString('2.to(4).setOfMaps(5.to(7)).subseteq(2.to(4).setOfMaps(4.to(8)))', 'true')
assertResultAsString('2.to(4).setOfMaps(5.to(10)).subseteq(2.to(4).setOfMaps(4.to(8)))', 'false')
assertResultAsString('2.to(3).setOfMaps(5.to(6)).contains(Map(2 -> 5, 3 -> 5))', 'true')
assertResultAsString('2.to(3).setOfMaps(5.to(6)) == 2.to(4 - 1).setOfMaps(5.to(7 - 1))', 'true')
})
})
describe('compile runs', () => {
it('then ok', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 1).then(n' = n + 2).then(n' = n * 4)
`
)
assertVarAfterCall('n', '12', 'run1', input)
})
it('then fails when rhs is unreachable', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 1).then(all { n == 0, n' = n + 2 }).then(n' = 3)
`
)
evalRun('run1', input)
.mapRight(result => assert.fail(`Expected the run to fail, found: ${result}`))
.mapLeft(m => assert.equal(m, "[QNT513] Cannot continue in A.then(B), A evaluates to 'false'"))
})
it('then returns false when rhs is false', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 1).then(all { n == 0, n' = n + 2 })
`
)
evalRun('run1', input)
.mapRight(result => assert.equal(result, 'false'))
.mapLeft(m => assert.fail(`Expected the run to return false, found: ${m}`))
})
it('reps', () => {
const input = dedent(
`var n: int
|var hist: List[int]
|run run1 = (all { n' = 1, hist' = [] })
| .then(3.reps(_ => all { n' = n + 1, hist' = hist.append(n) }))
|run run2 = (all { n' = 1, hist' = [] })
| .then(3.reps(i => all { n' = i, hist' = hist.append(i) }))
`
)
assertVarAfterCall('hist', 'List(1, 2, 3)', 'run1', input)
assertVarAfterCall('hist', 'List(0, 1, 2)', 'run2', input)
})
it('reps fails when action is false', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 0).then(10.reps(i => all { n < 5, n' = n + 1 }))
`
)
evalRun('run1', input)
.mapRight(result => assert.fail(`Expected the run to fail, found: ${result}`))
.mapLeft(m => assert.equal(m, '[QNT513] Reps loop could not continue after iteration #6 evaluated to false'))
})
it('fail', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 1).fail()
`
)
evalVarAfterRun('n', 'run1', input).mapRight(m => assert.fail(`Expected the run to fail, found: ${m}`))
})
it('assert', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 3).then(and { assert(n < 3), n' = n })
`
)
evalVarAfterRun('n', 'run1', input).mapRight(m => assert.fail(`Expected an error, found: ${m}`))
})
it('expect fails', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 0).then(n' = 3).expect(n < 3)
`
)
evalVarAfterRun('n', 'run1', input).mapRight(m => assert.fail(`Expected the run to fail, found: ${m}`))
})
it('expect ok', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 0).then(n' = 3).expect(n == 3)
`
)
assertVarAfterCall('n', '3', 'run1', input)
})
it('expect fails when left-hand side is false', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 0).then(all { n == 1, n' = 3 }).expect(n < 3)
`
)
evalVarAfterRun('n', 'run1', input).mapRight(m => assert.fail(`Expected the run to fail, found: ${m}`))
})
it('expect and then expect fail', () => {
const input = dedent(
`var n: int
|run run1 = (n' = 0).then(n' = 3).expect(n == 3).then(n' = 4).expect(n == 3)
`
)
evalVarAfterRun('n', 'run1', input).mapRight(m => assert.fail(`Expected the run to fail, found: ${m}`))
})
it('q::debug', () => {
// `q::debug(s, a)` returns `a`
const input = dedent(
`var n: int
|run run1 = (n' = 1).then(n' = q::debug("n plus one", n + 1))
`
)
assertVarAfterCall('n', '2', 'run1', input)
})
it('q::debug with single argument', () => {
// `q::debug(a)` returns `a` and shows the expression and its value
const input = dedent(
`var n: int
|run run1 = (n' = 1).then(n' = q::debug(n + 1))
`
)
assertVarAfterCall('n', '2', 'run1', input)
})
it('unsupported operators', () => {
assertResultAsString('allLists(1.to(3))', undefined)
assertResultAsString('chooseSome(1.to(3))', undefined)
assertResultAsString('always(true)', undefined)
assertResultAsString('eventually(true)', undefined)
assertResultAsString('enabled(true)', undefined)
assertResultAsString('orKeep(true, [])', undefined)
assertResultAsString('mustChange(true, [])', undefined)
assertResultAsString('weakFair(true, [])', undefined)
assertResultAsString('strongFair(true, [])', undefined)
})
})
})