Skip to content

Commit e3cbbeb

Browse files
authored
Merge pull request #1562 from informalsystems/gabriela/counting-witnesses
Counting witnesses
2 parents 9076637 + 5b04cdd commit e3cbbeb

File tree

9 files changed

+98
-21
lines changed

9 files changed

+98
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## UNRELEASED
99

1010
### Added
11+
12+
- Added a `--witnesses` option to `quint run` that enables counting for how many explored traces each of the given predicates (witnesses) were true (#1562)
13+
1114
### Changed
1215

1316
- Bumped Apalache to 0.47.2 (#1565)

docs/pages/docs/quint.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ Options:
235235
--invariant invariant to check: a definition name or an expression
236236
[string] [default: ["true"]]
237237
--seed random seed to use for non-deterministic choice [string]
238+
--witnesses space separated list of witnesses to report on (counting for
239+
how many traces the witness is true) [array] [default: []]
238240
--mbt (experimental) whether to produce metadata to be used by
239241
model-based testing [boolean] [default: false]
240242
```

examples/games/tictactoe/tictactoe.qnt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ module tictactoe {
127127

128128
action MoveX = all {
129129
nextTurn == X,
130-
not(won(O)),
130+
not(gameOver),
131131
if (boardEmpty) StartInCorner else
132132
if (canWin) Win else
133133
if (canBlock) Block else
@@ -139,7 +139,7 @@ module tictactoe {
139139

140140
action MoveO = all {
141141
nextTurn == O,
142-
not(won(X)),
142+
not(gameOver),
143143
MoveToEmpty(O),
144144
nextTurn' = X,
145145
}

quint/io-cli-tests.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,3 +1410,22 @@ exit $exit_code
14101410
14111411
error: parsing failed
14121412
```
1413+
### Prints witnesses counts
1414+
1415+
<!-- !test exit 0 -->
1416+
<!-- !test in witnesses -->
1417+
```
1418+
output=$(quint run ../examples/games/tictactoe/tictactoe.qnt --witnesses="won(X)" stalemate --max-samples=100 --seed=0x2b442ab439177 --verbosity=1)
1419+
exit_code=$?
1420+
echo "$output" | sed -e 's/([0-9]*ms)/(duration)/g'
1421+
exit $exit_code
1422+
```
1423+
1424+
<!-- !test out witnesses -->
1425+
```
1426+
[ok] No violation found (duration).
1427+
Witnesses:
1428+
won(X) was witnessed in 99 trace(s) out of 100 explored (99.00%)
1429+
stalemate was witnessed in 1 trace(s) out of 100 explored (1.00%)
1430+
Use --seed=0x2b442ab439177 to reproduce.
1431+
```

quint/src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ const runCmd = {
273273
type: 'string',
274274
default: 'true',
275275
})
276+
.option('witnesses', {
277+
desc: 'space separated list of witnesses to report on (counting for how many traces the witness is true)',
278+
type: 'array',
279+
default: [],
280+
})
276281
.option('seed', {
277282
desc: 'random seed to use for non-deterministic choice',
278283
type: 'string',

quint/src/cliCommands.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { DocumentationEntry, produceDocs, toMarkdown } from './docs'
3737
import { QuintError, quintErrorToString } from './quintError'
3838
import { TestOptions, TestResult } from './runtime/testing'
3939
import { IdGenerator, newIdGenerator, zerog } from './idGenerator'
40-
import { Outcome, SimulatorOptions } from './simulation'
40+
import { Outcome, SimulationResult, SimulatorOptions } from './simulation'
4141
import { ofItf, toItf } from './itf'
4242
import { printExecutionFrameRec, printTrace, terminalWidth } from './graphics'
4343
import { verbosity } from './verbosity'
@@ -480,6 +480,28 @@ function maybePrintCounterExample(verbosityLevel: number, states: QuintEx[], fra
480480
}
481481
}
482482

483+
function maybePrintWitnesses(
484+
verbosityLevel: number,
485+
evalResult: Either<QuintError, SimulationResult>,
486+
witnesses: string[]
487+
) {
488+
if (verbosity.hasWitnessesOutput(verbosityLevel)) {
489+
evalResult.map(r => {
490+
if (r.witnessingTraces.length > 0) {
491+
console.log(chalk.green('Witnesses:'))
492+
}
493+
r.witnessingTraces.forEach((n, i) => {
494+
const percentage = chalk.gray(`(${(((1.0 * n) / r.samples) * 100).toFixed(2)}%)`)
495+
console.log(
496+
`${chalk.yellow(witnesses[i])} was witnessed in ${chalk.green(n)} trace(s) out of ${
497+
r.samples
498+
} explored ${percentage}`
499+
)
500+
})
501+
})
502+
}
503+
}
504+
483505
/**
484506
* Run the simulator.
485507
*
@@ -545,20 +567,23 @@ export async function runSimulator(prev: TypecheckedStage): Promise<CLIProcedure
545567
return right(parseResult.expr)
546568
}
547569

548-
const argsParsingResult = mergeInMany([prev.args.init, prev.args.step, prev.args.invariant].map(toExpr))
570+
const argsParsingResult = mergeInMany(
571+
[prev.args.init, prev.args.step, prev.args.invariant, ...prev.args.witnesses].map(toExpr)
572+
)
549573
if (argsParsingResult.isLeft()) {
550574
return cliErr('Argument error', {
551575
...simulator,
552576
errors: argsParsingResult.value.map(mkErrorMessage(new Map())),
553577
})
554578
}
555-
const [init, step, invariant] = argsParsingResult.value
579+
const [init, step, invariant, ...witnesses] = argsParsingResult.value
556580

557581
const evaluator = new Evaluator(prev.resolver.table, recorder, options.rng, options.storeMetadata)
558582
const evalResult = evaluator.simulate(
559583
init,
560584
step,
561585
invariant,
586+
witnesses,
562587
prev.args.maxSamples,
563588
prev.args.maxSteps,
564589
prev.args.nTraces ?? 1
@@ -567,7 +592,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise<CLIProcedure
567592
const elapsedMs = Date.now() - startMs
568593

569594
const outcome: Outcome = evalResult.isRight()
570-
? { status: (evalResult.value as QuintBool).value ? 'ok' : 'violation' }
595+
? { status: (evalResult.value.result as QuintBool).value ? 'ok' : 'violation' }
571596
: { status: 'error', errors: [evalResult.value] }
572597

573598
const states = recorder.bestTraces[0]?.frame?.args?.map(e => e.toQuintEx(zerog))
@@ -609,6 +634,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise<CLIProcedure
609634
console.log(chalk.gray('Use --verbosity to produce more (or less) output.'))
610635
}
611636
}
637+
maybePrintWitnesses(verbosityLevel, evalResult, prev.args.witnesses)
612638

613639
return right({
614640
...simulator,
@@ -625,6 +651,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise<CLIProcedure
625651
console.log(chalk.gray('Use --verbosity=3 to show executions.'))
626652
}
627653
}
654+
maybePrintWitnesses(verbosityLevel, evalResult, prev.args.witnesses)
628655

629656
return cliErr('Invariant violated', {
630657
...simulator,

quint/src/runtime/impl/evaluator.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { zerog } from '../../idGenerator'
2828
import { List } from 'immutable'
2929
import { Builder, buildDef, buildExpr, nameWithNamespaces } from './builder'
3030
import { Presets, SingleBar } from 'cli-progress'
31+
import { SimulationResult } from '../../simulation'
3132

3233
/**
3334
* An evaluator for Quint in Node TS runtime.
@@ -141,10 +142,11 @@ export class Evaluator {
141142
init: QuintEx,
142143
step: QuintEx,
143144
inv: QuintEx,
145+
witnesses: QuintEx[],
144146
nruns: number,
145147
nsteps: number,
146148
ntraces: number
147-
): Either<QuintError, QuintEx> {
149+
): Either<QuintError, SimulationResult> {
148150
let errorsFound = 0
149151
let failure: QuintError | undefined = undefined
150152

@@ -161,9 +163,13 @@ export class Evaluator {
161163
const initEval = buildExpr(this.builder, init)
162164
const stepEval = buildExpr(this.builder, step)
163165
const invEval = buildExpr(this.builder, inv)
166+
const witnessesEvals = witnesses.map(w => buildExpr(this.builder, w))
167+
const witnessingTraces = new Array(witnesses.length).fill(0)
164168

165-
for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) {
169+
let runNo = 0
170+
for (; errorsFound < ntraces && !failure && runNo < nruns; runNo++) {
166171
progressBar.update(runNo)
172+
const traceWitnessed = new Array(witnesses.length).fill(false)
167173

168174
this.recorder.onRunCall()
169175
this.reset()
@@ -208,12 +214,25 @@ export class Evaluator {
208214

209215
this.shift()
210216

217+
witnessesEvals.forEach((witnessEval, i) => {
218+
const witnessResult = witnessEval(this.ctx)
219+
if (isTrue(witnessResult)) {
220+
traceWitnessed[i] = true
221+
}
222+
})
223+
211224
const invResult = invEval(this.ctx).mapLeft(error => (failure = error))
212225
if (!isTrue(invResult)) {
213226
errorsFound++
214227
}
215228
this.recorder.onUserOperatorReturn(stepApp, [], invResult)
216229
}
230+
231+
traceWitnessed.forEach((witnessed, i) => {
232+
if (witnessed) {
233+
witnessingTraces[i] = witnessingTraces[i] + 1
234+
}
235+
})
217236
}
218237
}
219238

@@ -222,11 +241,9 @@ export class Evaluator {
222241
}
223242
progressBar.stop()
224243

225-
const outcome: Either<QuintError, QuintEx> = failure
244+
return failure
226245
? left(failure)
227-
: right({ id: 0n, kind: 'bool', value: errorsFound == 0 })
228-
229-
return outcome
246+
: right({ result: { id: 0n, kind: 'bool', value: errorsFound == 0 }, witnessingTraces, samples: runNo })
230247
}
231248

232249
/**
@@ -386,10 +403,10 @@ export class Evaluator {
386403
private evaluateSimulation(expr: QuintApp): Either<QuintError, QuintEx> {
387404
if (expr.opcode === 'q::testOnce') {
388405
const [nsteps, ntraces, init, step, inv] = expr.args
389-
return this.simulate(init, step, inv, 1, toNumber(nsteps), toNumber(ntraces))
406+
return this.simulate(init, step, inv, [], 1, toNumber(nsteps), toNumber(ntraces)).map(r => r.result)
390407
} else {
391408
const [nruns, nsteps, ntraces, init, step, inv] = expr.args
392-
return this.simulate(init, step, inv, toNumber(nruns), toNumber(nsteps), toNumber(ntraces))
409+
return this.simulate(init, step, inv, [], toNumber(nruns), toNumber(nsteps), toNumber(ntraces)).map(r => r.result)
393410
}
394411
}
395412
}

quint/src/simulation.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
*/
1010

1111
import { QuintEx } from './ir/quintIr'
12-
import { ExecutionFrame } from './runtime/trace'
1312
import { Rng } from './rng'
1413
import { QuintError } from './quintError'
1514

@@ -39,10 +38,8 @@ export type Outcome =
3938
/**
4039
* A result returned by the simulator.
4140
*/
42-
export interface SimulatorResult {
43-
outcome: Outcome
44-
vars: string[]
45-
states: QuintEx[]
46-
frames: ExecutionFrame[]
47-
seed: bigint
41+
export interface SimulationResult {
42+
result: QuintEx
43+
witnessingTraces: number[]
44+
samples: number
4845
}

quint/src/verbosity.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ export const verbosity = {
6363
return level >= 2
6464
},
6565

66+
/**
67+
* Shall the tool output witnesses counts.
68+
*/
69+
hasWitnessesOutput: (level: number): boolean => {
70+
return level >= 1
71+
},
72+
6673
/**
6774
* Shall the tool track and output actions that were executed.
6875
*/

0 commit comments

Comments
 (0)