@informalsystems/quint
Version:
Core tool for the Quint specification language
226 lines • 9.08 kB
JavaScript
"use strict";
/*
* Data structures for recording execution traces.
*
* Igor Konnov, 2023
*
* Copyright 2022-2023 Informal Systems
* Licensed under the Apache License, Version 2.0.
* See LICENSE in the project root for license information.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.newTraceRecorder = exports.noExecutionListener = void 0;
const assert_1 = require("assert");
const verbosity_1 = require("./../verbosity");
const either_1 = require("@sweet-monads/either");
const runtimeValue_1 = require("./impl/runtimeValue");
const util_1 = require("./../util");
const emptyFrameError = { code: 'QNT501', message: 'empty frame' };
/**
* An implementation of ExecutionListener that does nothing.
*/
exports.noExecutionListener = {
onUserOperatorCall: (_app) => { },
onUserOperatorReturn: (_app, _args, _result) => { },
onAnyOptionCall: (_anyExpr, _position) => { },
onAnyOptionReturn: (_anyExpr, _position) => { },
onAnyReturn: (_noptions, _choice) => { },
onNextState: (_oldState, _newState) => { },
onRunCall: () => { },
onRunReturn: (_outcome, _trace) => { },
};
// a trace recording listener
const newTraceRecorder = (verbosityLevel, rng, tracesToRecord = 1) => {
return new TraceRecorderImpl(verbosityLevel, rng, tracesToRecord);
};
exports.newTraceRecorder = newTraceRecorder;
// a private implementation of a trace recorder
class TraceRecorderImpl {
constructor(verbosityLevel, rng, tracesToStore) {
this.verbosityLevel = verbosityLevel;
this.rng = rng;
const bottom = this.newBottomFrame();
this.tracesToStore = tracesToStore;
this.bestTraces = [];
this.runSeed = this.rng.getState();
this.currentFrame = bottom;
this.frameStack = [bottom];
}
clear() {
this.bestTraces = [];
const bottom = this.newBottomFrame();
this.currentFrame = bottom;
this.frameStack = [bottom];
}
onUserOperatorCall(app) {
// For now, we cannot tell apart actions from other user definitions.
// https://github.com/informalsystems/quint/issues/747
if (verbosity_1.verbosity.hasUserOpTracking(this.verbosityLevel)) {
const newFrame = {
app: app,
args: [],
result: (0, either_1.left)(emptyFrameError),
subframes: [],
};
if (this.frameStack.length == 0) {
// this should not happen, as there is always bottomFrame,
// but we do not throw here, as trace collection is not the primary
// function of the simulator.
this.frameStack = [newFrame];
}
else if (this.frameStack.length === 2 && this.frameStack[1].app.opcode === '_') {
// A placeholder frame created from `q::input` or `then`. Modify in place.
const frame = this.frameStack[1];
frame.app = app;
frame.args = [];
frame.result = (0, either_1.left)(emptyFrameError);
frame.subframes = [];
}
else {
// connect the new frame to the previous frame
this.frameStack[this.frameStack.length - 1].subframes.push(newFrame);
// and push the new frame to be on top of the stack
this.frameStack.push(newFrame);
}
}
}
onUserOperatorReturn(_app, args, result) {
if (verbosity_1.verbosity.hasUserOpTracking(this.verbosityLevel)) {
const top = this.frameStack.pop();
if (top) {
// since this frame is connected via the parent frame,
// the result will not disappear
top.args = args;
top.result = result;
}
}
}
onAnyOptionCall(anyExpr, _position) {
if (verbosity_1.verbosity.hasActionTracking(this.verbosityLevel)) {
// the option has to be hidden under its own frame,
// so it can be popped as a single frame later
const newFrame = {
app: anyExpr,
args: [],
result: (0, either_1.left)(emptyFrameError),
subframes: [],
};
if (this.frameStack.length > 0) {
// add the option directly to the stack of the current expression
// that contains `any { ... }`.
this.frameStack[this.frameStack.length - 1].subframes.push(newFrame);
this.frameStack.push(newFrame);
}
else {
this.frameStack = [newFrame];
}
}
}
onAnyOptionReturn(_anyExpr, _position) {
if (verbosity_1.verbosity.hasActionTracking(this.verbosityLevel)) {
this.frameStack.pop();
}
}
onAnyReturn(noptions, choice) {
if (verbosity_1.verbosity.hasActionTracking(this.verbosityLevel)) {
(0, assert_1.strict)(this.frameStack.length > 0);
const top = this.frameStack[this.frameStack.length - 1];
const start = top.subframes.length - noptions;
// leave only the chosen frame as well as the older ones
top.subframes = top.subframes.filter((_, i) => i < start || i == start + choice);
if (choice >= 0) {
// The top frame contains the frames of the chosen option that are
// wrapped in anyExpr.args[position], see onAnyOptionCall.
// Unwrap the option, as we do not need it any longer.
const optionFrame = top.subframes.pop();
if (optionFrame) {
top.subframes = top.subframes.concat(optionFrame.subframes);
}
}
}
}
onNextState(_oldState, _newState) {
// introduce a new frame that is labelled with a dummy operator
if (verbosity_1.verbosity.hasUserOpTracking(this.verbosityLevel)) {
const dummy = { id: 0n, kind: 'app', opcode: '_', args: [] };
const newFrame = { app: dummy, args: [], result: (0, either_1.left)(emptyFrameError), subframes: [] };
// forget the frames, except the bottom one, and push the new one
this.frameStack = [this.frameStack[0], newFrame];
// connect the new frame to the topmost frame, which effects in a new step
this.frameStack[0].subframes.push(newFrame);
}
}
onRunCall() {
// reset the stack
this.frameStack = [this.newBottomFrame()];
this.runSeed = this.rng.getState();
}
onRunReturn(outcome, trace) {
(0, assert_1.strict)(this.frameStack.length > 0);
const traceToSave = this.frameStack[0];
traceToSave.result = outcome;
traceToSave.args = trace;
const traceWithSeed = { frame: traceToSave, seed: this.runSeed };
// Insert the trace into the list of best traces,
// keeping the list sorted by descending quality.
this.insertTraceSortedByQuality(traceWithSeed);
// If there are more traces than needed, remove the worst trace,
// ie. the last one, since the traces are sorted by descending quality.
if (this.bestTraces.length > this.tracesToStore) {
this.bestTraces.pop();
}
}
insertTraceSortedByQuality(trace) {
(0, util_1.insertSorted)(this.bestTraces, trace, compareTracesByQuality);
}
// create a bottom frame, which encodes the whole trace
newBottomFrame() {
return {
// this is just a dummy operator application
app: { id: 0n, kind: 'app', opcode: 'Rec', args: [] },
// we will store the sequence of states here
args: [],
// the result of the trace evaluation
result: (0, either_1.right)(runtimeValue_1.rv.mkBool(true)),
// and here we store the subframes for the top-level actions
subframes: [],
};
}
}
// Compare two traces by quality.
//
// Prefer short traces for error, and longer traces for non error.
// Therefore, trace a is better than trace b iff
// - when a has an error: a is shorter or b has no error;
// - when a has no error: a is longer and b has no error.
function compareTracesByQuality(a, b) {
const fromResult = (r) => {
if (r.isLeft()) {
return true;
}
else {
const rex = r.value.toQuintEx({ nextId: () => 0n });
return rex.kind === 'bool' && !rex.value;
}
};
const aNotOk = fromResult(a.frame.result);
const bNotOk = fromResult(b.frame.result);
if (aNotOk) {
if (bNotOk) {
return a.frame.args.length - b.frame.args.length;
}
else {
return -1;
}
}
else {
// a is ok
if (bNotOk) {
return 1;
}
else {
return b.frame.args.length - a.frame.args.length;
}
}
}
//# sourceMappingURL=trace.js.map