From 1bf92b62f49fdc7f22cefadfb19e9604b964eef4 Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 20 Aug 2024 09:46:17 -0300 Subject: [PATCH 01/37] Rewrite evaluator to not depend on flattening --- quint/src/cliCommands.ts | 179 +- quint/src/graphics.ts | 5 +- quint/src/names/base.ts | 4 +- quint/src/names/collector.ts | 7 +- quint/src/names/resolver.ts | 13 +- quint/src/parsing/quintParserFrontend.ts | 3 +- quint/src/repl.ts | 248 ++- quint/src/runtime/compile.ts | 338 ---- quint/src/runtime/impl/base.ts | 59 +- quint/src/runtime/impl/builtins.ts | 562 +++++++ quint/src/runtime/impl/compilerImpl.ts | 1630 ------------------- quint/src/runtime/impl/evaluator.ts | 388 +++++ quint/src/runtime/impl/operatorEvaluator.ts | 181 -- quint/src/runtime/impl/runtimeValue.ts | 149 +- quint/src/runtime/impl/trace.ts | 10 +- quint/src/runtime/runtime.ts | 49 +- quint/src/runtime/testing.ts | 182 +-- quint/src/runtime/trace.ts | 52 +- quint/src/simulation.ts | 158 +- quint/test/runtime/trace.test.ts | 29 +- 20 files changed, 1353 insertions(+), 2893 deletions(-) delete mode 100644 quint/src/runtime/compile.ts create mode 100644 quint/src/runtime/impl/builtins.ts delete mode 100644 quint/src/runtime/impl/compilerImpl.ts create mode 100644 quint/src/runtime/impl/evaluator.ts delete mode 100644 quint/src/runtime/impl/operatorEvaluator.ts diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index ca2c8d76a..b6350c62d 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -17,6 +17,7 @@ import { SourceMap, compactSourceMap, parseDefOrThrow, + parseExpressionOrDeclaration, parsePhase1fromText, parsePhase2sourceResolution, parsePhase3importAndNameResolution, @@ -24,19 +25,29 @@ import { } from './parsing/quintParserFrontend' import { ErrorMessage } from './ErrorMessage' -import { Either, left, right } from '@sweet-monads/either' +import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { fail } from 'assert' import { EffectScheme } from './effects/base' -import { LookupTable, UnusedDefinitions } from './names/base' +import { LookupTable, NameResolutionResult, UnusedDefinitions } from './names/base' import { ReplOptions, quintRepl } from './repl' -import { FlatModule, OpQualifier, QuintEx, QuintModule, QuintOpDef, qualifier } from './ir/quintIr' +import { + FlatModule, + OpQualifier, + QuintApp, + QuintBool, + QuintEx, + QuintModule, + QuintOpDef, + isDef, + qualifier, +} from './ir/quintIr' import { TypeScheme } from './types/base' import { createFinders, formatError } from './errorReporter' import { DocumentationEntry, produceDocs, toMarkdown } from './docs' import { QuintError, quintErrorToString } from './quintError' -import { TestOptions, TestResult, compileAndTest } from './runtime/testing' +import { TestOptions, TestResult } from './runtime/testing' import { IdGenerator, newIdGenerator } from './idGenerator' -import { SimulatorOptions, compileAndRun } from './simulation' +import { Outcome, SimulatorOptions } from './simulation' import { ofItf, toItf } from './itf' import { printExecutionFrameRec, printTrace, terminalWidth } from './graphics' import { verbosity } from './verbosity' @@ -45,10 +56,13 @@ import { fileSourceResolver } from './parsing/sourceResolver' import { verify } from './quintVerifier' import { flattenModules } from './flattening/fullFlattener' import { AnalysisOutput, analyzeInc, analyzeModules } from './quintAnalyzer' -import { ExecutionFrame } from './runtime/trace' +import { ExecutionFrame, newTraceRecorder } from './runtime/trace' import { flow, isEqual, uniqWith } from 'lodash' import { Maybe, just, none } from '@sweet-monads/maybe' import { compileToTlaplus } from './compileToTlaplus' +import { Evaluator } from './runtime/impl/evaluator' +import { NameResolver } from './names/resolver' +import { walkExpression } from './ir/IRVisitor' export type stage = | 'loading' @@ -65,7 +79,7 @@ interface OutputStage { stage: stage // the modules and the lookup table produced by 'parse' modules?: QuintModule[] - table?: LookupTable + names?: NameResolutionResult unusedDefinitions?: UnusedDefinitions // the tables produced by 'typecheck' types?: Map @@ -91,7 +105,7 @@ const pickOutputStage = ({ stage, warnings, modules, - table, + names, unusedDefinitions, types, effects, @@ -107,7 +121,7 @@ const pickOutputStage = ({ stage, warnings, modules, - table, + names, unusedDefinitions, types, effects, @@ -137,6 +151,7 @@ interface ParsedStage extends LoadedStage { sourceMap: SourceMap table: LookupTable unusedDefinitions: UnusedDefinitions + resolver: NameResolver idGen: IdGenerator } @@ -321,6 +336,7 @@ export async function runTests(prev: TypecheckedStage): Promise isMatchingTest(testing.args.match, n) + const maxSamples = testing.args.maxSamples const options: TestOptions = { testMatch: matchFun, maxSamples: testing.args.maxSamples, @@ -351,69 +367,24 @@ export async function runTests(prev: TypecheckedStage): Promise d.kind === 'def' && options.testMatch(d.name) - ) - // Define name expressions referencing each test that is not referenced elsewhere, adding the references to the lookup - // table - const args: QuintEx[] = unusedTests.map(defToAdd => { - const id = testing.idGen.nextId() - testing.table.set(id, defToAdd) - const namespace = defToAdd.importedFrom ? qualifier(defToAdd.importedFrom) : undefined - const name = namespace ? [namespace, defToAdd.name].join('::') : defToAdd.name - - return { kind: 'name', id, name } - }) - // Wrap all expressions in a single declaration - const testDeclaration: QuintOpDef = { - id: testing.idGen.nextId(), - name: 'q::unitTests', - kind: 'def', - qualifier: 'run', - expr: { kind: 'app', opcode: 'actionAll', args, id: testing.idGen.nextId() }, - } - // Add the declaration to the main module - const main = testing.modules.find(m => m.name === mainName) - main?.declarations.push(testDeclaration) - - // TODO The following block should all be moved into compileAndTest - const analysisOutput = { types: testing.types, effects: testing.effects, modes: testing.modes } - const { flattenedModules, flattenedTable, flattenedAnalysis } = flattenModules( - testing.modules, - testing.table, - testing.idGen, - testing.sourceMap, - analysisOutput - ) - const compilationState = { - originalModules: testing.modules, - modules: flattenedModules, - sourceMap: testing.sourceMap, - analysisOutput: flattenedAnalysis, - idGen: testing.idGen, - sourceCode: testing.sourceCode, - } + const main = prev.modules.find(m => m.name === mainName)! + const testDefs = main.declarations.filter(d => d.kind === 'def' && options.testMatch(d.name)) as QuintOpDef[] - const testResult = compileAndTest(compilationState, mainName, flattenedTable, options) + const evaluator = new Evaluator(testing.table, newTraceRecorder(verbosityLevel, rng, 1), rng) + const results = testDefs.map(def => { + return evaluator.test(def.name, def.expr, maxSamples) + }) // We have a compilation failure, so early exit without reporting test results - if (testResult.isLeft()) { - return cliErr('Tests could not be run due to an error during compilation', { - ...testing, - errors: testResult.value.map(mkErrorMessage(testing.sourceMap)), - }) - } + // if (testResult.isLeft()) { + // return cliErr('Tests could not be run due to an error during compilation', { + // ...testing, + // errors: testResult.value.map(mkErrorMessage(testing.sourceMap)), + // }) + // } // We're finished running the tests const elapsedMs = Date.now() - startMs - // So we can analyze the results now - const results = testResult.value // output the status for every test let nFailures = 1 @@ -529,6 +500,7 @@ function maybePrintCounterExample(verbosityLevel: number, states: QuintEx[], fra */ export async function runSimulator(prev: TypecheckedStage): Promise> { const simulator = { ...prev, stage: 'running' as stage } + const startMs = Date.now() const verbosityLevel = deriveVerbosity(prev.args) const mainName = guessMainModule(prev) @@ -563,35 +535,66 @@ export async function runSimulator(prev: TypecheckedStage): Promise { + const parseResult = parseExpressionOrDeclaration(input, '', prev.idGen, prev.sourceMap) + if (parseResult.kind !== 'expr') { + return left({ code: 'QNT501', message: `Expected ${input} to be a valid expression` }) + } + + prev.resolver.switchToModule(mainName) + walkExpression(prev.resolver, parseResult.expr) + if (prev.resolver.errors.length > 0) { + return left(prev.resolver.errors[0]) + } - const mainText = prev.sourceCode.get(prev.path)! - const mainPath = fileSourceResolver(prev.sourceCode).lookupPath(dirname(prev.args.input), basename(prev.args.input)) - const mainModule = prev.modules.find(m => m.name === mainName) - if (mainModule === undefined) { - return cliErr(`Main module ${mainName} not found`, { ...simulator, errors: [] }) + return right(parseResult.expr) } - const mainId = mainModule.id - const mainStart = prev.sourceMap.get(mainId)!.start.index - const mainEnd = prev.sourceMap.get(mainId)!.end!.index - const result = compileAndRun(newIdGenerator(), mainText, mainStart, mainEnd, mainName, mainPath, options) + + const argsParsingResult = mergeInMany([prev.args.init, prev.args.step, prev.args.invariant].map(toExpr)) + if (argsParsingResult.isLeft()) { + return cliErr('Argument error', { + ...simulator, + errors: argsParsingResult.value.map(mkErrorMessage(new Map())), + }) + } + const [init, step, invariant] = argsParsingResult.value + + const evaluator = new Evaluator(prev.resolver.table, recorder, options.rng) + const evalResult = evaluator.simulate( + init, + step, + invariant, + prev.args.maxSamples, + prev.args.maxSteps, + prev.args.nTraces ?? 1, + prev.effects + ) const elapsedMs = Date.now() - startMs - switch (result.outcome.status) { + const outcome: Outcome = evalResult.isRight() + ? { status: (evalResult.value as QuintBool).value ? 'ok' : 'violation' } + : { status: 'error', errors: [evalResult.value] } + + const states = recorder.bestTraces[0].frame.args + const frames = recorder.bestTraces[0].frame.subframes + const seed = options.rng.getState() + switch (outcome.status) { case 'error': return cliErr('Runtime error', { ...simulator, - status: result.outcome.status, - trace: result.states, - errors: result.outcome.errors.map(mkErrorMessage(prev.sourceMap)), + status: outcome.status, + trace: states, + errors: outcome.errors.map(mkErrorMessage(prev.sourceMap)), }) case 'ok': - maybePrintCounterExample(verbosityLevel, result.states, result.frames) + maybePrintCounterExample(verbosityLevel, states, frames) if (verbosity.hasResults(verbosityLevel)) { console.log(chalk.green('[ok]') + ' No violation found ' + chalk.gray(`(${elapsedMs}ms).`)) - console.log(chalk.gray(`Use --seed=0x${result.seed.toString(16)} to reproduce.`)) + console.log(chalk.gray(`Use --seed=0x${seed.toString(16)} to reproduce.`)) if (verbosity.hasHints(verbosityLevel)) { console.log(chalk.gray('You may increase --max-samples and --max-steps.')) console.log(chalk.gray('Use --verbosity to produce more (or less) output.')) @@ -600,15 +603,15 @@ export async function runSimulator(prev: TypecheckedStage): Promise prettyQuintEx(a.toQuintEx(zerog))) + frame.args.map(a => prettyQuintEx(a)) ) - const r = frame.result.isNone() ? text('none') : prettyQuintEx(frame.result.value.toQuintEx(zerog)) + const r = frame.result.isLeft() ? text('none') : prettyQuintEx(frame.result.value) const depth = isLast.length // generate the tree ASCII graphics for this frame let treeArt = isLast diff --git a/quint/src/names/base.ts b/quint/src/names/base.ts index f625be68b..b26549027 100644 --- a/quint/src/names/base.ts +++ b/quint/src/names/base.ts @@ -13,9 +13,10 @@ */ import { cloneDeep, compact } from 'lodash' -import { QuintDef, QuintExport, QuintImport, QuintInstance, QuintLambdaParameter } from '../ir/quintIr' +import { OpQualifier, QuintDef, QuintExport, QuintImport, QuintInstance, QuintLambdaParameter } from '../ir/quintIr' import { QuintType } from '../ir/quintTypes' import { QuintError } from '../quintError' +import { NameResolver } from './resolver' /** * Possible kinds for definitions @@ -84,6 +85,7 @@ export type NameResolutionResult = { table: LookupTable unusedDefinitions: UnusedDefinitions errors: QuintError[] + resolver: NameResolver } export function getTopLevelDef(defs: DefinitionsByName, name: string): LookupDefinition | undefined { diff --git a/quint/src/names/collector.ts b/quint/src/names/collector.ts index 393353459..701fe15eb 100644 --- a/quint/src/names/collector.ts +++ b/quint/src/names/collector.ts @@ -63,6 +63,11 @@ export class NameCollector implements IRVisitor { private currentModuleName: string = '' + switchToModule(moduleName: string): void { + this.currentModuleName = moduleName + this.definitionsByName = this.definitionsByModule.get(moduleName) ?? new Map() + } + enterModule(module: QuintModule): void { this.currentModuleName = module.name this.definitionsByName = new Map() @@ -144,7 +149,7 @@ export class NameCollector implements IRVisitor { } // Update the definition to point to the expression being overriden - constDef.id = ex.id + // constDef.id = ex.id constDef.hidden = false }) diff --git a/quint/src/names/resolver.ts b/quint/src/names/resolver.ts index e9adb4c42..71c549b4f 100644 --- a/quint/src/names/resolver.ts +++ b/quint/src/names/resolver.ts @@ -32,14 +32,19 @@ export function resolveNames(quintModules: QuintModule[]): NameResolutionResult quintModules.forEach(module => { walkModule(visitor, module) }) - return { table: visitor.table, unusedDefinitions: visitor.unusedDefinitions, errors: visitor.errors } + return { + table: visitor.table, + unusedDefinitions: visitor.unusedDefinitions, + errors: visitor.errors, + resolver: visitor, + } } /** * `NameResolver` uses `NameCollector` to collect top-level definitions. Scoped * definitions are collected inside of `NameResolver` as it navigates the IR. */ -class NameResolver implements IRVisitor { +export class NameResolver implements IRVisitor { collector: NameCollector errors: QuintError[] = [] table: LookupTable = new Map() @@ -64,6 +69,10 @@ class NameResolver implements IRVisitor { return new Set(difference(definitions, usedDefinitions)) } + switchToModule(moduleName: string): void { + this.collector.switchToModule(moduleName) + } + enterModule(module: QuintModule): void { // First thing to do in resolving names for a module is to collect all // top-level definitions for that module. This has to be done in a separate diff --git a/quint/src/parsing/quintParserFrontend.ts b/quint/src/parsing/quintParserFrontend.ts index 513b232ff..da53fa659 100644 --- a/quint/src/parsing/quintParserFrontend.ts +++ b/quint/src/parsing/quintParserFrontend.ts @@ -20,7 +20,7 @@ import { QuintDeclaration, QuintDef, QuintEx, QuintModule, isDef } from '../ir/q import { IdGenerator, newIdGenerator } from '../idGenerator' import { ToIrListener } from './ToIrListener' import { LookupTable, UnusedDefinitions } from '../names/base' -import { resolveNames } from '../names/resolver' +import { NameResolver, resolveNames } from '../names/resolver' import { QuintError } from '../quintError' import { SourceLookupPath, SourceResolver, fileSourceResolver } from './sourceResolver' import { CallGraphVisitor, mkCallGraphContext } from '../static/callgraph' @@ -60,6 +60,7 @@ export interface ParserPhase2 extends ParserPhase1 {} export interface ParserPhase3 extends ParserPhase2 { table: LookupTable unusedDefinitions: UnusedDefinitions + resolver: NameResolver } /** diff --git a/quint/src/repl.ts b/quint/src/repl.ts index 968c2b9aa..3bba45757 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -12,37 +12,30 @@ import * as readline from 'readline' import { Readable, Writable } from 'stream' import { readFileSync, writeFileSync } from 'fs' import { Maybe, just, none } from '@sweet-monads/maybe' -import { Either, right } from '@sweet-monads/either' +import { Either, left, right } from '@sweet-monads/either' import chalk from 'chalk' import { format } from './prettierimp' -import { FlatModule, QuintDef, QuintEx } from './ir/quintIr' -import { - CompilationContext, - CompilationState, - compileDecls, - compileExpr, - compileFromCode, - contextNameLookup, - inputDefName, - newCompilationState, -} from './runtime/compile' +import { FlatModule, QuintDef, QuintEx, QuintModule } from './ir/quintIr' import { createFinders, formatError } from './errorReporter' import { Register } from './runtime/runtime' import { TraceRecorder, newTraceRecorder } from './runtime/trace' -import { parseDefOrThrow, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' +import { SourceMap, parse, parseDefOrThrow, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' import { prettyQuintEx, printExecutionFrameRec, terminalWidth } from './graphics' import { verbosity } from './verbosity' import { Rng, newRng } from './rng' import { version } from './version' import { fileSourceResolver } from './parsing/sourceResolver' import { cwd } from 'process' -import { newIdGenerator } from './idGenerator' -import { moduleToString } from './ir/IRprinting' +import { IdGenerator, newIdGenerator } from './idGenerator' +import { expressionToString, moduleToString } from './ir/IRprinting' import { mkErrorMessage } from './cliCommands' -import { QuintError } from './quintError' +import { QuintError, quintErrorToString } from './quintError' import { ErrorMessage } from './ErrorMessage' -import { EvaluationState, newEvaluationState } from './runtime/impl/base' +import { Evaluator } from './runtime/impl/evaluator' +import { walkDeclaration, walkExpression } from './ir/IRVisitor' +import { AnalysisOutput, analyzeModules } from './quintAnalyzer' +import { NameResolver } from './names/resolver' // tunable settings export const settings = { @@ -52,6 +45,39 @@ export const settings = { type writer = (_text: string) => void +/** + * A data structure that holds the state of the compilation process. + */ +export interface CompilationState { + // The ID generator used during compilation. + idGen: IdGenerator + // File content loaded for each source, used to report errors + sourceCode: Map + // A list of modules in context + modules: QuintModule[] + // The name of the main module. + mainName?: string + // The source map for the compiled code. + sourceMap: SourceMap + // The output of the Quint analyzer. + analysisOutput: AnalysisOutput +} + +/* An empty initial compilation state */ +export function newCompilationState(): CompilationState { + return { + idGen: newIdGenerator(), + sourceCode: new Map(), + modules: [], + sourceMap: new Map(), + analysisOutput: { + types: new Map(), + effects: new Map(), + modes: new Map(), + }, + } +} + /** * The internal state of the REPL. */ @@ -64,15 +90,19 @@ class ReplState { lastLoadedFileAndModule: [string?, string?] // The state of pre-compilation phases compilationState: CompilationState - // The state of the compiler visitor - evaluationState: EvaluationState + // The evaluator to be used + evaluator: Evaluator + // The name resolver to be used + nameResolver: NameResolver constructor(verbosityLevel: number, rng: Rng) { + const recorder = newTraceRecorder(verbosityLevel, rng) this.moduleHist = '' this.exprHist = [] this.lastLoadedFileAndModule = [undefined, undefined] this.compilationState = newCompilationState() - this.evaluationState = newEvaluationState(newTraceRecorder(verbosityLevel, rng)) + this.evaluator = new Evaluator(new Map(), recorder, rng) + this.nameResolver = new NameResolver() } clone() { @@ -87,21 +117,24 @@ class ReplState { addReplModule() { const replModule: FlatModule = { name: '__repl__', declarations: simulatorBuiltins(this.compilationState), id: 0n } this.compilationState.modules.push(replModule) - this.compilationState.originalModules.push(replModule) this.compilationState.mainName = '__repl__' this.moduleHist += moduleToString(replModule) } clear() { + const rng = newRng(this.rng.getState()) + const recorder = newTraceRecorder(this.verbosity, rng) + this.moduleHist = '' this.exprHist = [] this.compilationState = newCompilationState() - this.evaluationState = newEvaluationState(newTraceRecorder(this.verbosity, this.rng)) + this.evaluator = new Evaluator(new Map(), recorder, rng) + this.nameResolver = new NameResolver() } get recorder(): TraceRecorder { // ReplState always passes TraceRecorder in the evaluation state - return this.evaluationState.listener as TraceRecorder + return this.evaluator.recorder } get rng(): Rng { @@ -268,7 +301,6 @@ export function quintRepl( state.moduleHist = newState.moduleHist state.exprHist = newState.exprHist state.compilationState = newState.compilationState - state.evaluationState = newState.evaluationState } // the read-eval-print loop @@ -457,9 +489,9 @@ function saveVars(vars: Register[], nextvars: Register[]): Maybe { const nonUpdated = vars.reduce((acc, varRegister) => { const nextVarRegister = nextvars.find(v => v.name === varRegister.name) - if (nextVarRegister && nextVarRegister.registerValue.isJust()) { + if (nextVarRegister && nextVarRegister.registerValue.isRight()) { varRegister.registerValue = nextVarRegister.registerValue - nextVarRegister.registerValue = none() + nextVarRegister.registerValue = left({ code: 'QNT501', message: 'var ${nextVarRegiter.name} not set' }) isAction = true } else { // No nextvar for this variable, so it was not updated @@ -499,35 +531,22 @@ function tryEvalModule(out: writer, state: ReplState, mainName: string): boolean const mainPath = fileSourceResolver(state.compilationState.sourceCode).lookupPath(cwd(), 'repl.ts') state.compilationState.sourceCode.set(mainPath.toSourceName(), modulesText) - const context = compileFromCode( - newIdGenerator(), - modulesText, - mainName, - mainPath, - state.evaluationState.listener, - state.rng.next, - false - ) - if ( - context.evaluationState?.context.size === 0 || - context.compileErrors.length > 0 || - context.syntaxErrors.length > 0 - ) { - const tempState = state.clone() - // The compilation state has updated source code maps, to be used in error reporting - tempState.compilationState = context.compilationState - printErrors(out, tempState, context) - return false - } + // FIXME(#1052): We should build a proper sourceCode map from the files we previously loaded + const sourceCode: Map = new Map() + const idGen = newIdGenerator() + const { modules, table, sourceMap, errors } = parse(idGen, mainPath.toSourceName(), mainPath, modulesText, sourceCode) + // On errors, we'll produce the computational context up to this point + const [analysisErrors, analysisOutput] = analyzeModules(table, modules) + + state.compilationState = { idGen, sourceCode, modules, sourceMap, analysisOutput } - if (context.analysisErrors.length > 0) { - printErrors(out, state, context) - // provisionally, continue on type & effects errors + if (errors.length > 0 || analysisErrors.length > 0) { + printErrorMessages(out, state, 'syntax error', modulesText, errors) + printErrorMessages(out, state, 'static analysis error', modulesText, analysisErrors) + return false } - // Save compilation state - state.compilationState = context.compilationState - state.evaluationState = context.evaluationState + state.evaluator.updateTable(table) return true } @@ -542,6 +561,8 @@ function tryEvalAndClearRecorder(out: writer, state: ReplState, newInput: string // try to evaluate the expression in a string and print it, if successful function tryEval(out: writer, state: ReplState, newInput: string): boolean { + const columns = terminalWidth() + if (state.compilationState.modules.length === 0) { state.addReplModule() tryEvalModule(out, state, '__repl__') @@ -564,66 +585,52 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { } // evaluate the input, depending on its type if (parseResult.kind === 'expr') { - const context = compileExpr(state.compilationState, state.evaluationState, state.rng, false, parseResult.expr) - - if (context.syntaxErrors.length > 0 || context.compileErrors.length > 0 || context.analysisErrors.length > 0) { - printErrors(out, state, context, newInput) - if (context.syntaxErrors.length > 0 || context.compileErrors.length > 0) { - return false - } // else: provisionally, continue on type & effects errors + walkExpression(state.nameResolver, parseResult.expr) + if (state.nameResolver.errors.length > 0) { + printErrorMessages(out, state, 'static analysis error', newInput, state.nameResolver.errors) + return false } + state.evaluator.updateTable(state.nameResolver.table) - state.exprHist.push(newInput.trim()) - // Save the evaluation state only, as state vars changes should persist - state.evaluationState = context.evaluationState - - return evalExpr(state, out) - .mapLeft(msg => { - // when #618 is implemented, we should remove this - printErrorMessages(out, state, 'runtime error', newInput, context.getRuntimeErrors()) - // print the error message produced by the lookup - out(chalk.red(msg)) - out('\n') // be nice to external programs - }) - .isRight() - } - if (parseResult.kind === 'declaration') { - // compile the module and add it to history if everything worked - const context = compileDecls(state.compilationState, state.evaluationState, state.rng, false, parseResult.decls) - - if ( - context.evaluationState.context.size === 0 || - context.compileErrors.length > 0 || - context.syntaxErrors.length > 0 - ) { - printErrors(out, state, context, newInput) + const newEval = state.evaluator.evaluate(parseResult.expr) + if (newEval.isLeft()) { + printErrorMessages(out, state, 'runtime error', newInput, [newEval.value]) return false } - if (context.analysisErrors.length > 0) { - printErrors(out, state, context, newInput) - // provisionally, continue on type & effects errors - } + newEval.map(ex => { + out(format(columns, 0, prettyQuintEx(ex))) + out('\n') - out('\n') // be nice to external programs - state.moduleHist = state.moduleHist.slice(0, state.moduleHist.length - 1) + newInput + '\n}' // update the history + if (ex.kind === 'bool' && ex.value) { + // A Boolean expression may be an action or a run. + // Save the state, if there were any updates to variables. + const missing = state.evaluator.shiftAndCheck() + if (missing.length > 0) { + out(chalk.yellow('[warning] some variables are undefined: ' + missing.join(', ') + '\n')) + } + } + return ex + }) - // Save compilation state - state.compilationState = context.compilationState - state.evaluationState = context.evaluationState + return true + } + if (parseResult.kind === 'declaration') { + parseResult.decls.forEach(decl => { + walkDeclaration(state.nameResolver.collector, decl) + walkDeclaration(state.nameResolver, decl) + }) + if (state.nameResolver.errors.length > 0) { + printErrorMessages(out, state, 'static analysis error', newInput, state.nameResolver.errors) + out('\n') + return false + } + out('\n') } return true } -// output errors to the console in red -function printErrors(out: writer, state: ReplState, context: CompilationContext, newInput: string = '') { - printErrorMessages(out, state, 'syntax error', newInput, context.syntaxErrors) - printErrorMessages(out, state, 'static analysis error', newInput, context.analysisErrors, chalk.yellow) - printErrorMessages(out, state, 'compile error', newInput, context.compileErrors) - out('\n') // be nice to external programs -} - // print error messages with proper colors function printErrorMessages( out: writer, @@ -714,47 +721,6 @@ function countBraces(str: string): [number, number, number] { return [nOpenBraces, nOpenParen, nOpenComments] } -function evalExpr(state: ReplState, out: writer): Either { - const computable = contextNameLookup(state.evaluationState.context, inputDefName, 'callable') - const columns = terminalWidth() - const result = computable.chain(comp => { - return comp - .eval() - .chain(value => { - const ex = value.toQuintEx(state.compilationState.idGen) - out(format(columns, 0, prettyQuintEx(ex))) - out('\n') - - if (ex.kind === 'bool' && ex.value) { - // A Boolean expression may be an action or a run. - // Save the state, if there were any updates to variables. - saveVars(state.evaluationState.vars, state.evaluationState.nextVars).map(missing => { - if (missing.length > 0) { - out(chalk.yellow('[warning] some variables are undefined: ' + missing.join(', ') + '\n')) - } - }) - } - return right(ex) - }) - .mapLeft(e => state.evaluationState.errorTracker.addRuntimeError(e.reference, e)) - .mapLeft(_ => '') - }) - - if (verbosity.hasUserOpTracking(state.verbosity)) { - const trace = state.recorder.currentFrame - if (trace.subframes.length > 0) { - out('\n') - trace.subframes.forEach((f, i) => { - out(`[Frame ${i}]\n`) - printExecutionFrameRec({ width: columns, out }, f, []) - out('\n') - }) - } - } - - return result -} - function getMainModuleAnnotation(moduleHist: string): string | undefined { const moduleName = moduleHist.match(/^\/\/ @mainModule\s+(\w+)\n/) return moduleName?.at(1) diff --git a/quint/src/runtime/compile.ts b/quint/src/runtime/compile.ts deleted file mode 100644 index e37060722..000000000 --- a/quint/src/runtime/compile.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * A compiler to the runtime environment. - * - * Igor Konnov, Informal Systems, 2022-2023 - * - * Copyright 2022-2023 Informal Systems - * Licensed under the Apache License, Version 2.0. - * See LICENSE in the project root for license information. - */ - -import { Either, left, right } from '@sweet-monads/either' -import { SourceMap, parse, parsePhase3importAndNameResolution } from '../parsing/quintParserFrontend' -import { Computable, ComputableKind, kindName } from './runtime' -import { ExecutionListener } from './trace' -import { FlatModule, QuintDeclaration, QuintDef, QuintEx, QuintModule } from '../ir/quintIr' -import { CompilerVisitor } from './impl/compilerImpl' -import { walkDefinition } from '../ir/IRVisitor' -import { LookupTable } from '../names/base' -import { AnalysisOutput, analyzeInc, analyzeModules } from '../quintAnalyzer' -import { IdGenerator, newIdGenerator } from '../idGenerator' -import { SourceLookupPath } from '../parsing/sourceResolver' -import { Rng } from '../rng' -import { flattenModules } from '../flattening/fullFlattener' -import { QuintError } from '../quintError' -import { EvaluationState, newEvaluationState } from './impl/base' - -/** - * The name of the builtin name that returns the last found trace. - */ -export const lastTraceName = 'q::lastTrace' - -/** - * The name of a definition that wraps the user input, e.g., in REPL. - */ -export const inputDefName: string = 'q::input' - -/** - * A compilation context returned by 'compile'. - */ -export interface CompilationContext { - // the lookup table to query for values and definitions - lookupTable: LookupTable - // messages that are produced during parsing - syntaxErrors: QuintError[] - // messages that are produced by static analysis - analysisErrors: QuintError[] - // messages that are produced during compilation - compileErrors: QuintError[] - // messages that get populated as the compiled code is executed - getRuntimeErrors: () => QuintError[] - // The state of pre-compilation phases. - compilationState: CompilationState - // The state of the compiler visitor. - evaluationState: EvaluationState -} - -/** - * A data structure that holds the state of the compilation process. - */ -export interface CompilationState { - // The ID generator used during compilation. - idGen: IdGenerator - // File content loaded for each source, used to report errors - sourceCode: Map - // A list of modules as they are constructed, without flattening. This is - // needed to derive correct name resolution during incremental compilation in - // a flattened context. - originalModules: QuintModule[] - // A list of flattened modules. - modules: FlatModule[] - // The name of the main module. - mainName?: string - // The source map for the compiled code. - sourceMap: SourceMap - // The output of the Quint analyzer. - analysisOutput: AnalysisOutput -} - -/* An empty initial compilation state */ -export function newCompilationState(): CompilationState { - return { - idGen: newIdGenerator(), - sourceCode: new Map(), - originalModules: [], - modules: [], - sourceMap: new Map(), - analysisOutput: { - types: new Map(), - effects: new Map(), - modes: new Map(), - }, - } -} - -export function errorContextFromMessage( - listener: ExecutionListener -): (_: { errors: QuintError[]; sourceMap: SourceMap }) => CompilationContext { - const evaluationState = newEvaluationState(listener) - return ({ errors, sourceMap }) => { - return { - lookupTable: new Map(), - syntaxErrors: errors, - analysisErrors: [], - compileErrors: [], - getRuntimeErrors: () => [], - compilationState: { ...newCompilationState(), sourceMap }, - evaluationState, - } - } -} - -export function contextNameLookup( - context: Map, - defName: string, - kind: ComputableKind -): Either { - const value = context.get(kindName(kind, defName)) - if (!value) { - console.log(`key = ${kindName(kind, defName)}`) - return left(`No value for definition ${defName}`) - } else { - return right(value) - } -} - -/** - * Compile Quint defs to JS runtime objects from the parsed and type-checked - * data structures. This is a user-facing function. In case of an error, the - * error messages are passed to an error handler and the function returns - * undefined. - * - * @param compilationState the state of the compilation process - * @param evaluationState the state of the compiler visitor - * @param lookupTable lookup table as produced by the parser - * @param rand the random number generator - * @param storeMetadata whether to store metadata in the trace states - * @param defs the definitions to compile - * @returns the compilation context - */ -export function compile( - compilationState: CompilationState, - evaluationState: EvaluationState, - lookupTable: LookupTable, - rand: (bound: bigint) => bigint, - storeMetadata: boolean, - defs: QuintDef[] -): CompilationContext { - const visitor = new CompilerVisitor(lookupTable, rand, evaluationState, storeMetadata) - - defs.forEach(def => walkDefinition(visitor, def)) - - return { - lookupTable, - syntaxErrors: [], - analysisErrors: [], - compileErrors: visitor.getCompileErrors(), - getRuntimeErrors: () => { - return visitor.getRuntimeErrors().splice(0) - }, - compilationState, - evaluationState: visitor.getEvaluationState(), - } -} - -/** - * Compile a single Quint expression, given a non-empty compilation and - * evaluation state. That is, those states should have the results of the - * compilation of at least one module. - * - * @param state - The current compilation state - * @param evaluationState - The current evaluation state - * @param rng - The random number generator - * @param storeMetadata - whether to store metadata in the trace states - * @param expr - The Quint exporession to be compiled - * - * @returns A compilation context with the compiled expression or its errors - */ -export function compileExpr( - state: CompilationState, - evaluationState: EvaluationState, - rng: Rng, - storeMetadata: boolean, - expr: QuintEx -): CompilationContext { - // Create a definition to encapsulate the parsed expression. - // Note that the expression may contain nested definitions. - // Hence, we have to compile it via an auxilliary definition. - const def: QuintDef = { kind: 'def', qualifier: 'action', name: inputDefName, expr, id: state.idGen.nextId() } - - return compileDecls(state, evaluationState, rng, storeMetadata, [def]) -} - -/** - * Compile a single Quint definition, given a non-empty compilation and - * evaluation state. That is, those states should have the results of the - * compilation of at least one module. - * - * @param state - The current compilation state - * @param evaluationState - The current evaluation state - * @param rng - The random number generator - * @param storeMetadata - whether to store metadata in the trace states - * @param decls - The Quint declarations to be compiled - * - * @returns A compilation context with the compiled definition or its errors - */ -export function compileDecls( - state: CompilationState, - evaluationState: EvaluationState, - rng: Rng, - storeMetadata: boolean, - decls: QuintDeclaration[] -): CompilationContext { - if (state.originalModules.length === 0 || state.modules.length === 0) { - throw new Error('No modules in state') - } - - // Define a new module list with the new definition in the main module, - // ensuring the original object is not modified - const originalModules = state.originalModules.map(m => { - if (m.name === state.mainName) { - return { ...m, declarations: [...m.declarations, ...decls] } - } - return m - }) - - const mainModule = state.modules.find(m => m.name === state.mainName)! - - // We need to resolve names for this new definition. Incremental name - // resolution is not our focus now, so just resolve everything again. - const { table, errors } = parsePhase3importAndNameResolution({ - modules: originalModules, - sourceMap: state.sourceMap, - errors: [], - }) - - if (errors.length > 0) { - // For now, don't try to run analysis and flattening if there are errors - return errorContextFromMessage(evaluationState.listener)({ errors, sourceMap: state.sourceMap }) - } - - const [analysisErrors, analysisOutput] = analyzeInc(state.analysisOutput, table, decls) - - const { - flattenedModules: flatModules, - flattenedTable, - flattenedAnalysis, - } = flattenModules(originalModules, table, state.idGen, state.sourceMap, analysisOutput) - - const newState = { - ...state, - analysisOutput: flattenedAnalysis, - modules: flatModules, - originalModules: originalModules, - } - - const flatDefinitions = flatModules.find(m => m.name === state.mainName)!.declarations - - // Filter definitions that were not compiled yet - const defsToCompile = flatDefinitions.filter(d => !mainModule.declarations.some(d2 => d2.id === d.id)) - - const ctx = compile(newState, evaluationState, flattenedTable, rng.next, storeMetadata, defsToCompile) - - return { ...ctx, analysisErrors } -} - -/** - * Parse a string that contains Quint modules and compile it to executable - * objects. This is a user-facing function. In case of an error, the error - * messages are passed to an error handler and the function returns undefined. - * - * @param code text that stores one or several Quint modules, - * which should be parseable without any context - * @param mainName the name of the module that may contain state varibles - * @param execListener execution listener - * @param rand the random number generator - * @param storeMetadata whether to store metadata in the trace states - * @returns the compilation context - */ -export function compileFromCode( - idGen: IdGenerator, - code: string, - mainName: string, - mainPath: SourceLookupPath, - execListener: ExecutionListener, - rand: (bound: bigint) => bigint, - storeMetadata: boolean -): CompilationContext { - // parse the module text - // FIXME(#1052): We should build a proper sourceCode map from the files we previously loaded - const sourceCode: Map = new Map() - const { modules, table, sourceMap, errors } = parse(idGen, mainPath.toSourceName(), mainPath, code, sourceCode) - // On errors, we'll produce the computational context up to this point - const [analysisErrors, analysisOutput] = analyzeModules(table, modules) - - const { flattenedModules, flattenedTable, flattenedAnalysis } = flattenModules( - modules, - table, - idGen, - sourceMap, - analysisOutput - ) - const compilationState: CompilationState = { - originalModules: modules, - modules: flattenedModules, - mainName, - sourceMap, - analysisOutput: flattenedAnalysis, - idGen, - sourceCode, - } - - const main = flattenedModules.find(m => m.name === mainName) - // when the main module is not found, we will report an error - const mainNotFoundError: QuintError[] = main - ? [] - : [ - { - code: 'QNT405', - message: `Main module ${mainName} not found`, - }, - ] - const defsToCompile = main ? main.declarations : [] - const ctx = compile( - compilationState, - newEvaluationState(execListener), - flattenedTable, - rand, - storeMetadata, - defsToCompile - ) - - return { - ...ctx, - compileErrors: ctx.compileErrors.concat(mainNotFoundError), - analysisErrors, - syntaxErrors: errors, - } -} diff --git a/quint/src/runtime/impl/base.ts b/quint/src/runtime/impl/base.ts index aea0e74df..e3f7db18c 100644 --- a/quint/src/runtime/impl/base.ts +++ b/quint/src/runtime/impl/base.ts @@ -14,50 +14,15 @@ import { Maybe, just, none } from '@sweet-monads/maybe' import { ErrorCode, QuintError } from '../../quintError' -import { Computable, EvaluationResult, Register, kindName, mkCallable } from '../runtime' -import { ExecutionListener } from '../trace' -import { Trace } from './trace' +import { EvaluationResult } from '../runtime' import { Either, right } from '@sweet-monads/either' -import { RuntimeValue, rv } from './runtimeValue' +import { RuntimeValue } from './runtimeValue' // Internal names in the compiler, which have special treatment. // For some reason, if we replace 'q::input' with inputDefName, everything breaks. // What kind of JS magic is that? export const specialNames = ['q::input', 'q::runResult', 'q::nruns', 'q::nsteps', 'q::init', 'q::next', 'q::inv'] -/** - * Returns a Map containing the built-in Computable objects for the Quint language. - * These include the callable objects for Bool, Int, and Nat. - * - * @returns a Map containing the built-in Computable objects. - */ -export function builtinContext() { - return new Map([ - [kindName('callable', 'Bool'), mkCallable([], mkConstComputable(rv.mkSet([rv.mkBool(false), rv.mkBool(true)])))], - [kindName('callable', 'Int'), mkCallable([], mkConstComputable(rv.mkInfSet('Int')))], - [kindName('callable', 'Nat'), mkCallable([], mkConstComputable(rv.mkInfSet('Nat')))], - ]) -} - -/** - * Represents the state of evaluation of Quint code. - * All the fields are mutated by CompilerVisitor, either directly, or via calls. - */ -export interface EvaluationState { - // The context of the evaluation, containing the Computable objects. - context: Map - // The list of variables in the current state. - vars: Register[] - // The list of variables in the next state. - nextVars: Register[] - // The current trace of states - trace: Trace - // The error tracker for the evaluation to store errors on callbacks. - errorTracker: CompilerErrorTracker - // The execution listener that the compiled code uses to report execution info. - listener: ExecutionListener -} - /** * Creates a new EvaluationState object. * @@ -74,26 +39,8 @@ export class CompilerErrorTracker { } addRuntimeError(reference: bigint | undefined, error: QuintError) { - this.runtimeErrors.push({ ...error, reference }) - } -} - -/** - * Creates a new EvaluationState object with the initial state of the evaluation. - * - * @returns a new EvaluationState object - */ -export function newEvaluationState(listener: ExecutionListener): EvaluationState { - const state: EvaluationState = { - context: builtinContext(), - vars: [], - nextVars: [], - trace: new Trace(), - errorTracker: new CompilerErrorTracker(), - listener: listener, + this.runtimeErrors.push({ ...error, reference: error.reference ?? reference }) } - - return state } export function toMaybe(r: Either): Maybe { diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts new file mode 100644 index 000000000..dd4240ca4 --- /dev/null +++ b/quint/src/runtime/impl/builtins.ts @@ -0,0 +1,562 @@ +import { Either, left, mergeInMany, right } from '@sweet-monads/either' +import { QuintError, quintErrorToString } from '../../quintError' +import { Map, is, Set, Range, List } from 'immutable' +import { evaluateExpr, evaluateUnderDefContext, isTrue } from './evaluator' +import { Context } from './Context' +import { RuntimeValue, rv } from './runtimeValue' +import { chunk } from 'lodash' +import { expressionToString } from '../../ir/IRprinting' +import { zerog } from '../../idGenerator' +import { QuintEx } from '../../ir/quintIr' + +export function builtinValue(name: string): Either { + switch (name) { + case 'Bool': + return right(rv.mkSet([rv.mkBool(false), rv.mkBool(true)])) + case 'Int': + return right(rv.mkInfSet('Int')) + case 'Nat': + return right(rv.mkInfSet('Nat')) + default: + return left({ code: 'QNT404', message: `Unknown builtin ${name}` }) + } +} + +export const lazyOps = [ + 'assign', + 'actionAny', + 'actionAll', + 'ite', + 'matchVariant', + 'oneOf', + 'and', + 'or', + 'next', + 'implies', + 'then', +] + +export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) => Either { + switch (op) { + case 'and': + return args => { + return args.reduce((acc: Either, arg: QuintEx) => { + return acc.chain(accValue => { + if (accValue.toBool() === true) { + return evaluateExpr(ctx, arg) + } + return acc + }) + }, right(rv.mkBool(true))) + } + case 'or': + return args => { + return args.reduce((acc: Either, arg: QuintEx) => { + return acc.chain(accValue => { + if (accValue.toBool() === false) { + return evaluateExpr(ctx, arg) + } + return acc + }) + }, right(rv.mkBool(false))) + } + + case 'implies': + return args => { + return evaluateExpr(ctx, args[0]).chain(l => { + if (!l.toBool()) { + return right(rv.mkBool(true)) + } + + return evaluateExpr(ctx, args[1]) + }) + } + + case 'assign': + return args => { + const varDef = ctx.table.get(args[0].id)! + // Eval var just to make sure it is registered in the storage + evaluateExpr(ctx, args[0]) + + return evaluateUnderDefContext(ctx, varDef, () => { + return evaluateExpr(ctx, args[1]).map(value => { + ctx.setNextVar(varDef.id, value) + return rv.mkBool(true) + }) + }) + } + case 'actionAny': + return args => { + const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() + const evaluationResults = args.map(arg => { + const result = evaluateExpr(ctx, arg).map(result => { + // Save vars + const successor = ctx.varStorage.nextVarsSnapshot() + + return result.toBool() ? [successor] : [] + }) + + // Recover snapshot (regardless of success or failure) + ctx.varStorage.nextVars = nextVarsSnapshot + + return result + }) + + const processedResults = mergeInMany(evaluationResults) + .map(suc => suc.flat()) + .mapLeft(errors => errors[0]) + + return processedResults.map(potentialSuccessors => { + switch (potentialSuccessors.length) { + case 0: + return rv.mkBool(false) + case 1: + ctx.varStorage.nextVars = potentialSuccessors[0] + return rv.mkBool(true) + default: + const choice = Number(ctx.rand(BigInt(potentialSuccessors.length))) + ctx.varStorage.nextVars = potentialSuccessors[choice] + return rv.mkBool(true) + } + }) + } + case 'actionAll': + return args => { + const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() + for (const action of args) { + const result = evaluateExpr(ctx, action) + if (!isTrue(result)) { + ctx.varStorage.nextVars = nextVarsSnapshot + return result.map(_ => rv.mkBool(false)) + } + } + + return right(rv.mkBool(true)) + } + case 'ite': + return args => { + return evaluateExpr(ctx, args[0]).chain(condition => { + return condition.toBool() ? evaluateExpr(ctx, args[1]) : evaluateExpr(ctx, args[2]) + }) + } + + case 'matchVariant': + return args => { + const matchedEx = args[0] + return evaluateExpr(ctx, matchedEx).chain(expr => { + const [label, value] = expr.toVariant() + + const cases = args.slice(1) + + const caseForVariant = chunk(cases, 2).find( + ([caseLabel, _caseElim]) => + rv.fromQuintEx(caseLabel).toStr() === '_' || rv.fromQuintEx(caseLabel).toStr() === label + ) + if (!caseForVariant) { + return left({ code: 'QNT505', message: `No match for variant ${label}` }) + } + + const [_caseLabel, caseElim] = caseForVariant + const elim = rv.fromQuintEx(caseElim).toArrow(ctx) + return elim([value]) + }) + } + + case 'oneOf': + return args => { + return evaluateExpr(ctx, args[0]).chain(set => { + const bounds = set.bounds() + const positions: Either = mergeInMany( + bounds.map((b): Either => { + if (b.isJust()) { + const sz = b.value + + if (sz === 0n) { + return left({ code: 'QNT509', message: `Applied oneOf to an empty set` }) + } + return right(ctx.rand(sz)) + } else { + // An infinite set, pick an integer from the range [-2^255, 2^255). + // Note that pick on Nat uses the absolute value of the passed integer. + // TODO: make it a configurable parameter: + // https://github.com/informalsystems/quint/issues/279 + return right(-(2n ** 255n) + ctx.rand(2n ** 256n)) + } + }) + ).mapLeft(errors => errors[0]) + + return positions.chain(ps => set.pick(ps.values())) + }) + } + case 'then': + default: + return () => left({ code: 'QNT000', message: 'Unknown stateful op' }) + } +} + +export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) => Either { + switch (op) { + case 'Set': + return args => right(rv.mkSet(args)) + case 'Rec': + return args => right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) + case 'List': + return args => right(rv.mkList(List(args))) + case 'Tup': + return args => right(rv.mkTuple(List(args))) + case 'Map': + return args => right(rv.mkMap(args.map(kv => kv.toTuple2()))) + case 'variant': + return args => right(rv.mkVariant(args[0].toStr(), args[1])) + case 'not': + return args => right(rv.mkBool(!args[0].toBool())) + case 'iff': + return args => right(rv.mkBool(args[0].toBool() === args[1].toBool())) + case 'eq': + return args => right(rv.mkBool(args[0].equals(args[1]))) + case 'neq': + return args => right(rv.mkBool(!args[0].equals(args[1]))) + case 'iadd': + return args => right(rv.mkInt(args[0].toInt() + args[1].toInt())) + case 'isub': + return args => right(rv.mkInt(args[0].toInt() - args[1].toInt())) + case 'imul': + return args => right(rv.mkInt(args[0].toInt() * args[1].toInt())) + case 'idiv': + return args => { + const divisor = args[1].toInt() + if (divisor === 0n) { + return left({ code: 'QNT503', message: `Division by zero` }) + } + return right(rv.mkInt(args[0].toInt() / divisor)) + } + case 'imod': + return args => right(rv.mkInt(args[0].toInt() % args[1].toInt())) + case 'ipow': + return args => { + const base = args[0].toInt() + const exp = args[1].toInt() + if (base === 0n && exp === 0n) { + return left({ code: 'QNT503', message: `0^0 is undefined` }) + } + if (exp < 0n) { + return left({ code: 'QNT503', message: 'i^j is undefined for j < 0' }) + } + + return right(rv.mkInt(base ** exp)) + } + case 'iuminus': + return args => right(rv.mkInt(-args[0].toInt())) + case 'ilt': + return args => right(rv.mkBool(args[0].toInt() < args[1].toInt())) + case 'ilte': + return args => right(rv.mkBool(args[0].toInt() <= args[1].toInt())) + case 'igt': + return args => right(rv.mkBool(args[0].toInt() > args[1].toInt())) + case 'igte': + return args => right(rv.mkBool(args[0].toInt() >= args[1].toInt())) + + case 'item': + return args => { + // Access a tuple: tuples are 1-indexed, that is, _1, _2, etc. + return getListElem(args[0].toList(), Number(args[1].toInt()) - 1) + } + case 'tuples': + return args => right(rv.mkCrossProd(args)) + + case 'range': + return args => { + const start = Number(args[0].toInt()) + const end = Number(args[1].toInt()) + return right(rv.mkList(List(Range(start, end).map(rv.mkInt)))) + } + + case 'nth': + return args => getListElem(args[0].toList(), Number(args[1].toInt())) + + case 'replaceAt': + return args => { + const list = args[0].toList() + const idx = Number(args[1].toInt()) + if (idx < 0 || idx >= list.size) { + return left({ code: 'QNT510', message: `Out of bounds, replaceAt(${idx})` }) + } + + return right(rv.mkList(list.set(idx, args[2]))) + } + + case 'head': + return args => { + const list = args[0].toList() + if (list.size === 0) { + return left({ code: 'QNT505', message: `Called 'head' on an empty list` }) + } + return right(list.first()!) + } + + case 'tail': + return args => { + const list = args[0].toList() + if (list.size === 0) { + return left({ code: 'QNT505', message: `Called 'tail' on an empty list` }) + } + return right(rv.mkList(list.rest())) + } + + case 'slice': + return args => { + const list = args[0].toList() + const start = Number(args[1].toInt()) + const end = Number(args[2].toInt()) + if (start < 0 || start > end || end > list.size) { + return left({ + code: 'QNT506', + message: `slice(..., ${start}, ${end}) applied to a list of size ${list.size}`, + }) + } + + return right(rv.mkList(list.slice(start, end))) + } + + case 'length': + return args => right(rv.mkInt(args[0].toList().size)) + case 'append': + return args => right(rv.mkList(args[0].toList().push(args[1]))) + case 'concat': + return args => right(rv.mkList(args[0].toList().concat(args[1].toList()))) + case 'indices': + return args => right(rv.mkInterval(0n, args[0].toList().size - 1)) + + case 'field': + return args => { + const field = args[1].toStr() + const result = args[0].toOrderedMap().get(field) + return result ? right(result) : left({ code: 'QNT501', message: `Accessing a missing record field ${field}` }) + } + + case 'fieldNames': + return args => right(rv.mkSet(args[0].toOrderedMap().keySeq().map(rv.mkStr))) + + case 'with': + return args => { + const record = args[0].toOrderedMap() + const field = args[1].toStr() + const value = args[2] + + if (!record.has(field)) { + return left({ code: 'QNT501', message: `Called 'with' with a non-existent field ${field}` }) + } + + return right(rv.mkRecord(record.set(field, value))) + } + + case 'powerset': + return args => right(rv.mkPowerset(args[0])) + case 'contains': + return args => right(rv.mkBool(args[0].contains(args[1]))) + case 'in': + return args => right(rv.mkBool(args[1].contains(args[0]))) + case 'subseteq': + return args => right(rv.mkBool(args[0].isSubset(args[1]))) + case 'exclude': + return args => right(rv.mkSet(args[0].toSet().subtract(args[1].toSet()))) + case 'union': + return args => right(rv.mkSet(args[0].toSet().union(args[1].toSet()))) + case 'intersect': + return args => right(rv.mkSet(args[0].toSet().intersect(args[1].toSet()))) + case 'size': + return args => args[0].cardinality().map(rv.mkInt) + case 'isFinite': + // at the moment, we support only finite sets, so just return true + return _args => right(rv.mkBool(true)) + + case 'to': + return args => right(rv.mkInterval(args[0].toInt(), args[1].toInt())) + case 'fold': + return args => applyFold('fwd', args[0].toSet(), args[1], args[2].toArrow(ctx)) + case 'foldl': + return args => applyFold('fwd', args[0].toList(), args[1], args[2].toArrow(ctx)) + case 'foldr': + return args => applyFold('rev', args[0].toList(), args[1], args[2].toArrow(ctx)) + + case 'flatten': + return args => { + const s = args[0].toSet().map(s => s.toSet()) + return right(rv.mkSet(s.flatten(1) as Set)) + } + + case 'get': + return args => { + const map = args[0].toMap() + const key = args[1].normalForm() + const value = map.get(key) + return value + ? right(value) + : left({ + code: 'QNT507', + message: `Called 'get' with a non-existing key. Key is ${expressionToString( + key.toQuintEx(zerog) + )}. Map has keys: ${map + .toMap() + .keySeq() + .map(k => expressionToString(k.toQuintEx(zerog))) + .join(', ')}`, + }) + } + + case 'set': + return args => { + const map = args[0].toMap() + const key = args[1].normalForm() + if (!map.has(key)) { + return left({ code: 'QNT507', message: "Called 'set' with a non-existing key" }) + } + const value = args[2] + return right(rv.fromMap(map.set(key, value))) + } + + case 'put': + return args => { + const map = args[0].toMap() + const key = args[1].normalForm() + const value = args[2] + return right(rv.fromMap(map.set(key, value))) + } + + case 'setBy': + return args => { + const map = args[0].toMap() + const key = args[1].normalForm() + if (!map.has(key)) { + return left({ code: 'QNT507', message: `Called 'setBy' with a non-existing key ${key}` }) + } + + const value = map.get(key)! + const lam = args[2].toArrow(ctx) + return lam([value]).map(v => rv.fromMap(map.set(key, v))) + } + + case 'keys': + return args => right(rv.mkSet(args[0].toMap().keys())) + + case 'exists': + return args => + applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.some(v => v.toBool()) === true)) + + case 'forall': + return args => + applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.every(v => v.toBool()) === true)) + + case 'map': + return args => { + return applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkSet(values)) + } + + case 'filter': + return args => { + const set = args[0].toSet() + const lam = args[1].toArrow(ctx) + const reducer = ([acc, arg]: RuntimeValue[]) => + lam([arg]).map(condition => (condition.toBool() === true ? rv.mkSet(acc.toSet().add(arg.normalForm())) : acc)) + + return applyFold('fwd', set, rv.mkSet([]), reducer) + } + + case 'select': + return args => { + const list = args[0].toList() + const lam = args[1].toArrow(ctx) + const reducer = ([acc, arg]: RuntimeValue[]) => + lam([arg]).map(condition => (condition.toBool() === true ? rv.mkList(acc.toList().push(arg)) : acc)) + + return applyFold('fwd', list, rv.mkList([]), reducer) + } + + case 'mapBy': + return args => { + const lambda = args[1].toArrow(ctx) + const keys = args[0].toSet() + const reducer = ([acc, arg]: RuntimeValue[]) => + lambda([arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) + + return applyFold('fwd', keys, rv.mkMap([]), reducer) + } + + case 'setToMap': + return args => { + const set = args[0].toSet() + return right(rv.mkMap(Map(set.map(s => s.toTuple2())))) + } + + case 'setOfMaps': + return args => right(rv.mkMapSet(args[0], args[1])) + + case 'fail': + return args => right(rv.mkBool(!args[0].toBool())) + case 'assert': + return args => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT502', message: `Assertion failed` })) + case 'expect': + case 'reps': + + default: + return () => left({ code: 'QNT000', message: `Unknown builtin ${op}` }) + } +} +export function applyLambdaToSet( + ctx: Context, + lambda: RuntimeValue, + set: RuntimeValue +): Either> { + const f = lambda.toArrow(ctx) + const results = set.toSet().map(value => f([value])) + const err = results.find(result => result.isLeft()) + if (err !== undefined && err.isLeft()) { + return left(err.value) + } + + return right( + results.map(result => { + if (result.isLeft()) { + throw new Error(`Impossible, result is left: ${quintErrorToString(result.value)}`) + } + + return result.value + }) + ) +} + +function applyFold( + order: 'fwd' | 'rev', + iterable: Iterable, + initial: RuntimeValue, + lambda: (args: RuntimeValue[]) => Either +): Either { + const reducer = (acc: Either, val: RuntimeValue) => { + return acc.chain(accValue => { + if (order === 'fwd') { + return lambda([accValue, val]) + } else { + return lambda([val, accValue]) + } + }) + } + + const array = Array.from(iterable) + if (order === 'fwd') { + return array.reduce(reducer, right(initial)) + } else { + return array.reduceRight(reducer, right(initial)) + } +} + +// Access a list via an index +function getListElem(list: List, idx: number): Either { + if (idx >= 0n && idx < list.size) { + const elem = list.get(Number(idx)) + if (elem) { + return right(elem) + } + } + + return left({ code: 'QNT510', message: `Out of bounds, nth(${idx})` }) +} diff --git a/quint/src/runtime/impl/compilerImpl.ts b/quint/src/runtime/impl/compilerImpl.ts deleted file mode 100644 index e825ed166..000000000 --- a/quint/src/runtime/impl/compilerImpl.ts +++ /dev/null @@ -1,1630 +0,0 @@ -/* - * Compiler of Quint expressions and definitions to Computable values - * that can be evaluated in the runtime. - * - * Igor Konnov, Gabriela Moreira, 2022-2024 - * - * Copyright 2022-2024 Informal Systems - * Licensed under the Apache License, Version 2.0. - * See LICENSE in the project root for license information. - */ - -import { strict as assert } from 'assert' -import { Maybe, just, none } from '@sweet-monads/maybe' -import { OrderedMap, Set } from 'immutable' - -import { LookupTable } from '../../names/base' -import { IRVisitor } from '../../ir/IRVisitor' -import { - Callable, - Computable, - ComputableKind, - EvaluationResult, - Register, - fail, - kindName, - mkCallable, - mkRegister, -} from '../runtime' - -import { ExecutionListener } from '../trace' - -import * as ir from '../../ir/quintIr' - -import { RuntimeValue, RuntimeValueLambda, RuntimeValueVariant, rv } from './runtimeValue' -import { Trace } from './trace' -import { ErrorCode, QuintError, quintErrorToString } from '../../quintError' - -import { inputDefName, lastTraceName } from '../compile' -import { prettyQuintEx, terminalWidth } from '../../graphics' -import { format } from '../../prettierimp' -import { unreachable } from '../../util' -import { zerog } from '../../idGenerator' -import { chunk, times } from 'lodash' -import { Either, left, mergeInMany, right } from '@sweet-monads/either' -import { applyBoolOp, applyFold, applyFun, getListElem, mapLambdaThenReduce } from './operatorEvaluator' -import { - CompilerErrorTracker, - EvaluationState, - mkConstComputable, - mkFunComputable, - specialNames, - toMaybe, -} from './base' -import { updateList } from './operatorEvaluator' -import { sliceList } from './operatorEvaluator' - -/** - * Compiler visitor turns Quint definitions and expressions into Computable - * objects, essentially, lazy JavaScript objects. Importantly, it does not do - * any evaluation during the translation and thus delegates the actual - * computation to the JavaScript engine. Since many of Quint operators may be - * computationally expensive, it is crucial to maintain this separation of - * compilation vs. computation. - * - * This class does not do any dynamic type checking, assuming that the type - * checker will be run before the translation in the future. As we do not have - * the type checker yet, computations may fail with weird JavaScript errors. - */ -export class CompilerVisitor implements IRVisitor { - // the lookup table to use for the module - private lookupTable: LookupTable - // the stack of computable values - private compStack: Computable[] = [] - // The map of identifiers (and sometimes, names) to their compiled values: - // - wrappers around RuntimeValue - // - an instance of Register - // - an instance of Callable. - // The keys should be constructed via `kindName`. - context: Map - - // all variables declared during compilation - private vars: Register[] - // the registers allocated for the next-state values of vars - private nextVars: Register[] - // keeps errors in a state - private errorTracker: CompilerErrorTracker - // pre-initialized random number generator - private rand - // execution listener - private execListener: ExecutionListener - // a tracker for the current execution trace - private trace: Trace - - // whether to track `actionTaken` and `nondetPicks` - private storeMetadata: boolean - // the chosen action in the last `any` evaluation - private actionTaken: Maybe = none() - // a record with nondet definition names as fields and their last chosen value as values - private nondetPicks: RuntimeValue // initialized at constructor - - // the current depth of operator definitions: top-level defs are depth 0 - // FIXME(#1279): The walk* functions update this value, but they need to be - // initialized to -1 here for that to work on all scenarios. - definitionDepth: number = -1 - - constructor( - lookupTable: LookupTable, - rand: (bound: bigint) => bigint, - evaluationState: EvaluationState, - storeMetadata: boolean - ) { - this.lookupTable = lookupTable - this.rand = rand - this.storeMetadata = storeMetadata - - this.context = evaluationState.context - this.vars = evaluationState.vars - this.nextVars = evaluationState.nextVars - this.errorTracker = evaluationState.errorTracker - this.execListener = evaluationState.listener - this.trace = evaluationState.trace - - this.nondetPicks = this.emptyNondetPicks() - } - - /** - * Get the compiler state. - */ - getEvaluationState(): EvaluationState { - return { - context: this.context, - vars: this.vars, - nextVars: this.nextVars, - errorTracker: this.errorTracker, - listener: this.execListener, - trace: this.trace, - } - } - - /** - * Get the names of the compiled variables. - */ - getVars(): string[] { - return this.vars.map(r => r.name) - } - - /** - * Get the array of compile errors, which changes as the code gets executed. - */ - getCompileErrors(): QuintError[] { - return this.errorTracker.compileErrors - } - - /** - * Get the array of runtime errors, which changes as the code gets executed. - */ - getRuntimeErrors(): QuintError[] { - return this.errorTracker.runtimeErrors - } - - exitOpDef(opdef: ir.QuintOpDef) { - // Either a runtime value, or a def, action, etc. - // All of them are compiled to callables, which may have zero parameters. - let boundValue = this.compStack.pop() as Callable - if (boundValue === undefined) { - this.errorTracker.addCompileError(opdef.id, 'QNT501', `No expression for ${opdef.name} on compStack`) - return - } - - if (opdef.qualifier === 'action' && opdef.expr.kind !== 'lambda') { - // A nullary action like `init` or `step`. - // It is not handled via applyUserDefined. - // Wrap this value with the listener calls. - // Importantly, we do not touch the original boundValue, but decorate it. - // Consider the following definitions: - // action input1 = step - // action input2 = step - // - // Both input1 and input2 wrap step, but in their individual computables. - const unwrappedValue = boundValue - const app: ir.QuintApp = { id: opdef.id, kind: 'app', opcode: opdef.name, args: [] } - const evalApp: ir.QuintApp = { id: 0n, kind: 'app', opcode: '_', args: [app] } - boundValue = { - eval: () => { - if (this.actionTaken.isNone()) { - this.actionTaken = just(rv.mkStr(opdef.name)) - } - - if (app.opcode === inputDefName) { - this.execListener.onUserOperatorCall(evalApp) - // do not call onUserOperatorReturn on '_' later, as it may span over multiple frames - } else { - this.execListener.onUserOperatorCall(app) - } - const r = unwrappedValue.eval() - this.execListener.onUserOperatorReturn(app, [], toMaybe(r)) - return r - }, - nparams: unwrappedValue.nparams, - } - } - - if (this.definitionDepth === 0 && opdef.qualifier === 'pureval') { - // a pure value may be cached, once evaluated - const originalEval = boundValue.eval - let cache: EvaluationResult | undefined = undefined - boundValue.eval = () => { - if (cache !== undefined) { - return cache - } else { - cache = originalEval() - return cache - } - } - } - - if (opdef.qualifier === 'action' && opdef.expr.kind === 'lambda') { - const unwrappedValue = boundValue - boundValue = { - eval: (args?: Either[]) => { - if (this.actionTaken.isNone()) { - this.actionTaken = just(rv.mkStr(opdef.name)) - } - - return unwrappedValue.eval(args) - }, - nparams: unwrappedValue.nparams, - } - } - - const kname = kindName('callable', opdef.id) - // bind the callable from the stack - this.context.set(kname, boundValue) - - if (specialNames.includes(opdef.name)) { - // bind the callable under its name as well - this.context.set(kindName('callable', opdef.name), boundValue) - } - } - - exitLet(letDef: ir.QuintLet) { - // When dealing with a `val` or `nondet`, freeze the callable under - // the let definition. Otherwise, forms like 'nondet x = oneOf(S); A' - // may produce multiple values for the same name 'x' - // inside a single evaluation of A. - // In case of `val`, this is simply an optimization. - const qualifier = letDef.opdef.qualifier - if (qualifier !== 'val' && qualifier !== 'nondet') { - // a non-constant value, ignore - return - } - - // get the expression that is evaluated in the context of let. - const exprUnderLet = this.compStack.slice(-1).pop() - if (exprUnderLet === undefined) { - this.errorTracker.addCompileError( - letDef.opdef.id, - 'QNT501', - `No expression for ${letDef.opdef.name} on compStack` - ) - return - } - - const kname = kindName('callable', letDef.opdef.id) - const boundValue = this.context.get(kname) ?? fail - // Override the behavior of the expression under let: - // It precomputes the bound value and uses it in the evaluation. - // Once the evaluation is done, the value is reset, so that - // a new random value may be produced later. - const undecoratedEval = exprUnderLet.eval - const boundValueEval = boundValue.eval - exprUnderLet.eval = () => { - const cachedValue = boundValueEval() - boundValue.eval = function () { - return cachedValue - } - // compute the result and immediately reset the cache - const result = undecoratedEval() - - // Store the nondet picks after evaluation as we want to collect them while we move up the IR tree, - // to make sure all nondet values in scenarios like this are collected: - // action step = any { - // someAction, - // nondet time = oneOf(times) - // timeRelatedAction(time) - // } - // - // action timeRelatedAction(time) = any { AdvanceTime(time), RevertTime(time) } - - if (result.isRight() && qualifier === 'nondet') { - // A nondet value was just defined, save it in the nondetPicks record. - const value = rv.mkVariant('Some', cachedValue.value as RuntimeValue) - this.nondetPicks = rv.mkRecord(this.nondetPicks.toOrderedMap().set(letDef.opdef.name, value)) - } - boundValue.eval = boundValueEval - return result - } - } - - exitConst(cdef: ir.QuintConst) { - // all constants should be instantiated before running the simulator - const code: ErrorCode = 'QNT500' - const msg = `Uninitialized const ${cdef.name}. Use: import (${cdef.name}=).*` - this.errorTracker.addCompileError(cdef.id, code, msg) - } - - exitVar(vardef: ir.QuintVar) { - const varName = vardef.name - - // In the process of incremental compilation, we might revisit the same var - // definition. Don't overwrite the register if that happens. In some cases - // (with instances), the variable can have a different ID, but the same - // name. In that case, we assign the register with that name for the new ID. - if (this.context.has(kindName('var', varName))) { - const register = this.context.get(kindName('var', varName))! - this.context.set(kindName('var', vardef.id), register) - - if (this.context.has(kindName('nextvar', varName))) { - const register = this.context.get(kindName('nextvar', varName))! - this.context.set(kindName('nextvar', vardef.id), register) - } - - return - } - - // simply introduce two registers: - // one for the variable, and - // one for its next-state version - const prevRegister = mkRegister('var', varName, none(), { - code: 'QNT502', - message: `Variable ${varName} is not set`, - reference: vardef.id, - }) - this.vars.push(prevRegister) - // at the moment, we have to refer to variables both via id and name - this.context.set(kindName('var', varName), prevRegister) - this.context.set(kindName('var', vardef.id), prevRegister) - const nextRegister = mkRegister('nextvar', varName, none(), { - code: 'QNT502', - message: `${varName}' is not set`, - reference: vardef.id, - }) - this.nextVars.push(nextRegister) - // at the moment, we have to refer to variables both via id and name - this.context.set(kindName('nextvar', varName), nextRegister) - this.context.set(kindName('nextvar', vardef.id), nextRegister) - } - - enterLiteral(expr: ir.QuintBool | ir.QuintInt | ir.QuintStr) { - switch (expr.kind) { - case 'bool': - this.compStack.push(mkConstComputable(rv.mkBool(expr.value))) - break - - case 'int': - this.compStack.push(mkConstComputable(rv.mkInt(expr.value))) - break - - case 'str': - this.compStack.push(mkConstComputable(rv.mkStr(expr.value))) - break - - default: - unreachable(expr) - } - } - - enterName(name: ir.QuintName) { - if (name.name === lastTraceName) { - this.compStack.push(mkConstComputable(rv.mkList(this.trace.get()))) - return - } - // The name belongs to one of the objects: - // a shadow variable, a variable, an argument, a callable. - // The order is important, as defines the name priority. - const comp = - this.contextLookup(name.id, ['arg', 'var', 'callable']) ?? - // a backup case for Nat, Int, and Bool, and special names such as q::input - this.contextGet(name.name, ['arg', 'callable']) - if (comp) { - // this name has an associated computable object already - this.compStack.push(comp) - } else { - // this should not happen, due to the name resolver - this.errorTracker.addCompileError(name.id, 'QNT502', `Name ${name.name} not found`) - this.compStack.push(fail) - } - } - - exitApp(app: ir.QuintApp) { - if (!ir.isQuintBuiltin(app)) { - this.applyUserDefined(app) - } else { - switch (app.opcode) { - case 'next': - { - const register = this.compStack.pop() - if (register) { - const name = (register as Register).name - const nextvar = this.contextGet(name, ['nextvar']) - this.compStack.push(nextvar ?? fail) - } else { - this.errorTracker.addCompileError(app.id, 'QNT502', 'Operator next(...) needs one argument') - this.compStack.push(fail) - } - } - break - - case 'assign': - this.translateAssign(app.id) - break - - case 'eq': - this.applyFun(app.id, 2, (x, y) => right(rv.mkBool(x.equals(y)))) - break - - case 'neq': - this.applyFun(app.id, 2, (x, y) => right(rv.mkBool(!x.equals(y)))) - break - - // conditional - case 'ite': - this.translateIfThenElse(app.id) - break - - // Booleans - case 'not': - this.applyFun(app.id, 1, p => right(rv.mkBool(!p.toBool()))) - break - - case 'and': - // a conjunction over expressions is lazy - this.translateBoolOp(app, rv.mkBool(true), (_, r) => (!r ? rv.mkBool(false) : undefined)) - break - - case 'actionAll': - this.translateAllOrThen(app) - break - - case 'or': - // a disjunction over expressions is lazy - this.translateBoolOp(app, rv.mkBool(false), (_, r) => (r ? rv.mkBool(true) : undefined)) - break - - case 'actionAny': - this.translateOrAction(app) - break - - case 'implies': - // an implication is lazy - this.translateBoolOp(app, rv.mkBool(false), (n, r) => (n == 0 && !r ? rv.mkBool(true) : undefined)) - break - - case 'iff': - this.applyFun(app.id, 2, (p, q) => right(rv.mkBool(p.toBool() === q.toBool()))) - break - - // integers - case 'iuminus': - this.applyFun(app.id, 1, n => right(rv.mkInt(-n.toInt()))) - break - - case 'iadd': - this.applyFun(app.id, 2, (p, q) => right(rv.mkInt(p.toInt() + q.toInt()))) - break - - case 'isub': - this.applyFun(app.id, 2, (p, q) => right(rv.mkInt(p.toInt() - q.toInt()))) - break - - case 'imul': - this.applyFun(app.id, 2, (p, q) => right(rv.mkInt(p.toInt() * q.toInt()))) - break - - case 'idiv': - this.applyFun(app.id, 2, (p, q) => { - if (q.toInt() !== 0n) { - return right(rv.mkInt(p.toInt() / q.toInt())) - } else { - return left({ code: 'QNT503', message: 'Division by zero', reference: app.id }) - } - }) - break - - case 'imod': - this.applyFun(app.id, 2, (p, q) => right(rv.mkInt(p.toInt() % q.toInt()))) - break - - case 'ipow': - this.applyFun(app.id, 2, (p, q) => { - if (q.toInt() == 0n && p.toInt() == 0n) { - return left({ code: 'QNT503', message: '0^0 is undefined', reference: app.id }) - } else if (q.toInt() < 0n) { - return left({ code: 'QNT503', message: 'i^j is undefined for j < 0', reference: app.id }) - } else { - return right(rv.mkInt(p.toInt() ** q.toInt())) - } - }) - break - - case 'igt': - this.applyFun(app.id, 2, (p, q) => right(rv.mkBool(p.toInt() > q.toInt()))) - break - - case 'ilt': - this.applyFun(app.id, 2, (p, q) => right(rv.mkBool(p.toInt() < q.toInt()))) - break - - case 'igte': - this.applyFun(app.id, 2, (p, q) => right(rv.mkBool(p.toInt() >= q.toInt()))) - break - - case 'ilte': - this.applyFun(app.id, 2, (p, q) => right(rv.mkBool(p.toInt() <= q.toInt()))) - break - - case 'Tup': - // Construct a tuple from an array of values - this.applyFun(app.id, app.args.length, (...values: RuntimeValue[]) => right(rv.mkTuple(values))) - break - - case 'item': - // Access a tuple: tuples are 1-indexed, that is, _1, _2, etc. - this.applyFun(app.id, 2, (tuple, idx) => getListElem(tuple.toList(), Number(idx.toInt()) - 1)) - break - - case 'tuples': - // Construct a cross product - this.applyFun(app.id, app.args.length, (...sets: RuntimeValue[]) => right(rv.mkCrossProd(sets))) - break - - case 'List': - // Construct a list from an array of values - this.applyFun(app.id, app.args.length, (...values: RuntimeValue[]) => right(rv.mkList(values))) - break - - case 'range': - this.applyFun(app.id, 2, (start, end) => { - const [s, e] = [Number(start.toInt()), Number(end.toInt())] - if (s <= e) { - const arr: RuntimeValue[] = [] - for (let i = s; i < e; i++) { - arr.push(rv.mkInt(BigInt(i))) - } - return right(rv.mkList(arr)) - } else { - return left({ code: 'QNT504', message: `range(${s}, ${e}) is out of bounds` }) - } - }) - break - - case 'nth': - // Access a list - this.applyFun(app.id, 2, (list, idx) => getListElem(list.toList(), Number(idx.toInt()))) - break - - case 'replaceAt': - this.applyFun(app.id, 3, (list, idx, value) => updateList(list.toList(), Number(idx.toInt()), value)) - break - - case 'head': - this.applyFun(app.id, 1, list => getListElem(list.toList(), 0)) - break - - case 'tail': - this.applyFun(app.id, 1, list => { - const l = list.toList() - if (l.size > 0) { - return sliceList(l, 1, l.size) - } else { - return left({ code: 'QNT505', message: 'Applied tail to an empty list' }) - } - }) - break - - case 'slice': - this.applyFun(app.id, 3, (list, start, end) => { - const [l, s, e] = [list.toList(), Number(start.toInt()), Number(end.toInt())] - if (s >= 0 && s <= l.size && e <= l.size && e >= s) { - return sliceList(l, s, e) - } else { - return left({ code: 'QNT506', message: `slice(..., ${s}, ${e}) applied to a list of size ${l.size}` }) - } - }) - break - - case 'length': - this.applyFun(app.id, 1, list => right(rv.mkInt(BigInt(list.toList().size)))) - break - - case 'append': - this.applyFun(app.id, 2, (list, elem) => right(rv.mkList(list.toList().push(elem)))) - break - - case 'concat': - this.applyFun(app.id, 2, (list1, list2) => right(rv.mkList(list1.toList().concat(list2.toList())))) - break - - case 'indices': - this.applyFun(app.id, 1, list => right(rv.mkInterval(0n, BigInt(list.toList().size - 1)))) - break - - case 'Rec': - // Construct a record - this.applyFun(app.id, app.args.length, (...values: RuntimeValue[]) => { - const keys = values.filter((e, i) => i % 2 === 0).map(k => k.toStr()) - const map: OrderedMap = keys.reduce((map, key, i) => { - const v = values[2 * i + 1] - return v ? map.set(key, v) : map - }, OrderedMap()) - return right(rv.mkRecord(map)) - }) - break - - case 'field': - // Access a record via the field name - this.applyFun(app.id, 2, (rec, fieldName) => { - const name = fieldName.toStr() - const fieldValue = rec.toOrderedMap().get(name) - if (fieldValue) { - return right(fieldValue) - } else { - return left({ code: 'QNT501', message: `Accessing a missing record field ${name}` }) - } - }) - break - - case 'fieldNames': - this.applyFun(app.id, 1, rec => { - const keysAsRuntimeValues = rec - .toOrderedMap() - .keySeq() - .map(key => rv.mkStr(key)) - return right(rv.mkSet(keysAsRuntimeValues)) - }) - break - - case 'with': - // update a record - this.applyFun(app.id, 3, (rec, fieldName, fieldValue) => { - const oldMap = rec.toOrderedMap() - const key = fieldName.toStr() - if (oldMap.has(key)) { - const newMap = rec.toOrderedMap().set(key, fieldValue) - return right(rv.mkRecord(newMap)) - } else { - return left({ code: 'QNT501', message: `Called 'with' with a non-existent key ${key}` }) - } - }) - break - - case 'variant': - // Construct a variant of a sum type. - this.applyFun(app.id, 2, (labelName, value) => right(rv.mkVariant(labelName.toStr(), value))) - break - - case 'matchVariant': - this.applyFun(app.id, app.args.length, (variantExpr, ...cases) => { - // Type checking ensures that this is a variant expression - assert(variantExpr instanceof RuntimeValueVariant, 'invalid value in match expression') - const label = variantExpr.label - const value = variantExpr.value - - // Find the eliminator marked with the variant's label - let result: EvaluationResult | undefined - for (const [caseLabel, caseElim] of chunk(cases, 2)) { - const caseLabelStr = caseLabel.toStr() - if (caseLabelStr === '_' || caseLabelStr === label) { - // Type checking ensures the second item of each case is a lambda - assert(caseElim instanceof RuntimeValueLambda, 'invalid eliminator in match expression') - const eliminator = caseElim as RuntimeValueLambda - result = eliminator.eval([right(value)]).map(r => r as RuntimeValue) - break - } - } - // Type checking ensures we have cases for every possible variant of a sum type. - assert(result, 'non-exhaustive match expression') - - return result - }) - break - - case 'Set': - // Construct a set from an array of values. - this.applyFun(app.id, app.args.length, (...values: RuntimeValue[]) => right(rv.mkSet(values))) - break - - case 'powerset': - this.applyFun(app.id, 1, (baseset: RuntimeValue) => right(rv.mkPowerset(baseset))) - break - - case 'contains': - this.applyFun(app.id, 2, (set, value) => right(rv.mkBool(set.contains(value)))) - break - - case 'in': - this.applyFun(app.id, 2, (value, set) => right(rv.mkBool(set.contains(value)))) - break - - case 'subseteq': - this.applyFun(app.id, 2, (l, r) => right(rv.mkBool(l.isSubset(r)))) - break - - case 'union': - this.applyFun(app.id, 2, (l, r) => right(rv.mkSet(l.toSet().union(r.toSet())))) - break - - case 'intersect': - this.applyFun(app.id, 2, (l, r) => right(rv.mkSet(l.toSet().intersect(r.toSet())))) - break - - case 'exclude': - this.applyFun(app.id, 2, (l, r) => right(rv.mkSet(l.toSet().subtract(r.toSet())))) - break - - case 'size': - this.applyFun(app.id, 1, set => set.cardinality().map(rv.mkInt)) - break - - case 'isFinite': - // at the moment, we support only finite sets, so just return true - this.applyFun(app.id, 1, _set => right(rv.mkBool(true))) - break - - case 'to': - this.applyFun(app.id, 2, (i, j) => right(rv.mkInterval(i.toInt(), j.toInt()))) - break - - case 'fold': - this.applyFold(app.id, 'fwd') - break - - case 'foldl': - this.applyFold(app.id, 'fwd') - break - - case 'foldr': - this.applyFold(app.id, 'rev') - break - - case 'flatten': - this.applyFun(app.id, 1, set => { - // unpack the sets from runtime values - const setOfSets = set.toSet().map(e => e.toSet()) - // and flatten the set of sets via immutable-js - return right(rv.mkSet(setOfSets.flatten(1) as Set)) - }) - break - - case 'get': - // Get a map value - this.applyFun(app.id, 2, (map, key) => { - const value = map.toMap().get(key.normalForm()) - if (value) { - return right(value) - } else { - // Should we print the key? It may be a complex expression. - return left({ code: 'QNT507', message: "Called 'get' with a non-existing key" }) - } - }) - break - - case 'set': - // Update a map value - this.applyFun(app.id, 3, (map, key, newValue) => { - const normalKey = key.normalForm() - const asMap = map.toMap() - if (asMap.has(normalKey)) { - const newMap = asMap.set(normalKey, newValue) - return right(rv.fromMap(newMap)) - } else { - return left({ code: 'QNT507', message: "Called 'set' with a non-existing key" }) - } - }) - break - - case 'put': - // add a value to a map - this.applyFun(app.id, 3, (map, key, newValue) => { - const normalKey = key.normalForm() - const asMap = map.toMap() - const newMap = asMap.set(normalKey, newValue) - return right(rv.fromMap(newMap)) - }) - break - - case 'setBy': { - // Update a map value via a lambda - const fun = this.compStack.pop() ?? fail - this.applyFun(app.id, 2, (map, key) => { - const normalKey = key.normalForm() - const asMap = map.toMap() - if (asMap.has(normalKey)) { - return fun.eval([right(asMap.get(normalKey))]).map(newValue => { - const newMap = asMap.set(normalKey, newValue as RuntimeValue) - return rv.fromMap(newMap) - }) - } else { - return left({ code: 'QNT507', message: "Called 'setBy' with a non-existing key" }) - } - }) - break - } - - case 'keys': - // map keys as a set - this.applyFun(app.id, 1, map => { - return right(rv.mkSet(map.toMap().keys())) - }) - break - - case 'oneOf': - this.applyOneOf(app.id) - break - - case 'exists': - this.mapLambdaThenReduce(app.id, set => rv.mkBool(set.find(([result, _]) => result.toBool()) !== undefined)) - break - - case 'forall': - this.mapLambdaThenReduce(app.id, set => rv.mkBool(set.find(([result, _]) => !result.toBool()) === undefined)) - break - - case 'map': - this.mapLambdaThenReduce(app.id, array => rv.mkSet(array.map(([result, _]) => result))) - break - - case 'filter': - this.mapLambdaThenReduce(app.id, arr => rv.mkSet(arr.filter(([r, _]) => r.toBool()).map(([_, e]) => e))) - break - - case 'select': - this.mapLambdaThenReduce(app.id, arr => rv.mkList(arr.filter(([r, _]) => r.toBool()).map(([_, e]) => e))) - break - - case 'mapBy': - this.mapLambdaThenReduce(app.id, arr => rv.mkMap(arr.map(([v, k]) => [k, v]))) - break - - case 'Map': - this.applyFun(app.id, app.args.length, (...pairs: any[]) => right(rv.mkMap(pairs))) - break - - case 'setToMap': - this.applyFun(app.id, 1, (set: RuntimeValue) => - right( - rv.mkMap( - set.toSet().map(p => { - const arr = p.toList().toArray() - return [arr[0], arr[1]] - }) - ) - ) - ) - break - - case 'setOfMaps': - this.applyFun(app.id, 2, (dom, rng) => { - return right(rv.mkMapSet(dom, rng)) - }) - break - - case 'then': - this.translateAllOrThen(app) - break - - case 'fail': - this.applyFun(app.id, 1, result => { - return right(rv.mkBool(!result.toBool())) - }) - break - - case 'expect': - this.translateExpect(app) - break - - case 'assert': - this.applyFun(app.id, 1, cond => { - if (!cond.toBool()) { - return left({ code: 'QNT508', message: 'Assertion failed', reference: app.id }) - } - return right(cond) - }) - break - - case 'reps': - this.translateReps(app) - break - - case 'q::test': - // the special operator that runs random simulation - this.test(app.id) - break - - case 'q::testOnce': - // the special operator that runs random simulation - this.testOnce(app.id) - break - - case 'q::debug': - this.applyFun(app.id, 2, (msg, value) => { - let columns = terminalWidth() - let valuePretty = format(columns, 0, prettyQuintEx(value.toQuintEx(zerog))) - console.log('>', msg.toStr(), valuePretty.toString()) - return right(value) - }) - break - - case 'allListsUpTo': - this.applyFun(app.id, 2, (set: RuntimeValue, max_length: RuntimeValue) => { - let lists: Set = Set([[]]) - let last_lists: Set = Set([[]]) - times(Number(max_length.toInt())).forEach(_length => { - // Generate all lists of length `length` from the set - const new_lists: Set = set.toSet().flatMap(value => { - // for each value in the set, append it to all lists of length `length - 1` - return last_lists.map(list => list.concat(value)) - }) - - lists = lists.merge(new_lists) - last_lists = new_lists - }) - - return right(rv.mkSet(lists.map(list => rv.mkList(list)).toOrderedSet())) - }) - break - - // standard unary operators that are not handled by REPL - case 'allLists': - case 'chooseSome': - case 'always': - case 'eventually': - case 'enabled': - this.applyFun(app.id, 1, _ => { - return left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${app.opcode}'` }) - }) - break - - // builtin operators that are not handled by REPL - case 'orKeep': - case 'mustChange': - case 'weakFair': - case 'strongFair': - this.applyFun(app.id, 2, _ => { - return left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${app.opcode}'` }) - }) - break - - default: - unreachable(app.opcode) - } - } - } - - private applyUserDefined(app: ir.QuintApp) { - const onError = (sourceId: bigint, msg: string): void => { - const error: EvaluationResult = left({ code: 'QNT501', message: msg, reference: sourceId }) - this.errorTracker.addCompileError(sourceId, 'QNT501', msg) - this.compStack.push({ eval: () => error }) - } - - // look up for the operator to see, whether it's just an operator, or a parameter - const lookupEntry = this.lookupTable.get(app.id) - if (lookupEntry === undefined) { - return onError(app.id, `Called unknown operator ${app.opcode}`) - } - - // this function gives us access to the compiled operator later - let callableRef: () => Either - - if (lookupEntry.kind !== 'param') { - // The common case: the operator has been defined elsewhere. - // We simply look up for the operator and return it via callableRef. - const callable = this.contextLookup(app.id, ['callable']) as Callable - if (callable === undefined || callable.nparams === undefined) { - return onError(app.id, `Called unknown operator ${app.opcode}`) - } - callableRef = () => right(callable) - } else { - // The operator is a parameter of another operator. - // We do not have access to the operator yet. - let register = this.contextLookup(app.id, ['arg']) as Register - if (register === undefined) { - return onError(app.id, `Parameter ${app.opcode} is not found`) - } - // every time we need a Callable, we retrieve it from the register - callableRef = () => { - const result = register.registerValue.map(v => v as Callable) - if (result.isJust()) { - return right(result.value) - } else { - return left({ code: 'QNT501', message: `Parameter ${app.opcode} is not set` }) - } - } - } - - const nargs = app.args.length // === operScheme.type.args.length - const nactual = this.compStack.length - if (nactual < nargs) { - return onError(app.id, `Expected ${nargs} arguments for ${app.opcode}, found: ${nactual}`) - } else { - // pop nargs elements of the compStack - const args = this.compStack.splice(-nargs, nargs) - // Produce the new computable value. - // This code is similar to applyFun, but it calls the listener before - const comp = { - eval: () => { - this.execListener.onUserOperatorCall(app) - // compute the values of the arguments at this point - const values = args.map(arg => arg.eval()) - const result = callableRef().chain(callable => { - return callable.eval(values) - }) - mergeInMany(values).map(vs => this.execListener.onUserOperatorReturn(app, vs, toMaybe(result))) - return result - }, - } - this.compStack.push(comp) - } - } - - enterLambda(lam: ir.QuintLambda) { - // introduce a register for every parameter - lam.params.forEach(p => { - const register = mkRegister('arg', p.name, none(), { - code: 'QNT501', - message: `Parameter ${p} is not set`, - reference: p.id, - }) - this.context.set(kindName('arg', p.id), register) - - if (specialNames.includes(p.name)) { - this.context.set(kindName('arg', p.name), register) - } - }) - // After this point, the body of the lambda gets compiled. - // The body of the lambda may refer to the parameter via names, - // which are stored in the registers we've just created. - } - - exitLambda(lam: ir.QuintLambda) { - // The expression on the stack is the body of the lambda. - // Transform it to Callable together with the registers. - const registers: Register[] = [] - lam.params.forEach(p => { - const id = specialNames.includes(p.name) ? p.name : p.id - - const key = kindName('arg', id) - const register = this.contextGet(id, ['arg']) as Register - - if (register && register.registerValue) { - this.context.delete(key) - registers.push(register) - } else { - this.errorTracker.addCompileError(p.id, 'QNT501', `Parameter ${p.name} not found`) - } - }) - - const lambdaBody = this.compStack.pop() - if (lambdaBody) { - this.compStack.push(mkCallable(registers, lambdaBody)) - } else { - this.errorTracker.addCompileError(lam.id, 'QNT501', 'Compilation of lambda failed') - } - } - - private translateAssign(sourceId: bigint): void { - if (this.compStack.length < 2) { - this.errorTracker.addCompileError(sourceId, 'QNT501', `Assignment '=' needs two arguments`) - return - } - const [register, rhs] = this.compStack.splice(-2) - const name = (register as Register).name - if (name === undefined) { - this.errorTracker.addCompileError(sourceId, 'QNT501', `Assignment '=' applied to a non-variable`) - this.compStack.push(fail) - return - } - - const nextvar = this.contextGet(name, ['nextvar']) as Register - if (nextvar) { - this.compStack.push(rhs) - this.applyFun(sourceId, 1, value => { - nextvar.registerValue = just(value) - return right(rv.mkBool(true)) - }) - } else { - this.errorTracker.addCompileError(sourceId, 'QNT502', `Undefined next variable in ${name} = ...`) - this.compStack.push(fail) - } - } - - /** - * A generalized application of a one-argument Callable to a set-like - * runtime value, as required by `exists`, `forall`, `map`, and `filter`. - * - * This method expects `compStack` to look like follows: - * - * - `(top)` translated lambda, as `Callable`. - * - * - `(top - 1)`: a set-like value to iterate over, as `Computable`. - * - * The method evaluates the Callable for each element of the iterable value - * and either produces `none`, if evaluation failed for one of the elements, - * or it applies `mapResultAndElems` to the pairs that consists of the Callable - * result and the original element of the iterable value. - * The final result is stored on the stack. - */ - private mapLambdaThenReduce( - sourceId: bigint, - reduceFunction: (_array: Array<[RuntimeValue, RuntimeValue]>) => RuntimeValue - ): void { - this.popArgs(2) - .map(args => mapLambdaThenReduce(sourceId, reduceFunction, args)) - .map(comp => this.compStack.push(comp)) - .mapLeft(e => this.errorTracker.addRuntimeError(sourceId, e)) - } - - /** - * Translate one of the operators: fold, foldl, and foldr. - */ - private applyFold(sourceId: bigint, order: 'fwd' | 'rev'): void { - this.popArgs(3) - .map(args => applyFold(order, args)) - .map(comp => this.compStack.push(comp)) - .mapLeft(e => this.errorTracker.addRuntimeError(sourceId, e)) - } - - // pop nargs computable values, pass them the 'fun' function, and - // push the combined computable value on the stack - private applyFun(sourceId: bigint, nargs: number, fun: (..._args: RuntimeValue[]) => EvaluationResult) { - this.popArgs(nargs) - .map(args => applyFun(sourceId, fun, args)) - .map(comp => this.compStack.push(comp)) - .mapLeft(e => this.errorTracker.addRuntimeError(sourceId, e)) - } - - private popArgs(nargs: number): Either { - if (this.compStack.length < nargs) { - return left({ code: 'QNT501', message: 'Not enough arguments' }) - } - return right(this.compStack.splice(-nargs, nargs)) - } - - // if-then-else requires special treatment, - // as it should not evaluate both arms - private translateIfThenElse(sourceId: bigint) { - if (this.compStack.length < 3) { - this.errorTracker.addCompileError(sourceId, 'QNT501', 'Not enough arguments') - } else { - // pop 3 elements of the compStack - const [cond, thenArm, elseArm] = this.compStack.splice(-3, 3) - // produce the new computable value - const comp = { - eval: () => { - // compute the values of the arguments at this point - // TODO: register errors - const v = cond.eval().map(pred => (pred.equals(rv.mkBool(true)) ? thenArm.eval() : elseArm.eval())) - return v.join() - }, - } - this.compStack.push(comp) - } - } - - /** - * Compute all { ... } or A.then(B)...then(E) for a chain of actions. - * @param actions actions as computable to execute - * @param kind is it 'all { ... }' or 'A.then(B)'? - * @param actionId given the action index, return the id that produced this action - * @returns evaluation result - */ - private chainAllOrThen( - actions: Computable[], - kind: 'all' | 'then', - actionId: (idx: number) => bigint - ): EvaluationResult { - // save the values of the next variables, as actions may update them - const savedValues = this.snapshotNextVars() - const savedTrace = this.trace.get() - - let result: EvaluationResult = right(rv.mkBool(true)) - // Evaluate arguments iteratively. - // Stop as soon as one of the arguments returns false. - // This is a form of Boolean short-circuiting. - let nactionsLeft = actions.length - for (const action of actions) { - nactionsLeft-- - result = action.eval() - const isFalse = result.isRight() && !(result.value as RuntimeValue).toBool() - if (result.isLeft() || isFalse) { - // As soon as one of the arguments does not evaluate to true, - // break out of the loop. - // Restore the values of the next variables, - // as evaluation was not successful. - this.recoverNextVars(savedValues) - this.trace.reset(savedTrace) - - if (kind === 'then' && nactionsLeft > 0 && isFalse) { - // Cannot extend a run. Emit an error message. - const actionNo = actions.length - (nactionsLeft + 1) - result = left({ - code: 'QNT513', - message: `Cannot continue in A.then(B), A evaluates to 'false'`, - reference: actionId(actionNo), - }) - return result - } else { - return result - } - } - - // switch to the next frame, when implementing A.then(B) - if (kind === 'then' && nactionsLeft > 0) { - const oldState: RuntimeValue = this.varsToRecord() - this.shiftVars() - const newState: RuntimeValue = this.varsToRecord() - this.trace.extend(newState) - this.execListener.onNextState(oldState, newState) - } - } - - return result - } - - // translate all { A, ..., C } or A.then(B) - private translateAllOrThen(app: ir.QuintApp): void { - const kind = app.opcode === 'then' ? 'then' : 'all' - - this.popArgs(app.args.length) - .map(args => { - const lazyComputable = () => this.chainAllOrThen(args, kind, idx => app.args[idx].id) - return this.compStack.push(mkFunComputable(lazyComputable)) - }) - .mapLeft(e => this.errorTracker.addRuntimeError(app.id, e)) - } - - // Translate A.expect(P): - // - Evaluate A. - // - When A's result is 'false', emit a runtime error. - // - When A's result is 'true': - // - Commit the variable updates: Shift the primed variables to unprimed. - // - Evaluate `P`. - // - If `P` evaluates to `false`, emit a runtime error (similar to `assert`). - // - If `P` evaluates to `true`, rollback to the previous state and return `true`. - private translateExpect(app: ir.QuintApp): void { - // The code below is an adaption of chainAllOrThen. - // If someone finds how to nicely combine both, please refactor. - if (this.compStack.length !== 2) { - this.errorTracker.addCompileError(app.id, 'QNT501', `Not enough arguments on stack for "${app.opcode}"`) - return - } - const [action, pred] = this.compStack.splice(-2) - const lazyCompute = (): EvaluationResult => { - const savedNextVars = this.snapshotNextVars() - const savedTrace = this.trace.get() - const actionResult = action.eval() - if (actionResult.isLeft() || !(actionResult.value as RuntimeValue).toBool()) { - // 'A' evaluates to 'false', or produces an error. - // Restore the values of the next variables. - this.recoverNextVars(savedNextVars) - this.trace.reset(savedTrace) - // expect emits an error when the run could not finish - return left({ code: 'QNT508', message: 'Cannot continue to "expect"', reference: app.args[0].id }) - } else { - const savedVarsAfterAction = this.snapshotVars() - const savedNextVarsAfterAction = this.snapshotNextVars() - const savedTraceAfterAction = this.trace.get() - // Temporarily, switch to the next frame, to make a look-ahead evaluation. - // For example, if `x == 1` and `x' == 2`, we would have `x == 2` and `x'` would be undefined. - this.shiftVars() - // evaluate P - const predResult = pred.eval() - // Recover the assignments to unprimed and primed variables. - // E.g., we recover variables to `x == 1` and `x' == 2` in the above example. - // This lets us combine the result of `expect` with other actions via `then`. - // For example: `A.expect(P).then(B)`. - this.recoverVars(savedVarsAfterAction) - this.recoverNextVars(savedNextVarsAfterAction) - this.trace.reset(savedTraceAfterAction) - if (predResult.isLeft() || !(predResult.value as RuntimeValue).toBool()) { - return left({ code: 'QNT508', message: 'Expect condition does not hold true', reference: app.args[1].id }) - } - return predResult - } - } - - this.compStack.push(mkFunComputable(lazyCompute)) - } - - // translate n.reps(A) - private translateReps(app: ir.QuintApp): void { - if (this.compStack.length < 2) { - this.errorTracker.addCompileError(app.id, 'QNT501', `Not enough arguments on stack for "${app.opcode}"`) - return - } - const [niterations, action] = this.compStack.splice(-2) - - const lazyCompute = () => { - // compute the number of iterations and repeat 'action' that many times - return niterations - .eval() - .map(num => { - const n = Number((num as RuntimeValue).toInt()) - const indices = [...Array(n).keys()] - const actions = indices.map(i => { - return { - eval: () => { - return action.eval([right(rv.mkInt(BigInt(i)))]) - }, - } - }) - // In case the case of reps, we have multiple copies of the same action. - // This is why all occurrences have the same id. - return this.chainAllOrThen(actions, 'then', _ => app.args[1].id) - }) - .join() - } - - this.compStack.push(mkFunComputable(lazyCompute)) - } - - // translate one of the Boolean operators with short-circuiting: - // - or { A, ..., C } - // - and { A, ..., C } - // - A implies B - private translateBoolOp( - app: ir.QuintApp, - defaultValue: RuntimeValue, - shortCircuit: (no: number, r: boolean) => RuntimeValue | undefined - ): void { - this.popArgs(app.args.length) - .map(args => applyBoolOp(defaultValue, shortCircuit, args)) - .map(comp => this.compStack.push(comp)) - .mapLeft(e => this.errorTracker.addRuntimeError(app.id, e)) - } - - // translate any { A, ..., C } - private translateOrAction(app: ir.QuintApp): void { - if (this.compStack.length < app.args.length) { - this.errorTracker.addCompileError(app.id, 'QNT501', 'Not enough arguments on stack for "any"') - return - } - const args = this.compStack.splice(-app.args.length) - - // According to the semantics of action-level disjunctions, - // we have to find out which branches are enabled and pick one of them - // non-deterministically. Instead of modeling non-determinism, - // we use a random number generator. This may change in the future. - const lazyCompute = () => { - // on `any`, we reset the action taken as the goal is to save the last - // action picked in an `any` call - this.actionTaken = none() - // we also reset nondet picks as they are collected when we move up the - // tree, and this is now a leaf - this.nondetPicks = this.emptyNondetPicks() - - // save the values of the next variables, as actions may update them - const valuesBefore = this.snapshotNextVars() - // we store the potential successor values in this array - const successors: Maybe[][] = [] - const successorIndices: number[] = [] - // Evaluate arguments iteratively. - args.forEach((arg, i) => { - this.recoverNextVars(valuesBefore) - // either the argument is evaluated to false, or fails - this.execListener.onAnyOptionCall(app, i) - const result = arg.eval().or(right(rv.mkBool(false))) - const boolResult = (result.unwrap() as RuntimeValue).toBool() - this.execListener.onAnyOptionReturn(app, i) - // if this arm evaluates to true, save it in the candidates - if (boolResult === true) { - successors.push(this.snapshotNextVars()) - successorIndices.push(i) - } - }) - - const ncandidates = successors.length - let choice - if (ncandidates === 0) { - // no successor: restore the state and return false - this.recoverNextVars(valuesBefore) - this.execListener.onAnyReturn(args.length, -1) - return right(rv.mkBool(false)) - } else if (ncandidates === 1) { - // There is exactly one successor, the execution is deterministic. - // No need for randomization. This may reduce the number of tests. - choice = 0 - } else { - // randomly pick a successor and return true - choice = Number(this.rand(BigInt(ncandidates))) - } - - this.recoverNextVars(successors[choice]) - this.execListener.onAnyReturn(args.length, successorIndices[choice]) - return right(rv.mkBool(true)) - } - - this.compStack.push(mkFunComputable(lazyCompute)) - } - - // Apply the operator oneOf - private applyOneOf(sourceId: bigint) { - this.applyFun(sourceId, 1, set => { - const bounds = set.bounds() - const positions: Either = mergeInMany( - bounds.map((b): Either => { - if (b.isJust()) { - const sz = b.value - - if (sz === 0n) { - return left({ code: 'QNT509', message: `Applied oneOf to an empty set` }) - } - return right(this.rand(sz)) - } else { - // An infinite set, pick an integer from the range [-2^255, 2^255). - // Note that pick on Nat uses the absolute value of the passed integer. - // TODO: make it a configurable parameter: - // https://github.com/informalsystems/quint/issues/279 - return right(-(2n ** 255n) + this.rand(2n ** 256n)) - } - }) - ).mapLeft(errors => ({ code: 'QNT501', message: errors.map(quintErrorToString).join('\n') })) - - return positions.chain(ps => set.pick(ps.values())) - }) - } - - private test(sourceId: bigint) { - if (this.compStack.length < 6) { - this.errorTracker.addCompileError(sourceId, 'QNT501', 'Not enough arguments on stack for "q::test"') - return - } - - const [nruns, nsteps, ntraces, init, next, inv] = this.compStack.splice(-6) - this.runTestSimulation(nruns, nsteps, ntraces, init, next, inv) - } - - private testOnce(sourceId: bigint) { - if (this.compStack.length < 5) { - this.errorTracker.addCompileError(sourceId, 'QNT501', 'Not enough arguments on stack for "q::testOnce"') - return - } - - const [nsteps, ntraces, init, next, inv] = this.compStack.splice(-5) - const nruns = mkConstComputable(rv.mkInt(1n)) - this.runTestSimulation(nruns, nsteps, ntraces, init, next, inv) - } - - // The simulator core: produce multiple random runs - // and check the given state invariant (state assertion). - // - // Technically, this is similar to the implementation of folds. - // However, it also restores the state and saves a trace, if there is any. - private runTestSimulation( - nrunsComp: Computable, - nstepsComp: Computable, - ntracesComp: Computable, - init: Computable, - next: Computable, - inv: Computable - ) { - const doRun = (): EvaluationResult => { - return mergeInMany([nrunsComp, nstepsComp, ntracesComp].map(c => c.eval())) - .mapLeft((errors): QuintError => { - return { code: 'QNT501', message: errors.map(quintErrorToString).join('\n') } - }) - .chain(([nrunsRes, nstepsRes, nTracesRes]) => { - const isTrue = (res: EvaluationResult) => { - return !res.isLeft() && (res.value as RuntimeValue).toBool() === true - } - // a failure flag for the case a runtime error is found - let failure = false - // counter for errors found - let errorsFound = 0 - // save the registers to recover them later - const vars = this.snapshotVars() - const nextVars = this.snapshotNextVars() - // do multiple runs, stop at the first failing run - const nruns = (nrunsRes as RuntimeValue).toInt() - const ntraces = (nTracesRes as RuntimeValue).toInt() - for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { - this.execListener.onRunCall() - this.trace.reset() - // check Init() - const initApp: ir.QuintApp = { id: 0n, kind: 'app', opcode: 'q::initAndInvariant', args: [] } - this.execListener.onUserOperatorCall(initApp) - const initResult = init.eval() - failure = initResult.isLeft() || failure - if (!isTrue(initResult)) { - this.execListener.onUserOperatorReturn(initApp, [], toMaybe(initResult)) - } else { - // The initial action evaluates to true. - // Our guess of values was good. - this.shiftVars() - this.trace.extend(this.varsToRecord()) - // check the invariant Inv - const invResult = inv.eval() - this.execListener.onUserOperatorReturn(initApp, [], toMaybe(initResult)) - failure = invResult.isLeft() || failure - if (!isTrue(invResult)) { - errorsFound++ - } else { - // check all { Next(), shift(), Inv } in a loop - const nsteps = (nstepsRes as RuntimeValue).toInt() - for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { - const nextApp: ir.QuintApp = { - id: 0n, - kind: 'app', - opcode: 'q::stepAndInvariant', - args: [], - } - this.execListener.onUserOperatorCall(nextApp) - const nextResult = next.eval() - failure = nextResult.isLeft() || failure - if (isTrue(nextResult)) { - this.shiftVars() - this.trace.extend(this.varsToRecord()) - errorsFound += isTrue(inv.eval()) ? 0 : 1 - this.execListener.onUserOperatorReturn(nextApp, [], toMaybe(nextResult)) - } else { - // Otherwise, the run cannot be extended. - // In some cases, this may indicate a deadlock. - // Since we are doing random simulation, it is very likely - // that we have not generated good values for extending - // the run. Hence, do not report an error here, but simply - // drop the run. Otherwise, we would have a lot of false - // positives, which look like deadlocks but they are not. - this.execListener.onUserOperatorReturn(nextApp, [], toMaybe(nextResult)) - this.execListener.onRunReturn(just(rv.mkBool(true)), this.trace.get()) - break - } - } - } - } - const outcome = !failure ? just(rv.mkBool(errorsFound == 0)) : none() - this.execListener.onRunReturn(outcome, this.trace.get()) - // recover the state variables - this.recoverVars(vars) - this.recoverNextVars(nextVars) - } // end of a single random run - - // finally, return true, if no error was found - return !failure ? right(rv.mkBool(errorsFound == 0)) : left({ code: 'QNT501', message: 'Simulation failure' }) - }) - } - this.compStack.push(mkFunComputable(doRun)) - } - - // convert the current variable values to a record - private varsToRecord(): RuntimeValue { - const map: [string, RuntimeValue][] = this.vars - .filter(r => r.registerValue.isJust()) - .map(r => [r.name, r.registerValue.value as RuntimeValue]) - - if (this.storeMetadata) { - if (this.actionTaken.isJust()) { - map.push(['action_taken', this.actionTaken.value!]) - map.push(['nondet_picks', this.nondetPicks]) - } - } - - return rv.mkRecord(map) - } - - private shiftVars() { - this.recoverVars(this.snapshotNextVars()) - this.nextVars.forEach(r => (r.registerValue = none())) - } - - // save the values of the vars into an array - private snapshotVars(): Maybe[] { - return this.vars.map(r => r.registerValue).concat([this.actionTaken, just(this.nondetPicks)]) - } - - // save the values of the next vars into an array - private snapshotNextVars(): Maybe[] { - return this.nextVars.map(r => r.registerValue).concat([this.actionTaken, just(this.nondetPicks)]) - } - - // load the values of the variables from an array - private recoverVars(values: Maybe[]) { - this.vars.forEach((r, i) => (r.registerValue = values[i])) - this.actionTaken = values[this.vars.length] ?? none() - this.nondetPicks = values[this.vars.length + 1].unwrap() - } - - // load the values of the next variables from an array - private recoverNextVars(values: Maybe[]) { - this.nextVars.forEach((r, i) => (r.registerValue = values[i])) - this.actionTaken = values[this.vars.length] ?? none() - this.nondetPicks = values[this.vars.length + 1].unwrap() - } - - // The initial value of nondet picks should already have record fields for all - // nondet values so the type of `nondet_picks` is the same throughout the - // trace. The field values are initialized as None. - private emptyNondetPicks() { - const nondetNames = [...this.lookupTable.values()] - .filter(d => d.kind === 'def' && d.qualifier === 'nondet') - .map(d => d.name) - return rv.mkRecord(OrderedMap(nondetNames.map(n => [n, rv.mkVariant('None', rv.mkTuple([]))]))) - } - - private contextGet(name: string | bigint, kinds: ComputableKind[]) { - for (const k of kinds) { - const value = this.context.get(kindName(k, name)) - if (value) { - return value - } - } - - return undefined - } - - private contextLookup(id: bigint, kinds: ComputableKind[]) { - const vdef = this.lookupTable.get(id) - if (vdef) { - const refId = vdef.id! - for (const k of kinds) { - const value = this.context.get(kindName(k, refId)) - if (value) { - return value - } - } - } - - return undefined - } -} diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts new file mode 100644 index 000000000..b8b667b41 --- /dev/null +++ b/quint/src/runtime/impl/evaluator.ts @@ -0,0 +1,388 @@ +import { Either, left, right } from '@sweet-monads/either' +import { EffectScheme } from '../../effects/base' +import { QuintEx } from '../../ir/quintIr' +import { LookupDefinition, LookupTable } from '../../names/base' +import { QuintError } from '../../quintError' +import { ExecutionListener, TraceRecorder } from '../trace' +import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' +import { Trace } from './trace' +import { RuntimeValue, rv } from './runtimeValue' +import { Context } from './Context' +import { TestResult } from '../testing' +import { Rng } from '../../rng' +import { zerog } from '../../idGenerator' + +export class Evaluator { + public ctx: Context + public recorder: TraceRecorder + private trace: Trace = new Trace() + private rng: Rng + + constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { + this.ctx = new Context(table, rng.next) + this.recorder = recorder + this.rng = rng + } + + updateTable(table: LookupTable) { + this.ctx.table = table + } + + shift() { + if (this.ctx.varStorage.nextVars.size === 0) { + return + } + this.ctx.varStorage.shiftVars() + this.trace.extend(this.ctx.varStorage.asRecord()) + this.ctx.clearMemo() // FIXME: clear only non-pure + } + + shiftAndCheck(): string[] { + const missing = [...this.ctx.varStorage.vars.keys()].filter(name => !this.ctx.varStorage.nextVars.has(name)) + + if (missing.length === this.ctx.varStorage.varNames.size) { + // Nothing was changed, don't shift + return [] + } + + this.shift() + return missing.map(name => name.split('#')[1]) + } + + evaluate(expr: QuintEx): Either { + const value = evaluateExpr(this.ctx, expr) + return value.map(rv.toQuintEx) + } + + simulate( + init: QuintEx, + step: QuintEx, + inv: QuintEx, + nruns: number, + nsteps: number, + ntraces: number, + effects: Map + ): Either { + let errorsFound = 0 + let failure: QuintError | undefined = undefined + + // effects.forEach((scheme, id) => { + // const [mode] = modeForEffect(scheme) + // if (mode === 'pureval' || mode === 'puredef') { + // console.log('pure key', id) + // this.ctx.pureKeys = this.ctx.pureKeys.add(id) + // } + // }) + + // TODO: room for improvement here + for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { + this.recorder.onRunCall() + this.trace.reset() + this.ctx.reset() + // this.execListener.onUserOperatorCall(init) + + const initResult = evaluateExpr(this.ctx, init).mapLeft(error => (failure = error)) + if (!isTrue(initResult)) { + // this.execListener.onUserOperatorReturn(init, [], initResult) + continue + } + + this.shift() + + const invResult = evaluateExpr(this.ctx, inv).mapLeft(error => (failure = error)) + if (!isTrue(invResult)) { + errorsFound++ + } else { + // check all { step, shift(), inv } in a loop + + // FIXME: errorsFound < ntraces is not good, because we continue after invariant violation. + // This is the same in the old version, so I'll fix later. + for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { + const stepResult = evaluateExpr(this.ctx, step).mapLeft(error => (failure = error)) + if (!isTrue(stepResult)) { + // The run cannot be extended. In some cases, this may indicate a deadlock. + // Since we are doing random simulation, it is very likely + // that we have not generated good values for extending + // the run. Hence, do not report an error here, but simply + // drop the run. Otherwise, we would have a lot of false + // positives, which look like deadlocks but they are not. + + // this.execListener.onRunReturn(right({ id: 0n, kind: 'bool', value: true }), this.trace.get()) + break + } + + this.shift() + + const invResult = evaluateExpr(this.ctx, inv).mapLeft(error => (failure = error)) + if (!isTrue(invResult)) { + errorsFound++ + } + } + } + + const outcome: Either = failure + ? left(failure) + : right({ id: 0n, kind: 'bool', value: errorsFound == 0 }) + this.recorder.onRunReturn(outcome, this.trace.get()) + // this.nextVars = new Map([...nextVarsSnapshot.entries()]) + } + + const outcome: Either = failure + ? left(failure) + : right({ id: 0n, kind: 'bool', value: errorsFound == 0 }) + + return outcome + } + + test(name: string, test: QuintEx, maxSamples: number): TestResult { + this.trace.reset() + // save the initial seed + let seed = this.rng.getState() + + let nsamples = 1 + // run up to maxSamples, stop on the first failure + for (; nsamples <= maxSamples; nsamples++) { + // record the seed value + seed = this.rng.getState() + this.recorder.onRunCall() + // reset the trace + this.trace.reset() + // run the test + const result = evaluateExpr(this.ctx, test).map(e => e.toQuintEx(zerog)) + + // extract the trace + const trace = this.trace.get() + + if (trace.length > 0) { + this.recorder.onRunReturn(result, trace) + } else { + // Report a non-critical error + console.error('Missing a trace') + this.recorder.onRunReturn(result, []) + } + + const bestTrace = this.recorder.bestTraces[0].frame + // evaluate the result + if (result.isLeft()) { + // if the test failed, return immediately + return { + name, + status: 'failed', + errors: [result.value], + seed, + frames: bestTrace.subframes, + nsamples: nsamples, + } + } + + const ex = result.value + if (ex.kind !== 'bool') { + // if the test returned a malformed result, return immediately + return { + name, + status: 'ignored', + errors: [], + seed: seed, + frames: bestTrace.subframes, + nsamples: nsamples, + } + } + + if (!ex.value) { + // if the test returned false, return immediately + const error: QuintError = { + code: 'QNT511', + message: `Test ${name} returned false`, + reference: test.id, + } + // options.onTrace( + // index, + // name, + // status, + // ctx.evaluationState.vars.map(v => v.name), + // states + // ) + + // saveTrace(bestTrace, index, name, 'failed') + return { + name, + status: 'failed', + errors: [error], + seed: seed, + frames: bestTrace.subframes, + nsamples: nsamples, + } + } else { + if (this.rng.getState() === seed) { + // This successful test did not use non-determinism. + // Running it one time is sufficient. + + // saveTrace(bestTrace, index, name, 'passed') + return { + name, + status: 'passed', + errors: [], + seed: seed, + frames: bestTrace.subframes, + nsamples: nsamples, + } + } + } + } + + // the test was run maxSamples times, and no errors were found + const bestTrace = this.recorder.bestTraces[0].frame + // saveTrace(bestTrace, index, name, 'passed') + return { + name, + status: 'passed', + errors: [], + seed: seed, + frames: bestTrace.subframes, + nsamples: nsamples - 1, + } + } +} + +export function evaluateExpr(ctx: Context, expr: QuintEx): Either { + const id = expr.id + if (id === 0n || !ctx.memoEnabled) { + return evaluateNewExpr(ctx, expr).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) + } + + if (ctx.memo.has(id)) { + // if (ctx.pureKeys.has(id)) { + // console.log('pure key', id, expressionToString(expr)) + // } + return ctx.memo.get(id)! + } + + const result = evaluateNewExpr(ctx, expr).mapLeft(err => + err.reference === undefined ? { ...err, reference: id } : err + ) + ctx.memo.set(id, result) + return result +} + +export function evaluateUnderDefContext( + ctx: Context, + def: LookupDefinition, + evaluate: () => Either +): Either { + if (def.kind === 'def' && def.qualifier === 'nondet' && ctx.memoEnabled) { + ctx.disableMemo() + const r = evaluateUnderDefContext(ctx, def, evaluate) + ctx.enableMemo() + return r + } + + if (!def.importedFrom || def.importedFrom.kind !== 'instance') { + return evaluate() + } + + const instance = def.importedFrom + const overrides: [bigint, Either][] = instance.overrides.map(([param, expr]) => { + const id = ctx.table.get(param.id)!.id + + return [id, evaluateExpr(ctx, expr)] + }) + + ctx.addConstants(overrides) + ctx.addNamespaces(def.namespaces) + ctx.disableMemo() // We could have one memo per constant + + const result = evaluate() + + ctx.removeConstants(overrides) + ctx.removeNamespaces(def.namespaces) + ctx.enableMemo() + + return result +} + +function evaluateDef(ctx: Context, def: LookupDefinition): Either { + return evaluateUnderDefContext(ctx, def, () => { + switch (def.kind) { + case 'def': + return evaluateExpr(ctx, def.expr) + case 'param': { + const result = ctx.params.get(def.id) + + if (!result) { + return left({ code: 'QNT501', message: `Parameter ${def.name} not set in context` }) + } + return result + } + case 'var': { + ctx.discoverVar(def.id, def.name) + const result = ctx.getVar(def.id) + + if (!result) { + return left({ code: 'QNT502', message: `Variable ${def.name} not set` }) + } + return result + } + case 'const': + const constValue = ctx.consts.get(def.id) + if (!constValue) { + return left({ code: 'QNT503', message: `Constant ${def.name}(id: ${def.id}) not set` }) + } + return constValue + + default: + return left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) + } + }) +} + +function evaluateNewExpr(ctx: Context, expr: QuintEx): Either { + switch (expr.kind) { + case 'int': + case 'bool': + case 'str': + case 'lambda': + // These are already values, just return them + return right(rv.fromQuintEx(expr)) + case 'name': + const def = ctx.table.get(expr.id) + if (!def) { + return builtinValue(expr.name) + } + return evaluateDef(ctx, def) + case 'app': + // In these special ops, we don't want to evaluate the arguments before evaluating application + if (lazyOps.includes(expr.opcode)) { + return lazyBuiltinLambda(ctx, expr.opcode)(expr.args) + } + + const op = lambdaForName(ctx, expr.id, expr.opcode) + const args = expr.args.map(arg => evaluateExpr(ctx, arg)) + if (args.some(arg => arg.isLeft())) { + return args.find(arg => arg.isLeft())! + } + return op(args.map(a => a.unwrap())) + case 'let': + return evaluateExpr(ctx, expr.expr) + } +} + +function lambdaForName( + ctx: Context, + id: bigint, + name: string +): (args: RuntimeValue[]) => Either { + if (!ctx.table.has(id)) { + return builtinLambda(ctx, name) + } + + const def = ctx.table.get(id)! + const value = evaluateDef(ctx, def) + if (value.isLeft()) { + return _ => value + } + return value.value.toArrow(ctx) +} + +export function isTrue(value: Either): boolean { + return value.isRight() && value.value.toBool() === true +} diff --git a/quint/src/runtime/impl/operatorEvaluator.ts b/quint/src/runtime/impl/operatorEvaluator.ts deleted file mode 100644 index 3c22f17d0..000000000 --- a/quint/src/runtime/impl/operatorEvaluator.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* ---------------------------------------------------------------------------------- - * Copyright 2022-2024 Informal Systems - * Licensed under the Apache License, Version 2.0. - * See LICENSE in the project root for license information. - * --------------------------------------------------------------------------------- */ - -/** - * Stateless implementations for evaluating more complex operators, and helpers - * to operator evaluation. - * - * @author Igor Konnov, Gabriela Moreira - * - * @module - */ - -import { Either, left, mergeInMany, right } from '@sweet-monads/either' -import { Callable, Computable, EvaluationResult } from '../runtime' -import { RuntimeValue, rv } from './runtimeValue' -import { strict as assert } from 'assert' -import { List } from 'immutable' -import { QuintError, quintErrorToString } from '../../quintError' - -// pop nargs computable values, pass them the 'fun' function, and -// push the combined computable value on the stack -export function applyFun( - sourceId: bigint, - fun: (..._args: RuntimeValue[]) => EvaluationResult, - args: Computable[] -): Computable { - // produce the new computable value - return { - eval: () => { - try { - // compute the values of the arguments at this point - const values = args.map(a => a.eval()) - // if they are all defined, apply the function 'fun' to the arguments - return mergeInMany(values) - .mapLeft((errors): QuintError => ({ code: 'QNT501', message: errors.map(quintErrorToString).join('\n') })) - .chain(vs => fun(...vs.map(v => v as RuntimeValue))) - } catch (error) { - const msg = error instanceof Error ? error.message : 'unknown error' - return left({ code: 'QNT501', message: msg, reference: sourceId }) - } - }, - } -} - -export function applyFold(order: 'fwd' | 'rev', args: Computable[]): Computable { - const [iterableComp, initialComp, callableComp] = args - return { - eval: () => { - const iterableResult = iterableComp.eval() - const initialResult = initialComp.eval() - - const callable = callableComp as Callable - assert(callable.nparams === 2) - - if (iterableResult.isLeft()) { - return iterableResult - } - - const iterable = Array.from(iterableResult.value as RuntimeValue as Iterable) - - const reducer = (acc: EvaluationResult, val: RuntimeValue) => { - if (order === 'fwd') { - return callable.eval([acc, right(val)]) - } else { - return callable.eval([right(val), acc]) - } - } - - if (order === 'fwd') { - return iterable.reduce(reducer, initialResult) - } else { - return iterable.reduceRight(reducer, initialResult) - } - }, - } -} -// Access a list via an index -export function getListElem(list: List, idx: number): EvaluationResult { - if (idx >= 0n && idx < list.size) { - const elem = list.get(Number(idx)) - if (elem) { - return right(elem) - } - } - - return left({ code: 'QNT510', message: `Out of bounds, nth(${idx})` }) -} -// Update a list via an index -export function updateList(list: List, idx: number, value: RuntimeValue): EvaluationResult { - if (idx >= 0n && idx < list.size) { - return right(rv.mkList(list.set(Number(idx), value))) - } else { - return left({ code: 'QNT510', message: `Out of bounds, replaceAt(..., ${idx}, ...)` }) - } -} - -// slice a list -export function sliceList(list: List, start: number, end: number) { - return right(rv.mkList(list.slice(start, end))) -} - -// translate one of the Boolean operators with short-circuiting: -// - or { A, ..., C } -// - and { A, ..., C } -// - A implies B -export function applyBoolOp( - defaultValue: RuntimeValue, - shortCircuit: (no: number, r: boolean) => RuntimeValue | undefined, - args: Computable[] -): Computable { - return { - eval: () => { - let result: EvaluationResult = right(defaultValue) - // Evaluate arguments iteratively. - // Stop as soon as shortCircuit tells us to stop. - let no = 0 - for (const arg of args) { - // either the argument is evaluated to a Boolean, or fails - result = arg.eval() - if (result.isLeft()) { - return result - } - - // if shortCircuit returns a value, return the value immediately - const b = shortCircuit(no, (result.value as RuntimeValue).toBool()) - if (b) { - return right(b) - } - no += 1 - } - - return result - }, - } -} - -/** - * A generalized application of a one-argument Callable to a set-like - * runtime value, as required by `exists`, `forall`, `map`, and `filter`. - * - * This method expects `compStack` to look like follows: - * - * - `(top)` translated lambda, as `Callable`. - * - * - `(top - 1)`: a set-like value to iterate over, as `Computable`. - * - * The method evaluates the Callable for each element of the iterable value - * and either produces `none`, if evaluation failed for one of the elements, - * or it applies `mapResultAndElems` to the pairs that consists of the Callable - * result and the original element of the iterable value. - * The final result is stored on the stack. - */ -export function mapLambdaThenReduce( - sourceId: bigint, - reduceFunction: (_array: Array<[RuntimeValue, RuntimeValue]>) => RuntimeValue, - args: Computable[] -): Computable { - const [setComp, callableComp] = args - const callable = callableComp as Callable - // apply the lambda to a single element of the set - const evaluateElem = (elem: RuntimeValue): Either => { - // evaluate the predicate against the actual arguments - const result = callable.eval([right(elem)]) - return result.map(result => [result as RuntimeValue, elem]) - } - return applyFun( - sourceId, - (iterable: Iterable): EvaluationResult => { - const lambdaApplicationResults = mergeInMany(Array.from(iterable).map(value => evaluateElem(value))) - return lambdaApplicationResults - .mapLeft((errors): QuintError => { - return { code: 'QNT501', message: errors.map(quintErrorToString).join('\n') } - }) - .map(array => reduceFunction(array)) - }, - [setComp] - ) -} diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index c9f95f5bc..418bbc164 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -64,17 +64,16 @@ import { List, Map, OrderedMap, Set, ValueObject, hash, is as immutableIs } from import { Maybe, just, merge, none } from '@sweet-monads/maybe' import { strict as assert } from 'assert' -import { IdGenerator } from '../../idGenerator' +import { IdGenerator, zerog } from '../../idGenerator' import { expressionToString } from '../../ir/IRprinting' import { Callable, EvalResult } from '../runtime' -import { QuintEx } from '../../ir/quintIr' +import { QuintEx, QuintLambdaParameter, QuintName } from '../../ir/quintIr' import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { toMaybe } from './base' - -/** The default entry point of this module */ -export default rv +import { evaluateExpr } from './evaluator' +import { Context } from './Context' /** * A factory of runtime values that should be used to instantiate new values. @@ -96,8 +95,8 @@ export const rv = { * @param value an integer value * @return a new runtime value that carries the integer value */ - mkInt: (value: bigint): RuntimeValue => { - return new RuntimeValueInt(value) + mkInt: (value: bigint | number): RuntimeValue => { + return new RuntimeValueInt(BigInt(value)) }, /** @@ -211,8 +210,8 @@ export const rv = { * @param last the maximal poitn of the interval (inclusive) * @return a new runtime value that the interval */ - mkInterval: (first: bigint, last: bigint): RuntimeValue => { - return new RuntimeValueInterval(first, last) + mkInterval: (first: bigint | number, last: bigint | number): RuntimeValue => { + return new RuntimeValueInterval(BigInt(first), BigInt(last)) }, /** @@ -249,15 +248,31 @@ export const rv = { /** * Make a runtime value that represents a lambda. * - * @param nparams the number of lambda parameters - * @param callable a callable that evaluates the lambda + * @param params the lambda parameters + * @param body the lambda body expression * @returns a runtime value of lambda */ - mkLambda: (nparams: number, callable: Callable) => { - return new RuntimeValueLambda(nparams, callable) + mkLambda: (params: QuintLambdaParameter[], body: QuintEx) => { + return new RuntimeValueLambda(params, body) + }, + + fromQuintEx: (ex: QuintEx): RuntimeValue => { + const v = fromQuintEx(ex) + if (v.isJust()) { + return v.value + } else { + throw new Error(`Cannot convert ${expressionToString(ex)} to a runtime value`) + } + }, + + toQuintEx: (value: RuntimeValue): QuintEx => { + return value.toQuintEx(zerog) }, } +/** The default entry point of this module */ +export default rv + /** * Get a ground expression, that is, an expression * that contains only literals and constructors, and @@ -315,11 +330,22 @@ export function fromQuintEx(ex: QuintEx): Maybe { return just(rv.mkRecord(pairs)) } + case 'variant': { + const label = (ex.args[0] as QuintName).name + return fromQuintEx(ex.args[1]).map(v => rv.mkVariant(label, v)) + } + default: // no other case should be possible return none() } + case 'lambda': + if (ex.kind !== 'lambda') { + return none() + } + return just(rv.mkLambda(ex.params, ex.expr)) + default: // no other case should be possible return none() @@ -408,6 +434,27 @@ export interface RuntimeValue extends EvalResult, ValueObject, Iterable Either + + /** + * If the result is a variant, return the label and the value. + * + * @return the label and the value of the variant. + */ + toVariant(): [string, RuntimeValue] + /** * If the result is set-like, does it contain contain a value? * If the result is not set-like, return false. @@ -515,7 +562,7 @@ abstract class RuntimeValueBase implements RuntimeValue { if (this instanceof RuntimeValueRecord) { return this.map } else { - throw new Error('Expected a record value') + throw new Error(`Expected a record value but got ${expressionToString(this.toQuintEx(zerog))}`) } } @@ -551,6 +598,54 @@ abstract class RuntimeValueBase implements RuntimeValue { } } + toTuple2(): [RuntimeValue, RuntimeValue] { + if (this instanceof RuntimeValueTupleOrList) { + const list = this.list + + if (list.size === 2) { + return [list.get(0)!, list.get(1)!] + } + } + throw new Error('Expected a 2-tuple') + } + + toArrow(ctx: Context): (args: RuntimeValue[]) => Either { + if (!(this instanceof RuntimeValueLambda)) { + throw new Error('Expected a lambda value') + } + + const lam = this as RuntimeValueLambda + + return (args: RuntimeValue[]) => { + if (lam.params.length !== args.length) { + return left({ + code: 'QNT506', + message: `Lambda expects ${lam.params.length} arguments, but got ${args.length}`, + }) + } + const paramEntries: [bigint, RuntimeValue][] = lam.params.map((param, i) => { + // console.log('param', param.id, param.name, 'set to', valueToString(args[i])) + return [param.id, args[i]] + }) + + ctx.addParams(paramEntries) + ctx.disableMemo() + const result = evaluateExpr(ctx, lam.body) + ctx.removeParams(paramEntries) + ctx.enableMemo() + + return result + } + } + + toVariant(): [string, RuntimeValue] { + if (this instanceof RuntimeValueVariant) { + return [(this as RuntimeValueVariant).label, (this as RuntimeValueVariant).value] + } else { + throw new Error('Expected a variant value') + } + } + contains(elem: RuntimeValue): boolean { // the default search is done via iteration, which is the worst case let found = false @@ -1547,18 +1642,20 @@ class RuntimeValueInfSet extends RuntimeValueBase implements RuntimeValue { * * RuntimeValueLambda cannot be compared with other values. */ -export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue, Callable { - nparams: number - callable: Callable +export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue { + params: QuintLambdaParameter[] + body: QuintEx - constructor(nparams: number, callable: Callable) { + constructor(params: QuintLambdaParameter[], body: QuintEx) { super(false) - this.nparams = nparams - this.callable = callable + this.params = params + this.body = body } eval(args?: any[]) { - return this.callable.eval(args) + return () => { + throw new Error('Not implemented') + } } toQuintEx(gen: IdGenerator): QuintEx { @@ -1568,15 +1665,9 @@ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue return { id: gen.nextId(), kind: 'lambda', - params: Array.from(Array(this.nparams).keys()).map(i => { - return { id: gen.nextId(), name: `_a${i}` } - }), + params: this.params, qualifier: 'def', - expr: { - kind: 'str', - value: `lambda_${this.nparams}_params`, - id: gen.nextId(), - }, + expr: this.body, } } } diff --git a/quint/src/runtime/impl/trace.ts b/quint/src/runtime/impl/trace.ts index 987dc548d..8bee2471e 100644 --- a/quint/src/runtime/impl/trace.ts +++ b/quint/src/runtime/impl/trace.ts @@ -13,20 +13,20 @@ */ import { List } from 'immutable' -import { RuntimeValue } from './runtimeValue' +import { QuintEx } from '../../ir/quintIr' export class Trace { - private states: List = List() + private states: List = List() - get(): RuntimeValue[] { + get(): QuintEx[] { return this.states.toArray() } - reset(values: RuntimeValue[] = []) { + reset(values: QuintEx[] = []) { this.states = List(values) } - extend(state: RuntimeValue) { + extend(state: QuintEx) { this.states = this.states.push(state) } } diff --git a/quint/src/runtime/runtime.ts b/quint/src/runtime/runtime.ts index d0f466a65..da3921190 100644 --- a/quint/src/runtime/runtime.ts +++ b/quint/src/runtime/runtime.ts @@ -12,18 +12,14 @@ * See LICENSE in the project root for license information. */ -import { Maybe } from '@sweet-monads/maybe' - import { ValueObject } from 'immutable' import { QuintEx } from '../ir/quintIr' import { IdGenerator } from '../idGenerator' -import { rv } from './impl/runtimeValue' -import { Either, left, right } from '@sweet-monads/either' +import { Either, left } from '@sweet-monads/either' import { QuintError } from '../quintError' -import { toMaybe } from './impl/base' /** * Evaluation result. @@ -84,34 +80,26 @@ export interface Register extends Computable { // register kind kind: ComputableKind // register is a placeholder where iterators can put their values - registerValue: Maybe + registerValue: Either } /** * Create an object that implements Register. */ -export function mkRegister( - kind: ComputableKind, - registerName: string, - initValue: Maybe, - errorForMissing: QuintError -): Register { +export function mkRegister(kind: ComputableKind, registerName: string, initValue: Either): Register { const reg: Register = { name: registerName, kind, registerValue: initValue, // first, define a fruitless eval, as we cannot refer to registerValue yet eval: () => { - return left(errorForMissing) + return initValue }, } // override `eval`, as we can use `reg` now reg.eval = () => { // computing a register just evaluates to the contents that it stores - if (reg.registerValue.isNone()) { - return left(errorForMissing) - } - return right(reg.registerValue.value) + return reg.registerValue } return reg @@ -128,33 +116,6 @@ export interface Callable extends Computable { nparams: number } -/** - * Create an object that implements Callable. - */ -export function mkCallable(registers: Register[], body: Computable): Callable { - const callable: Callable = { - nparams: registers.length, - eval: () => body.eval(), - } - callable.eval = args => { - if (registers.length === 0) { - // simply evaluate the body, no parameters are needed - return body.eval() - } - if (args && args.length >= registers.length) { - // All parameters are passed via `args`. Store them in the registers. - registers.forEach((r, i) => (r.registerValue = toMaybe(args[i]))) - // Evaluate the body under for the registers set to `args`. - return body.eval() - } else { - // The lambda is evaluated without giving the arguments. - // All we can do is to return this lambda as a runtime value. - return right(rv.mkLambda(registers.length, callable)) - } - } - return callable -} - /** * An implementation of Computable that always fails. */ diff --git a/quint/src/runtime/testing.ts b/quint/src/runtime/testing.ts index 25cf87ea9..0ca0538fb 100644 --- a/quint/src/runtime/testing.ts +++ b/quint/src/runtime/testing.ts @@ -8,18 +8,11 @@ * See LICENSE in the project root for license information. */ -import { Either, left, mergeInMany, right } from '@sweet-monads/either' +import { QuintEx } from '../ir/quintIr' -import { QuintEx, QuintOpDef } from '../ir/quintIr' - -import { CompilationContext, CompilationState, compile } from './compile' -import { zerog } from './../idGenerator' -import { LookupTable } from '../names/base' -import { Computable, kindName } from './runtime' -import { ExecutionFrame, newTraceRecorder } from './trace' +import { ExecutionFrame } from './trace' import { Rng } from '../rng' import { QuintError } from '../quintError' -import { newEvaluationState, toMaybe } from './impl/base' /** * Various settings to be passed to the testing framework. @@ -62,174 +55,3 @@ export interface TestResult { */ nsamples: number } - -/** - * Run a test suite of a single module. - * - * @param modules Quint modules in the intermediate representation - * @param main the module that should be used as a state machine - * @param sourceMap source map as produced by the parser - * @param lookupTable lookup table as produced by the parser - * @param analysisOutput the maps produced by the static analysis - * @param options misc test options - * @returns the `right(results)` of running the tests if the tests could be - * run, or `left(errors)` with any compilation or analysis errors that - * prevent the tests from running - */ -export function compileAndTest( - compilationState: CompilationState, - mainName: string, - lookupTable: LookupTable, - options: TestOptions -): Either { - const main = compilationState.modules.find(m => m.name === mainName) - if (!main) { - return left([{ code: 'QNT405', message: `Main module ${mainName} not found` }]) - } - - const recorder = newTraceRecorder(options.verbosity, options.rng) - const ctx = compile( - compilationState, - newEvaluationState(recorder), - lookupTable, - options.rng.next, - false, - main.declarations - ) - - const ctxErrors = ctx.syntaxErrors.concat(ctx.compileErrors, ctx.analysisErrors) - if (ctxErrors.length > 0) { - // In principle, these errors should have been caught earlier. - // But if they did not, return immediately. - return left(ctxErrors) - } - - const saveTrace = (trace: ExecutionFrame, index: number, name: string, status: string) => { - // Save the best traces that are reported by the recorder: - // If a test failed, it is the first failing trace. - // Otherwise, it is the longest trace explored by the test. - const states = trace.args.map(e => e.toQuintEx(zerog)) - options.onTrace( - index, - name, - status, - ctx.evaluationState.vars.map(v => v.name), - states - ) - } - - const testDefs = main.declarations.filter(d => d.kind === 'def' && options.testMatch(d.name)) as QuintOpDef[] - - return mergeInMany( - testDefs.map((def, index) => { - return getComputableForDef(ctx, def).map(comp => { - recorder.clear() - const name = def.name - // save the initial seed - let seed = options.rng.getState() - - let nsamples = 1 - // run up to maxSamples, stop on the first failure - for (; nsamples <= options.maxSamples; nsamples++) { - // record the seed value - seed = options.rng.getState() - recorder.onRunCall() - // reset the trace - ctx.evaluationState.trace.reset() - // run the test - const result = comp.eval() - // extract the trace - const trace = ctx.evaluationState.trace.get() - - if (trace.length > 0) { - recorder.onRunReturn(toMaybe(result), trace) - } else { - // Report a non-critical error - console.error('Missing a trace') - recorder.onRunReturn(toMaybe(result), []) - } - - const bestTrace = recorder.bestTraces[0].frame - // evaluate the result - if (result.isLeft()) { - // if the test failed, return immediately - return { - name, - status: 'failed', - errors: ctx.getRuntimeErrors().concat(result.value), - seed, - frames: bestTrace.subframes, - nsamples: nsamples, - } - } - - const ex = result.value.toQuintEx(zerog) - if (ex.kind !== 'bool') { - // if the test returned a malformed result, return immediately - return { - name, - status: 'ignored', - errors: [], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, - } - } - - if (!ex.value) { - // if the test returned false, return immediately - const error: QuintError = { - code: 'QNT511', - message: `Test ${name} returned false`, - reference: def.id, - } - saveTrace(bestTrace, index, name, 'failed') - return { - name, - status: 'failed', - errors: [error], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, - } - } else { - if (options.rng.getState() === seed) { - // This successful test did not use non-determinism. - // Running it one time is sufficient. - saveTrace(bestTrace, index, name, 'passed') - return { - name, - status: 'passed', - errors: [], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, - } - } - } - } - - // the test was run maxSamples times, and no errors were found - const bestTrace = recorder.bestTraces[0].frame - saveTrace(bestTrace, index, name, 'passed') - return { - name, - status: 'passed', - errors: [], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples - 1, - } - }) - }) - ) -} - -function getComputableForDef(ctx: CompilationContext, def: QuintOpDef): Either { - const comp = ctx.evaluationState.context.get(kindName('callable', def.id)) - if (comp) { - return right(comp) - } else { - return left({ code: 'QNT501', message: `Cannot find computable for ${def.name}`, reference: def.id }) - } -} diff --git a/quint/src/runtime/trace.ts b/quint/src/runtime/trace.ts index e2efa9311..0a2ab5e85 100644 --- a/quint/src/runtime/trace.ts +++ b/quint/src/runtime/trace.ts @@ -8,14 +8,13 @@ * See LICENSE in the project root for license information. */ -import { Maybe, just, none } from '@sweet-monads/maybe' import { strict as assert } from 'assert' -import { QuintApp } from '../ir/quintIr' -import { EvalResult } from './runtime' +import { QuintApp, QuintEx } from '../ir/quintIr' import { verbosity } from './../verbosity' import { Rng } from './../rng' -import { rv } from './impl/runtimeValue' +import { Either, left, right } from '@sweet-monads/either' +import { QuintError } from '../quintError' /** * A snapshot of how a single operator (e.g., an action) was executed. @@ -39,11 +38,11 @@ export interface ExecutionFrame { /** * The actual runtime values that were used in the call. */ - args: EvalResult[] + args: QuintEx[] /** * An optional result of the execution. */ - result: Maybe + result: Either /** * The frames of the operators that were called by this operator. */ @@ -55,6 +54,8 @@ export interface Trace { seed: bigint } +const emptyFrameError: QuintError = { code: 'QNT501', message: 'empty frame' } + /** * A listener that receives events in the course of Quint evaluation. * This listener may be used to collect a trace, or to record profiling data. @@ -76,7 +77,7 @@ export interface ExecutionListener { * @param args the actual arguments obtained in evaluation * @param result optional result of the evaluation */ - onUserOperatorReturn(app: QuintApp, args: EvalResult[], result: Maybe): void + onUserOperatorReturn(app: QuintApp, args: QuintEx[], result: Either): void /** * This callback is called *before* one of the arguments of `any {...}` @@ -115,7 +116,7 @@ export interface ExecutionListener { * @param oldState the old state that is about to be discarded * @param newState the new state, from which the execution continues */ - onNextState(oldState: EvalResult, newState: EvalResult): void + onNextState(oldState: QuintEx, newState: QuintEx): void /** * This callback is called when a new run is executed, @@ -132,7 +133,7 @@ export interface ExecutionListener { * - finished after finding a violation, `just(mkBool(false))` * @param trace the array of produced states (each state is a record) */ - onRunReturn(outcome: Maybe, trace: EvalResult[]): void + onRunReturn(outcome: Either, trace: QuintEx[]): void } /** @@ -140,13 +141,13 @@ export interface ExecutionListener { */ export const noExecutionListener: ExecutionListener = { onUserOperatorCall: (_app: QuintApp) => {}, - onUserOperatorReturn: (_app: QuintApp, _args: EvalResult[], _result: Maybe) => {}, + onUserOperatorReturn: (_app: QuintApp, _args: QuintEx[], _result: Either) => {}, onAnyOptionCall: (_anyExpr: QuintApp, _position: number) => {}, onAnyOptionReturn: (_anyExpr: QuintApp, _position: number) => {}, onAnyReturn: (_noptions: number, _choice: number) => {}, - onNextState: (_oldState: EvalResult, _newState: EvalResult) => {}, + onNextState: (_oldState: QuintEx, _newState: QuintEx) => {}, onRunCall: () => {}, - onRunReturn: (_outcome: Maybe, _trace: EvalResult[]) => {}, + onRunReturn: (_outcome: Either, _trace: QuintEx[]) => {}, } /** @@ -223,7 +224,12 @@ class TraceRecorderImpl implements TraceRecorder { // For now, we cannot tell apart actions from other user definitions. // https://github.com/informalsystems/quint/issues/747 if (verbosity.hasUserOpTracking(this.verbosityLevel)) { - const newFrame = { app: app, args: [], result: none(), subframes: [] } + const newFrame: ExecutionFrame = { + app: app, + args: [], + result: 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 @@ -234,7 +240,7 @@ class TraceRecorderImpl implements TraceRecorder { const frame = this.frameStack[1] frame.app = app frame.args = [] - frame.result = none() + frame.result = left(emptyFrameError) frame.subframes = [] } else { // connect the new frame to the previous frame @@ -245,7 +251,7 @@ class TraceRecorderImpl implements TraceRecorder { } } - onUserOperatorReturn(_app: QuintApp, args: EvalResult[], result: Maybe) { + onUserOperatorReturn(_app: QuintApp, args: QuintEx[], result: Either) { if (verbosity.hasUserOpTracking(this.verbosityLevel)) { const top = this.frameStack.pop() if (top) { @@ -264,7 +270,7 @@ class TraceRecorderImpl implements TraceRecorder { const newFrame = { app: anyExpr, args: [], - result: none(), + result: left(emptyFrameError), subframes: [], } if (this.frameStack.length > 0) { @@ -303,11 +309,11 @@ class TraceRecorderImpl implements TraceRecorder { } } - onNextState(_oldState: EvalResult, _newState: EvalResult) { + onNextState(_oldState: QuintEx, _newState: QuintEx) { // introduce a new frame that is labelled with a dummy operator if (verbosity.hasUserOpTracking(this.verbosityLevel)) { const dummy: QuintApp = { id: 0n, kind: 'app', opcode: '_', args: [] } - const newFrame = { app: dummy, args: [], result: none(), subframes: [] } + const newFrame = { app: dummy, args: [], result: 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 @@ -321,7 +327,7 @@ class TraceRecorderImpl implements TraceRecorder { this.runSeed = this.rng.getState() } - onRunReturn(outcome: Maybe, trace: EvalResult[]) { + onRunReturn(outcome: Either, trace: QuintEx[]) { assert(this.frameStack.length > 0) const traceToSave = this.frameStack[0] traceToSave.result = outcome @@ -338,11 +344,11 @@ class TraceRecorderImpl implements TraceRecorder { } private sortTracesByQuality() { - const fromResult = (r: Maybe) => { - if (r.isNone()) { + const fromResult = (r: Either) => { + if (r.isLeft()) { return true } else { - const rex = r.value.toQuintEx({ nextId: () => 0n }) + const rex = r.value return rex.kind === 'bool' && !rex.value } } @@ -379,7 +385,7 @@ class TraceRecorderImpl implements TraceRecorder { // we will store the sequence of states here args: [], // the result of the trace evaluation - result: just(rv.mkBool(true)), + result: right({ id: 0n, kind: 'bool', value: true }), // and here we store the subframes for the top-level actions subframes: [], } diff --git a/quint/src/simulation.ts b/quint/src/simulation.ts index effa6e451..5e3e00898 100644 --- a/quint/src/simulation.ts +++ b/quint/src/simulation.ts @@ -8,19 +8,10 @@ * See LICENSE in the project root for license information. */ -import { Either } from '@sweet-monads/either' - -import { compileFromCode, contextNameLookup } from './runtime/compile' -import { QuintEx } from './ir/quintIr' -import { Computable } from './runtime/runtime' +import { QuintBool, QuintEx } from './ir/quintIr' import { ExecutionFrame, Trace, newTraceRecorder } from './runtime/trace' -import { IdGenerator } from './idGenerator' import { Rng } from './rng' -import { SourceLookupPath } from './parsing/sourceResolver' import { QuintError } from './quintError' -import { mkErrorMessage } from './cliCommands' -import { createFinders, formatError } from './errorReporter' -import assert from 'assert' /** * Various settings that have to be passed to the simulator to run. @@ -55,150 +46,3 @@ export interface SimulatorResult { frames: ExecutionFrame[] seed: bigint } - -function errSimulationResult(errors: QuintError[]): SimulatorResult { - return { - outcome: { status: 'error', errors }, - vars: [], - states: [], - frames: [], - seed: 0n, - } -} - -/** - * Execute a run. - * - * @param idGen a unique generator of identifiers - * @param code the source code of the modules - * @param mainStart the start index of the main module in the code - * @param mainEnd the end index of the main module in the code - * @param mainName the module that should be used as a state machine - * @param mainPath the lookup path that was used to retrieve the main module - * @param options simulator settings - * @returns either error messages (left), - or the trace as an expression (right) - */ -export function compileAndRun( - idGen: IdGenerator, - code: string, - mainStart: number, - mainEnd: number, - mainName: string, - mainPath: SourceLookupPath, - options: SimulatorOptions -): SimulatorResult { - // Once we have 'import from ...' implemented, we should pass - // a filename instead of the source code (see #8) - - // Parse the code once again, but this time include the special definitions - // that are required by the runner. - // This code should be revisited in #618. - const o = options - // Defs required by the simulator, to be added to the main module before compilation - const extraDefs = [ - `def q::test(q::nrunsArg, q::nstepsArg, q::ntracesArg, q::initArg, q::nextArg, q::invArg) = false`, - `action q::init = { ${o.init} }`, - `action q::step = { ${o.step} }`, - `val q::inv = { ${o.invariant} }`, - `val q::runResult = q::test(${o.maxSamples}, ${o.maxSteps}, ${o.numberOfTraces}, q::init, q::step, q::inv)`, - ] - - // Construct the modules' code, adding the extra definitions to the main module - const newMainModuleCode = code.slice(mainStart, mainEnd - 1) + '\n' + extraDefs.join('\n') - const codeWithExtraDefs = code.slice(0, mainStart) + newMainModuleCode + code.slice(mainEnd) - - const recorder = newTraceRecorder(options.verbosity, options.rng, options.numberOfTraces) - const ctx = compileFromCode( - idGen, - codeWithExtraDefs, - mainName, - mainPath, - recorder, - options.rng.next, - options.storeMetadata - ) - - const compilationErrors = ctx.syntaxErrors.concat(ctx.analysisErrors).concat(ctx.compileErrors) - if (compilationErrors.length > 0) { - return errSimulationResult(compilationErrors) - } - - // evaluate q::runResult, which triggers the simulator - const evaluationState = ctx.evaluationState - const res: Either = contextNameLookup(evaluationState.context, 'q::runResult', 'callable') - if (res.isLeft()) { - const errors = [{ code: 'QNT512', message: res.value }] as QuintError[] - return errSimulationResult(errors) - } else { - res.value.eval() - } - - const topTraces: Trace[] = recorder.bestTraces - const vars = evaluationState.vars.map(v => v.name) - - topTraces.forEach((trace, index) => { - const maybeEvalResult = trace.frame.result - assert(maybeEvalResult.isJust(), 'invalid simulation failed to produce a result') - const quintExResult = maybeEvalResult.value.toQuintEx(idGen) - assert(quintExResult.kind === 'bool', 'invalid simulation produced non-boolean value ') - const simulationSucceeded = quintExResult.value - const status = simulationSucceeded ? 'ok' : 'violation' - const states = trace.frame.args.map(e => e.toQuintEx(idGen)) - - options.onTrace(index, status, vars, states) - }) - - const topFrame = topTraces[0].frame - const seed = topTraces[0].seed - // Validate required outcome of correct simulation - const maybeEvalResult = topFrame.result - assert(maybeEvalResult.isJust(), 'invalid simulation failed to produce a result') - const quintExResult = maybeEvalResult.value.toQuintEx(idGen) - assert(quintExResult.kind === 'bool', 'invalid simulation produced non-boolean value ') - const simulationSucceeded = quintExResult.value - - const states = topFrame.args.map(e => e.toQuintEx(idGen)) - const frames = topFrame.subframes - - const runtimeErrors = ctx.getRuntimeErrors() - if (runtimeErrors.length > 0) { - // FIXME(#1052) we shouldn't need to do this if the error id was not some non-sense generated in `compileFromCode` - // The evaluated code source is not included in the context, so we crete a version with it for the error reporter - const code = new Map([...ctx.compilationState.sourceCode.entries(), [mainPath.normalizedPath, codeWithExtraDefs]]) - const finders = createFinders(code) - - const locatedErrors = runtimeErrors.map(error => ({ - code: error.code, - // Include the location information (locs) in the error message - this - // is the hacky part, as it should only be included at the CLI level - message: formatError(code, finders, { - // This will create the `locs` attribute and an explanation - ...mkErrorMessage(ctx.compilationState.sourceMap)(error), - // We override the explanation to keep the original one to avoid - // duplication, since `mkErrorMessage` will be called again at the CLI - // level. `locs` won't be touched then because this error doesn't - // include a `reference` field - explanation: error.message, - }), - })) - - // This should be kept after the hack is removed - return { - vars, - states, - frames, - seed, - outcome: { status: 'error', errors: locatedErrors }, - } - } else { - const status = simulationSucceeded ? 'ok' : 'violation' - return { - vars, - states, - frames, - seed, - outcome: { status }, - } - } -} diff --git a/quint/test/runtime/trace.test.ts b/quint/test/runtime/trace.test.ts index ef210f7cf..fb6799830 100644 --- a/quint/test/runtime/trace.test.ts +++ b/quint/test/runtime/trace.test.ts @@ -1,20 +1,23 @@ import { describe, it } from 'mocha' import { assert } from 'chai' -import { none } from '@sweet-monads/maybe' import { newTraceRecorder } from '../../src/runtime/trace' import { QuintApp } from '../../src/ir/quintIr' import { verbosity } from '../../src/verbosity' import { newRng } from '../../src/rng' +import { QuintError } from '../../src/quintError' +import { left } from '@sweet-monads/either' + +const emptyFrameError: QuintError = { code: 'QNT501', message: 'empty frame' } describe('newTraceRecorder', () => { it('one layer', () => { const rec = newTraceRecorder(verbosity.maxVerbosity, newRng()) const A: QuintApp = { id: 0n, kind: 'app', opcode: 'A', args: [] } rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) const trace = rec.currentFrame assert(trace.subframes.length === 2) assert(trace.subframes[0].app === A) @@ -30,12 +33,12 @@ describe('newTraceRecorder', () => { // (A calls (B, after that it calls A)), after that another A is called rec.onUserOperatorCall(A) rec.onUserOperatorCall(B) - rec.onUserOperatorReturn(B, [], none()) + rec.onUserOperatorReturn(B, [], left(emptyFrameError)) rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) const trace = rec.currentFrame assert(trace.subframes.length === 2) assert(trace.subframes[0].app === A) @@ -59,33 +62,33 @@ describe('newTraceRecorder', () => { } // A() rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) // any { rec.onAnyOptionCall(anyEx, 0) // A() rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) rec.onAnyOptionReturn(anyEx, 0) rec.onAnyOptionCall(anyEx, 1) // B() rec.onUserOperatorCall(B) - rec.onUserOperatorReturn(B, [], none()) + rec.onUserOperatorReturn(B, [], left(emptyFrameError)) // C() rec.onUserOperatorCall(C) - rec.onUserOperatorReturn(C, [], none()) + rec.onUserOperatorReturn(C, [], left(emptyFrameError)) rec.onAnyOptionReturn(anyEx, 1) rec.onAnyOptionCall(anyEx, 2) // C() rec.onUserOperatorCall(C) - rec.onUserOperatorReturn(C, [], none()) + rec.onUserOperatorReturn(C, [], left(emptyFrameError)) rec.onAnyOptionReturn(anyEx, 2) rec.onAnyReturn(3, 1) // } // any rec.onUserOperatorCall(A) - rec.onUserOperatorReturn(A, [], none()) + rec.onUserOperatorReturn(A, [], left(emptyFrameError)) const trace = rec.currentFrame assert(trace.subframes.length === 4) From 36bf10eca625a13a550eb3c84f3f924603c8cdfa Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 20 Aug 2024 15:58:21 -0300 Subject: [PATCH 02/37] Fix many evaluation issues --- quint/src/cli.ts | 2 +- quint/src/cliCommands.ts | 6 +- quint/src/repl.ts | 13 ++- quint/src/runtime/impl/Context.ts | 127 +++++++++++++++++++++++++ quint/src/runtime/impl/VarStorage.ts | 35 +++++++ quint/src/runtime/impl/builtins.ts | 21 ++-- quint/src/runtime/impl/evaluator.ts | 80 ++++++++++++---- quint/src/runtime/impl/runtimeValue.ts | 53 ++++++----- 8 files changed, 281 insertions(+), 56 deletions(-) create mode 100644 quint/src/runtime/impl/Context.ts create mode 100644 quint/src/runtime/impl/VarStorage.ts diff --git a/quint/src/cli.ts b/quint/src/cli.ts index c22ec696f..819cc31d9 100755 --- a/quint/src/cli.ts +++ b/quint/src/cli.ts @@ -256,7 +256,7 @@ const runCmd = { .option('invariant', { desc: 'invariant to check: a definition name or an expression', type: 'string', - default: ['true'], + default: 'true', }) .option('seed', { desc: 'random seed to use for non-deterministic choice', diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index b6350c62d..11adf5ffc 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -578,9 +578,9 @@ export async function runSimulator(prev: TypecheckedStage): Promise = new Map() const idGen = newIdGenerator() - const { modules, table, sourceMap, errors } = parse(idGen, mainPath.toSourceName(), mainPath, modulesText, sourceCode) + const { modules, table, resolver, sourceMap, errors } = parse( + idGen, + mainPath.toSourceName(), + mainPath, + modulesText, + sourceCode + ) // On errors, we'll produce the computational context up to this point const [analysisErrors, analysisOutput] = analyzeModules(table, modules) @@ -546,6 +554,9 @@ function tryEvalModule(out: writer, state: ReplState, mainName: string): boolean return false } + resolver.switchToModule(mainName) + state.nameResolver = resolver + state.evaluator.updateTable(table) return true diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts new file mode 100644 index 000000000..c5d9b9b77 --- /dev/null +++ b/quint/src/runtime/impl/Context.ts @@ -0,0 +1,127 @@ +import { Either, left, right } from '@sweet-monads/either' +import { isEqual, takeRight } from 'lodash' +import { LookupTable } from '../../names/base' +import { QuintError } from '../../quintError' +import { RuntimeValue } from './runtimeValue' +import { TraceRecorder } from '../trace' +import { Map, List, is } from 'immutable' +import { VarStorage } from './VarStorage' + +export interface NondetPick { + name: string + value: Either +} + +export class Context { + public memo: Map> = Map() + public memoEnabled: boolean = true + public params: Map> = Map() + public consts: Map> = Map() + public namespaces: List = List() + public varStorage: VarStorage = new VarStorage() + public rand: (n: bigint) => bigint + public table: LookupTable + public pureKeys: Set = new Set() + public nondetPicks: Map = Map() + public recorder: TraceRecorder + + private constHistory: Map>[] = [] + private paramHistory: Map>[] = [] + private namespacesHistory: List[] = [] + + constructor(table: LookupTable, recorder: TraceRecorder, rand: (n: bigint) => bigint) { + this.table = table + this.recorder = recorder + this.rand = rand + } + + reset() { + this.memo = Map() + this.params = Map() + this.varStorage = new VarStorage() + } + + discoverVar(id: bigint, name: string) { + this.varStorage.varNames.set(id, name) + } + + getVar(id: bigint): Either { + const varName = this.varWithNamespaces(id) + const key = [id, varName].join('#') + const result = this.varStorage.vars.get(key) + // console.log('getting', id, varName, result) + if (!result) { + return left({ code: 'QNT502', message: `Variable ${varName} not set` }) + } + + return result + } + + setNextVar(id: bigint, value: RuntimeValue) { + const varName = this.varWithNamespaces(id) + // console.log('setting', id, varName, value) + const key = [id, varName].join('#') + this.varStorage.nextVars.set(key, right(value)) + } + + private varWithNamespaces(id: bigint): string { + const revertedNamespaces = this.namespaces.slice().reverse() + return revertedNamespaces.concat([this.varStorage.varNames.get(id)!] || []).join('::') + } + + addParams(params: [bigint, RuntimeValue][]) { + this.paramHistory.push(this.params) + this.params = this.params.merge(params.map(([id, param]) => [id, right(param)])) + } + + removeParams() { + this.params = this.paramHistory.pop()! + } + + clearMemo() { + this.memo = Map() + } + + disableMemo() { + this.memoEnabled = false + } + + enableMemo() { + this.memoEnabled = true + } + + withMemo(f: () => Either) { + const memoStateBefore = this.memoEnabled + this.memoEnabled = true + const result = f() + this.memoEnabled = memoStateBefore + return result + } + + addConstants(consts: Map>) { + this.constHistory.push(this.consts) + this.consts = this.consts.merge(consts) + } + + removeConstants() { + this.consts = this.constHistory.pop()! + } + + addNamespaces(namespaces: List | undefined) { + this.namespacesHistory.push(this.namespaces) + if (is(this.namespaces.take(namespaces?.size ?? 0), namespaces)) { + // Redundant namespaces, nothing to add + return + } + + this.namespaces = this.namespaces.concat(namespaces || []) + } + + removeNamespaces() { + this.namespaces = this.namespacesHistory.pop()! + } + + constsSnapshot(): [bigint, Either][] { + return [...this.consts.entries()] + } +} diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts new file mode 100644 index 000000000..b7c16bfe0 --- /dev/null +++ b/quint/src/runtime/impl/VarStorage.ts @@ -0,0 +1,35 @@ +import { Either } from '@sweet-monads/either' +import { QuintError } from '../../quintError' +import { RuntimeValue, rv } from './runtimeValue' +import { QuintEx, QuintStr } from '../../ir/quintIr' +import { Map as ImmutableMap } from 'immutable' + +export class VarStorage { + public vars: ImmutableMap> = ImmutableMap() + public nextVars: Map> = new Map() + public varNames: Map = new Map() + + shiftVars() { + this.vars = ImmutableMap(this.nextVars) + this.nextVars = new Map() + } + + asRecord(): QuintEx { + return { + id: 0n, + kind: 'app', + opcode: 'Rec', + args: [...this.vars.entries()] + .map(([key, value]) => { + const [id, varName] = key.split('#') + const nameEx: QuintStr = { id: BigInt(id), kind: 'str', value: varName } + return [nameEx, rv.toQuintEx(value.unwrap())] + }) + .flat(), + } + } + + nextVarsSnapshot(): Map> { + return this.nextVars + } +} diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index dd4240ca4..f929b262a 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -157,7 +157,10 @@ export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) = } const [_caseLabel, caseElim] = caseForVariant - const elim = rv.fromQuintEx(caseElim).toArrow(ctx) + if (caseElim.kind !== 'lambda') { + return left({ code: 'QNT505', message: `Expected lambda in matchVariant` }) + } + const elim = rv.mkLambda(caseElim.params, caseElim.expr, ctx).toArrow() return elim([value]) }) } @@ -373,11 +376,11 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) case 'to': return args => right(rv.mkInterval(args[0].toInt(), args[1].toInt())) case 'fold': - return args => applyFold('fwd', args[0].toSet(), args[1], args[2].toArrow(ctx)) + return args => applyFold('fwd', args[0].toSet(), args[1], args[2].toArrow()) case 'foldl': - return args => applyFold('fwd', args[0].toList(), args[1], args[2].toArrow(ctx)) + return args => applyFold('fwd', args[0].toList(), args[1], args[2].toArrow()) case 'foldr': - return args => applyFold('rev', args[0].toList(), args[1], args[2].toArrow(ctx)) + return args => applyFold('rev', args[0].toList(), args[1], args[2].toArrow()) case 'flatten': return args => { @@ -432,7 +435,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } const value = map.get(key)! - const lam = args[2].toArrow(ctx) + const lam = args[2].toArrow() return lam([value]).map(v => rv.fromMap(map.set(key, v))) } @@ -455,7 +458,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) case 'filter': return args => { const set = args[0].toSet() - const lam = args[1].toArrow(ctx) + const lam = args[1].toArrow() const reducer = ([acc, arg]: RuntimeValue[]) => lam([arg]).map(condition => (condition.toBool() === true ? rv.mkSet(acc.toSet().add(arg.normalForm())) : acc)) @@ -465,7 +468,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) case 'select': return args => { const list = args[0].toList() - const lam = args[1].toArrow(ctx) + const lam = args[1].toArrow() const reducer = ([acc, arg]: RuntimeValue[]) => lam([arg]).map(condition => (condition.toBool() === true ? rv.mkList(acc.toList().push(arg)) : acc)) @@ -474,7 +477,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) case 'mapBy': return args => { - const lambda = args[1].toArrow(ctx) + const lambda = args[1].toArrow() const keys = args[0].toSet() const reducer = ([acc, arg]: RuntimeValue[]) => lambda([arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) @@ -507,7 +510,7 @@ export function applyLambdaToSet( lambda: RuntimeValue, set: RuntimeValue ): Either> { - const f = lambda.toArrow(ctx) + const f = lambda.toArrow() const results = set.toSet().map(value => f([value])) const err = results.find(result => result.isLeft()) if (err !== undefined && err.isLeft()) { diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index b8b667b41..841d474f8 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -1,9 +1,9 @@ import { Either, left, right } from '@sweet-monads/either' import { EffectScheme } from '../../effects/base' -import { QuintEx } from '../../ir/quintIr' +import { QuintApp, QuintEx, QuintOpDef } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { QuintError } from '../../quintError' -import { ExecutionListener, TraceRecorder } from '../trace' +import { TraceRecorder } from '../trace' import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' import { Trace } from './trace' import { RuntimeValue, rv } from './runtimeValue' @@ -11,6 +11,7 @@ import { Context } from './Context' import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' +import { List, Map as ImmutableMap } from 'immutable' export class Evaluator { public ctx: Context @@ -19,7 +20,7 @@ export class Evaluator { private rng: Rng constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { - this.ctx = new Context(table, rng.next) + this.ctx = new Context(table, recorder, rng.next) this.recorder = recorder this.rng = rng } @@ -79,17 +80,20 @@ export class Evaluator { this.recorder.onRunCall() this.trace.reset() this.ctx.reset() - // this.execListener.onUserOperatorCall(init) + // Mocked def for the trace recorder + const initApp: QuintApp = { id: 0n, kind: 'app', opcode: 'q::initAndInvariant', args: [] } + this.recorder.onUserOperatorCall(initApp) const initResult = evaluateExpr(this.ctx, init).mapLeft(error => (failure = error)) if (!isTrue(initResult)) { - // this.execListener.onUserOperatorReturn(init, [], initResult) + this.recorder.onUserOperatorReturn(initApp, [], initResult.map(rv.toQuintEx)) continue } this.shift() const invResult = evaluateExpr(this.ctx, inv).mapLeft(error => (failure = error)) + this.recorder.onUserOperatorReturn(initApp, [], invResult.map(rv.toQuintEx)) if (!isTrue(invResult)) { errorsFound++ } else { @@ -98,6 +102,14 @@ export class Evaluator { // FIXME: errorsFound < ntraces is not good, because we continue after invariant violation. // This is the same in the old version, so I'll fix later. for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { + const stepApp: QuintApp = { + id: 0n, + kind: 'app', + opcode: 'q::stepAndInvariant', + args: [], + } + this.recorder.onUserOperatorCall(stepApp) + const stepResult = evaluateExpr(this.ctx, step).mapLeft(error => (failure = error)) if (!isTrue(stepResult)) { // The run cannot be extended. In some cases, this may indicate a deadlock. @@ -107,7 +119,8 @@ export class Evaluator { // drop the run. Otherwise, we would have a lot of false // positives, which look like deadlocks but they are not. - // this.execListener.onRunReturn(right({ id: 0n, kind: 'bool', value: true }), this.trace.get()) + this.recorder.onUserOperatorReturn(stepApp, [], stepResult.map(rv.toQuintEx)) + this.recorder.onRunReturn(right({ id: 0n, kind: 'bool', value: true }), this.trace.get()) break } @@ -117,6 +130,7 @@ export class Evaluator { if (!isTrue(invResult)) { errorsFound++ } + this.recorder.onUserOperatorReturn(stepApp, [], invResult.map(rv.toQuintEx)) } } @@ -287,23 +301,45 @@ export function evaluateUnderDefContext( return [id, evaluateExpr(ctx, expr)] }) - ctx.addConstants(overrides) - ctx.addNamespaces(def.namespaces) + ctx.addConstants(ImmutableMap(overrides)) + ctx.addNamespaces(List(def.namespaces)) ctx.disableMemo() // We could have one memo per constant const result = evaluate() - ctx.removeConstants(overrides) - ctx.removeNamespaces(def.namespaces) + ctx.removeConstants() + ctx.removeNamespaces() ctx.enableMemo() return result } +function evaluateNondet(ctx: Context, def: QuintOpDef): Either { + const previousPick = ctx.nondetPicks.get(def.id) + if (previousPick) { + return previousPick.value + } + + const pick = evaluateExpr(ctx, def.expr) + ctx.nondetPicks = ctx.nondetPicks.set(def.id, { name: def.name, value: pick }) + return pick +} + function evaluateDef(ctx: Context, def: LookupDefinition): Either { return evaluateUnderDefContext(ctx, def, () => { switch (def.kind) { case 'def': + if (def.qualifier === 'nondet') { + return evaluateNondet(ctx, def) + } + + if (def.qualifier === 'action') { + const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } + ctx.recorder.onUserOperatorCall(app) + const result = evaluateExpr(ctx, def.expr) + ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) + } + return evaluateExpr(ctx, def.expr) case 'param': { const result = ctx.params.get(def.id) @@ -340,9 +376,11 @@ function evaluateNewExpr(ctx: Context, expr: QuintEx): Either evaluateExpr(ctx, arg)) if (args.some(arg => arg.isLeft())) { return args.find(arg => arg.isLeft())! @@ -366,13 +404,11 @@ function evaluateNewExpr(ctx: Context, expr: QuintEx): Either Either { +function lambdaForApp(ctx: Context, app: QuintApp): (args: RuntimeValue[]) => Either { + const { id, opcode } = app + if (!ctx.table.has(id)) { - return builtinLambda(ctx, name) + return builtinLambda(ctx, opcode) } const def = ctx.table.get(id)! @@ -380,7 +416,13 @@ function lambdaForName( if (value.isLeft()) { return _ => value } - return value.value.toArrow(ctx) + const arrow = value.value.toArrow() + return args => { + ctx.recorder.onUserOperatorCall(app) + const result = arrow(args) + ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) + return result + } } export function isTrue(value: Either): boolean { diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index 418bbc164..83f919411 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -60,7 +60,7 @@ * See LICENSE in the project root for license information. */ -import { List, Map, OrderedMap, Set, ValueObject, hash, is as immutableIs } from 'immutable' +import { List, Map as ImmutableMap, OrderedMap, Set, ValueObject, hash, is as immutableIs } from 'immutable' import { Maybe, just, merge, none } from '@sweet-monads/maybe' import { strict as assert } from 'assert' @@ -158,7 +158,7 @@ export const rv = { mkMap: (elems: Iterable<[RuntimeValue, RuntimeValue]>): RuntimeValue => { // convert the keys to the normal form, as they are hashed const arr: [RuntimeValue, RuntimeValue][] = Array.from(elems).map(([k, v]) => [k.normalForm(), v]) - return new RuntimeValueMap(Map(arr)) + return new RuntimeValueMap(ImmutableMap(arr)) }, /** @@ -167,7 +167,7 @@ export const rv = { * @param value an iterable collection of pairs of runtime values * @return a new runtime value that carries the map */ - fromMap: (map: Map): RuntimeValue => { + fromMap: (map: ImmutableMap): RuntimeValue => { // convert the keys to the normal form, as they are hashed return new RuntimeValueMap(map) }, @@ -252,8 +252,8 @@ export const rv = { * @param body the lambda body expression * @returns a runtime value of lambda */ - mkLambda: (params: QuintLambdaParameter[], body: QuintEx) => { - return new RuntimeValueLambda(params, body) + mkLambda: (params: QuintLambdaParameter[], body: QuintEx, ctx: Context) => { + return new RuntimeValueLambda(params, body, ctx) }, fromQuintEx: (ex: QuintEx): RuntimeValue => { @@ -341,10 +341,8 @@ export function fromQuintEx(ex: QuintEx): Maybe { } case 'lambda': - if (ex.kind !== 'lambda') { - return none() - } - return just(rv.mkLambda(ex.params, ex.expr)) + // We don't have enough information to convert lambdas directly + return none() default: // no other case should be possible @@ -403,7 +401,7 @@ export interface RuntimeValue extends EvalResult, ValueObject, Iterable + toMap(): ImmutableMap /** * If the result is a record, transform it to a map of values. @@ -446,7 +444,7 @@ export interface RuntimeValue extends EvalResult, ValueObject, Iterable Either + toArrow(): (args: RuntimeValue[]) => Either /** * If the result is a variant, return the label and the value. @@ -566,7 +564,7 @@ abstract class RuntimeValueBase implements RuntimeValue { } } - toMap(): Map { + toMap(): ImmutableMap { if (this instanceof RuntimeValueMap) { return this.map } else { @@ -586,7 +584,7 @@ abstract class RuntimeValueBase implements RuntimeValue { if (this instanceof RuntimeValueInt) { return (this as RuntimeValueInt).value } else { - throw new Error('Expected an integer value') + throw new Error(`Expected an integer value, got ${expressionToString(this.toQuintEx(zerog))}`) } } @@ -609,7 +607,7 @@ abstract class RuntimeValueBase implements RuntimeValue { throw new Error('Expected a 2-tuple') } - toArrow(ctx: Context): (args: RuntimeValue[]) => Either { + toArrow(): (args: RuntimeValue[]) => Either { if (!(this instanceof RuntimeValueLambda)) { throw new Error('Expected a lambda value') } @@ -624,15 +622,18 @@ abstract class RuntimeValueBase implements RuntimeValue { }) } const paramEntries: [bigint, RuntimeValue][] = lam.params.map((param, i) => { - // console.log('param', param.id, param.name, 'set to', valueToString(args[i])) return [param.id, args[i]] }) - ctx.addParams(paramEntries) - ctx.disableMemo() - const result = evaluateExpr(ctx, lam.body) - ctx.removeParams(paramEntries) - ctx.enableMemo() + lam.ctx.addConstants(lam.consts) + lam.ctx.addNamespaces(lam.namespaces) + lam.ctx.addParams(paramEntries) + lam.ctx.disableMemo() + const result = evaluateExpr(lam.ctx, lam.body) + lam.ctx.removeConstants() + lam.ctx.removeNamespaces() + lam.ctx.removeParams() + lam.ctx.enableMemo() return result } @@ -955,9 +956,9 @@ export class RuntimeValueVariant extends RuntimeValueBase implements RuntimeValu * This is an internal class. */ class RuntimeValueMap extends RuntimeValueBase implements RuntimeValue { - map: Map + map: ImmutableMap - constructor(keyValues: Map) { + constructor(keyValues: ImmutableMap) { super(true) this.map = keyValues } @@ -1645,11 +1646,17 @@ class RuntimeValueInfSet extends RuntimeValueBase implements RuntimeValue { export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue { params: QuintLambdaParameter[] body: QuintEx + ctx: Context + consts: ImmutableMap> + namespaces: List - constructor(params: QuintLambdaParameter[], body: QuintEx) { + constructor(params: QuintLambdaParameter[], body: QuintEx, ctx: Context) { super(false) this.params = params this.body = body + this.ctx = ctx + this.consts = ctx.consts + this.namespaces = ctx.namespaces } eval(args?: any[]) { From 4a2ab82c3b9b6aa70242f9091899ffb5856ce576 Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 20 Aug 2024 16:07:11 -0300 Subject: [PATCH 03/37] Fix memo by making it mutable again --- quint/src/runtime/impl/Context.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index c5d9b9b77..91d8038b3 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -4,7 +4,7 @@ import { LookupTable } from '../../names/base' import { QuintError } from '../../quintError' import { RuntimeValue } from './runtimeValue' import { TraceRecorder } from '../trace' -import { Map, List, is } from 'immutable' +import { Map as ImmutableMap, List, is } from 'immutable' import { VarStorage } from './VarStorage' export interface NondetPick { @@ -13,20 +13,20 @@ export interface NondetPick { } export class Context { - public memo: Map> = Map() + public memo: Map> = new Map() public memoEnabled: boolean = true - public params: Map> = Map() - public consts: Map> = Map() + public params: ImmutableMap> = ImmutableMap() + public consts: ImmutableMap> = ImmutableMap() public namespaces: List = List() public varStorage: VarStorage = new VarStorage() public rand: (n: bigint) => bigint public table: LookupTable public pureKeys: Set = new Set() - public nondetPicks: Map = Map() + public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder - private constHistory: Map>[] = [] - private paramHistory: Map>[] = [] + private constHistory: ImmutableMap>[] = [] + private paramHistory: ImmutableMap>[] = [] private namespacesHistory: List[] = [] constructor(table: LookupTable, recorder: TraceRecorder, rand: (n: bigint) => bigint) { @@ -36,8 +36,8 @@ export class Context { } reset() { - this.memo = Map() - this.params = Map() + this.memo = new Map() + this.params = ImmutableMap() this.varStorage = new VarStorage() } @@ -79,7 +79,7 @@ export class Context { } clearMemo() { - this.memo = Map() + this.memo = new Map() } disableMemo() { @@ -98,7 +98,7 @@ export class Context { return result } - addConstants(consts: Map>) { + addConstants(consts: ImmutableMap>) { this.constHistory.push(this.consts) this.consts = this.consts.merge(consts) } From f3c519c822c7c73c883c7674decd39db58816ac3 Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 20 Aug 2024 17:44:06 -0300 Subject: [PATCH 04/37] Attempt some improvements over context performance --- quint/src/runtime/impl/Context.ts | 10 +--- quint/src/runtime/impl/evaluator.ts | 81 +++++++++++++++++--------- quint/src/runtime/impl/runtimeValue.ts | 4 +- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 91d8038b3..823380fae 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -25,6 +25,8 @@ export class Context { public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder + public constsByInstance: Map>> = new Map() + private constHistory: ImmutableMap>[] = [] private paramHistory: ImmutableMap>[] = [] private namespacesHistory: List[] = [] @@ -90,14 +92,6 @@ export class Context { this.memoEnabled = true } - withMemo(f: () => Either) { - const memoStateBefore = this.memoEnabled - this.memoEnabled = true - const result = f() - this.memoEnabled = memoStateBefore - return result - } - addConstants(consts: ImmutableMap>) { this.constHistory.push(this.consts) this.consts = this.consts.merge(consts) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 841d474f8..52e36da3c 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -12,6 +12,7 @@ import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' import { List, Map as ImmutableMap } from 'immutable' +import { expressionToString } from '../../ir/IRprinting' export class Evaluator { public ctx: Context @@ -35,7 +36,9 @@ export class Evaluator { } this.ctx.varStorage.shiftVars() this.trace.extend(this.ctx.varStorage.asRecord()) - this.ctx.clearMemo() // FIXME: clear only non-pure + // TODO: save on trace + this.ctx.nondetPicks = ImmutableMap() + // this.ctx.clearMemo() // FIXME: clear only non-pure } shiftAndCheck(): string[] { @@ -260,21 +263,25 @@ export class Evaluator { export function evaluateExpr(ctx: Context, expr: QuintEx): Either { const id = expr.id - if (id === 0n || !ctx.memoEnabled) { + if (id === 0n) { return evaluateNewExpr(ctx, expr).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) } - if (ctx.memo.has(id)) { - // if (ctx.pureKeys.has(id)) { - // console.log('pure key', id, expressionToString(expr)) - // } - return ctx.memo.get(id)! - } + // console.log('[get] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) + // if (ctx.memo.has(id)) { + // // if (ctx.pureKeys.has(id)) { + // // console.log('pure key', id, expressionToString(expr)) + // // } + // return ctx.memo.get(id)! + // } const result = evaluateNewExpr(ctx, expr).mapLeft(err => err.reference === undefined ? { ...err, reference: id } : err ) - ctx.memo.set(id, result) + // console.log('[set] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) + if (ctx.memoEnabled) { + ctx.memo.set(id, result) + } return result } @@ -283,33 +290,43 @@ export function evaluateUnderDefContext( def: LookupDefinition, evaluate: () => Either ): Either { - if (def.kind === 'def' && def.qualifier === 'nondet' && ctx.memoEnabled) { - ctx.disableMemo() - const r = evaluateUnderDefContext(ctx, def, evaluate) - ctx.enableMemo() - return r - } + // if (def.kind === 'def' && def.qualifier === 'nondet' && ctx.memoEnabled) { + // ctx.disableMemo() + // const r = evaluateUnderDefContext(ctx, def, evaluate) + // ctx.enableMemo() + // return r + // } if (!def.importedFrom || def.importedFrom.kind !== 'instance') { return evaluate() } - const instance = def.importedFrom - const overrides: [bigint, Either][] = instance.overrides.map(([param, expr]) => { - const id = ctx.table.get(param.id)!.id - return [id, evaluateExpr(ctx, expr)] - }) + const constsBefore = ctx.consts + const namespacesBefore = ctx.namespaces - ctx.addConstants(ImmutableMap(overrides)) - ctx.addNamespaces(List(def.namespaces)) - ctx.disableMemo() // We could have one memo per constant + let consts = ctx.constsByInstance.get(def.importedFrom.id) + if (!consts) { + const overrides: [bigint, Either][] = instance.overrides.map(([param, expr]) => { + const id = ctx.table.get(param.id)!.id + + return [id, evaluateExpr(ctx, expr)] + }) + consts = ImmutableMap(overrides) + ctx.constsByInstance.set(def.importedFrom.id, consts) + } + + ctx.consts = consts + ctx.namespaces = List(def.namespaces) + // ctx.addNamespaces(List(def.namespaces)) + // ctx.disableMemo() // We could have one memo per constant const result = evaluate() - ctx.removeConstants() - ctx.removeNamespaces() - ctx.enableMemo() + ctx.consts = constsBefore + ctx.namespaces = namespacesBefore + // ctx.removeNamespaces() + // ctx.enableMemo() return result } @@ -320,7 +337,7 @@ function evaluateNondet(ctx: Context, def: QuintOpDef): Either Date: Tue, 20 Aug 2024 20:12:01 -0300 Subject: [PATCH 05/37] Avoid many lookups --- quint/src/runtime/impl/Context.ts | 8 +- quint/src/runtime/impl/builtins.ts | 216 ++++++++++---------- quint/src/runtime/impl/evaluator.ts | 272 ++++++++++++++++--------- quint/src/runtime/impl/runtimeValue.ts | 45 ++-- 4 files changed, 298 insertions(+), 243 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 823380fae..dedd75895 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -1,6 +1,4 @@ import { Either, left, right } from '@sweet-monads/either' -import { isEqual, takeRight } from 'lodash' -import { LookupTable } from '../../names/base' import { QuintError } from '../../quintError' import { RuntimeValue } from './runtimeValue' import { TraceRecorder } from '../trace' @@ -13,14 +11,13 @@ export interface NondetPick { } export class Context { - public memo: Map> = new Map() + public memo: Map Either> = new Map() public memoEnabled: boolean = true public params: ImmutableMap> = ImmutableMap() public consts: ImmutableMap> = ImmutableMap() public namespaces: List = List() public varStorage: VarStorage = new VarStorage() public rand: (n: bigint) => bigint - public table: LookupTable public pureKeys: Set = new Set() public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder @@ -31,8 +28,7 @@ export class Context { private paramHistory: ImmutableMap>[] = [] private namespacesHistory: List[] = [] - constructor(table: LookupTable, recorder: TraceRecorder, rand: (n: bigint) => bigint) { - this.table = table + constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint) { this.recorder = recorder this.rand = rand } diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index f929b262a..9df478dd8 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,7 +1,7 @@ import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { QuintError, quintErrorToString } from '../../quintError' import { Map, is, Set, Range, List } from 'immutable' -import { evaluateExpr, evaluateUnderDefContext, isTrue } from './evaluator' +import { EvalFunction, evaluateExpr, evaluateUnderDefContext, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' import { chunk } from 'lodash' @@ -36,25 +36,27 @@ export const lazyOps = [ 'then', ] -export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) => Either { +export function lazyBuiltinLambda( + op: string +): (ctx: Context, args: EvalFunction[]) => Either { switch (op) { case 'and': - return args => { - return args.reduce((acc: Either, arg: QuintEx) => { + return (ctx, args) => { + return args.reduce((acc: Either, arg: EvalFunction) => { return acc.chain(accValue => { if (accValue.toBool() === true) { - return evaluateExpr(ctx, arg) + return arg(ctx) } return acc }) }, right(rv.mkBool(true))) } case 'or': - return args => { - return args.reduce((acc: Either, arg: QuintEx) => { + return (ctx, args) => { + return args.reduce((acc: Either, arg: EvalFunction) => { return acc.chain(accValue => { if (accValue.toBool() === false) { - return evaluateExpr(ctx, arg) + return arg(ctx) } return acc }) @@ -62,34 +64,21 @@ export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) = } case 'implies': - return args => { - return evaluateExpr(ctx, args[0]).chain(l => { + return (ctx, args) => { + return args[0](ctx).chain(l => { if (!l.toBool()) { return right(rv.mkBool(true)) } - return evaluateExpr(ctx, args[1]) + return args[1](ctx) }) } - case 'assign': - return args => { - const varDef = ctx.table.get(args[0].id)! - // Eval var just to make sure it is registered in the storage - evaluateExpr(ctx, args[0]) - - return evaluateUnderDefContext(ctx, varDef, () => { - return evaluateExpr(ctx, args[1]).map(value => { - ctx.setNextVar(varDef.id, value) - return rv.mkBool(true) - }) - }) - } case 'actionAny': - return args => { + return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() const evaluationResults = args.map(arg => { - const result = evaluateExpr(ctx, arg).map(result => { + const result = arg(ctx).map(result => { // Save vars const successor = ctx.varStorage.nextVarsSnapshot() @@ -121,10 +110,10 @@ export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) = }) } case 'actionAll': - return args => { + return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() for (const action of args) { - const result = evaluateExpr(ctx, action) + const result = action(ctx) if (!isTrue(result)) { ctx.varStorage.nextVars = nextVarsSnapshot return result.map(_ => rv.mkBool(false)) @@ -134,40 +123,37 @@ export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) = return right(rv.mkBool(true)) } case 'ite': - return args => { - return evaluateExpr(ctx, args[0]).chain(condition => { - return condition.toBool() ? evaluateExpr(ctx, args[1]) : evaluateExpr(ctx, args[2]) + return (ctx, args) => { + return args[0](ctx).chain(condition => { + return condition.toBool() ? args[1](ctx) : args[2](ctx) }) } case 'matchVariant': - return args => { + return (ctx, args) => { const matchedEx = args[0] - return evaluateExpr(ctx, matchedEx).chain(expr => { + return matchedEx(ctx).chain(expr => { const [label, value] = expr.toVariant() const cases = args.slice(1) - const caseForVariant = chunk(cases, 2).find( - ([caseLabel, _caseElim]) => - rv.fromQuintEx(caseLabel).toStr() === '_' || rv.fromQuintEx(caseLabel).toStr() === label - ) + const caseForVariant = chunk(cases, 2).find(([caseLabel, _caseElim]) => { + const l = caseLabel(ctx).unwrap().toStr() + return l === '_' || l === label + }) + if (!caseForVariant) { return left({ code: 'QNT505', message: `No match for variant ${label}` }) } const [_caseLabel, caseElim] = caseForVariant - if (caseElim.kind !== 'lambda') { - return left({ code: 'QNT505', message: `Expected lambda in matchVariant` }) - } - const elim = rv.mkLambda(caseElim.params, caseElim.expr, ctx).toArrow() - return elim([value]) + return caseElim(ctx).chain(elim => elim.toArrow()(ctx, [value])) }) } case 'oneOf': - return args => { - return evaluateExpr(ctx, args[0]).chain(set => { + return (ctx, args) => { + return args[0](ctx).chain(set => { const bounds = set.bounds() const positions: Either = mergeInMany( bounds.map((b): Either => { @@ -197,36 +183,36 @@ export function lazyBuiltinLambda(ctx: Context, op: string): (args: QuintEx[]) = } } -export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) => Either { +export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) => Either { switch (op) { case 'Set': - return args => right(rv.mkSet(args)) + return (_, args) => right(rv.mkSet(args)) case 'Rec': - return args => right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) + return (_, args) => right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) case 'List': - return args => right(rv.mkList(List(args))) + return (_, args) => right(rv.mkList(List(args))) case 'Tup': - return args => right(rv.mkTuple(List(args))) + return (_, args) => right(rv.mkTuple(List(args))) case 'Map': - return args => right(rv.mkMap(args.map(kv => kv.toTuple2()))) + return (_, args) => right(rv.mkMap(args.map(kv => kv.toTuple2()))) case 'variant': - return args => right(rv.mkVariant(args[0].toStr(), args[1])) + return (_, args) => right(rv.mkVariant(args[0].toStr(), args[1])) case 'not': - return args => right(rv.mkBool(!args[0].toBool())) + return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'iff': - return args => right(rv.mkBool(args[0].toBool() === args[1].toBool())) + return (_, args) => right(rv.mkBool(args[0].toBool() === args[1].toBool())) case 'eq': - return args => right(rv.mkBool(args[0].equals(args[1]))) + return (_, args) => right(rv.mkBool(args[0].equals(args[1]))) case 'neq': - return args => right(rv.mkBool(!args[0].equals(args[1]))) + return (_, args) => right(rv.mkBool(!args[0].equals(args[1]))) case 'iadd': - return args => right(rv.mkInt(args[0].toInt() + args[1].toInt())) + return (_, args) => right(rv.mkInt(args[0].toInt() + args[1].toInt())) case 'isub': - return args => right(rv.mkInt(args[0].toInt() - args[1].toInt())) + return (_, args) => right(rv.mkInt(args[0].toInt() - args[1].toInt())) case 'imul': - return args => right(rv.mkInt(args[0].toInt() * args[1].toInt())) + return (_, args) => right(rv.mkInt(args[0].toInt() * args[1].toInt())) case 'idiv': - return args => { + return (_, args) => { const divisor = args[1].toInt() if (divisor === 0n) { return left({ code: 'QNT503', message: `Division by zero` }) @@ -234,9 +220,9 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) return right(rv.mkInt(args[0].toInt() / divisor)) } case 'imod': - return args => right(rv.mkInt(args[0].toInt() % args[1].toInt())) + return (_, args) => right(rv.mkInt(args[0].toInt() % args[1].toInt())) case 'ipow': - return args => { + return (_, args) => { const base = args[0].toInt() const exp = args[1].toInt() if (base === 0n && exp === 0n) { @@ -249,36 +235,36 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) return right(rv.mkInt(base ** exp)) } case 'iuminus': - return args => right(rv.mkInt(-args[0].toInt())) + return (_, args) => right(rv.mkInt(-args[0].toInt())) case 'ilt': - return args => right(rv.mkBool(args[0].toInt() < args[1].toInt())) + return (_, args) => right(rv.mkBool(args[0].toInt() < args[1].toInt())) case 'ilte': - return args => right(rv.mkBool(args[0].toInt() <= args[1].toInt())) + return (_, args) => right(rv.mkBool(args[0].toInt() <= args[1].toInt())) case 'igt': - return args => right(rv.mkBool(args[0].toInt() > args[1].toInt())) + return (_, args) => right(rv.mkBool(args[0].toInt() > args[1].toInt())) case 'igte': - return args => right(rv.mkBool(args[0].toInt() >= args[1].toInt())) + return (_, args) => right(rv.mkBool(args[0].toInt() >= args[1].toInt())) case 'item': - return args => { + return (_, args) => { // Access a tuple: tuples are 1-indexed, that is, _1, _2, etc. return getListElem(args[0].toList(), Number(args[1].toInt()) - 1) } case 'tuples': - return args => right(rv.mkCrossProd(args)) + return (_, args) => right(rv.mkCrossProd(args)) case 'range': - return args => { + return (_, args) => { const start = Number(args[0].toInt()) const end = Number(args[1].toInt()) return right(rv.mkList(List(Range(start, end).map(rv.mkInt)))) } case 'nth': - return args => getListElem(args[0].toList(), Number(args[1].toInt())) + return (_, args) => getListElem(args[0].toList(), Number(args[1].toInt())) case 'replaceAt': - return args => { + return (_, args) => { const list = args[0].toList() const idx = Number(args[1].toInt()) if (idx < 0 || idx >= list.size) { @@ -289,7 +275,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'head': - return args => { + return (_, args) => { const list = args[0].toList() if (list.size === 0) { return left({ code: 'QNT505', message: `Called 'head' on an empty list` }) @@ -298,7 +284,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'tail': - return args => { + return (_, args) => { const list = args[0].toList() if (list.size === 0) { return left({ code: 'QNT505', message: `Called 'tail' on an empty list` }) @@ -307,7 +293,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'slice': - return args => { + return (_, args) => { const list = args[0].toList() const start = Number(args[1].toInt()) const end = Number(args[2].toInt()) @@ -322,26 +308,26 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'length': - return args => right(rv.mkInt(args[0].toList().size)) + return (_, args) => right(rv.mkInt(args[0].toList().size)) case 'append': - return args => right(rv.mkList(args[0].toList().push(args[1]))) + return (_, args) => right(rv.mkList(args[0].toList().push(args[1]))) case 'concat': - return args => right(rv.mkList(args[0].toList().concat(args[1].toList()))) + return (_, args) => right(rv.mkList(args[0].toList().concat(args[1].toList()))) case 'indices': - return args => right(rv.mkInterval(0n, args[0].toList().size - 1)) + return (_, args) => right(rv.mkInterval(0n, args[0].toList().size - 1)) case 'field': - return args => { + return (_, args) => { const field = args[1].toStr() const result = args[0].toOrderedMap().get(field) return result ? right(result) : left({ code: 'QNT501', message: `Accessing a missing record field ${field}` }) } case 'fieldNames': - return args => right(rv.mkSet(args[0].toOrderedMap().keySeq().map(rv.mkStr))) + return (_, args) => right(rv.mkSet(args[0].toOrderedMap().keySeq().map(rv.mkStr))) case 'with': - return args => { + return (_, args) => { const record = args[0].toOrderedMap() const field = args[1].toStr() const value = args[2] @@ -354,42 +340,42 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'powerset': - return args => right(rv.mkPowerset(args[0])) + return (_, args) => right(rv.mkPowerset(args[0])) case 'contains': - return args => right(rv.mkBool(args[0].contains(args[1]))) + return (_, args) => right(rv.mkBool(args[0].contains(args[1]))) case 'in': - return args => right(rv.mkBool(args[1].contains(args[0]))) + return (_, args) => right(rv.mkBool(args[1].contains(args[0]))) case 'subseteq': - return args => right(rv.mkBool(args[0].isSubset(args[1]))) + return (_, args) => right(rv.mkBool(args[0].isSubset(args[1]))) case 'exclude': - return args => right(rv.mkSet(args[0].toSet().subtract(args[1].toSet()))) + return (_, args) => right(rv.mkSet(args[0].toSet().subtract(args[1].toSet()))) case 'union': - return args => right(rv.mkSet(args[0].toSet().union(args[1].toSet()))) + return (_, args) => right(rv.mkSet(args[0].toSet().union(args[1].toSet()))) case 'intersect': - return args => right(rv.mkSet(args[0].toSet().intersect(args[1].toSet()))) + return (_, args) => right(rv.mkSet(args[0].toSet().intersect(args[1].toSet()))) case 'size': - return args => args[0].cardinality().map(rv.mkInt) + return (_, args) => args[0].cardinality().map(rv.mkInt) case 'isFinite': // at the moment, we support only finite sets, so just return true return _args => right(rv.mkBool(true)) case 'to': - return args => right(rv.mkInterval(args[0].toInt(), args[1].toInt())) + return (_, args) => right(rv.mkInterval(args[0].toInt(), args[1].toInt())) case 'fold': - return args => applyFold('fwd', args[0].toSet(), args[1], args[2].toArrow()) + return (ctx, args) => applyFold('fwd', args[0].toSet(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'foldl': - return args => applyFold('fwd', args[0].toList(), args[1], args[2].toArrow()) + return (ctx, args) => applyFold('fwd', args[0].toList(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'foldr': - return args => applyFold('rev', args[0].toList(), args[1], args[2].toArrow()) + return (ctx, args) => applyFold('rev', args[0].toList(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'flatten': - return args => { + return (_, args) => { const s = args[0].toSet().map(s => s.toSet()) return right(rv.mkSet(s.flatten(1) as Set)) } case 'get': - return args => { + return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() const value = map.get(key) @@ -408,7 +394,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'set': - return args => { + return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() if (!map.has(key)) { @@ -419,7 +405,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'put': - return args => { + return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() const value = args[2] @@ -427,7 +413,7 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) } case 'setBy': - return args => { + return (ctx, args) => { const map = args[0].toMap() const key = args[1].normalForm() if (!map.has(key)) { @@ -436,68 +422,70 @@ export function builtinLambda(ctx: Context, op: string): (args: RuntimeValue[]) const value = map.get(key)! const lam = args[2].toArrow() - return lam([value]).map(v => rv.fromMap(map.set(key, v))) + return lam(ctx, [value]).map(v => rv.fromMap(map.set(key, v))) } case 'keys': - return args => right(rv.mkSet(args[0].toMap().keys())) + return (_, args) => right(rv.mkSet(args[0].toMap().keys())) case 'exists': - return args => + return (ctx, args) => applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.some(v => v.toBool()) === true)) case 'forall': - return args => + return (ctx, args) => applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.every(v => v.toBool()) === true)) case 'map': - return args => { + return (ctx, args) => { return applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkSet(values)) } case 'filter': - return args => { + return (ctx, args) => { const set = args[0].toSet() const lam = args[1].toArrow() const reducer = ([acc, arg]: RuntimeValue[]) => - lam([arg]).map(condition => (condition.toBool() === true ? rv.mkSet(acc.toSet().add(arg.normalForm())) : acc)) + lam(ctx, [arg]).map(condition => + condition.toBool() === true ? rv.mkSet(acc.toSet().add(arg.normalForm())) : acc + ) return applyFold('fwd', set, rv.mkSet([]), reducer) } case 'select': - return args => { + return (ctx, args) => { const list = args[0].toList() const lam = args[1].toArrow() const reducer = ([acc, arg]: RuntimeValue[]) => - lam([arg]).map(condition => (condition.toBool() === true ? rv.mkList(acc.toList().push(arg)) : acc)) + lam(ctx, [arg]).map(condition => (condition.toBool() === true ? rv.mkList(acc.toList().push(arg)) : acc)) return applyFold('fwd', list, rv.mkList([]), reducer) } case 'mapBy': - return args => { + return (ctx, args) => { const lambda = args[1].toArrow() const keys = args[0].toSet() const reducer = ([acc, arg]: RuntimeValue[]) => - lambda([arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) + lambda(ctx, [arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) return applyFold('fwd', keys, rv.mkMap([]), reducer) } case 'setToMap': - return args => { + return (_, args) => { const set = args[0].toSet() return right(rv.mkMap(Map(set.map(s => s.toTuple2())))) } case 'setOfMaps': - return args => right(rv.mkMapSet(args[0], args[1])) + return (_, args) => right(rv.mkMapSet(args[0], args[1])) case 'fail': - return args => right(rv.mkBool(!args[0].toBool())) + return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'assert': - return args => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT502', message: `Assertion failed` })) + return (_, args) => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT502', message: `Assertion failed` })) case 'expect': case 'reps': @@ -511,7 +499,7 @@ export function applyLambdaToSet( set: RuntimeValue ): Either> { const f = lambda.toArrow() - const results = set.toSet().map(value => f([value])) + const results = set.toSet().map(value => f(ctx, [value])) const err = results.find(result => result.isLeft()) if (err !== undefined && err.isLeft()) { return left(err.value) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 52e36da3c..cf8f15834 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -14,20 +14,29 @@ import { zerog } from '../../idGenerator' import { List, Map as ImmutableMap } from 'immutable' import { expressionToString } from '../../ir/IRprinting' +export type EvalFunction = (ctx: Context) => Either + export class Evaluator { public ctx: Context public recorder: TraceRecorder private trace: Trace = new Trace() private rng: Rng + private lookupTable: LookupTable constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { - this.ctx = new Context(table, recorder, rng.next) + this.ctx = new Context(recorder, rng.next) this.recorder = recorder this.rng = rng + this.lookupTable = table + } + + get table(): LookupTable { + console.log('getting table') + return this.lookupTable } updateTable(table: LookupTable) { - this.ctx.table = table + this.lookupTable = table } shift() { @@ -54,7 +63,7 @@ export class Evaluator { } evaluate(expr: QuintEx): Either { - const value = evaluateExpr(this.ctx, expr) + const value = evaluateExpr(this.table, expr)(this.ctx) return value.map(rv.toQuintEx) } @@ -77,6 +86,11 @@ export class Evaluator { // this.ctx.pureKeys = this.ctx.pureKeys.add(id) // } // }) + // + + const initEval = evaluateExpr(this.table, init) + const stepEval = evaluateExpr(this.table, step) + const invEval = evaluateExpr(this.table, inv) // TODO: room for improvement here for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { @@ -87,7 +101,7 @@ export class Evaluator { const initApp: QuintApp = { id: 0n, kind: 'app', opcode: 'q::initAndInvariant', args: [] } this.recorder.onUserOperatorCall(initApp) - const initResult = evaluateExpr(this.ctx, init).mapLeft(error => (failure = error)) + const initResult = initEval(this.ctx).mapLeft(error => (failure = error)) if (!isTrue(initResult)) { this.recorder.onUserOperatorReturn(initApp, [], initResult.map(rv.toQuintEx)) continue @@ -95,7 +109,7 @@ export class Evaluator { this.shift() - const invResult = evaluateExpr(this.ctx, inv).mapLeft(error => (failure = error)) + const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) this.recorder.onUserOperatorReturn(initApp, [], invResult.map(rv.toQuintEx)) if (!isTrue(invResult)) { errorsFound++ @@ -113,7 +127,7 @@ export class Evaluator { } this.recorder.onUserOperatorCall(stepApp) - const stepResult = evaluateExpr(this.ctx, step).mapLeft(error => (failure = error)) + const stepResult = stepEval(this.ctx).mapLeft(error => (failure = error)) if (!isTrue(stepResult)) { // The run cannot be extended. In some cases, this may indicate a deadlock. // Since we are doing random simulation, it is very likely @@ -129,7 +143,7 @@ export class Evaluator { this.shift() - const invResult = evaluateExpr(this.ctx, inv).mapLeft(error => (failure = error)) + const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) if (!isTrue(invResult)) { errorsFound++ } @@ -156,6 +170,8 @@ export class Evaluator { // save the initial seed let seed = this.rng.getState() + const testEval = evaluateExpr(this.table, test) + let nsamples = 1 // run up to maxSamples, stop on the first failure for (; nsamples <= maxSamples; nsamples++) { @@ -165,7 +181,7 @@ export class Evaluator { // reset the trace this.trace.reset() // run the test - const result = evaluateExpr(this.ctx, test).map(e => e.toQuintEx(zerog)) + const result = testEval(this.ctx).map(e => e.toQuintEx(zerog)) // extract the trace const trace = this.trace.get() @@ -261,11 +277,27 @@ export class Evaluator { } } -export function evaluateExpr(ctx: Context, expr: QuintEx): Either { - const id = expr.id - if (id === 0n) { - return evaluateNewExpr(ctx, expr).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) - } +export function evaluateExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => Either { + console.log('building expr', expr.id) + const exprEval = evaluateNewExpr(table, expr) + return ctx => exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) + // return ctx => { + // const id = expr.id + // if (id === 0n) { + // return evaluateNewExpr( + // ctx, + // expr + // )(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) + // } + // const memoValue = ctx.memo.get(id) + // if (memoValue === undefined) { + // const result = evaluateNewExpr(ctx, expr) + // ctx.memo.set(id, result) + // return result(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) + // } + + // return memoValue!(ctx) + // } // console.log('[get] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) // if (ctx.memo.has(id)) { @@ -275,95 +307,94 @@ export function evaluateExpr(ctx: Context, expr: QuintEx): Either - err.reference === undefined ? { ...err, reference: id } : err - ) + // return ctx => + // evaluateNewExpr(ctx, expr)(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) // console.log('[set] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) - if (ctx.memoEnabled) { - ctx.memo.set(id, result) - } - return result + // if (ctx.memoEnabled) { + // ctx.memo.set(id, result) + // } } export function evaluateUnderDefContext( - ctx: Context, + table: LookupTable, def: LookupDefinition, - evaluate: () => Either -): Either { - // if (def.kind === 'def' && def.qualifier === 'nondet' && ctx.memoEnabled) { - // ctx.disableMemo() - // const r = evaluateUnderDefContext(ctx, def, evaluate) - // ctx.enableMemo() - // return r - // } - + evaluate: (ctx: Context) => Either +): (ctx: Context) => Either { if (!def.importedFrom || def.importedFrom.kind !== 'instance') { - return evaluate() + return evaluate } const instance = def.importedFrom - const constsBefore = ctx.consts - const namespacesBefore = ctx.namespaces + const overrides: [bigint, (ctx: Context) => Either][] = instance.overrides.map( + ([param, expr]) => { + const id = table.get(param.id)!.id - let consts = ctx.constsByInstance.get(def.importedFrom.id) - if (!consts) { - const overrides: [bigint, Either][] = instance.overrides.map(([param, expr]) => { - const id = ctx.table.get(param.id)!.id - - return [id, evaluateExpr(ctx, expr)] - }) - consts = ImmutableMap(overrides) - ctx.constsByInstance.set(def.importedFrom.id, consts) - } + return [id, evaluateExpr(table, expr)] + } + ) - ctx.consts = consts - ctx.namespaces = List(def.namespaces) - // ctx.addNamespaces(List(def.namespaces)) - // ctx.disableMemo() // We could have one memo per constant + return ctx => { + const constsBefore = ctx.consts + const namespacesBefore = ctx.namespaces - const result = evaluate() + ctx.consts = ImmutableMap(overrides.map(([id, evaluate]) => [id, evaluate(ctx)])) + ctx.namespaces = List(def.namespaces) + // ctx.addNamespaces(List(def.namespaces)) + // ctx.disableMemo() // We could have one memo per constant - ctx.consts = constsBefore - ctx.namespaces = namespacesBefore - // ctx.removeNamespaces() - // ctx.enableMemo() + const result = evaluate(ctx) - return result + ctx.consts = constsBefore + ctx.namespaces = namespacesBefore + // ctx.removeNamespaces() + // ctx.enableMemo() + return result + } } -function evaluateNondet(ctx: Context, def: QuintOpDef): Either { +function evaluateNondet( + ctx: Context, + def: QuintOpDef, + bodyEval: (ctx: Context) => Either +): Either { const previousPick = ctx.nondetPicks.get(def.id) if (previousPick) { return previousPick.value } - const pick = evaluateNewExpr(ctx, def.expr) + const pick = bodyEval(ctx) ctx.nondetPicks = ctx.nondetPicks.set(def.id, { name: def.name, value: pick }) return pick } -function evaluateDef(ctx: Context, def: LookupDefinition): Either { - return evaluateUnderDefContext(ctx, def, () => { - switch (def.kind) { - case 'def': - if (def.qualifier === 'nondet') { +function evaluateDefCore( + table: LookupTable, + def: LookupDefinition +): (ctx: Context) => Either { + switch (def.kind) { + case 'def': + if (def.qualifier === 'nondet') { + const body = evaluateExpr(table, def.expr) + return (ctx: Context) => { ctx.disableMemo() - return evaluateNondet(ctx, def) + return evaluateNondet(ctx, def, body) } + } - const memoStateBefore = ctx.memoEnabled - - if (def.qualifier === 'action') { - const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } + if (def.qualifier === 'action') { + const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } + const body = evaluateExpr(table, def.expr) + return (ctx: Context) => { ctx.recorder.onUserOperatorCall(app) - const result = evaluateExpr(ctx, def.expr) + const result = body(ctx) ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) + return result } + } - const result = evaluateExpr(ctx, def.expr) - ctx.memoEnabled = memoStateBefore - return result - case 'param': { + return evaluateExpr(table, def.expr) + case 'param': { + return (ctx: Context) => { ctx.disableMemo() const result = ctx.params.get(def.id) @@ -372,9 +403,10 @@ function evaluateDef(ctx: Context, def: LookupDefinition): Either { ctx.disableMemo() - ctx.discoverVar(def.id, def.name) const result = ctx.getVar(def.id) if (!result) { @@ -382,69 +414,107 @@ function evaluateDef(ctx: Context, def: LookupDefinition): Either { ctx.disableMemo() const constValue = ctx.consts.get(def.id) if (!constValue) { return left({ code: 'QNT503', message: `Constant ${def.name}(id: ${def.id}) not set` }) } return constValue + } - default: - return left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) - } - }) + default: + return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) + } } -function evaluateNewExpr(ctx: Context, expr: QuintEx): Either { +function evaluateDef(table: LookupTable, def: LookupDefinition): (ctx: Context) => Either { + return evaluateUnderDefContext(table, def, evaluateDefCore(table, def)) +} + +function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => Either { switch (expr.kind) { case 'int': case 'bool': case 'str': // These are already values, just return them - return right(rv.fromQuintEx(expr)) + return _ => right(rv.fromQuintEx(expr)) case 'lambda': // Lambda is also like a value, but we should construct it with the context - return right(rv.mkLambda(expr.params, expr.expr, ctx)) + const body = evaluateExpr(table, expr.expr) + const lambda = rv.mkLambda(expr.params, body) + return _ => right(lambda) case 'name': - const def = ctx.table.get(expr.id) + const def = table.get(expr.id) if (!def) { - return builtinValue(expr.name) + // TODO: Do we also need to return builtin ops for higher order usage? + const lambda = builtinValue(expr.name) + return _ => lambda } - return evaluateDef(ctx, def) + return evaluateDef(table, def) case 'app': + if (expr.opcode === 'assign') { + const varDef = table.get(expr.args[0].id)! + const exprEval = evaluateExpr(table, expr.args[1]) + // make sure it is registered in the storage + + return evaluateUnderDefContext(table, varDef, ctx => { + ctx.discoverVar(varDef.id, varDef.name) + return exprEval(ctx).map(value => { + ctx.setNextVar(varDef.id, value) + return rv.mkBool(true) + }) + }) + } + + const args = expr.args.map(arg => evaluateExpr(table, arg)) + // In these special ops, we don't want to evaluate the arguments before evaluating application if (lazyOps.includes(expr.opcode)) { - return lazyBuiltinLambda(ctx, expr.opcode)(expr.args) + const op = lazyBuiltinLambda(expr.opcode) + return ctx => op(ctx, args) } - const op = lambdaForApp(ctx, expr) - const args = expr.args.map(arg => evaluateExpr(ctx, arg)) - if (args.some(arg => arg.isLeft())) { - return args.find(arg => arg.isLeft())! + const op = lambdaForApp(table, expr) + return ctx => { + const argValues = args.map(arg => arg(ctx)) + if (argValues.some(arg => arg.isLeft())) { + return argValues.find(arg => arg.isLeft())! + } + + return op( + ctx, + argValues.map(a => a.unwrap()) + ) } - return op(args.map(a => a.unwrap())) case 'let': - return evaluateExpr(ctx, expr.expr) + return evaluateExpr(table, expr.expr) } } -function lambdaForApp(ctx: Context, app: QuintApp): (args: RuntimeValue[]) => Either { +function lambdaForApp( + table: LookupTable, + app: QuintApp +): (ctx: Context, args: RuntimeValue[]) => Either { const { id, opcode } = app - if (!ctx.table.has(id)) { - return builtinLambda(ctx, opcode) + const def = table.get(id)! + if (!def) { + return builtinLambda(opcode) } - const def = ctx.table.get(id)! - const value = evaluateDef(ctx, def) - if (value.isLeft()) { - return _ => value - } - const arrow = value.value.toArrow() - return args => { + const value = evaluateDef(table, def) + return (ctx, args) => { + const lambdaResult = value(ctx) + if (lambdaResult.isLeft()) { + return lambdaResult + } + const arrow = lambdaResult.value.toArrow() + ctx.recorder.onUserOperatorCall(app) - const result = arrow(args) + const result = arrow(ctx, args) ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) return result } diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index db9b25447..00fb2e269 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -72,7 +72,7 @@ import { QuintEx, QuintLambdaParameter, QuintName } from '../../ir/quintIr' import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { toMaybe } from './base' -import { evaluateExpr } from './evaluator' +import { EvalFunction, evaluateExpr } from './evaluator' import { Context } from './Context' /** @@ -252,8 +252,8 @@ export const rv = { * @param body the lambda body expression * @returns a runtime value of lambda */ - mkLambda: (params: QuintLambdaParameter[], body: QuintEx, ctx: Context) => { - return new RuntimeValueLambda(params, body, ctx) + mkLambda: (params: QuintLambdaParameter[], body: EvalFunction) => { + return new RuntimeValueLambda(params, body) }, fromQuintEx: (ex: QuintEx): RuntimeValue => { @@ -444,7 +444,7 @@ export interface RuntimeValue extends EvalResult, ValueObject, Iterable Either + toArrow(): (ctx: Context, args: RuntimeValue[]) => Either /** * If the result is a variant, return the label and the value. @@ -607,14 +607,15 @@ abstract class RuntimeValueBase implements RuntimeValue { throw new Error('Expected a 2-tuple') } - toArrow(): (args: RuntimeValue[]) => Either { + toArrow(): (ctx: Context, args: RuntimeValue[]) => Either { if (!(this instanceof RuntimeValueLambda)) { throw new Error('Expected a lambda value') } const lam = this as RuntimeValueLambda + const bodyEval = this.body - return (args: RuntimeValue[]) => { + return (ctx: Context, args: RuntimeValue[]) => { if (lam.params.length !== args.length) { return left({ code: 'QNT506', @@ -625,14 +626,14 @@ abstract class RuntimeValueBase implements RuntimeValue { return [param.id, args[i]] }) - lam.ctx.addConstants(lam.consts) - lam.ctx.addNamespaces(lam.namespaces) - lam.ctx.addParams(paramEntries) + // ctx.addConstants(lam.consts) + // ctx.addNamespaces(lam.namespaces) + ctx.addParams(paramEntries) // lam.ctx.disableMemo() - const result = evaluateExpr(lam.ctx, lam.body) - lam.ctx.removeConstants() - lam.ctx.removeNamespaces() - lam.ctx.removeParams() + const result = bodyEval(ctx) + // ctx.removeConstants() + // ctx.removeNamespaces() + ctx.removeParams() // lam.ctx.enableMemo() return result @@ -1645,18 +1646,18 @@ class RuntimeValueInfSet extends RuntimeValueBase implements RuntimeValue { */ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue { params: QuintLambdaParameter[] - body: QuintEx - ctx: Context - consts: ImmutableMap> - namespaces: List + body: EvalFunction + // ctx: Context + // consts: ImmutableMap> + // namespaces: List - constructor(params: QuintLambdaParameter[], body: QuintEx, ctx: Context) { + constructor(params: QuintLambdaParameter[], body: EvalFunction) { super(false) this.params = params this.body = body - this.ctx = ctx - this.consts = ctx.consts - this.namespaces = ctx.namespaces + // this.ctx = ctx + // this.consts = ctx.consts + // this.namespaces = ctx.namespaces } eval(args?: any[]) { @@ -1674,7 +1675,7 @@ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue kind: 'lambda', params: this.params, qualifier: 'def', - expr: this.body, + expr: { id: gen.nextId(), kind: 'name', name: 'lambda' }, } } } From 04ceb06b01c3e597f09f611e8c0d44a1e5f32a87 Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 20 Aug 2024 21:11:06 -0300 Subject: [PATCH 06/37] Use registers for parameters --- quint/src/cliCommands.ts | 3 +- quint/src/runtime/impl/Context.ts | 32 +----- quint/src/runtime/impl/evaluator.ts | 150 ++++++++----------------- quint/src/runtime/impl/runtimeValue.ts | 61 +++++----- 4 files changed, 84 insertions(+), 162 deletions(-) diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 11adf5ffc..3a9c0323c 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -568,8 +568,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise } +export interface Register { + value: Either +} + export class Context { - public memo: Map Either> = new Map() - public memoEnabled: boolean = true - public params: ImmutableMap> = ImmutableMap() public consts: ImmutableMap> = ImmutableMap() public namespaces: List = List() public varStorage: VarStorage = new VarStorage() @@ -25,7 +26,6 @@ export class Context { public constsByInstance: Map>> = new Map() private constHistory: ImmutableMap>[] = [] - private paramHistory: ImmutableMap>[] = [] private namespacesHistory: List[] = [] constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint) { @@ -34,8 +34,6 @@ export class Context { } reset() { - this.memo = new Map() - this.params = ImmutableMap() this.varStorage = new VarStorage() } @@ -47,7 +45,6 @@ export class Context { const varName = this.varWithNamespaces(id) const key = [id, varName].join('#') const result = this.varStorage.vars.get(key) - // console.log('getting', id, varName, result) if (!result) { return left({ code: 'QNT502', message: `Variable ${varName} not set` }) } @@ -67,27 +64,6 @@ export class Context { return revertedNamespaces.concat([this.varStorage.varNames.get(id)!] || []).join('::') } - addParams(params: [bigint, RuntimeValue][]) { - this.paramHistory.push(this.params) - this.params = this.params.merge(params.map(([id, param]) => [id, right(param)])) - } - - removeParams() { - this.params = this.paramHistory.pop()! - } - - clearMemo() { - this.memo = new Map() - } - - disableMemo() { - this.memoEnabled = false - } - - enableMemo() { - this.memoEnabled = true - } - addConstants(consts: ImmutableMap>) { this.constHistory.push(this.consts) this.consts = this.consts.merge(consts) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index cf8f15834..c6685fdea 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -1,5 +1,4 @@ import { Either, left, right } from '@sweet-monads/either' -import { EffectScheme } from '../../effects/base' import { QuintApp, QuintEx, QuintOpDef } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { QuintError } from '../../quintError' @@ -7,36 +6,40 @@ import { TraceRecorder } from '../trace' import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' import { Trace } from './trace' import { RuntimeValue, rv } from './runtimeValue' -import { Context } from './Context' +import { Context, Register } from './Context' import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' import { List, Map as ImmutableMap } from 'immutable' -import { expressionToString } from '../../ir/IRprinting' export type EvalFunction = (ctx: Context) => Either +export type Builder = { + table: LookupTable + paramRegistry: Map +} + export class Evaluator { public ctx: Context public recorder: TraceRecorder private trace: Trace = new Trace() private rng: Rng - private lookupTable: LookupTable + private builder: Builder constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { this.ctx = new Context(recorder, rng.next) this.recorder = recorder this.rng = rng - this.lookupTable = table + this.builder = { table, paramRegistry: new Map() } } get table(): LookupTable { console.log('getting table') - return this.lookupTable + return this.builder.table } updateTable(table: LookupTable) { - this.lookupTable = table + this.builder.table = table } shift() { @@ -47,7 +50,6 @@ export class Evaluator { this.trace.extend(this.ctx.varStorage.asRecord()) // TODO: save on trace this.ctx.nondetPicks = ImmutableMap() - // this.ctx.clearMemo() // FIXME: clear only non-pure } shiftAndCheck(): string[] { @@ -63,7 +65,7 @@ export class Evaluator { } evaluate(expr: QuintEx): Either { - const value = evaluateExpr(this.table, expr)(this.ctx) + const value = evaluateExpr(this.builder, expr)(this.ctx) return value.map(rv.toQuintEx) } @@ -73,24 +75,14 @@ export class Evaluator { inv: QuintEx, nruns: number, nsteps: number, - ntraces: number, - effects: Map + ntraces: number ): Either { let errorsFound = 0 let failure: QuintError | undefined = undefined - // effects.forEach((scheme, id) => { - // const [mode] = modeForEffect(scheme) - // if (mode === 'pureval' || mode === 'puredef') { - // console.log('pure key', id) - // this.ctx.pureKeys = this.ctx.pureKeys.add(id) - // } - // }) - // - - const initEval = evaluateExpr(this.table, init) - const stepEval = evaluateExpr(this.table, step) - const invEval = evaluateExpr(this.table, inv) + const initEval = evaluateExpr(this.builder, init) + const stepEval = evaluateExpr(this.builder, step) + const invEval = evaluateExpr(this.builder, inv) // TODO: room for improvement here for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { @@ -155,7 +147,6 @@ export class Evaluator { ? left(failure) : right({ id: 0n, kind: 'bool', value: errorsFound == 0 }) this.recorder.onRunReturn(outcome, this.trace.get()) - // this.nextVars = new Map([...nextVarsSnapshot.entries()]) } const outcome: Either = failure @@ -170,7 +161,7 @@ export class Evaluator { // save the initial seed let seed = this.rng.getState() - const testEval = evaluateExpr(this.table, test) + const testEval = evaluateExpr(this.builder, test) let nsamples = 1 // run up to maxSamples, stop on the first failure @@ -277,46 +268,13 @@ export class Evaluator { } } -export function evaluateExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => Either { - console.log('building expr', expr.id) - const exprEval = evaluateNewExpr(table, expr) +export function evaluateExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { + const exprEval = evaluateNewExpr(builder, expr) return ctx => exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) - // return ctx => { - // const id = expr.id - // if (id === 0n) { - // return evaluateNewExpr( - // ctx, - // expr - // )(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) - // } - // const memoValue = ctx.memo.get(id) - // if (memoValue === undefined) { - // const result = evaluateNewExpr(ctx, expr) - // ctx.memo.set(id, result) - // return result(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) - // } - - // return memoValue!(ctx) - // } - - // console.log('[get] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) - // if (ctx.memo.has(id)) { - // // if (ctx.pureKeys.has(id)) { - // // console.log('pure key', id, expressionToString(expr)) - // // } - // return ctx.memo.get(id)! - // } - - // return ctx => - // evaluateNewExpr(ctx, expr)(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: id } : err)) - // console.log('[set] expression', expressionToString(expr), 'memo enabled:', ctx.memoEnabled) - // if (ctx.memoEnabled) { - // ctx.memo.set(id, result) - // } } export function evaluateUnderDefContext( - table: LookupTable, + builder: Builder, def: LookupDefinition, evaluate: (ctx: Context) => Either ): (ctx: Context) => Either { @@ -327,9 +285,9 @@ export function evaluateUnderDefContext( const overrides: [bigint, (ctx: Context) => Either][] = instance.overrides.map( ([param, expr]) => { - const id = table.get(param.id)!.id + const id = builder.table.get(param.id)!.id - return [id, evaluateExpr(table, expr)] + return [id, evaluateExpr(builder, expr)] } ) @@ -339,15 +297,11 @@ export function evaluateUnderDefContext( ctx.consts = ImmutableMap(overrides.map(([id, evaluate]) => [id, evaluate(ctx)])) ctx.namespaces = List(def.namespaces) - // ctx.addNamespaces(List(def.namespaces)) - // ctx.disableMemo() // We could have one memo per constant const result = evaluate(ctx) ctx.consts = constsBefore ctx.namespaces = namespacesBefore - // ctx.removeNamespaces() - // ctx.enableMemo() return result } } @@ -367,23 +321,19 @@ function evaluateNondet( return pick } -function evaluateDefCore( - table: LookupTable, - def: LookupDefinition -): (ctx: Context) => Either { +function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { switch (def.kind) { case 'def': if (def.qualifier === 'nondet') { - const body = evaluateExpr(table, def.expr) + const body = evaluateExpr(builder, def.expr) return (ctx: Context) => { - ctx.disableMemo() return evaluateNondet(ctx, def, body) } } if (def.qualifier === 'action') { const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } - const body = evaluateExpr(table, def.expr) + const body = evaluateExpr(builder, def.expr) return (ctx: Context) => { ctx.recorder.onUserOperatorCall(app) const result = body(ctx) @@ -392,21 +342,19 @@ function evaluateDefCore( } } - return evaluateExpr(table, def.expr) + return evaluateExpr(builder, def.expr) case 'param': { - return (ctx: Context) => { - ctx.disableMemo() - const result = ctx.params.get(def.id) - - if (!result) { - return left({ code: 'QNT501', message: `Parameter ${def.name} not set in context` }) - } - return result + const register = builder.paramRegistry.get(def.id) + if (!register) { + const reg: Register = { value: left({ code: 'QNT501', message: `Parameter ${def.name} not set` }) } + builder.paramRegistry.set(def.id, reg) + return _ => reg.value } + return _ => register.value } + case 'var': { return (ctx: Context) => { - ctx.disableMemo() const result = ctx.getVar(def.id) if (!result) { @@ -417,7 +365,6 @@ function evaluateDefCore( } case 'const': return (ctx: Context) => { - ctx.disableMemo() const constValue = ctx.consts.get(def.id) if (!constValue) { return left({ code: 'QNT503', message: `Constant ${def.name}(id: ${def.id}) not set` }) @@ -430,11 +377,11 @@ function evaluateDefCore( } } -function evaluateDef(table: LookupTable, def: LookupDefinition): (ctx: Context) => Either { - return evaluateUnderDefContext(table, def, evaluateDefCore(table, def)) +function evaluateDef(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { + return evaluateUnderDefContext(builder, def, evaluateDefCore(builder, def)) } -function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => Either { +function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { switch (expr.kind) { case 'int': case 'bool': @@ -443,24 +390,23 @@ function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => E return _ => right(rv.fromQuintEx(expr)) case 'lambda': // Lambda is also like a value, but we should construct it with the context - const body = evaluateExpr(table, expr.expr) - const lambda = rv.mkLambda(expr.params, body) + const body = evaluateExpr(builder, expr.expr) + const lambda = rv.mkLambda(expr.params, body, builder.paramRegistry) return _ => right(lambda) case 'name': - const def = table.get(expr.id) + const def = builder.table.get(expr.id) if (!def) { // TODO: Do we also need to return builtin ops for higher order usage? const lambda = builtinValue(expr.name) return _ => lambda } - return evaluateDef(table, def) + return evaluateDef(builder, def) case 'app': if (expr.opcode === 'assign') { - const varDef = table.get(expr.args[0].id)! - const exprEval = evaluateExpr(table, expr.args[1]) - // make sure it is registered in the storage + const varDef = builder.table.get(expr.args[0].id)! + const exprEval = evaluateExpr(builder, expr.args[1]) - return evaluateUnderDefContext(table, varDef, ctx => { + return evaluateUnderDefContext(builder, varDef, ctx => { ctx.discoverVar(varDef.id, varDef.name) return exprEval(ctx).map(value => { ctx.setNextVar(varDef.id, value) @@ -469,7 +415,7 @@ function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => E }) } - const args = expr.args.map(arg => evaluateExpr(table, arg)) + const args = expr.args.map(arg => evaluateExpr(builder, arg)) // In these special ops, we don't want to evaluate the arguments before evaluating application if (lazyOps.includes(expr.opcode)) { @@ -477,7 +423,7 @@ function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => E return ctx => op(ctx, args) } - const op = lambdaForApp(table, expr) + const op = lambdaForApp(builder, expr) return ctx => { const argValues = args.map(arg => arg(ctx)) if (argValues.some(arg => arg.isLeft())) { @@ -490,22 +436,22 @@ function evaluateNewExpr(table: LookupTable, expr: QuintEx): (ctx: Context) => E ) } case 'let': - return evaluateExpr(table, expr.expr) + return evaluateExpr(builder, expr.expr) } } function lambdaForApp( - table: LookupTable, + builder: Builder, app: QuintApp ): (ctx: Context, args: RuntimeValue[]) => Either { const { id, opcode } = app - const def = table.get(id)! + const def = builder.table.get(id)! if (!def) { return builtinLambda(opcode) } - const value = evaluateDef(table, def) + const value = evaluateDef(builder, def) return (ctx, args) => { const lambdaResult = value(ctx) if (lambdaResult.isLeft()) { diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index 00fb2e269..620d1d7be 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -73,7 +73,7 @@ import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { toMaybe } from './base' import { EvalFunction, evaluateExpr } from './evaluator' -import { Context } from './Context' +import { Context, Register } from './Context' /** * A factory of runtime values that should be used to instantiate new values. @@ -252,8 +252,19 @@ export const rv = { * @param body the lambda body expression * @returns a runtime value of lambda */ - mkLambda: (params: QuintLambdaParameter[], body: EvalFunction) => { - return new RuntimeValueLambda(params, body) + mkLambda: (params: QuintLambdaParameter[], body: EvalFunction, paramRegistry: Map) => { + const registers = params.map(param => { + const register = paramRegistry.get(param.id)! + if (!register) { + const reg: Register = { value: left({ code: 'QNT501', message: `Parameter ${param.name} not set` }) } + paramRegistry.set(param.id, reg) + return reg + } + + return register + }) + + return new RuntimeValueLambda(body, registers) }, fromQuintEx: (ex: QuintEx): RuntimeValue => { @@ -613,30 +624,20 @@ abstract class RuntimeValueBase implements RuntimeValue { } const lam = this as RuntimeValueLambda - const bodyEval = this.body return (ctx: Context, args: RuntimeValue[]) => { - if (lam.params.length !== args.length) { + if (lam.registers.length !== args.length) { return left({ code: 'QNT506', - message: `Lambda expects ${lam.params.length} arguments, but got ${args.length}`, + message: `Lambda expects ${lam.registers.length} arguments, but got ${args.length}`, }) } - const paramEntries: [bigint, RuntimeValue][] = lam.params.map((param, i) => { - return [param.id, args[i]] - }) - // ctx.addConstants(lam.consts) - // ctx.addNamespaces(lam.namespaces) - ctx.addParams(paramEntries) - // lam.ctx.disableMemo() - const result = bodyEval(ctx) - // ctx.removeConstants() - // ctx.removeNamespaces() - ctx.removeParams() - // lam.ctx.enableMemo() + lam.registers.forEach((reg, i) => { + reg.value = right(args[i]) + }) - return result + return lam.body(ctx) } } @@ -1645,19 +1646,13 @@ class RuntimeValueInfSet extends RuntimeValueBase implements RuntimeValue { * RuntimeValueLambda cannot be compared with other values. */ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue { - params: QuintLambdaParameter[] body: EvalFunction - // ctx: Context - // consts: ImmutableMap> - // namespaces: List + registers: Register[] - constructor(params: QuintLambdaParameter[], body: EvalFunction) { + constructor(body: EvalFunction, registers: Register[]) { super(false) - this.params = params this.body = body - // this.ctx = ctx - // this.consts = ctx.consts - // this.namespaces = ctx.namespaces + this.registers = registers } eval(args?: any[]) { @@ -1673,9 +1668,15 @@ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue return { id: gen.nextId(), kind: 'lambda', - params: this.params, + params: Array.from(this.registers.keys()).map(i => { + return { id: gen.nextId(), name: `_a${i}` } + }), qualifier: 'def', - expr: { id: gen.nextId(), kind: 'name', name: 'lambda' }, + expr: { + kind: 'str', + value: `lambda_${this.registers.length}_params`, + id: gen.nextId(), + }, } } } From 76e069c9bbdfe8efcc0df965d6abd91ffe2ef3a8 Mon Sep 17 00:00:00 2001 From: bugarela Date: Wed, 21 Aug 2024 08:40:37 -0300 Subject: [PATCH 07/37] Save constants in registers as well --- quint/src/runtime/impl/Context.ts | 35 ---------------------------- quint/src/runtime/impl/VarStorage.ts | 2 +- quint/src/runtime/impl/evaluator.ts | 29 ++++++++++++----------- 3 files changed, 17 insertions(+), 49 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 613206d7f..e1bd3c354 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -15,19 +15,12 @@ export interface Register { } export class Context { - public consts: ImmutableMap> = ImmutableMap() public namespaces: List = List() public varStorage: VarStorage = new VarStorage() public rand: (n: bigint) => bigint - public pureKeys: Set = new Set() public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder - public constsByInstance: Map>> = new Map() - - private constHistory: ImmutableMap>[] = [] - private namespacesHistory: List[] = [] - constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint) { this.recorder = recorder this.rand = rand @@ -54,7 +47,6 @@ export class Context { setNextVar(id: bigint, value: RuntimeValue) { const varName = this.varWithNamespaces(id) - // console.log('setting', id, varName, value) const key = [id, varName].join('#') this.varStorage.nextVars.set(key, right(value)) } @@ -63,31 +55,4 @@ export class Context { const revertedNamespaces = this.namespaces.slice().reverse() return revertedNamespaces.concat([this.varStorage.varNames.get(id)!] || []).join('::') } - - addConstants(consts: ImmutableMap>) { - this.constHistory.push(this.consts) - this.consts = this.consts.merge(consts) - } - - removeConstants() { - this.consts = this.constHistory.pop()! - } - - addNamespaces(namespaces: List | undefined) { - this.namespacesHistory.push(this.namespaces) - if (is(this.namespaces.take(namespaces?.size ?? 0), namespaces)) { - // Redundant namespaces, nothing to add - return - } - - this.namespaces = this.namespaces.concat(namespaces || []) - } - - removeNamespaces() { - this.namespaces = this.namespacesHistory.pop()! - } - - constsSnapshot(): [bigint, Either][] { - return [...this.consts.entries()] - } } diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index b7c16bfe0..90a08a793 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -30,6 +30,6 @@ export class VarStorage { } nextVarsSnapshot(): Map> { - return this.nextVars + return new Map([...this.nextVars.entries()]) } } diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index c6685fdea..725783b43 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -17,6 +17,7 @@ export type EvalFunction = (ctx: Context) => Either export type Builder = { table: LookupTable paramRegistry: Map + constRegistry: Map } export class Evaluator { @@ -30,7 +31,7 @@ export class Evaluator { this.ctx = new Context(recorder, rng.next) this.recorder = recorder this.rng = rng - this.builder = { table, paramRegistry: new Map() } + this.builder = { table, paramRegistry: new Map(), constRegistry: new Map() } } get table(): LookupTable { @@ -283,24 +284,27 @@ export function evaluateUnderDefContext( } const instance = def.importedFrom - const overrides: [bigint, (ctx: Context) => Either][] = instance.overrides.map( + const overrides: [Register, (ctx: Context) => Either][] = instance.overrides.map( ([param, expr]) => { const id = builder.table.get(param.id)!.id + let register = builder.constRegistry.get(id) + if (!register) { + register = { value: left({ code: 'QNT504', message: `Constant ${param.name} not set` }) } + builder.constRegistry.set(id, register) + } - return [id, evaluateExpr(builder, expr)] + return [register, evaluateExpr(builder, expr)] } ) return ctx => { - const constsBefore = ctx.consts const namespacesBefore = ctx.namespaces - ctx.consts = ImmutableMap(overrides.map(([id, evaluate]) => [id, evaluate(ctx)])) + overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) ctx.namespaces = List(def.namespaces) const result = evaluate(ctx) - ctx.consts = constsBefore ctx.namespaces = namespacesBefore return result } @@ -364,14 +368,13 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context } } case 'const': - return (ctx: Context) => { - const constValue = ctx.consts.get(def.id) - if (!constValue) { - return left({ code: 'QNT503', message: `Constant ${def.name}(id: ${def.id}) not set` }) - } - return constValue + const register = builder.constRegistry.get(def.id) + if (!register) { + const reg: Register = { value: left({ code: 'QNT501', message: `Constant ${def.name} not set` }) } + builder.constRegistry.set(def.id, reg) + return _ => reg.value } - + return _ => register.value default: return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) } From ff60acce1081763330d5118ba1675536de68b09b Mon Sep 17 00:00:00 2001 From: bugarela Date: Wed, 21 Aug 2024 09:32:47 -0300 Subject: [PATCH 08/37] Implement `then` and add recorder calls --- quint/src/runtime/impl/Context.ts | 12 ++++++++++++ quint/src/runtime/impl/builtins.ts | 27 +++++++++++++++++++++++++-- quint/src/runtime/impl/evaluator.ts | 24 ++++++++++++++---------- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index e1bd3c354..637e3f529 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -4,6 +4,7 @@ import { RuntimeValue } from './runtimeValue' import { TraceRecorder } from '../trace' import { Map as ImmutableMap, List, is } from 'immutable' import { VarStorage } from './VarStorage' +import { Trace } from './trace' export interface NondetPick { name: string @@ -20,6 +21,7 @@ export class Context { public rand: (n: bigint) => bigint public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder + public trace: Trace = new Trace() constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint) { this.recorder = recorder @@ -30,6 +32,16 @@ export class Context { this.varStorage = new VarStorage() } + shift() { + if (this.varStorage.nextVars.size === 0) { + return + } + this.varStorage.shiftVars() + this.trace.extend(this.varStorage.asRecord()) + // TODO: save on trace + this.nondetPicks = ImmutableMap() + } + discoverVar(id: bigint, name: string) { this.varStorage.varNames.set(id, name) } diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index 9df478dd8..ba7a275e2 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -7,7 +7,7 @@ import { RuntimeValue, rv } from './runtimeValue' import { chunk } from 'lodash' import { expressionToString } from '../../ir/IRprinting' import { zerog } from '../../idGenerator' -import { QuintEx } from '../../ir/quintIr' +import { QuintApp, QuintEx } from '../../ir/quintIr' export function builtinValue(name: string): Either { switch (name) { @@ -75,15 +75,18 @@ export function lazyBuiltinLambda( } case 'actionAny': + const app: QuintApp = { id: 0n, kind: 'app', opcode: 'actionAny', args: [] } return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() - const evaluationResults = args.map(arg => { + const evaluationResults = args.map((arg, i) => { + ctx.recorder.onAnyOptionCall(app, i) const result = arg(ctx).map(result => { // Save vars const successor = ctx.varStorage.nextVarsSnapshot() return result.toBool() ? [successor] : [] }) + ctx.recorder.onAnyOptionReturn(app, i) // Recover snapshot (regardless of success or failure) ctx.varStorage.nextVars = nextVarsSnapshot @@ -96,6 +99,8 @@ export function lazyBuiltinLambda( .mapLeft(errors => errors[0]) return processedResults.map(potentialSuccessors => { + ctx.recorder.onAnyReturn(args.length, -1) + switch (potentialSuccessors.length) { case 0: return rv.mkBool(false) @@ -178,6 +183,24 @@ export function lazyBuiltinLambda( }) } case 'then': + return (ctx, args) => { + const oldState = ctx.varStorage.asRecord() + return args[0](ctx).chain(firstResult => { + if (!firstResult.toBool()) { + return left({ + code: 'QNT513', + message: `Cannot continue in A.then(B), A evaluates to 'false'`, + }) + } + + ctx.shift() + const newState = ctx.varStorage.asRecord() + ctx.recorder.onNextState(oldState, newState) + + return args[1](ctx) + }) + } + default: return () => left({ code: 'QNT000', message: 'Unknown stateful op' }) } diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 725783b43..fca7e0555 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -23,7 +23,6 @@ export type Builder = { export class Evaluator { public ctx: Context public recorder: TraceRecorder - private trace: Trace = new Trace() private rng: Rng private builder: Builder @@ -39,18 +38,16 @@ export class Evaluator { return this.builder.table } + get trace(): Trace { + return this.ctx.trace + } + updateTable(table: LookupTable) { this.builder.table = table } - shift() { - if (this.ctx.varStorage.nextVars.size === 0) { - return - } - this.ctx.varStorage.shiftVars() - this.trace.extend(this.ctx.varStorage.asRecord()) - // TODO: save on trace - this.ctx.nondetPicks = ImmutableMap() + shift(): void { + this.ctx.shift() } shiftAndCheck(): string[] { @@ -66,8 +63,15 @@ export class Evaluator { } evaluate(expr: QuintEx): Either { + if (expr.kind === 'app') { + this.recorder.onUserOperatorCall(expr) + } const value = evaluateExpr(this.builder, expr)(this.ctx) - return value.map(rv.toQuintEx) + const result = value.map(rv.toQuintEx) + if (expr.kind === 'app') { + this.recorder.onUserOperatorReturn(expr, expr.args, result) + } + return result } simulate( From 19a63bb07b8cd758b30bd7f24979118eda51ae56 Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 22 Aug 2024 11:19:38 -0300 Subject: [PATCH 09/37] Use registers for variables --- quint/src/runtime/impl/Context.ts | 37 +-------- quint/src/runtime/impl/VarStorage.ts | 48 ++++++++--- quint/src/runtime/impl/builtins.ts | 16 ++-- quint/src/runtime/impl/evaluator.ts | 116 +++++++++++++++++++-------- 4 files changed, 129 insertions(+), 88 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 637e3f529..0c6bc9fa4 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -3,7 +3,7 @@ import { QuintError } from '../../quintError' import { RuntimeValue } from './runtimeValue' import { TraceRecorder } from '../trace' import { Map as ImmutableMap, List, is } from 'immutable' -import { VarStorage } from './VarStorage' +import { VarRegister, VarStorage } from './VarStorage' import { Trace } from './trace' export interface NondetPick { @@ -16,16 +16,16 @@ export interface Register { } export class Context { - public namespaces: List = List() - public varStorage: VarStorage = new VarStorage() public rand: (n: bigint) => bigint public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder public trace: Trace = new Trace() + public varStorage: VarStorage - constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint) { + constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint, varStorage: VarStorage) { this.recorder = recorder this.rand = rand + this.varStorage = varStorage } reset() { @@ -33,38 +33,9 @@ export class Context { } shift() { - if (this.varStorage.nextVars.size === 0) { - return - } this.varStorage.shiftVars() this.trace.extend(this.varStorage.asRecord()) // TODO: save on trace this.nondetPicks = ImmutableMap() } - - discoverVar(id: bigint, name: string) { - this.varStorage.varNames.set(id, name) - } - - getVar(id: bigint): Either { - const varName = this.varWithNamespaces(id) - const key = [id, varName].join('#') - const result = this.varStorage.vars.get(key) - if (!result) { - return left({ code: 'QNT502', message: `Variable ${varName} not set` }) - } - - return result - } - - setNextVar(id: bigint, value: RuntimeValue) { - const varName = this.varWithNamespaces(id) - const key = [id, varName].join('#') - this.varStorage.nextVars.set(key, right(value)) - } - - private varWithNamespaces(id: bigint): string { - const revertedNamespaces = this.namespaces.slice().reverse() - return revertedNamespaces.concat([this.varStorage.varNames.get(id)!] || []).join('::') - } } diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 90a08a793..3b1f241f4 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -1,35 +1,59 @@ -import { Either } from '@sweet-monads/either' +import { Either, left } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' import { QuintEx, QuintStr } from '../../ir/quintIr' import { Map as ImmutableMap } from 'immutable' +import { Register } from './Context' + +export interface VarRegister extends Register { + name: string +} + +const initialRegisterValue: Either = left({ code: 'QNT502', message: 'Variable not set' }) export class VarStorage { - public vars: ImmutableMap> = ImmutableMap() - public nextVars: Map> = new Map() - public varNames: Map = new Map() + public vars: ImmutableMap = ImmutableMap() + public nextVars: ImmutableMap = ImmutableMap() shiftVars() { - this.vars = ImmutableMap(this.nextVars) - this.nextVars = new Map() + // TODO: change this so registers are kept + this.vars.forEach((reg, key) => { + reg.value = this.nextVars.get(key)?.value ?? initialRegisterValue + }) + + this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) } asRecord(): QuintEx { + // console.log('vars', [...this.vars.keySeq()]) return { id: 0n, kind: 'app', opcode: 'Rec', args: [...this.vars.entries()] - .map(([key, value]) => { - const [id, varName] = key.split('#') - const nameEx: QuintStr = { id: BigInt(id), kind: 'str', value: varName } - return [nameEx, rv.toQuintEx(value.unwrap())] + .map(([_key, reg]) => { + const nameEx: QuintStr = { id: 0n, kind: 'str', value: reg.name } + return [nameEx, rv.toQuintEx(reg.value.unwrap())] }) .flat(), } } - nextVarsSnapshot(): Map> { - return new Map([...this.nextVars.entries()]) + reset() { + this.vars.forEach(reg => (reg.value = initialRegisterValue)) + this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) + } + + nextVarsSnapshot(): ImmutableMap { + return this.nextVars.map(reg => ({ ...reg })) + } + + recoverNextVars(snapshot: ImmutableMap) { + this.nextVars.forEach((reg, key) => { + const snapshotReg = snapshot.get(key) + if (snapshotReg) { + reg.value = snapshotReg.value + } + }) } } diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index ba7a275e2..e41d5b6f0 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,7 +1,7 @@ import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { QuintError, quintErrorToString } from '../../quintError' import { Map, is, Set, Range, List } from 'immutable' -import { EvalFunction, evaluateExpr, evaluateUnderDefContext, isTrue } from './evaluator' +import { EvalFunction, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' import { chunk } from 'lodash' @@ -89,7 +89,7 @@ export function lazyBuiltinLambda( ctx.recorder.onAnyOptionReturn(app, i) // Recover snapshot (regardless of success or failure) - ctx.varStorage.nextVars = nextVarsSnapshot + ctx.varStorage.recoverNextVars(nextVarsSnapshot) return result }) @@ -105,11 +105,11 @@ export function lazyBuiltinLambda( case 0: return rv.mkBool(false) case 1: - ctx.varStorage.nextVars = potentialSuccessors[0] + ctx.varStorage.recoverNextVars(potentialSuccessors[0]) return rv.mkBool(true) default: const choice = Number(ctx.rand(BigInt(potentialSuccessors.length))) - ctx.varStorage.nextVars = potentialSuccessors[choice] + ctx.varStorage.recoverNextVars(potentialSuccessors[choice]) return rv.mkBool(true) } }) @@ -120,7 +120,7 @@ export function lazyBuiltinLambda( for (const action of args) { const result = action(ctx) if (!isTrue(result)) { - ctx.varStorage.nextVars = nextVarsSnapshot + ctx.varStorage.recoverNextVars(nextVarsSnapshot) return result.map(_ => rv.mkBool(false)) } } @@ -184,7 +184,7 @@ export function lazyBuiltinLambda( } case 'then': return (ctx, args) => { - const oldState = ctx.varStorage.asRecord() + // const oldState = ctx.varStorage.asRecord() return args[0](ctx).chain(firstResult => { if (!firstResult.toBool()) { return left({ @@ -194,8 +194,8 @@ export function lazyBuiltinLambda( } ctx.shift() - const newState = ctx.varStorage.asRecord() - ctx.recorder.onNextState(oldState, newState) + // const newState = ctx.varStorage.asRecord() + // ctx.recorder.onNextState(oldState, newState) return args[1](ctx) }) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index fca7e0555..15e497ef7 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -11,13 +11,56 @@ import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' import { List, Map as ImmutableMap } from 'immutable' +import { VarRegister, VarStorage } from './VarStorage' +import { expressionToString } from '../../ir/IRprinting' export type EvalFunction = (ctx: Context) => Either -export type Builder = { +export class Builder { table: LookupTable - paramRegistry: Map - constRegistry: Map + paramRegistry: Map = new Map() + constRegistry: Map = new Map() + public namespaces: List = List() + public varStorage: VarStorage = new VarStorage() + + constructor(table: LookupTable) { + this.table = table + } + + discoverVar(id: bigint, name: string) { + const key = [id, ...this.namespaces].join('#') + console.log('discovering var', id, name, key) + if (this.varStorage.vars.has(key)) { + return + } + + const revertedNamespaces = this.namespaces.reverse() + const varName = revertedNamespaces.push(name).join('::') + const register: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + const nextRegister: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + this.varStorage.vars = this.varStorage.vars.set(key, register) + this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) + } + + getVar(id: bigint): VarRegister { + const key = [id, ...this.namespaces].join('#') + const result = this.varStorage.vars.get(key) + if (!result) { + throw new Error(`Variable not found: ${key}`) + } + + return result + } + + getNextVar(id: bigint): VarRegister { + const key = [id, ...this.namespaces].join('#') + const result = this.varStorage.nextVars.get(key) + if (!result) { + throw new Error(`Variable not found: ${key}`) + } + + return result + } } export class Evaluator { @@ -27,10 +70,10 @@ export class Evaluator { private builder: Builder constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { - this.ctx = new Context(recorder, rng.next) this.recorder = recorder this.rng = rng - this.builder = { table, paramRegistry: new Map(), constRegistry: new Map() } + this.builder = new Builder(table) + this.ctx = new Context(recorder, rng.next, this.builder.varStorage) } get table(): LookupTable { @@ -53,7 +96,7 @@ export class Evaluator { shiftAndCheck(): string[] { const missing = [...this.ctx.varStorage.vars.keys()].filter(name => !this.ctx.varStorage.nextVars.has(name)) - if (missing.length === this.ctx.varStorage.varNames.size) { + if (missing.length === this.ctx.varStorage.vars.size) { // Nothing was changed, don't shift return [] } @@ -74,6 +117,11 @@ export class Evaluator { return result } + reset() { + this.trace.reset() + this.builder.varStorage.reset() + } + simulate( init: QuintEx, step: QuintEx, @@ -92,8 +140,7 @@ export class Evaluator { // TODO: room for improvement here for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { this.recorder.onRunCall() - this.trace.reset() - this.ctx.reset() + this.reset() // Mocked def for the trace recorder const initApp: QuintApp = { id: 0n, kind: 'app', opcode: 'q::initAndInvariant', args: [] } this.recorder.onUserOperatorCall(initApp) @@ -278,13 +325,13 @@ export function evaluateExpr(builder: Builder, expr: QuintEx): (ctx: Context) => return ctx => exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) } -export function evaluateUnderDefContext( +export function buildUnderDefContext( builder: Builder, def: LookupDefinition, - evaluate: (ctx: Context) => Either + buildFunction: () => (ctx: Context) => Either ): (ctx: Context) => Either { if (!def.importedFrom || def.importedFrom.kind !== 'instance') { - return evaluate + return buildFunction() } const instance = def.importedFrom @@ -301,16 +348,15 @@ export function evaluateUnderDefContext( } ) - return ctx => { - const namespacesBefore = ctx.namespaces - - overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) - ctx.namespaces = List(def.namespaces) + const namespacesBefore = builder.namespaces + builder.namespaces = List(def.namespaces) - const result = evaluate(ctx) + const result = buildFunction() + builder.namespaces = namespacesBefore - ctx.namespaces = namespacesBefore - return result + return ctx => { + overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) + return result(ctx) } } @@ -362,13 +408,9 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context } case 'var': { - return (ctx: Context) => { - const result = ctx.getVar(def.id) - - if (!result) { - return left({ code: 'QNT502', message: `Variable ${def.name} not set` }) - } - return result + const register = builder.getVar(def.id) + return _ => { + return register.value } } case 'const': @@ -385,7 +427,7 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context } function evaluateDef(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { - return evaluateUnderDefContext(builder, def, evaluateDefCore(builder, def)) + return buildUnderDefContext(builder, def, () => evaluateDefCore(builder, def)) } function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { @@ -411,14 +453,18 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit case 'app': if (expr.opcode === 'assign') { const varDef = builder.table.get(expr.args[0].id)! - const exprEval = evaluateExpr(builder, expr.args[1]) - - return evaluateUnderDefContext(builder, varDef, ctx => { - ctx.discoverVar(varDef.id, varDef.name) - return exprEval(ctx).map(value => { - ctx.setNextVar(varDef.id, value) - return rv.mkBool(true) - }) + return buildUnderDefContext(builder, varDef, () => { + console.log(expressionToString(expr)) + builder.discoverVar(varDef.id, varDef.name) + const register = builder.getNextVar(varDef.id) + const exprEval = evaluateExpr(builder, expr.args[1]) + + return ctx => { + return exprEval(ctx).map(value => { + register.value = right(value) + return rv.mkBool(true) + }) + } }) } From cefbc6f2442f821b8537a5469679687a98a39c41 Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 22 Aug 2024 14:24:10 -0300 Subject: [PATCH 10/37] Optimize a few things --- quint/src/runtime/impl/VarStorage.ts | 1 - quint/src/runtime/impl/builtins.ts | 33 +++++----- quint/src/runtime/impl/evaluator.ts | 93 +++++++++++++++++----------- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 3b1f241f4..b47185c45 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -25,7 +25,6 @@ export class VarStorage { } asRecord(): QuintEx { - // console.log('vars', [...this.vars.keySeq()]) return { id: 0n, kind: 'app', diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index e41d5b6f0..a37f0eee3 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,7 +1,7 @@ import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { QuintError, quintErrorToString } from '../../quintError' import { Map, is, Set, Range, List } from 'immutable' -import { EvalFunction, isTrue } from './evaluator' +import { EvalFunction, isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' import { chunk } from 'lodash' @@ -42,27 +42,24 @@ export function lazyBuiltinLambda( switch (op) { case 'and': return (ctx, args) => { - return args.reduce((acc: Either, arg: EvalFunction) => { - return acc.chain(accValue => { - if (accValue.toBool() === true) { - return arg(ctx) - } - return acc - }) - }, right(rv.mkBool(true))) + for (const arg of args) { + const result = arg(ctx) + if (!isTrue(result)) { + return result + } + } + return right(rv.mkBool(true)) } case 'or': return (ctx, args) => { - return args.reduce((acc: Either, arg: EvalFunction) => { - return acc.chain(accValue => { - if (accValue.toBool() === false) { - return arg(ctx) - } - return acc - }) - }, right(rv.mkBool(false))) + for (const arg of args) { + const result = arg(ctx) + if (!isFalse(result)) { + return result + } + } + return right(rv.mkBool(false)) } - case 'implies': return (ctx, args) => { return args[0](ctx).chain(l => { diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 15e497ef7..3e5a1b2ae 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -20,6 +20,7 @@ export class Builder { table: LookupTable paramRegistry: Map = new Map() constRegistry: Map = new Map() + scopedCachedValues: Map | undefined }> = new Map() public namespaces: List = List() public varStorage: VarStorage = new VarStorage() @@ -94,15 +95,18 @@ export class Evaluator { } shiftAndCheck(): string[] { - const missing = [...this.ctx.varStorage.vars.keys()].filter(name => !this.ctx.varStorage.nextVars.has(name)) + const missing = this.ctx.varStorage.nextVars.filter(reg => reg.value.isLeft()) - if (missing.length === this.ctx.varStorage.vars.size) { + if (missing.size === this.ctx.varStorage.vars.size) { // Nothing was changed, don't shift return [] } this.shift() - return missing.map(name => name.split('#')[1]) + return missing + .valueSeq() + .map(reg => reg.name) + .toArray() } evaluate(expr: QuintEx): Either { @@ -222,7 +226,7 @@ export class Evaluator { seed = this.rng.getState() this.recorder.onRunCall() // reset the trace - this.trace.reset() + this.reset() // run the test const result = testEval(this.ctx).map(e => e.toQuintEx(zerog)) @@ -360,30 +364,15 @@ export function buildUnderDefContext( } } -function evaluateNondet( - ctx: Context, - def: QuintOpDef, - bodyEval: (ctx: Context) => Either -): Either { - const previousPick = ctx.nondetPicks.get(def.id) - if (previousPick) { - return previousPick.value - } - - const pick = bodyEval(ctx) - ctx.nondetPicks = ctx.nondetPicks.set(def.id, { name: def.name, value: pick }) - return pick -} - function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { switch (def.kind) { case 'def': - if (def.qualifier === 'nondet') { - const body = evaluateExpr(builder, def.expr) - return (ctx: Context) => { - return evaluateNondet(ctx, def, body) - } - } + // if (def.qualifier === 'nondet') { + // const body = evaluateExpr(builder, def.expr) + // return (ctx: Context) => { + // return evaluateNondet(ctx, def, body) + // } + // } if (def.qualifier === 'action') { const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } @@ -396,7 +385,24 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context } } - return evaluateExpr(builder, def.expr) + if (def.expr.kind === 'lambda') { + return evaluateExpr(builder, def.expr) + } + + if (def.depth === undefined || def.depth === 0) { + return evaluateExpr(builder, def.expr) + } + + const cachedValue = builder.scopedCachedValues.get(def.id)! + const bodyEval = evaluateExpr(builder, def.expr) + + return ctx => { + if (cachedValue.value === undefined) { + cachedValue.value = bodyEval(ctx) + return cachedValue.value + } + return cachedValue.value + } case 'param': { const register = builder.paramRegistry.get(def.id) if (!register) { @@ -436,7 +442,8 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit case 'bool': case 'str': // These are already values, just return them - return _ => right(rv.fromQuintEx(expr)) + const value = right(rv.fromQuintEx(expr)) + return _ => value case 'lambda': // Lambda is also like a value, but we should construct it with the context const body = evaluateExpr(builder, expr.expr) @@ -478,18 +485,30 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit const op = lambdaForApp(builder, expr) return ctx => { - const argValues = args.map(arg => arg(ctx)) - if (argValues.some(arg => arg.isLeft())) { - return argValues.find(arg => arg.isLeft())! + const argValues = [] + for (const arg of args) { + const argValue = arg(ctx) + if (argValue.isLeft()) { + return argValue + } + argValues.push(argValue.unwrap()) } - return op( - ctx, - argValues.map(a => a.unwrap()) - ) + return op(ctx, argValues) } + case 'let': - return evaluateExpr(builder, expr.expr) + let cachedValue = builder.scopedCachedValues.get(expr.opdef.id) + if (!cachedValue) { + cachedValue = { value: undefined } + builder.scopedCachedValues.set(expr.opdef.id, cachedValue) + } + const bodyEval = evaluateExpr(builder, expr.expr) + return ctx => { + const result = bodyEval(ctx) + cachedValue!.value = undefined + return result + } } } @@ -522,3 +541,7 @@ function lambdaForApp( export function isTrue(value: Either): boolean { return value.isRight() && value.value.toBool() === true } + +export function isFalse(value: Either): boolean { + return value.isRight() && value.value.toBool() === false +} From 1f7cb83cb98ce241bb6ef2c23bc47b10251ff7cf Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 22 Aug 2024 14:57:13 -0300 Subject: [PATCH 11/37] Go back to using RuntimeValue in traces --- quint/src/cliCommands.ts | 6 ++--- quint/src/graphics.ts | 5 ++-- quint/src/runtime/impl/VarStorage.ts | 27 +++++++++++---------- quint/src/runtime/impl/evaluator.ts | 35 ++++++++++++++++------------ quint/src/runtime/impl/trace.ts | 10 ++++---- quint/src/runtime/trace.ts | 31 ++++++++++++------------ 6 files changed, 62 insertions(+), 52 deletions(-) diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 3a9c0323c..0b46c119f 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -46,7 +46,7 @@ import { createFinders, formatError } from './errorReporter' import { DocumentationEntry, produceDocs, toMarkdown } from './docs' import { QuintError, quintErrorToString } from './quintError' import { TestOptions, TestResult } from './runtime/testing' -import { IdGenerator, newIdGenerator } from './idGenerator' +import { IdGenerator, newIdGenerator, zerog } from './idGenerator' import { Outcome, SimulatorOptions } from './simulation' import { ofItf, toItf } from './itf' import { printExecutionFrameRec, printTrace, terminalWidth } from './graphics' @@ -577,8 +577,8 @@ export async function runSimulator(prev: TypecheckedStage): Promise e.toQuintEx(zerog)) + const frames = recorder.bestTraces[0]?.frame?.subframes ?? [] const seed = recorder.bestTraces[0]?.seed switch (outcome.status) { case 'error': diff --git a/quint/src/graphics.ts b/quint/src/graphics.ts index 4de52b2b1..629775ff0 100644 --- a/quint/src/graphics.ts +++ b/quint/src/graphics.ts @@ -28,6 +28,7 @@ import { import { QuintDeclaration, QuintEx, isAnnotatedDef } from './ir/quintIr' import { ExecutionFrame } from './runtime/trace' +import { zerog } from './idGenerator' import { ConcreteRow, QuintType, Row, isUnitType } from './ir/quintTypes' import { TypeScheme } from './types/base' import { canonicalTypeScheme } from './types/printing' @@ -257,9 +258,9 @@ export function printExecutionFrameRec(box: ConsoleBox, frame: ExecutionFrame, i // convert the arguments and the result to strings const args = docJoin( [text(','), line()], - frame.args.map(a => prettyQuintEx(a)) + frame.args.map(a => prettyQuintEx(a.toQuintEx(zerog))) ) - const r = frame.result.isLeft() ? text('none') : prettyQuintEx(frame.result.value) + const r = frame.result.isLeft() ? text('none') : prettyQuintEx(frame.result.value.toQuintEx(zerog)) const depth = isLast.length // generate the tree ASCII graphics for this frame let treeArt = isLast diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index b47185c45..d354890d8 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -24,18 +24,21 @@ export class VarStorage { this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) } - asRecord(): QuintEx { - return { - id: 0n, - kind: 'app', - opcode: 'Rec', - args: [...this.vars.entries()] - .map(([_key, reg]) => { - const nameEx: QuintStr = { id: 0n, kind: 'str', value: reg.name } - return [nameEx, rv.toQuintEx(reg.value.unwrap())] - }) - .flat(), - } + asRecord(): RuntimeValue { + const map: [string, RuntimeValue][] = this.vars + .valueSeq() + .toArray() + .filter(r => r.value.isRight()) + .map(r => [r.name, r.value.unwrap()]) + + // if (this.storeMetadata) { + // if (this.actionTaken.isJust()) { + // map.push(['action_taken', this.actionTaken.value!]) + // map.push(['nondet_picks', this.nondetPicks]) + // } + // } + + return rv.mkRecord(map) } reset() { diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 3e5a1b2ae..0ccd3ccb7 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -114,11 +114,10 @@ export class Evaluator { this.recorder.onUserOperatorCall(expr) } const value = evaluateExpr(this.builder, expr)(this.ctx) - const result = value.map(rv.toQuintEx) if (expr.kind === 'app') { - this.recorder.onUserOperatorReturn(expr, expr.args, result) + this.recorder.onUserOperatorReturn(expr, [], value) } - return result + return value.map(rv.toQuintEx) } reset() { @@ -151,14 +150,14 @@ export class Evaluator { const initResult = initEval(this.ctx).mapLeft(error => (failure = error)) if (!isTrue(initResult)) { - this.recorder.onUserOperatorReturn(initApp, [], initResult.map(rv.toQuintEx)) + this.recorder.onUserOperatorReturn(initApp, [], initResult) continue } this.shift() const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) - this.recorder.onUserOperatorReturn(initApp, [], invResult.map(rv.toQuintEx)) + this.recorder.onUserOperatorReturn(initApp, [], invResult) if (!isTrue(invResult)) { errorsFound++ } else { @@ -184,8 +183,8 @@ export class Evaluator { // drop the run. Otherwise, we would have a lot of false // positives, which look like deadlocks but they are not. - this.recorder.onUserOperatorReturn(stepApp, [], stepResult.map(rv.toQuintEx)) - this.recorder.onRunReturn(right({ id: 0n, kind: 'bool', value: true }), this.trace.get()) + this.recorder.onUserOperatorReturn(stepApp, [], stepResult) + this.recorder.onRunReturn(right(rv.mkBool(true)), this.trace.get()) break } @@ -195,13 +194,11 @@ export class Evaluator { if (!isTrue(invResult)) { errorsFound++ } - this.recorder.onUserOperatorReturn(stepApp, [], invResult.map(rv.toQuintEx)) + this.recorder.onUserOperatorReturn(stepApp, [], invResult) } } - const outcome: Either = failure - ? left(failure) - : right({ id: 0n, kind: 'bool', value: errorsFound == 0 }) + const outcome = failure ? left(failure) : right(rv.mkBool(errorsFound == 0)) this.recorder.onRunReturn(outcome, this.trace.get()) } @@ -228,7 +225,7 @@ export class Evaluator { // reset the trace this.reset() // run the test - const result = testEval(this.ctx).map(e => e.toQuintEx(zerog)) + const result = testEval(this.ctx) // extract the trace const trace = this.trace.get() @@ -255,7 +252,7 @@ export class Evaluator { } } - const ex = result.value + const ex = result.value.toQuintEx(zerog) if (ex.kind !== 'bool') { // if the test returned a malformed result, return immediately return { @@ -380,7 +377,7 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context return (ctx: Context) => { ctx.recorder.onUserOperatorCall(app) const result = body(ctx) - ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) + ctx.recorder.onUserOperatorReturn(app, [], result) return result } } @@ -533,7 +530,7 @@ function lambdaForApp( ctx.recorder.onUserOperatorCall(app) const result = arrow(ctx, args) - ctx.recorder.onUserOperatorReturn(app, [], result.map(rv.toQuintEx)) + ctx.recorder.onUserOperatorReturn(app, [], result) return result } } @@ -545,3 +542,11 @@ export function isTrue(value: Either): boolean { export function isFalse(value: Either): boolean { return value.isRight() && value.value.toBool() === false } + +export function profile(name: string, f: () => T): T { + // const start = Date.now() + const r = f() + // const end = Date.now() + // console.log(`${name} took ${end - start}ms`) + return r +} diff --git a/quint/src/runtime/impl/trace.ts b/quint/src/runtime/impl/trace.ts index 8bee2471e..987dc548d 100644 --- a/quint/src/runtime/impl/trace.ts +++ b/quint/src/runtime/impl/trace.ts @@ -13,20 +13,20 @@ */ import { List } from 'immutable' -import { QuintEx } from '../../ir/quintIr' +import { RuntimeValue } from './runtimeValue' export class Trace { - private states: List = List() + private states: List = List() - get(): QuintEx[] { + get(): RuntimeValue[] { return this.states.toArray() } - reset(values: QuintEx[] = []) { + reset(values: RuntimeValue[] = []) { this.states = List(values) } - extend(state: QuintEx) { + extend(state: RuntimeValue) { this.states = this.states.push(state) } } diff --git a/quint/src/runtime/trace.ts b/quint/src/runtime/trace.ts index 0a2ab5e85..50506ac9a 100644 --- a/quint/src/runtime/trace.ts +++ b/quint/src/runtime/trace.ts @@ -10,11 +10,12 @@ import { strict as assert } from 'assert' -import { QuintApp, QuintEx } from '../ir/quintIr' +import { QuintApp } from '../ir/quintIr' import { verbosity } from './../verbosity' import { Rng } from './../rng' import { Either, left, right } from '@sweet-monads/either' import { QuintError } from '../quintError' +import { RuntimeValue, rv } from './impl/runtimeValue' /** * A snapshot of how a single operator (e.g., an action) was executed. @@ -38,11 +39,11 @@ export interface ExecutionFrame { /** * The actual runtime values that were used in the call. */ - args: QuintEx[] + args: RuntimeValue[] /** * An optional result of the execution. */ - result: Either + result: Either /** * The frames of the operators that were called by this operator. */ @@ -77,7 +78,7 @@ export interface ExecutionListener { * @param args the actual arguments obtained in evaluation * @param result optional result of the evaluation */ - onUserOperatorReturn(app: QuintApp, args: QuintEx[], result: Either): void + onUserOperatorReturn(app: QuintApp, args: RuntimeValue[], result: Either): void /** * This callback is called *before* one of the arguments of `any {...}` @@ -116,7 +117,7 @@ export interface ExecutionListener { * @param oldState the old state that is about to be discarded * @param newState the new state, from which the execution continues */ - onNextState(oldState: QuintEx, newState: QuintEx): void + onNextState(oldState: RuntimeValue, newState: RuntimeValue): void /** * This callback is called when a new run is executed, @@ -133,7 +134,7 @@ export interface ExecutionListener { * - finished after finding a violation, `just(mkBool(false))` * @param trace the array of produced states (each state is a record) */ - onRunReturn(outcome: Either, trace: QuintEx[]): void + onRunReturn(outcome: Either, trace: RuntimeValue[]): void } /** @@ -141,13 +142,13 @@ export interface ExecutionListener { */ export const noExecutionListener: ExecutionListener = { onUserOperatorCall: (_app: QuintApp) => {}, - onUserOperatorReturn: (_app: QuintApp, _args: QuintEx[], _result: Either) => {}, + onUserOperatorReturn: (_app: QuintApp, _args: RuntimeValue[], _result: Either) => {}, onAnyOptionCall: (_anyExpr: QuintApp, _position: number) => {}, onAnyOptionReturn: (_anyExpr: QuintApp, _position: number) => {}, onAnyReturn: (_noptions: number, _choice: number) => {}, - onNextState: (_oldState: QuintEx, _newState: QuintEx) => {}, + onNextState: (_oldState: RuntimeValue, _newState: RuntimeValue) => {}, onRunCall: () => {}, - onRunReturn: (_outcome: Either, _trace: QuintEx[]) => {}, + onRunReturn: (_outcome: Either, _trace: RuntimeValue[]) => {}, } /** @@ -251,7 +252,7 @@ class TraceRecorderImpl implements TraceRecorder { } } - onUserOperatorReturn(_app: QuintApp, args: QuintEx[], result: Either) { + onUserOperatorReturn(_app: QuintApp, args: RuntimeValue[], result: Either) { if (verbosity.hasUserOpTracking(this.verbosityLevel)) { const top = this.frameStack.pop() if (top) { @@ -309,7 +310,7 @@ class TraceRecorderImpl implements TraceRecorder { } } - onNextState(_oldState: QuintEx, _newState: QuintEx) { + onNextState(_oldState: RuntimeValue, _newState: RuntimeValue) { // introduce a new frame that is labelled with a dummy operator if (verbosity.hasUserOpTracking(this.verbosityLevel)) { const dummy: QuintApp = { id: 0n, kind: 'app', opcode: '_', args: [] } @@ -327,7 +328,7 @@ class TraceRecorderImpl implements TraceRecorder { this.runSeed = this.rng.getState() } - onRunReturn(outcome: Either, trace: QuintEx[]) { + onRunReturn(outcome: Either, trace: RuntimeValue[]) { assert(this.frameStack.length > 0) const traceToSave = this.frameStack[0] traceToSave.result = outcome @@ -344,11 +345,11 @@ class TraceRecorderImpl implements TraceRecorder { } private sortTracesByQuality() { - const fromResult = (r: Either) => { + const fromResult = (r: Either) => { if (r.isLeft()) { return true } else { - const rex = r.value + const rex = r.value.toQuintEx({ nextId: () => 0n }) return rex.kind === 'bool' && !rex.value } } @@ -385,7 +386,7 @@ class TraceRecorderImpl implements TraceRecorder { // we will store the sequence of states here args: [], // the result of the trace evaluation - result: right({ id: 0n, kind: 'bool', value: true }), + result: right(rv.mkBool(true)), // and here we store the subframes for the top-level actions subframes: [], } From d135fc3663cd2b85cdd4bb864b4d5c169a703ed1 Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 22 Aug 2024 17:27:36 -0300 Subject: [PATCH 12/37] Fix and update many integration tests --- quint/io-cli-tests.md | 67 ++++++++++----- quint/src/cliCommands.ts | 76 +++++++++-------- quint/src/runtime/impl/builtins.ts | 29 ++++++- quint/src/runtime/impl/evaluator.ts | 123 +++++++++++++++++----------- quint/src/runtime/testing.ts | 2 +- 5 files changed, 190 insertions(+), 107 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 7697d5228..317c737c7 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -895,15 +895,13 @@ rm out-itf-example*.itf.json ### Test outputs ITF -TODO: output states after fix: https://github.com/informalsystems/quint/issues/288 - ``` output=$(quint test --output='coin_{#}_{}.itf.json' \ ../examples/tutorials/coin.qnt) exit_code=$? echo "$output" | sed -e 's/([0-9]*ms)/(duration)/g' -e 's#^.*coin.qnt# HOME/coin.qnt#g' -cat coin_0_sendWithoutMintTest.itf.json | jq '.states' +cat coin_0_sendWithoutMintTest.itf.json | jq '.states[0]."balances"."#map"' rm coin_0_sendWithoutMintTest.itf.json cat coin_1_mintSendTest.itf.json | jq '.states[0]."balances"."#map"' rm coin_1_mintSendTest.itf.json @@ -918,7 +916,38 @@ exit $exit_code ok mintSendTest passed 10000 test(s) 2 passing (duration) -[] +[ + [ + "alice", + { + "#bigint": "0" + } + ], + [ + "bob", + { + "#bigint": "0" + } + ], + [ + "charlie", + { + "#bigint": "0" + } + ], + [ + "eve", + { + "#bigint": "0" + } + ], + [ + "null", + { + "#bigint": "0" + } + ] +] [ [ "alice", @@ -1129,7 +1158,7 @@ echo -e "A1::f(1)\nA2::f(1)" | quint -r ../examples/language-features/instances. ``` -output=$(quint test testFixture/_1040compileError.qnt 2>&1) +output=$(quint test testFixture/_1040compileError.qnt --seed=1 2>&1) exit_code=$? echo "$output" | sed -e 's#^.*_1040compileError.qnt# HOME/_1040compileError.qnt#g' exit $exit_code @@ -1140,15 +1169,18 @@ exit $exit_code ``` _1040compileError - HOME/_1040compileError.qnt:2:3 - error: [QNT500] Uninitialized const n. Use: import (n=).* -2: const n: int - ^^^^^^^^^^^^ + 1) myTest failed after 1 test(s) + + 1 failed - HOME/_1040compileError.qnt:5:12 - error: [QNT502] Name n not found -5: assert(n > 0) - ^ + 1) myTest: + HOME/_1040compileError.qnt:5:12 - error: [QNT500] Uninitialized const n. Use: import (n=).* + 5: assert(n > 0) + Use --seed=0x1 --match=myTest to repeat. -error: Tests could not be run due to an error during compilation + + Use --verbosity=3 to show executions. + Further debug with: quint --verbosity=3 testFixture/_1040compileError.qnt ``` ### Fail on run with uninitialized constants @@ -1165,11 +1197,7 @@ exit $exit_code ``` -HOME/_1041compileConst.qnt:2:3 - error: [QNT500] Uninitialized const N. Use: import (N=).* -2: const N: int - ^^^^^^^^^^^^ - -HOME/_1041compileConst.qnt:5:24 - error: [QNT502] Name N not found +HOME/_1041compileConst.qnt:5:24 - error: [QNT500] Uninitialized const N. Use: import (N=).* 5: action init = { x' = N } ^ @@ -1245,7 +1273,8 @@ quint run --main=invalid ./testFixture/_1050diffName.qnt ``` -error: Main module invalid not found +error: [QNT405] Main module invalid not found +error: Argument error ``` ### test fails on invalid module @@ -1259,7 +1288,7 @@ quint test --main=invalid ./testFixture/_1050diffName.qnt ``` error: [QNT405] Main module invalid not found -error: Tests could not be run due to an error during compilation +error: Argument error ``` ### Multiple tests output different json diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 0b46c119f..07fdba71e 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -26,21 +26,11 @@ import { import { ErrorMessage } from './ErrorMessage' import { Either, left, mergeInMany, right } from '@sweet-monads/either' -import { fail } from 'assert' +import assert, { fail } from 'assert' import { EffectScheme } from './effects/base' -import { LookupTable, NameResolutionResult, UnusedDefinitions } from './names/base' +import { LookupTable } from './names/base' import { ReplOptions, quintRepl } from './repl' -import { - FlatModule, - OpQualifier, - QuintApp, - QuintBool, - QuintEx, - QuintModule, - QuintOpDef, - isDef, - qualifier, -} from './ir/quintIr' +import { FlatModule, OpQualifier, QuintBool, QuintEx, QuintModule } from './ir/quintIr' import { TypeScheme } from './types/base' import { createFinders, formatError } from './errorReporter' import { DocumentationEntry, produceDocs, toMarkdown } from './docs' @@ -79,8 +69,7 @@ interface OutputStage { stage: stage // the modules and the lookup table produced by 'parse' modules?: QuintModule[] - names?: NameResolutionResult - unusedDefinitions?: UnusedDefinitions + table?: LookupTable // the tables produced by 'typecheck' types?: Map effects?: Map @@ -105,8 +94,7 @@ const pickOutputStage = ({ stage, warnings, modules, - names, - unusedDefinitions, + table, types, effects, errors, @@ -121,8 +109,7 @@ const pickOutputStage = ({ stage, warnings, modules, - names, - unusedDefinitions, + table, types, effects, errors, @@ -150,7 +137,6 @@ interface ParsedStage extends LoadedStage { defaultModuleName: Maybe sourceMap: SourceMap table: LookupTable - unusedDefinitions: UnusedDefinitions resolver: NameResolver idGen: IdGenerator } @@ -328,6 +314,11 @@ export async function runTests(prev: TypecheckedStage): Promise m.name === mainName) + if (!main) { + const error: QuintError = { code: 'QNT405', message: `Main module ${mainName} not found` } + return cliErr('Argument error', { ...testing, errors: [mkErrorMessage(prev.sourceMap)(error)] }) + } const rngOrError = mkRng(prev.args.seed) if (rngOrError.isLeft()) { @@ -342,7 +333,7 @@ export async function runTests(prev: TypecheckedStage): Promise { + onTrace: (index: number) => (name: string, status: string, vars: string[], states: QuintEx[]) => { if (outputTemplate && outputTemplate.endsWith('.itf.json')) { const filename = outputTemplate.replaceAll('{}', name).replaceAll('{#}', index) const trace = toItf(vars, states) @@ -367,22 +358,15 @@ export async function runTests(prev: TypecheckedStage): Promise m.name === mainName)! - const testDefs = main.declarations.filter(d => d.kind === 'def' && options.testMatch(d.name)) as QuintOpDef[] + const testDefs = Array.from(prev.resolver.collector.definitionsByModule.get(mainName)!.values()) + .flat() + .filter(d => d.kind === 'def' && options.testMatch(d.name)) const evaluator = new Evaluator(testing.table, newTraceRecorder(verbosityLevel, rng, 1), rng) - const results = testDefs.map(def => { - return evaluator.test(def.name, def.expr, maxSamples) + const results = testDefs.map((def, index) => { + return evaluator.test(def, maxSamples, options.onTrace(index)) }) - // We have a compilation failure, so early exit without reporting test results - // if (testResult.isLeft()) { - // return cliErr('Tests could not be run due to an error during compilation', { - // ...testing, - // errors: testResult.value.map(mkErrorMessage(testing.sourceMap)), - // }) - // } - // We're finished running the tests const elapsedMs = Date.now() - startMs @@ -503,6 +487,11 @@ export async function runSimulator(prev: TypecheckedStage): Promise m.name === mainName) + if (!main) { + const error: QuintError = { code: 'QNT405', message: `Main module ${mainName} not found` } + return cliErr('Argument error', { ...prev, errors: [mkErrorMessage(prev.sourceMap)(error)] }) + } const rngOrError = mkRng(prev.args.seed) if (rngOrError.isLeft()) { @@ -577,6 +566,23 @@ export async function runSimulator(prev: TypecheckedStage): Promise { + const maybeEvalResult = trace.frame.result + if (maybeEvalResult.isLeft()) { + return cliErr('Runtime error', { + ...simulator, + errors: [mkErrorMessage(simulator.sourceMap)(maybeEvalResult.value)], + }) + } + const quintExResult = maybeEvalResult.value.toQuintEx(prev.idGen) + assert(quintExResult.kind === 'bool', 'invalid simulation produced non-boolean value ') + const simulationSucceeded = quintExResult.value + const status = simulationSucceeded ? 'ok' : 'violation' + const states = trace.frame.args.map(e => e.toQuintEx(prev.idGen)) + + options.onTrace(index, status, evaluator.varNames(), states) + }) + const states = recorder.bestTraces[0]?.frame?.args?.map(e => e.toQuintEx(zerog)) const frames = recorder.bestTraces[0]?.frame?.subframes ?? [] const seed = recorder.bestTraces[0]?.seed @@ -695,7 +701,9 @@ export async function outputCompilationTarget(compiled: CompiledStage): Promise< const args = compiled.args const verbosityLevel = deriveVerbosity(args) - const parsedSpecJson = jsonStringOfOutputStage(pickOutputStage({ ...compiled, modules: [compiled.mainModule] })) + const parsedSpecJson = jsonStringOfOutputStage( + pickOutputStage({ ...compiled, modules: [compiled.mainModule], table: compiled.resolver.table }) + ) switch ((compiled.args.target as string).toLowerCase()) { case 'json': process.stdout.write(parsedSpecJson) diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index a37f0eee3..fc2ade7c7 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -4,10 +4,12 @@ import { Map, is, Set, Range, List } from 'immutable' import { EvalFunction, isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' -import { chunk } from 'lodash' +import { chunk, times } from 'lodash' import { expressionToString } from '../../ir/IRprinting' import { zerog } from '../../idGenerator' import { QuintApp, QuintEx } from '../../ir/quintIr' +import { prettyQuintEx, terminalWidth } from '../../graphics' +import { format } from '../../prettierimp' export function builtinValue(name: string): Either { switch (name) { @@ -34,6 +36,7 @@ export const lazyOps = [ 'next', 'implies', 'then', + 'reps', ] export function lazyBuiltinLambda( @@ -197,6 +200,20 @@ export function lazyBuiltinLambda( return args[1](ctx) }) } + case 'reps': + return (ctx, args) => { + return args[0](ctx).chain(n => { + let result: Either = right(rv.mkBool(true)) + for (let i = 0; i < Number(n.toInt()); i++) { + result = args[1](ctx).chain(value => value.toArrow()(ctx, [rv.mkInt(i)])) + if (result.isLeft()) { + return result + } + ctx.shift() + } + return result + }) + } default: return () => left({ code: 'QNT000', message: 'Unknown stateful op' }) @@ -505,9 +522,15 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) case 'fail': return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'assert': - return (_, args) => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT502', message: `Assertion failed` })) + return (_, args) => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT508', message: `Assertion failed` })) + case 'q::debug': + return (_, args) => { + let columns = terminalWidth() + let valuePretty = format(columns, 0, prettyQuintEx(args[1].toQuintEx(zerog))) + console.log('>', args[0].toStr(), valuePretty.toString()) + return right(args[1]) + } case 'expect': - case 'reps': default: return () => left({ code: 'QNT000', message: `Unknown builtin ${op}` }) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 0ccd3ccb7..4f6005849 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -16,6 +16,11 @@ import { expressionToString } from '../../ir/IRprinting' export type EvalFunction = (ctx: Context) => Either +function nameWithNamespaces(name: string, namespaces: List): string { + const revertedNamespaces = namespaces.reverse() + return revertedNamespaces.push(name).join('::') +} + export class Builder { table: LookupTable paramRegistry: Map = new Map() @@ -30,13 +35,11 @@ export class Builder { discoverVar(id: bigint, name: string) { const key = [id, ...this.namespaces].join('#') - console.log('discovering var', id, name, key) if (this.varStorage.vars.has(key)) { return } - const revertedNamespaces = this.namespaces.reverse() - const varName = revertedNamespaces.push(name).join('::') + const varName = nameWithNamespaces(name, this.namespaces) const register: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } const nextRegister: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } this.varStorage.vars = this.varStorage.vars.set(key, register) @@ -62,6 +65,17 @@ export class Builder { return result } + + registerForConst(id: bigint, name: string): Register { + let register = this.constRegistry.get(id) + if (!register) { + const message = `Uninitialized const ${name}. Use: import (${name}=).*` + register = { value: left({ code: 'QNT500', message }) } + this.constRegistry.set(id, register) + return register + } + return register + } } export class Evaluator { @@ -77,11 +91,6 @@ export class Evaluator { this.ctx = new Context(recorder, rng.next, this.builder.varStorage) } - get table(): LookupTable { - console.log('getting table') - return this.builder.table - } - get trace(): Trace { return this.ctx.trace } @@ -209,12 +218,20 @@ export class Evaluator { return outcome } - test(name: string, test: QuintEx, maxSamples: number): TestResult { + test( + testDef: LookupDefinition, + maxSamples: number, + onTrace: (name: string, status: string, vars: string[], states: QuintEx[]) => void + ): TestResult { + const name = nameWithNamespaces(testDef.name, List(testDef.namespaces)) + this.trace.reset() + this.recorder.clear() + // save the initial seed let seed = this.rng.getState() - const testEval = evaluateExpr(this.builder, test) + const testEval = evaluateDef(this.builder, testDef) let nsamples = 1 // run up to maxSamples, stop on the first failure @@ -229,26 +246,24 @@ export class Evaluator { // extract the trace const trace = this.trace.get() - if (trace.length > 0) { this.recorder.onRunReturn(result, trace) } else { - // Report a non-critical error - console.error('Missing a trace') this.recorder.onRunReturn(result, []) } - const bestTrace = this.recorder.bestTraces[0].frame + const states = this.recorder.bestTraces[0]?.frame?.args?.map(rv.toQuintEx) + const frames = this.recorder.bestTraces[0]?.frame?.subframes ?? [] // evaluate the result if (result.isLeft()) { - // if the test failed, return immediately + // if there was an error, return immediately return { name, status: 'failed', errors: [result.value], seed, - frames: bestTrace.subframes, - nsamples: nsamples, + frames, + nsamples, } } @@ -259,9 +274,9 @@ export class Evaluator { name, status: 'ignored', errors: [], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, + seed, + frames, + nsamples, } } @@ -270,55 +285,60 @@ export class Evaluator { const error: QuintError = { code: 'QNT511', message: `Test ${name} returned false`, - reference: test.id, + reference: testDef.id, } - // options.onTrace( - // index, - // name, - // status, - // ctx.evaluationState.vars.map(v => v.name), - // states - // ) + onTrace(name, 'failed', this.varNames(), states) // saveTrace(bestTrace, index, name, 'failed') return { name, status: 'failed', errors: [error], - seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, + seed, + frames, + nsamples, } } else { if (this.rng.getState() === seed) { // This successful test did not use non-determinism. // Running it one time is sufficient. - // saveTrace(bestTrace, index, name, 'passed') + onTrace(name, 'passed', this.varNames(), states) + return { name, status: 'passed', errors: [], seed: seed, - frames: bestTrace.subframes, - nsamples: nsamples, + frames, + nsamples, } } } } // the test was run maxSamples times, and no errors were found - const bestTrace = this.recorder.bestTraces[0].frame - // saveTrace(bestTrace, index, name, 'passed') + const states = this.recorder.bestTraces[0]?.frame?.args?.map(rv.toQuintEx) + const frames = this.recorder.bestTraces[0]?.frame?.subframes ?? [] + + onTrace(name, 'passed', this.varNames(), states) + return { name, status: 'passed', errors: [], seed: seed, - frames: bestTrace.subframes, + frames, nsamples: nsamples - 1, } } + + varNames() { + return this.ctx.varStorage.vars + .valueSeq() + .toArray() + .map(v => v.name) + } } export function evaluateExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { @@ -339,11 +359,7 @@ export function buildUnderDefContext( const overrides: [Register, (ctx: Context) => Either][] = instance.overrides.map( ([param, expr]) => { const id = builder.table.get(param.id)!.id - let register = builder.constRegistry.get(id) - if (!register) { - register = { value: left({ code: 'QNT504', message: `Constant ${param.name} not set` }) } - builder.constRegistry.set(id, register) - } + const register = builder.registerForConst(id, param.name) return [register, evaluateExpr(builder, expr)] } @@ -390,7 +406,8 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context return evaluateExpr(builder, def.expr) } - const cachedValue = builder.scopedCachedValues.get(def.id)! + let cachedValue = builder.scopedCachedValues.get(def.id)! + const bodyEval = evaluateExpr(builder, def.expr) return ctx => { @@ -417,12 +434,7 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context } } case 'const': - const register = builder.constRegistry.get(def.id) - if (!register) { - const reg: Register = { value: left({ code: 'QNT501', message: `Constant ${def.name} not set` }) } - builder.constRegistry.set(def.id, reg) - return _ => reg.value - } + const register = builder.registerForConst(def.id, def.name) return _ => register.value default: return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) @@ -458,7 +470,6 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit if (expr.opcode === 'assign') { const varDef = builder.table.get(expr.args[0].id)! return buildUnderDefContext(builder, varDef, () => { - console.log(expressionToString(expr)) builder.discoverVar(varDef.id, varDef.name) const register = builder.getNextVar(varDef.id) const exprEval = evaluateExpr(builder, expr.args[1]) @@ -481,6 +492,18 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit } const op = lambdaForApp(builder, expr) + // return ctx => { + // const argValues = args.map(arg => arg(ctx)) + // if (argValues.some(arg => arg.isLeft())) { + // return argValues.find(arg => arg.isLeft())! + // } + + // return op( + // ctx, + // argValues.map(a => a.unwrap()) + // ) + // } + return ctx => { const argValues = [] for (const arg of args) { diff --git a/quint/src/runtime/testing.ts b/quint/src/runtime/testing.ts index 0ca0538fb..1bb5f6997 100644 --- a/quint/src/runtime/testing.ts +++ b/quint/src/runtime/testing.ts @@ -22,7 +22,7 @@ export interface TestOptions { maxSamples: number rng: Rng verbosity: number - onTrace(index: number, name: string, status: string, vars: string[], states: QuintEx[]): void + onTrace: (index: number) => (name: string, status: string, vars: string[], states: QuintEx[]) => void } /** From 57be2a21b993b02bdf7fe8330bf576e00792f58e Mon Sep 17 00:00:00 2001 From: bugarela Date: Fri, 23 Aug 2024 09:38:20 -0300 Subject: [PATCH 13/37] Track nondet picks in the trace --- quint/io-cli-tests.md | 18 ------------ quint/src/cliCommands.ts | 2 +- quint/src/runtime/impl/Context.ts | 16 +++------- quint/src/runtime/impl/VarStorage.ts | 37 +++++++++++++++-------- quint/src/runtime/impl/evaluator.ts | 44 ++++++++++++++++------------ 5 files changed, 55 insertions(+), 62 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 317c737c7..9a102400d 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -811,24 +811,6 @@ rm out-itf-mbt-example.itf.json "#bigint": "49617995555028370892926474303042238797407019137772330780016167115018841762373" } }, - "eveToBob": { - "tag": "None", - "value": { - "#tup": [] - } - }, - "mintBob": { - "tag": "None", - "value": { - "#tup": [] - } - }, - "mintEve": { - "tag": "None", - "value": { - "#tup": [] - } - }, "receiver": { "tag": "Some", "value": "charlie" diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 07fdba71e..db42add95 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -550,7 +550,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise } -export interface Register { - value: Either +export interface CachedValue { + value: Either | undefined } export class Context { public rand: (n: bigint) => bigint - public nondetPicks: ImmutableMap = ImmutableMap() public recorder: TraceRecorder public trace: Trace = new Trace() public varStorage: VarStorage @@ -28,14 +25,9 @@ export class Context { this.varStorage = varStorage } - reset() { - this.varStorage = new VarStorage() - } - shift() { this.varStorage.shiftVars() this.trace.extend(this.varStorage.asRecord()) // TODO: save on trace - this.nondetPicks = ImmutableMap() } } diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index d354890d8..bc296a1e8 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -1,19 +1,25 @@ import { Either, left } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' -import { QuintEx, QuintStr } from '../../ir/quintIr' import { Map as ImmutableMap } from 'immutable' import { Register } from './Context' -export interface VarRegister extends Register { +export interface NamedRegister extends Register { name: string } const initialRegisterValue: Either = left({ code: 'QNT502', message: 'Variable not set' }) export class VarStorage { - public vars: ImmutableMap = ImmutableMap() - public nextVars: ImmutableMap = ImmutableMap() + public vars: ImmutableMap = ImmutableMap() + public nextVars: ImmutableMap = ImmutableMap() + private storeMetadata: boolean + private nondetPicks: Map = new Map() + + constructor(storeMetadata: boolean, nondetPicks: Map) { + this.storeMetadata = storeMetadata + this.nondetPicks = nondetPicks + } shiftVars() { // TODO: change this so registers are kept @@ -31,12 +37,19 @@ export class VarStorage { .filter(r => r.value.isRight()) .map(r => [r.name, r.value.unwrap()]) - // if (this.storeMetadata) { - // if (this.actionTaken.isJust()) { - // map.push(['action_taken', this.actionTaken.value!]) - // map.push(['nondet_picks', this.nondetPicks]) - // } - // } + if (this.storeMetadata) { + const nondetPicksRecord = rv.mkRecord( + [...this.nondetPicks.entries()].map(([name, value]) => { + const valueVariant = value ? rv.mkVariant('Some', value) : rv.mkVariant('None', rv.mkTuple([])) + return [name, valueVariant] + }) + ) + map.push(['nondet_picks', nondetPicksRecord]) + + // if (this.actionTaken.isJust()) { + // map.push(['action_taken', this.actionTaken.value!]) + // } + } return rv.mkRecord(map) } @@ -46,11 +59,11 @@ export class VarStorage { this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) } - nextVarsSnapshot(): ImmutableMap { + nextVarsSnapshot(): ImmutableMap { return this.nextVars.map(reg => ({ ...reg })) } - recoverNextVars(snapshot: ImmutableMap) { + recoverNextVars(snapshot: ImmutableMap) { this.nextVars.forEach((reg, key) => { const snapshotReg = snapshot.get(key) if (snapshotReg) { diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 4f6005849..c61aa8ddb 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -6,12 +6,12 @@ import { TraceRecorder } from '../trace' import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' import { Trace } from './trace' import { RuntimeValue, rv } from './runtimeValue' -import { Context, Register } from './Context' +import { CachedValue, Context, Register } from './Context' import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' import { List, Map as ImmutableMap } from 'immutable' -import { VarRegister, VarStorage } from './VarStorage' +import { NamedRegister, VarStorage } from './VarStorage' import { expressionToString } from '../../ir/IRprinting' export type EvalFunction = (ctx: Context) => Either @@ -25,12 +25,14 @@ export class Builder { table: LookupTable paramRegistry: Map = new Map() constRegistry: Map = new Map() - scopedCachedValues: Map | undefined }> = new Map() + scopedCachedValues: Map = new Map() + nondetPicks: Map = new Map() public namespaces: List = List() - public varStorage: VarStorage = new VarStorage() + public varStorage: VarStorage - constructor(table: LookupTable) { + constructor(table: LookupTable, storeMetadata: boolean) { this.table = table + this.varStorage = new VarStorage(storeMetadata, this.nondetPicks) } discoverVar(id: bigint, name: string) { @@ -40,13 +42,13 @@ export class Builder { } const varName = nameWithNamespaces(name, this.namespaces) - const register: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } - const nextRegister: VarRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + const register: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + const nextRegister: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } this.varStorage.vars = this.varStorage.vars.set(key, register) this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) } - getVar(id: bigint): VarRegister { + getVar(id: bigint): NamedRegister { const key = [id, ...this.namespaces].join('#') const result = this.varStorage.vars.get(key) if (!result) { @@ -56,7 +58,7 @@ export class Builder { return result } - getNextVar(id: bigint): VarRegister { + getNextVar(id: bigint): NamedRegister { const key = [id, ...this.namespaces].join('#') const result = this.varStorage.nextVars.get(key) if (!result) { @@ -84,10 +86,10 @@ export class Evaluator { private rng: Rng private builder: Builder - constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng) { + constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng, storeMetadata: boolean = false) { this.recorder = recorder this.rng = rng - this.builder = new Builder(table) + this.builder = new Builder(table, storeMetadata) this.ctx = new Context(recorder, rng.next, this.builder.varStorage) } @@ -380,13 +382,6 @@ export function buildUnderDefContext( function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { switch (def.kind) { case 'def': - // if (def.qualifier === 'nondet') { - // const body = evaluateExpr(builder, def.expr) - // return (ctx: Context) => { - // return evaluateNondet(ctx, def, body) - // } - // } - if (def.qualifier === 'action') { const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } const body = evaluateExpr(builder, def.expr) @@ -409,11 +404,22 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context let cachedValue = builder.scopedCachedValues.get(def.id)! const bodyEval = evaluateExpr(builder, def.expr) + if (def.qualifier === 'nondet') { + builder.nondetPicks.set(def.name, undefined) + } return ctx => { if (cachedValue.value === undefined) { cachedValue.value = bodyEval(ctx) - return cachedValue.value + if (def.qualifier === 'nondet') { + cachedValue.value + .map(value => { + builder.nondetPicks.set(def.name, value) + }) + .mapLeft(_ => { + builder.nondetPicks.set(def.name, undefined) + }) + } } return cachedValue.value } From 5f067fd484782e83540c80ac26886fc3e2604830 Mon Sep 17 00:00:00 2001 From: bugarela Date: Fri, 23 Aug 2024 10:00:51 -0300 Subject: [PATCH 14/37] Track action taken in the trace --- quint/io-cli-tests.md | 2 +- quint/src/runtime/impl/VarStorage.ts | 29 +++++++++++++++++++--------- quint/src/runtime/impl/builtins.ts | 22 ++++++++++++++------- quint/src/runtime/impl/evaluator.ts | 15 +++++++++----- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 9a102400d..cabfabe5f 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -466,7 +466,7 @@ exit $exit_code ``` An example execution: -[State 0] { action_taken: "q::init", n: 1, nondet_picks: { } } +[State 0] { action_taken: "init", n: 1, nondet_picks: { } } [State 1] { action_taken: "OnPositive", n: 2, nondet_picks: { } } diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index bc296a1e8..ee4a9693c 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -8,13 +8,20 @@ export interface NamedRegister extends Register { name: string } +interface Snapshot { + nextVars: ImmutableMap + nondetPicks: Map + actionTaken: string | undefined +} + const initialRegisterValue: Either = left({ code: 'QNT502', message: 'Variable not set' }) export class VarStorage { public vars: ImmutableMap = ImmutableMap() public nextVars: ImmutableMap = ImmutableMap() + public nondetPicks: Map = new Map() + public actionTaken: string | undefined private storeMetadata: boolean - private nondetPicks: Map = new Map() constructor(storeMetadata: boolean, nondetPicks: Map) { this.storeMetadata = storeMetadata @@ -45,10 +52,7 @@ export class VarStorage { }) ) map.push(['nondet_picks', nondetPicksRecord]) - - // if (this.actionTaken.isJust()) { - // map.push(['action_taken', this.actionTaken.value!]) - // } + map.push(['action_taken', rv.mkStr(this.actionTaken ?? '')]) } return rv.mkRecord(map) @@ -59,16 +63,23 @@ export class VarStorage { this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) } - nextVarsSnapshot(): ImmutableMap { - return this.nextVars.map(reg => ({ ...reg })) + snapshot(): Snapshot { + return { + nextVars: this.nextVars.map(reg => ({ ...reg })), + nondetPicks: new Map(this.nondetPicks), + actionTaken: this.actionTaken, + } } - recoverNextVars(snapshot: ImmutableMap) { + recoverSnapshot(snapshot: Snapshot) { this.nextVars.forEach((reg, key) => { - const snapshotReg = snapshot.get(key) + // TODO can we make this more efficient? + const snapshotReg = snapshot.nextVars.get(key) if (snapshotReg) { reg.value = snapshotReg.value } }) + this.nondetPicks = snapshot.nondetPicks + this.actionTaken = snapshot.actionTaken } } diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index fc2ade7c7..b5fa93d3d 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -77,19 +77,27 @@ export function lazyBuiltinLambda( case 'actionAny': const app: QuintApp = { id: 0n, kind: 'app', opcode: 'actionAny', args: [] } return (ctx, args) => { - const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() + // on `any`, we reset the action taken as the goal is to save the last + // action picked in an `any` call + ctx.varStorage.actionTaken = undefined + ctx.varStorage.nondetPicks.forEach((_, key) => { + ctx.varStorage.nondetPicks.set(key, undefined) + }) + + const nextVarsSnapshot = ctx.varStorage.snapshot() + const evaluationResults = args.map((arg, i) => { ctx.recorder.onAnyOptionCall(app, i) const result = arg(ctx).map(result => { // Save vars - const successor = ctx.varStorage.nextVarsSnapshot() + const successor = ctx.varStorage.snapshot() return result.toBool() ? [successor] : [] }) ctx.recorder.onAnyOptionReturn(app, i) // Recover snapshot (regardless of success or failure) - ctx.varStorage.recoverNextVars(nextVarsSnapshot) + ctx.varStorage.recoverSnapshot(nextVarsSnapshot) return result }) @@ -105,22 +113,22 @@ export function lazyBuiltinLambda( case 0: return rv.mkBool(false) case 1: - ctx.varStorage.recoverNextVars(potentialSuccessors[0]) + ctx.varStorage.recoverSnapshot(potentialSuccessors[0]) return rv.mkBool(true) default: const choice = Number(ctx.rand(BigInt(potentialSuccessors.length))) - ctx.varStorage.recoverNextVars(potentialSuccessors[choice]) + ctx.varStorage.recoverSnapshot(potentialSuccessors[choice]) return rv.mkBool(true) } }) } case 'actionAll': return (ctx, args) => { - const nextVarsSnapshot = ctx.varStorage.nextVarsSnapshot() + const nextVarsSnapshot = ctx.varStorage.snapshot() for (const action of args) { const result = action(ctx) if (!isTrue(result)) { - ctx.varStorage.recoverNextVars(nextVarsSnapshot) + ctx.varStorage.recoverSnapshot(nextVarsSnapshot) return result.map(_ => rv.mkBool(false)) } } diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index c61aa8ddb..6145b26d1 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -26,13 +26,13 @@ export class Builder { paramRegistry: Map = new Map() constRegistry: Map = new Map() scopedCachedValues: Map = new Map() - nondetPicks: Map = new Map() + initialNondetPicks: Map = new Map() public namespaces: List = List() public varStorage: VarStorage constructor(table: LookupTable, storeMetadata: boolean) { this.table = table - this.varStorage = new VarStorage(storeMetadata, this.nondetPicks) + this.varStorage = new VarStorage(storeMetadata, this.initialNondetPicks) } discoverVar(id: bigint, name: string) { @@ -388,6 +388,11 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context return (ctx: Context) => { ctx.recorder.onUserOperatorCall(app) const result = body(ctx) + + if (ctx.varStorage.actionTaken === undefined) { + ctx.varStorage.actionTaken = def.name + } + ctx.recorder.onUserOperatorReturn(app, [], result) return result } @@ -405,7 +410,7 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context const bodyEval = evaluateExpr(builder, def.expr) if (def.qualifier === 'nondet') { - builder.nondetPicks.set(def.name, undefined) + builder.initialNondetPicks.set(def.name, undefined) } return ctx => { @@ -414,10 +419,10 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context if (def.qualifier === 'nondet') { cachedValue.value .map(value => { - builder.nondetPicks.set(def.name, value) + ctx.varStorage.nondetPicks.set(def.name, value) }) .mapLeft(_ => { - builder.nondetPicks.set(def.name, undefined) + ctx.varStorage.nondetPicks.set(def.name, undefined) }) } } From e1e0e8960c7d1436ac24b767ed64542ffa031606 Mon Sep 17 00:00:00 2001 From: bugarela Date: Fri, 23 Aug 2024 16:48:56 -0300 Subject: [PATCH 15/37] Fix/update all integration tests --- quint/io-cli-tests.md | 101 +++++++++++------------ quint/src/cliCommands.ts | 4 +- quint/src/runtime/impl/builtins.ts | 23 ++++-- quint/src/runtime/impl/evaluator.ts | 122 ++++++++++++++-------------- 4 files changed, 126 insertions(+), 124 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index cabfabe5f..25caca10e 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -616,8 +616,7 @@ An example execution: [Frame 0] q::initAndInvariant => true -├─ q::init => true -│ └─ init => true +├─ init => true └─ isUInt(0) => true [State 0] @@ -629,18 +628,17 @@ q::initAndInvariant => true [Frame 1] q::stepAndInvariant => true -├─ q::step => true -│ └─ step => true -│ └─ mint( -│ "alice", -│ "eve", -│ 33944027745092921485394061592130395256199599638916782090017603421409072478812 -│ ) => true -│ ├─ require(true) => true -│ └─ require(true) => true -│ └─ isUInt( -│ 33944027745092921485394061592130395256199599638916782090017603421409072478812 -│ ) => true +├─ step => true +│ └─ mint( +│ "alice", +│ "eve", +│ 33944027745092921485394061592130395256199599638916782090017603421409072478812 +│ ) => true +│ ├─ require(true) => true +│ └─ require(true) => true +│ └─ isUInt( +│ 33944027745092921485394061592130395256199599638916782090017603421409072478812 +│ ) => true └─ isUInt( 33944027745092921485394061592130395256199599638916782090017603421409072478812 ) => true @@ -661,18 +659,17 @@ q::stepAndInvariant => true [Frame 2] q::stepAndInvariant => true -├─ q::step => true -│ └─ step => true -│ └─ mint( -│ "alice", -│ "eve", -│ 37478542505835205046968520025158070945751003972871720238447843997511300995974 -│ ) => true -│ ├─ require(true) => true -│ └─ require(true) => true -│ └─ isUInt( -│ 71422570250928126532362581617288466201950603611788502328465447418920373474786 -│ ) => true +├─ step => true +│ └─ mint( +│ "alice", +│ "eve", +│ 37478542505835205046968520025158070945751003972871720238447843997511300995974 +│ ) => true +│ ├─ require(true) => true +│ └─ require(true) => true +│ └─ isUInt( +│ 71422570250928126532362581617288466201950603611788502328465447418920373474786 +│ ) => true └─ isUInt( 71422570250928126532362581617288466201950603611788502328465447418920373474786 ) => true @@ -692,19 +689,18 @@ q::stepAndInvariant => true } [Frame 3] -q::stepAndInvariant => true -├─ q::step => true -│ └─ step => true -│ └─ mint( -│ "alice", -│ "null", -│ 109067983118832076063755963802104322727953985633488183463930115464609414175363 -│ ) => true -│ ├─ require(true) => true -│ └─ require(true) => true -│ └─ isUInt( -│ 109067983118832076063755963802104322727953985633488183463930115464609414175363 -│ ) => true +q::stepAndInvariant => false +├─ step => true +│ └─ mint( +│ "alice", +│ "null", +│ 109067983118832076063755963802104322727953985633488183463930115464609414175363 +│ ) => true +│ ├─ require(true) => true +│ └─ require(true) => true +│ └─ isUInt( +│ 109067983118832076063755963802104322727953985633488183463930115464609414175363 +│ ) => true └─ isUInt( 180490553369760202596118545419392788929904589245276685792395562883529787650149 ) => false @@ -1012,7 +1008,7 @@ cd - > /dev/null ``` -output=$(quint test --seed=0x1cce8452305113 --match=mintTwiceThenSendError \ +output=$(quint test --seed=0x1286bf2e1dacb3 --match=mintTwiceThenSendError \ --verbosity=3 ../examples/tutorials/coin.qnt) exit_code=$? echo "$output" | sed -e 's/([0-9]*ms)/(duration)/g' -e 's#^.*coin.qnt# HOME/coin.qnt#g' @@ -1023,7 +1019,7 @@ exit $exit_code ``` coin - 1) mintTwiceThenSendError failed after 2 test(s) + 1) mintTwiceThenSendError failed after 1 test(s) 1 failed @@ -1036,45 +1032,45 @@ init => true [Frame 1] mint( - "alice", + "bob", "eve", - 62471107147077426559451191183102889181018012614560866566772535482230624081662 + 74252675173190743514494160784973331842148624838292266741626378055869698233769 ) => true ├─ require(true) => true └─ require(true) => true └─ isUInt( - 62471107147077426559451191183102889181018012614560866566772535482230624081662 + 74252675173190743514494160784973331842148624838292266741626378055869698233769 ) => true [Frame 2] mint( - "alice", "bob", - 108068598360285515924422306643202051157703255799754639660642704769543958308579 + "bob", + 97700478479458321253548605902971263977055085704583752584562220159652816914987 ) => true ├─ require(true) => true └─ require(true) => true └─ isUInt( - 108068598360285515924422306643202051157703255799754639660642704769543958308579 + 97700478479458321253548605902971263977055085704583752584562220159652816914987 ) => true [Frame 3] send( "eve", "bob", - 17111225533527540742175456584955554462321462289172248790585577326828420782641 + 47769583726968424739901588588333904197787985995488944788698867328177315688645 ) => false ├─ require(true) => true ├─ require(true) => true │ └─ isUInt( -│ 45359881613549885817275734598147334718696550325388617776186958155402203299021 +│ 26483091446222318774592572196639427644360638842803321952927510727692382545124 │ ) => true └─ require(false) => false └─ isUInt( - 125179823893813056666597763228157605620024718088926888451228282096372379091220 + 145470062206426745993450194491305168174843071700072697373261087487830132603632 ) => false - Use --seed=0x1cce845230512f --match=mintTwiceThenSendError to repeat. + Use --seed=0x1286bf2e1dacb3 --match=mintTwiceThenSendError to repeat. ``` ### test fails on invalid seed @@ -1142,7 +1138,7 @@ echo -e "A1::f(1)\nA2::f(1)" | quint -r ../examples/language-features/instances. ``` output=$(quint test testFixture/_1040compileError.qnt --seed=1 2>&1) exit_code=$? -echo "$output" | sed -e 's#^.*_1040compileError.qnt# HOME/_1040compileError.qnt#g' +echo "$output" | sed -E 's#(/[^ ]*/)_1040compileError.qnt#HOME/_1040compileError.qnt#g' exit $exit_code ``` @@ -1163,6 +1159,7 @@ exit $exit_code Use --verbosity=3 to show executions. Further debug with: quint --verbosity=3 testFixture/_1040compileError.qnt +error: Tests failed ``` ### Fail on run with uninitialized constants diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index db42add95..f952df240 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -413,7 +413,7 @@ export async function runTests(prev: TypecheckedStage): Promise r.name), failed: failed.map(r => r.name), ignored: ignored.map(r => r.name), - errors: namedErrors.map(([_, e]) => e), + errors: [], } // Nothing failed, so we are OK, and can exit early @@ -584,7 +584,7 @@ export async function runSimulator(prev: TypecheckedStage): Promise e.toQuintEx(zerog)) - const frames = recorder.bestTraces[0]?.frame?.subframes ?? [] + const frames = recorder.bestTraces[0]?.frame?.subframes const seed = recorder.bestTraces[0]?.seed switch (outcome.status) { case 'error': diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index b5fa93d3d..52ad6044e 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -92,7 +92,7 @@ export function lazyBuiltinLambda( // Save vars const successor = ctx.varStorage.snapshot() - return result.toBool() ? [successor] : [] + return result.toBool() ? [{ snapshot: successor, index: i }] : [] }) ctx.recorder.onAnyOptionReturn(app, i) @@ -107,17 +107,18 @@ export function lazyBuiltinLambda( .mapLeft(errors => errors[0]) return processedResults.map(potentialSuccessors => { - ctx.recorder.onAnyReturn(args.length, -1) - switch (potentialSuccessors.length) { case 0: + ctx.recorder.onAnyReturn(args.length, -1) return rv.mkBool(false) case 1: - ctx.varStorage.recoverSnapshot(potentialSuccessors[0]) + ctx.recorder.onAnyReturn(args.length, potentialSuccessors[0].index) + ctx.varStorage.recoverSnapshot(potentialSuccessors[0].snapshot) return rv.mkBool(true) default: const choice = Number(ctx.rand(BigInt(potentialSuccessors.length))) - ctx.varStorage.recoverSnapshot(potentialSuccessors[choice]) + ctx.recorder.onAnyReturn(args.length, potentialSuccessors[choice].index) + ctx.varStorage.recoverSnapshot(potentialSuccessors[choice].snapshot) return rv.mkBool(true) } }) @@ -192,7 +193,7 @@ export function lazyBuiltinLambda( } case 'then': return (ctx, args) => { - // const oldState = ctx.varStorage.asRecord() + const oldState = ctx.varStorage.asRecord() return args[0](ctx).chain(firstResult => { if (!firstResult.toBool()) { return left({ @@ -202,8 +203,8 @@ export function lazyBuiltinLambda( } ctx.shift() - // const newState = ctx.varStorage.asRecord() - // ctx.recorder.onNextState(oldState, newState) + const newState = ctx.varStorage.asRecord() + ctx.recorder.onNextState(oldState, newState) return args[1](ctx) }) @@ -217,7 +218,11 @@ export function lazyBuiltinLambda( if (result.isLeft()) { return result } - ctx.shift() + + // Don't shift after the last one + if (i < Number(n.toInt()) - 1) { + ctx.shift() + } } return result }) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 6145b26d1..74fdd0572 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -162,50 +162,49 @@ export class Evaluator { const initResult = initEval(this.ctx).mapLeft(error => (failure = error)) if (!isTrue(initResult)) { this.recorder.onUserOperatorReturn(initApp, [], initResult) - continue - } - - this.shift() - - const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) - this.recorder.onUserOperatorReturn(initApp, [], invResult) - if (!isTrue(invResult)) { - errorsFound++ } else { - // check all { step, shift(), inv } in a loop - - // FIXME: errorsFound < ntraces is not good, because we continue after invariant violation. - // This is the same in the old version, so I'll fix later. - for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { - const stepApp: QuintApp = { - id: 0n, - kind: 'app', - opcode: 'q::stepAndInvariant', - args: [], + this.shift() + + const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) + this.recorder.onUserOperatorReturn(initApp, [], invResult) + if (!isTrue(invResult)) { + errorsFound++ + } else { + // check all { step, shift(), inv } in a loop + + // FIXME: errorsFound < ntraces is not good, because we continue after invariant violation. + // This is the same in the old version, so I'll fix later. + for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { + const stepApp: QuintApp = { + id: 0n, + kind: 'app', + opcode: 'q::stepAndInvariant', + args: [], + } + this.recorder.onUserOperatorCall(stepApp) + + const stepResult = stepEval(this.ctx).mapLeft(error => (failure = error)) + if (!isTrue(stepResult)) { + // The run cannot be extended. In some cases, this may indicate a deadlock. + // Since we are doing random simulation, it is very likely + // that we have not generated good values for extending + // the run. Hence, do not report an error here, but simply + // drop the run. Otherwise, we would have a lot of false + // positives, which look like deadlocks but they are not. + + this.recorder.onUserOperatorReturn(stepApp, [], stepResult) + this.recorder.onRunReturn(right(rv.mkBool(true)), this.trace.get()) + break + } + + this.shift() + + const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) + if (!isTrue(invResult)) { + errorsFound++ + } + this.recorder.onUserOperatorReturn(stepApp, [], invResult) } - this.recorder.onUserOperatorCall(stepApp) - - const stepResult = stepEval(this.ctx).mapLeft(error => (failure = error)) - if (!isTrue(stepResult)) { - // The run cannot be extended. In some cases, this may indicate a deadlock. - // Since we are doing random simulation, it is very likely - // that we have not generated good values for extending - // the run. Hence, do not report an error here, but simply - // drop the run. Otherwise, we would have a lot of false - // positives, which look like deadlocks but they are not. - - this.recorder.onUserOperatorReturn(stepApp, [], stepResult) - this.recorder.onRunReturn(right(rv.mkBool(true)), this.trace.get()) - break - } - - this.shift() - - const invResult = invEval(this.ctx).mapLeft(error => (failure = error)) - if (!isTrue(invResult)) { - errorsFound++ - } - this.recorder.onUserOperatorReturn(stepApp, [], invResult) } } @@ -386,14 +385,19 @@ function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } const body = evaluateExpr(builder, def.expr) return (ctx: Context) => { - ctx.recorder.onUserOperatorCall(app) - const result = body(ctx) + if (def.expr.kind !== 'lambda') { + ctx.recorder.onUserOperatorCall(app) + } if (ctx.varStorage.actionTaken === undefined) { ctx.varStorage.actionTaken = def.name } - ctx.recorder.onUserOperatorReturn(app, [], result) + const result = body(ctx) + + if (def.expr.kind !== 'lambda') { + ctx.recorder.onUserOperatorReturn(app, [], result) + } return result } } @@ -473,6 +477,7 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit const def = builder.table.get(expr.id) if (!def) { // TODO: Do we also need to return builtin ops for higher order usage? + // Answer: yes, see #1332 const lambda = builtinValue(expr.name) return _ => lambda } @@ -502,20 +507,14 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit return ctx => op(ctx, args) } + const userDefined = builder.table.has(expr.id) + const op = lambdaForApp(builder, expr) - // return ctx => { - // const argValues = args.map(arg => arg(ctx)) - // if (argValues.some(arg => arg.isLeft())) { - // return argValues.find(arg => arg.isLeft())! - // } - - // return op( - // ctx, - // argValues.map(a => a.unwrap()) - // ) - // } return ctx => { + if (userDefined) { + ctx.recorder.onUserOperatorCall(expr) + } const argValues = [] for (const arg of args) { const argValue = arg(ctx) @@ -525,7 +524,11 @@ function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Eit argValues.push(argValue.unwrap()) } - return op(ctx, argValues) + const result = op(ctx, argValues) + if (userDefined) { + ctx.recorder.onUserOperatorReturn(expr, argValues, result) + } + return result } case 'let': @@ -562,10 +565,7 @@ function lambdaForApp( } const arrow = lambdaResult.value.toArrow() - ctx.recorder.onUserOperatorCall(app) - const result = arrow(ctx, args) - ctx.recorder.onUserOperatorReturn(app, [], result) - return result + return arrow(ctx, args) } } From ec043f07ce53969ccd7c10070003e0a71caf0ae2 Mon Sep 17 00:00:00 2001 From: bugarela Date: Fri, 23 Aug 2024 18:24:02 -0300 Subject: [PATCH 16/37] Organize between files --- quint/src/names/base.ts | 2 +- quint/src/repl.ts | 10 +- quint/src/runtime/impl/Context.ts | 3 +- quint/src/runtime/impl/VarStorage.ts | 2 - quint/src/runtime/impl/builtins.ts | 34 ++- quint/src/runtime/impl/evaluator.ts | 319 +------------------------ quint/src/runtime/impl/runtimeValue.ts | 6 +- quint/src/simulation.ts | 4 +- 8 files changed, 48 insertions(+), 332 deletions(-) diff --git a/quint/src/names/base.ts b/quint/src/names/base.ts index b26549027..2ddce97f5 100644 --- a/quint/src/names/base.ts +++ b/quint/src/names/base.ts @@ -13,7 +13,7 @@ */ import { cloneDeep, compact } from 'lodash' -import { OpQualifier, QuintDef, QuintExport, QuintImport, QuintInstance, QuintLambdaParameter } from '../ir/quintIr' +import { QuintDef, QuintExport, QuintImport, QuintInstance, QuintLambdaParameter } from '../ir/quintIr' import { QuintType } from '../ir/quintTypes' import { QuintError } from '../quintError' import { NameResolver } from './resolver' diff --git a/quint/src/repl.ts b/quint/src/repl.ts index 3a619d92a..c26bebc95 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -12,25 +12,25 @@ import * as readline from 'readline' import { Readable, Writable } from 'stream' import { readFileSync, writeFileSync } from 'fs' import { Maybe, just, none } from '@sweet-monads/maybe' -import { Either, left, right } from '@sweet-monads/either' +import { left } from '@sweet-monads/either' import chalk from 'chalk' import { format } from './prettierimp' -import { FlatModule, QuintDef, QuintEx, QuintModule } from './ir/quintIr' +import { FlatModule, QuintDef, QuintModule } from './ir/quintIr' import { createFinders, formatError } from './errorReporter' import { Register } from './runtime/runtime' import { TraceRecorder, newTraceRecorder } from './runtime/trace' import { SourceMap, parse, parseDefOrThrow, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' -import { prettyQuintEx, printExecutionFrameRec, terminalWidth } from './graphics' +import { prettyQuintEx, terminalWidth } from './graphics' import { verbosity } from './verbosity' import { Rng, newRng } from './rng' import { version } from './version' import { fileSourceResolver } from './parsing/sourceResolver' import { cwd } from 'process' import { IdGenerator, newIdGenerator } from './idGenerator' -import { expressionToString, moduleToString } from './ir/IRprinting' +import { moduleToString } from './ir/IRprinting' import { mkErrorMessage } from './cliCommands' -import { QuintError, quintErrorToString } from './quintError' +import { QuintError } from './quintError' import { ErrorMessage } from './ErrorMessage' import { Evaluator } from './runtime/impl/evaluator' import { walkDeclaration, walkExpression } from './ir/IRVisitor' diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 02d1bcce8..0d35e671f 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -1,4 +1,4 @@ -import { Either, left, right } from '@sweet-monads/either' +import { Either } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue } from './runtimeValue' import { TraceRecorder } from '../trace' @@ -28,6 +28,5 @@ export class Context { shift() { this.varStorage.shiftVars() this.trace.extend(this.varStorage.asRecord()) - // TODO: save on trace } } diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index ee4a9693c..7a3059252 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -29,7 +29,6 @@ export class VarStorage { } shiftVars() { - // TODO: change this so registers are kept this.vars.forEach((reg, key) => { reg.value = this.nextVars.get(key)?.value ?? initialRegisterValue }) @@ -73,7 +72,6 @@ export class VarStorage { recoverSnapshot(snapshot: Snapshot) { this.nextVars.forEach((reg, key) => { - // TODO can we make this more efficient? const snapshotReg = snapshot.nextVars.get(key) if (snapshotReg) { reg.value = snapshotReg.value diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index 52ad6044e..45b4a7c8d 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,13 +1,13 @@ import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { QuintError, quintErrorToString } from '../../quintError' -import { Map, is, Set, Range, List } from 'immutable' +import { List, Map, OrderedMap, Range, Set } from 'immutable' import { EvalFunction, isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' -import { chunk, times } from 'lodash' +import { chunk } from 'lodash' import { expressionToString } from '../../ir/IRprinting' import { zerog } from '../../idGenerator' -import { QuintApp, QuintEx } from '../../ir/quintIr' +import { QuintApp } from '../../ir/quintIr' import { prettyQuintEx, terminalWidth } from '../../graphics' import { format } from '../../prettierimp' @@ -238,7 +238,15 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) case 'Set': return (_, args) => right(rv.mkSet(args)) case 'Rec': - return (_, args) => right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) + return (_, args) => { + const keys = args.filter((e, i) => i % 2 === 0).map(k => k.toStr()) + const map: OrderedMap = keys.reduce((map, key, i) => { + const v = args[2 * i + 1] + return v ? map.set(key, v) : map + }, OrderedMap()) + return right(rv.mkRecord(map)) + } + // right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) case 'List': return (_, args) => right(rv.mkList(List(args))) case 'Tup': @@ -517,10 +525,22 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) return (ctx, args) => { const lambda = args[1].toArrow() const keys = args[0].toSet() - const reducer = ([acc, arg]: RuntimeValue[]) => - lambda(ctx, [arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) + const results: [RuntimeValue, RuntimeValue][] = [] + + for (const key of keys) { + const value = lambda(ctx, [key]) + if (value.isLeft()) { + return value + } + results.push([key, value.value]) + } + + return right(rv.mkMap(Map(results))) + + // const reducer = ([acc, arg]: RuntimeValue[]) => + // lambda(ctx, [arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) - return applyFold('fwd', keys, rv.mkMap([]), reducer) + // return applyFold('fwd', keys, rv.mkMap([]), reducer) } case 'setToMap': diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 74fdd0572..5b57160e3 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -1,85 +1,19 @@ import { Either, left, right } from '@sweet-monads/either' -import { QuintApp, QuintEx, QuintOpDef } from '../../ir/quintIr' +import { QuintApp, QuintEx } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { QuintError } from '../../quintError' import { TraceRecorder } from '../trace' -import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' import { Trace } from './trace' import { RuntimeValue, rv } from './runtimeValue' -import { CachedValue, Context, Register } from './Context' +import { Context } from './Context' import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' -import { List, Map as ImmutableMap } from 'immutable' -import { NamedRegister, VarStorage } from './VarStorage' -import { expressionToString } from '../../ir/IRprinting' +import { List } from 'immutable' +import { Builder, buildDef, buildExpr, nameWithNamespaces } from './compiler' export type EvalFunction = (ctx: Context) => Either -function nameWithNamespaces(name: string, namespaces: List): string { - const revertedNamespaces = namespaces.reverse() - return revertedNamespaces.push(name).join('::') -} - -export class Builder { - table: LookupTable - paramRegistry: Map = new Map() - constRegistry: Map = new Map() - scopedCachedValues: Map = new Map() - initialNondetPicks: Map = new Map() - public namespaces: List = List() - public varStorage: VarStorage - - constructor(table: LookupTable, storeMetadata: boolean) { - this.table = table - this.varStorage = new VarStorage(storeMetadata, this.initialNondetPicks) - } - - discoverVar(id: bigint, name: string) { - const key = [id, ...this.namespaces].join('#') - if (this.varStorage.vars.has(key)) { - return - } - - const varName = nameWithNamespaces(name, this.namespaces) - const register: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } - const nextRegister: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } - this.varStorage.vars = this.varStorage.vars.set(key, register) - this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) - } - - getVar(id: bigint): NamedRegister { - const key = [id, ...this.namespaces].join('#') - const result = this.varStorage.vars.get(key) - if (!result) { - throw new Error(`Variable not found: ${key}`) - } - - return result - } - - getNextVar(id: bigint): NamedRegister { - const key = [id, ...this.namespaces].join('#') - const result = this.varStorage.nextVars.get(key) - if (!result) { - throw new Error(`Variable not found: ${key}`) - } - - return result - } - - registerForConst(id: bigint, name: string): Register { - let register = this.constRegistry.get(id) - if (!register) { - const message = `Uninitialized const ${name}. Use: import (${name}=).*` - register = { value: left({ code: 'QNT500', message }) } - this.constRegistry.set(id, register) - return register - } - return register - } -} - export class Evaluator { public ctx: Context public recorder: TraceRecorder @@ -124,7 +58,7 @@ export class Evaluator { if (expr.kind === 'app') { this.recorder.onUserOperatorCall(expr) } - const value = evaluateExpr(this.builder, expr)(this.ctx) + const value = buildExpr(this.builder, expr)(this.ctx) if (expr.kind === 'app') { this.recorder.onUserOperatorReturn(expr, [], value) } @@ -147,9 +81,9 @@ export class Evaluator { let errorsFound = 0 let failure: QuintError | undefined = undefined - const initEval = evaluateExpr(this.builder, init) - const stepEval = evaluateExpr(this.builder, step) - const invEval = evaluateExpr(this.builder, inv) + const initEval = buildExpr(this.builder, init) + const stepEval = buildExpr(this.builder, step) + const invEval = buildExpr(this.builder, inv) // TODO: room for improvement here for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { @@ -232,7 +166,7 @@ export class Evaluator { // save the initial seed let seed = this.rng.getState() - const testEval = evaluateDef(this.builder, testDef) + const testEval = buildDef(this.builder, testDef) let nsamples = 1 // run up to maxSamples, stop on the first failure @@ -342,233 +276,6 @@ export class Evaluator { } } -export function evaluateExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { - const exprEval = evaluateNewExpr(builder, expr) - return ctx => exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) -} - -export function buildUnderDefContext( - builder: Builder, - def: LookupDefinition, - buildFunction: () => (ctx: Context) => Either -): (ctx: Context) => Either { - if (!def.importedFrom || def.importedFrom.kind !== 'instance') { - return buildFunction() - } - const instance = def.importedFrom - - const overrides: [Register, (ctx: Context) => Either][] = instance.overrides.map( - ([param, expr]) => { - const id = builder.table.get(param.id)!.id - const register = builder.registerForConst(id, param.name) - - return [register, evaluateExpr(builder, expr)] - } - ) - - const namespacesBefore = builder.namespaces - builder.namespaces = List(def.namespaces) - - const result = buildFunction() - builder.namespaces = namespacesBefore - - return ctx => { - overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) - return result(ctx) - } -} - -function evaluateDefCore(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { - switch (def.kind) { - case 'def': - if (def.qualifier === 'action') { - const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } - const body = evaluateExpr(builder, def.expr) - return (ctx: Context) => { - if (def.expr.kind !== 'lambda') { - ctx.recorder.onUserOperatorCall(app) - } - - if (ctx.varStorage.actionTaken === undefined) { - ctx.varStorage.actionTaken = def.name - } - - const result = body(ctx) - - if (def.expr.kind !== 'lambda') { - ctx.recorder.onUserOperatorReturn(app, [], result) - } - return result - } - } - - if (def.expr.kind === 'lambda') { - return evaluateExpr(builder, def.expr) - } - - if (def.depth === undefined || def.depth === 0) { - return evaluateExpr(builder, def.expr) - } - - let cachedValue = builder.scopedCachedValues.get(def.id)! - - const bodyEval = evaluateExpr(builder, def.expr) - if (def.qualifier === 'nondet') { - builder.initialNondetPicks.set(def.name, undefined) - } - - return ctx => { - if (cachedValue.value === undefined) { - cachedValue.value = bodyEval(ctx) - if (def.qualifier === 'nondet') { - cachedValue.value - .map(value => { - ctx.varStorage.nondetPicks.set(def.name, value) - }) - .mapLeft(_ => { - ctx.varStorage.nondetPicks.set(def.name, undefined) - }) - } - } - return cachedValue.value - } - case 'param': { - const register = builder.paramRegistry.get(def.id) - if (!register) { - const reg: Register = { value: left({ code: 'QNT501', message: `Parameter ${def.name} not set` }) } - builder.paramRegistry.set(def.id, reg) - return _ => reg.value - } - return _ => register.value - } - - case 'var': { - const register = builder.getVar(def.id) - return _ => { - return register.value - } - } - case 'const': - const register = builder.registerForConst(def.id, def.name) - return _ => register.value - default: - return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) - } -} - -function evaluateDef(builder: Builder, def: LookupDefinition): (ctx: Context) => Either { - return buildUnderDefContext(builder, def, () => evaluateDefCore(builder, def)) -} - -function evaluateNewExpr(builder: Builder, expr: QuintEx): (ctx: Context) => Either { - switch (expr.kind) { - case 'int': - case 'bool': - case 'str': - // These are already values, just return them - const value = right(rv.fromQuintEx(expr)) - return _ => value - case 'lambda': - // Lambda is also like a value, but we should construct it with the context - const body = evaluateExpr(builder, expr.expr) - const lambda = rv.mkLambda(expr.params, body, builder.paramRegistry) - return _ => right(lambda) - case 'name': - const def = builder.table.get(expr.id) - if (!def) { - // TODO: Do we also need to return builtin ops for higher order usage? - // Answer: yes, see #1332 - const lambda = builtinValue(expr.name) - return _ => lambda - } - return evaluateDef(builder, def) - case 'app': - if (expr.opcode === 'assign') { - const varDef = builder.table.get(expr.args[0].id)! - return buildUnderDefContext(builder, varDef, () => { - builder.discoverVar(varDef.id, varDef.name) - const register = builder.getNextVar(varDef.id) - const exprEval = evaluateExpr(builder, expr.args[1]) - - return ctx => { - return exprEval(ctx).map(value => { - register.value = right(value) - return rv.mkBool(true) - }) - } - }) - } - - const args = expr.args.map(arg => evaluateExpr(builder, arg)) - - // In these special ops, we don't want to evaluate the arguments before evaluating application - if (lazyOps.includes(expr.opcode)) { - const op = lazyBuiltinLambda(expr.opcode) - return ctx => op(ctx, args) - } - - const userDefined = builder.table.has(expr.id) - - const op = lambdaForApp(builder, expr) - - return ctx => { - if (userDefined) { - ctx.recorder.onUserOperatorCall(expr) - } - const argValues = [] - for (const arg of args) { - const argValue = arg(ctx) - if (argValue.isLeft()) { - return argValue - } - argValues.push(argValue.unwrap()) - } - - const result = op(ctx, argValues) - if (userDefined) { - ctx.recorder.onUserOperatorReturn(expr, argValues, result) - } - return result - } - - case 'let': - let cachedValue = builder.scopedCachedValues.get(expr.opdef.id) - if (!cachedValue) { - cachedValue = { value: undefined } - builder.scopedCachedValues.set(expr.opdef.id, cachedValue) - } - const bodyEval = evaluateExpr(builder, expr.expr) - return ctx => { - const result = bodyEval(ctx) - cachedValue!.value = undefined - return result - } - } -} - -function lambdaForApp( - builder: Builder, - app: QuintApp -): (ctx: Context, args: RuntimeValue[]) => Either { - const { id, opcode } = app - - const def = builder.table.get(id)! - if (!def) { - return builtinLambda(opcode) - } - - const value = evaluateDef(builder, def) - return (ctx, args) => { - const lambdaResult = value(ctx) - if (lambdaResult.isLeft()) { - return lambdaResult - } - const arrow = lambdaResult.value.toArrow() - - return arrow(ctx, args) - } -} - export function isTrue(value: Either): boolean { return value.isRight() && value.value.toBool() === true } @@ -576,11 +283,3 @@ export function isTrue(value: Either): boolean { export function isFalse(value: Either): boolean { return value.isRight() && value.value.toBool() === false } - -export function profile(name: string, f: () => T): T { - // const start = Date.now() - const r = f() - // const end = Date.now() - // console.log(`${name} took ${end - start}ms`) - return r -} diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index 620d1d7be..9a9d6aa0e 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -60,19 +60,19 @@ * See LICENSE in the project root for license information. */ -import { List, Map as ImmutableMap, OrderedMap, Set, ValueObject, hash, is as immutableIs } from 'immutable' +import { Map as ImmutableMap, List, OrderedMap, Set, ValueObject, hash, is as immutableIs } from 'immutable' import { Maybe, just, merge, none } from '@sweet-monads/maybe' import { strict as assert } from 'assert' import { IdGenerator, zerog } from '../../idGenerator' import { expressionToString } from '../../ir/IRprinting' -import { Callable, EvalResult } from '../runtime' +import { EvalResult } from '../runtime' import { QuintEx, QuintLambdaParameter, QuintName } from '../../ir/quintIr' import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { toMaybe } from './base' -import { EvalFunction, evaluateExpr } from './evaluator' +import { EvalFunction } from './evaluator' import { Context, Register } from './Context' /** diff --git a/quint/src/simulation.ts b/quint/src/simulation.ts index 5e3e00898..ea2221e79 100644 --- a/quint/src/simulation.ts +++ b/quint/src/simulation.ts @@ -8,8 +8,8 @@ * See LICENSE in the project root for license information. */ -import { QuintBool, QuintEx } from './ir/quintIr' -import { ExecutionFrame, Trace, newTraceRecorder } from './runtime/trace' +import { QuintEx } from './ir/quintIr' +import { ExecutionFrame } from './runtime/trace' import { Rng } from './rng' import { QuintError } from './quintError' From 6e8576fe0b7089e9fd54ad6733bf0547d2cfb96e Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 27 Aug 2024 16:49:57 -0300 Subject: [PATCH 17/37] Separate and memoize building --- quint/src/runtime/impl/VarStorage.ts | 1 + quint/src/runtime/impl/builtins.ts | 84 +++++-- quint/src/runtime/impl/compiler.ts | 330 +++++++++++++++++++++++++ quint/src/runtime/impl/evaluator.ts | 2 - quint/src/runtime/impl/runtimeValue.ts | 8 +- 5 files changed, 394 insertions(+), 31 deletions(-) create mode 100644 quint/src/runtime/impl/compiler.ts diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 7a3059252..31679c439 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -14,6 +14,7 @@ interface Snapshot { actionTaken: string | undefined } +// TODO: Add name to error message const initialRegisterValue: Either = left({ code: 'QNT502', message: 'Variable not set' }) export class VarStorage { diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index 45b4a7c8d..f9a8efa88 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,7 +1,7 @@ import { Either, left, mergeInMany, right } from '@sweet-monads/either' -import { QuintError, quintErrorToString } from '../../quintError' +import { QuintError } from '../../quintError' import { List, Map, OrderedMap, Range, Set } from 'immutable' -import { EvalFunction, isFalse, isTrue } from './evaluator' +import { isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' import { chunk } from 'lodash' @@ -10,6 +10,7 @@ import { zerog } from '../../idGenerator' import { QuintApp } from '../../ir/quintIr' import { prettyQuintEx, terminalWidth } from '../../graphics' import { format } from '../../prettierimp' +import { EvalFunction } from './compiler' export function builtinValue(name: string): Either { switch (name) { @@ -37,6 +38,7 @@ export const lazyOps = [ 'implies', 'then', 'reps', + 'expect', ] export function lazyBuiltinLambda( @@ -96,9 +98,6 @@ export function lazyBuiltinLambda( }) ctx.recorder.onAnyOptionReturn(app, i) - // Recover snapshot (regardless of success or failure) - ctx.varStorage.recoverSnapshot(nextVarsSnapshot) - return result }) @@ -110,6 +109,7 @@ export function lazyBuiltinLambda( switch (potentialSuccessors.length) { case 0: ctx.recorder.onAnyReturn(args.length, -1) + ctx.varStorage.recoverSnapshot(nextVarsSnapshot) return rv.mkBool(false) case 1: ctx.recorder.onAnyReturn(args.length, potentialSuccessors[0].index) @@ -227,6 +227,35 @@ export function lazyBuiltinLambda( return result }) } + case 'expect': + // Translate A.expect(P): + // - Evaluate A. + // - When A's result is 'false', emit a runtime error. + // - When A's result is 'true': + // - Commit the variable updates: Shift the primed variables to unprimed. + // - Evaluate `P`. + // - If `P` evaluates to `false`, emit a runtime error (similar to `assert`). + // - If `P` evaluates to `true`, rollback to the previous state and return `true`. + return (ctx, args) => { + const result: Either = args[0](ctx).chain(action => { + if (!action.toBool()) { + return left({ code: 'QNT508', message: 'Cannot continue to "expect"' }) + } + + const nextVarsSnapshot = ctx.varStorage.snapshot() + ctx.shift() + return args[1](ctx).chain(expectation => { + ctx.varStorage.recoverSnapshot(nextVarsSnapshot) + + if (!expectation.toBool()) { + return left({ code: 'QNT508', message: 'Expect condition does not hold true' }) + } + + return right(rv.mkBool(true)) + }) + }) + return result + } default: return () => left({ code: 'QNT000', message: 'Unknown stateful op' }) @@ -532,10 +561,10 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) if (value.isLeft()) { return value } - results.push([key, value.value]) + results.push([key.normalForm(), value.value]) } - return right(rv.mkMap(Map(results))) + return right(rv.fromMap(Map(results))) // const reducer = ([acc, arg]: RuntimeValue[]) => // lambda(ctx, [arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) @@ -563,7 +592,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) console.log('>', args[0].toStr(), valuePretty.toString()) return right(args[1]) } - case 'expect': + // TODO: add missing operators default: return () => left({ code: 'QNT000', message: `Unknown builtin ${op}` }) @@ -575,21 +604,32 @@ export function applyLambdaToSet( set: RuntimeValue ): Either> { const f = lambda.toArrow() - const results = set.toSet().map(value => f(ctx, [value])) - const err = results.find(result => result.isLeft()) - if (err !== undefined && err.isLeft()) { - return left(err.value) + const elements = set.toSet() + const results = [] + for (const element of elements) { + const result = f(ctx, [element]) + if (result.isLeft()) { + return left(result.value) + } + results.push(result.value) } - - return right( - results.map(result => { - if (result.isLeft()) { - throw new Error(`Impossible, result is left: ${quintErrorToString(result.value)}`) - } - - return result.value - }) - ) + return right(Set(results)) + + // map(value => f(ctx, [value])) + // const err = results.find(result => result.isLeft()) + // if (err !== undefined && err.isLeft()) { + // return left(err.value) + // } + + // return right( + // results.map(result => { + // if (result.isLeft()) { + // throw new Error(`Impossible, result is left: ${quintErrorToString(result.value)}`) + // } + + // return result.value + // }) + // ) } function applyFold( diff --git a/quint/src/runtime/impl/compiler.ts b/quint/src/runtime/impl/compiler.ts new file mode 100644 index 000000000..a4b8bb647 --- /dev/null +++ b/quint/src/runtime/impl/compiler.ts @@ -0,0 +1,330 @@ +import { Either, left, right } from '@sweet-monads/either' +import { QuintError } from '../../quintError' +import { RuntimeValue, rv } from './runtimeValue' +import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' +import { CachedValue, Context, Register } from './Context' +import { QuintApp, QuintEx } from '../../ir/quintIr' +import { LookupDefinition, LookupTable } from '../../names/base' +import { NamedRegister, VarStorage } from './VarStorage' +import { List } from 'immutable' + +export type EvalFunction = (ctx: Context) => Either + +export class Builder { + table: LookupTable + paramRegistry: Map = new Map() + constRegistry: Map = new Map() + scopedCachedValues: Map = new Map() + initialNondetPicks: Map = new Map() + memo: Map = new Map() + memoByInstance: Map> = new Map() + public namespaces: List = List() + public varStorage: VarStorage + + constructor(table: LookupTable, storeMetadata: boolean) { + this.table = table + this.varStorage = new VarStorage(storeMetadata, this.initialNondetPicks) + } + + discoverVar(id: bigint, name: string) { + const key = [id, ...this.namespaces].join('#') + if (this.varStorage.vars.has(key)) { + return + } + + const varName = nameWithNamespaces(name, this.namespaces) + const register: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + const nextRegister: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + this.varStorage.vars = this.varStorage.vars.set(key, register) + this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) + } + + getVar(id: bigint): NamedRegister { + const key = [id, ...this.namespaces].join('#') + const result = this.varStorage.vars.get(key) + if (!result) { + throw new Error(`Variable not found: ${key}`) + } + + return result + } + + getNextVar(id: bigint): NamedRegister { + const key = [id, ...this.namespaces].join('#') + const result = this.varStorage.nextVars.get(key) + if (!result) { + throw new Error(`Variable not found: ${key}`) + } + + return result + } + + registerForConst(id: bigint, name: string): Register { + let register = this.constRegistry.get(id) + if (!register) { + const message = `Uninitialized const ${name}. Use: import (${name}=).*` + register = { value: left({ code: 'QNT500', message }) } + this.constRegistry.set(id, register) + return register + } + return register + } +} + +export function buildExpr(builder: Builder, expr: QuintEx): EvalFunction { + if (builder.memo.has(expr.id)) { + return builder.memo.get(expr.id)! + } + const exprEval = buildExprCore(builder, expr) + const wrappedEval: EvalFunction = ctx => + exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) + builder.memo.set(expr.id, wrappedEval) + return wrappedEval +} + +export function buildUnderDefContext( + builder: Builder, + def: LookupDefinition, + buildFunction: () => EvalFunction +): EvalFunction { + if (!def.importedFrom || def.importedFrom.kind !== 'instance') { + return buildFunction() + } + const instance = def.importedFrom + const memoBefore = builder.memo + if (builder.memoByInstance.has(instance.id)) { + builder.memo = builder.memoByInstance.get(instance.id)! + } else { + builder.memo = new Map() + builder.memoByInstance.set(instance.id, builder.memo) + } + + const overrides: [Register, EvalFunction][] = instance.overrides.map(([param, expr]) => { + const id = builder.table.get(param.id)!.id + const register = builder.registerForConst(id, param.name) + + return [register, buildExpr(builder, expr)] + }) + + const namespacesBefore = builder.namespaces + builder.namespaces = List(def.namespaces) + + const result = buildFunction() + builder.namespaces = namespacesBefore + builder.memo = memoBefore + + return ctx => { + overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) + return result(ctx) + } +} + +function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { + switch (def.kind) { + case 'def': + if (def.qualifier === 'action') { + const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } + const body = buildExpr(builder, def.expr) + return (ctx: Context) => { + if (def.expr.kind !== 'lambda') { + ctx.recorder.onUserOperatorCall(app) + } + + if (ctx.varStorage.actionTaken === undefined) { + ctx.varStorage.actionTaken = def.name + } + + const result = body(ctx) + + if (def.expr.kind !== 'lambda') { + ctx.recorder.onUserOperatorReturn(app, [], result) + } + return result + } + } + + if (def.expr.kind === 'lambda') { + return buildExpr(builder, def.expr) + } + + if (def.depth === undefined || def.depth === 0) { + return buildExpr(builder, def.expr) + } + + let cachedValue = builder.scopedCachedValues.get(def.id)! + + const bodyEval = buildExpr(builder, def.expr) + if (def.qualifier === 'nondet') { + builder.initialNondetPicks.set(def.name, undefined) + } + + return ctx => { + if (cachedValue.value === undefined) { + cachedValue.value = bodyEval(ctx) + if (def.qualifier === 'nondet') { + cachedValue.value + .map(value => { + ctx.varStorage.nondetPicks.set(def.name, value) + }) + .mapLeft(_ => { + ctx.varStorage.nondetPicks.set(def.name, undefined) + }) + } + } + return cachedValue.value + } + case 'param': { + const register = builder.paramRegistry.get(def.id) + if (!register) { + const reg: Register = { value: left({ code: 'QNT501', message: `Parameter ${def.name} not set` }) } + builder.paramRegistry.set(def.id, reg) + return _ => reg.value + } + return _ => register.value + } + + case 'var': { + const register = builder.getVar(def.id) + return _ => { + return register.value + } + } + case 'const': + const register = builder.registerForConst(def.id, def.name) + return _ => register.value + default: + return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) + } +} + +function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction { + if (builder.memo.has(def.id)) { + return builder.memo.get(def.id)! + } + + const defEval = buildDefCore(builder, def) + builder.memo.set(def.id, defEval) + return defEval +} + +export function buildDef(builder: Builder, def: LookupDefinition): EvalFunction { + if (!def.importedFrom || def.importedFrom.kind !== 'instance') { + return buildDefWithMemo(builder, def) + } + + return buildUnderDefContext(builder, def, () => buildDefWithMemo(builder, def)) +} + +function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { + switch (expr.kind) { + case 'int': + case 'bool': + case 'str': + // These are already values, just return them + const value = right(rv.fromQuintEx(expr)) + return _ => value + case 'lambda': + // Lambda is also like a value, but we should construct it with the context + const body = buildExpr(builder, expr.expr) + const lambda = rv.mkLambda(expr.params, body, builder.paramRegistry) + return _ => right(lambda) + case 'name': + const def = builder.table.get(expr.id) + if (!def) { + // TODO: Do we also need to return builtin ops for higher order usage? + // Answer: yes, see #1332 + const lambda = builtinValue(expr.name) + return _ => lambda + } + return buildDef(builder, def) + case 'app': + if (expr.opcode === 'assign') { + const varDef = builder.table.get(expr.args[0].id)! + return buildUnderDefContext(builder, varDef, () => { + builder.discoverVar(varDef.id, varDef.name) + const register = builder.getNextVar(varDef.id) + const exprEval = buildExpr(builder, expr.args[1]) + + return ctx => { + return exprEval(ctx).map(value => { + register.value = right(value) + return rv.mkBool(true) + }) + } + }) + } + + const args = expr.args.map(arg => buildExpr(builder, arg)) + + // In these special ops, we don't want to evaluate the arguments before evaluating application + if (lazyOps.includes(expr.opcode)) { + const op = lazyBuiltinLambda(expr.opcode) + return ctx => op(ctx, args) + } + + const userDefined = builder.table.has(expr.id) + + const op = lambdaForApp(builder, expr) + + return ctx => { + if (userDefined) { + ctx.recorder.onUserOperatorCall(expr) + } + const argValues = [] + for (const arg of args) { + const argValue = arg(ctx) + if (argValue.isLeft()) { + return argValue + } + argValues.push(argValue.unwrap()) + } + + const result = op(ctx, argValues) + if (userDefined) { + ctx.recorder.onUserOperatorReturn(expr, argValues, result) + } + return result + } + + case 'let': + let cachedValue = builder.scopedCachedValues.get(expr.opdef.id) + if (!cachedValue) { + cachedValue = { value: undefined } + builder.scopedCachedValues.set(expr.opdef.id, cachedValue) + } + const bodyEval = buildExpr(builder, expr.expr) + return ctx => { + const result = bodyEval(ctx) + cachedValue!.value = undefined + return result + } + } +} + +function lambdaForApp( + builder: Builder, + app: QuintApp +): (ctx: Context, args: RuntimeValue[]) => Either { + const { id, opcode } = app + + const def = builder.table.get(id)! + if (!def) { + return builtinLambda(opcode) + } + + const value = buildDef(builder, def) + return (ctx, args) => { + const lambdaResult = value(ctx) + if (lambdaResult.isLeft()) { + return lambdaResult + } + const arrow = lambdaResult.value.toArrow() + + return arrow(ctx, args) + } +} + +export function nameWithNamespaces(name: string, namespaces: List): string { + const revertedNamespaces = namespaces.reverse() + return revertedNamespaces.push(name).join('::') +} diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 5b57160e3..c9b731ff2 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -12,8 +12,6 @@ import { zerog } from '../../idGenerator' import { List } from 'immutable' import { Builder, buildDef, buildExpr, nameWithNamespaces } from './compiler' -export type EvalFunction = (ctx: Context) => Either - export class Evaluator { public ctx: Context public recorder: TraceRecorder diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index 9a9d6aa0e..f7802516f 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -72,7 +72,7 @@ import { QuintEx, QuintLambdaParameter, QuintName } from '../../ir/quintIr' import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { toMaybe } from './base' -import { EvalFunction } from './evaluator' +import { EvalFunction } from './compiler' import { Context, Register } from './Context' /** @@ -1655,12 +1655,6 @@ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue this.registers = registers } - eval(args?: any[]) { - return () => { - throw new Error('Not implemented') - } - } - toQuintEx(gen: IdGenerator): QuintEx { // We produce a mock Quint expression. // It is not going to be used, From d4ad6bc32cbce63bb5a89300a90733b4d54e325e Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 27 Aug 2024 17:27:28 -0300 Subject: [PATCH 18/37] Improve performance by caching val (temporarly) and pure val (permanently) --- quint/src/runtime/impl/VarStorage.ts | 11 ++++++++++- quint/src/runtime/impl/compiler.ts | 24 ++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 31679c439..55a20a1cb 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -2,7 +2,7 @@ import { Either, left } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' import { Map as ImmutableMap } from 'immutable' -import { Register } from './Context' +import { CachedValue, Register } from './Context' export interface NamedRegister extends Register { name: string @@ -22,6 +22,8 @@ export class VarStorage { public nextVars: ImmutableMap = ImmutableMap() public nondetPicks: Map = new Map() public actionTaken: string | undefined + public cachesToClear: CachedValue[] = [] + private storeMetadata: boolean constructor(storeMetadata: boolean, nondetPicks: Map) { @@ -35,6 +37,7 @@ export class VarStorage { }) this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) + this.clearCaches() } asRecord(): RuntimeValue { @@ -81,4 +84,10 @@ export class VarStorage { this.nondetPicks = snapshot.nondetPicks this.actionTaken = snapshot.actionTaken } + + private clearCaches() { + this.cachesToClear.forEach(cachedValue => { + cachedValue.value = undefined + }) + } } diff --git a/quint/src/runtime/impl/compiler.ts b/quint/src/runtime/impl/compiler.ts index a4b8bb647..32581c527 100644 --- a/quint/src/runtime/impl/compiler.ts +++ b/quint/src/runtime/impl/compiler.ts @@ -203,8 +203,28 @@ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction } const defEval = buildDefCore(builder, def) - builder.memo.set(def.id, defEval) - return defEval + + if (!(def.kind === 'def' && (def.qualifier === 'pureval' || def.qualifier === 'val') && def.depth === 0)) { + builder.memo.set(def.id, defEval) + return defEval + } + + // Since we cache things separately per instance, we can cache the value here + const cachedValue: CachedValue = { value: undefined } + if (def.qualifier === 'val') { + console.log('temp cache', def.name) + builder.varStorage.cachesToClear.push(cachedValue) + } else { + console.log('perm cache', def.name) + } + const wrappedEval: EvalFunction = ctx => { + if (cachedValue.value === undefined) { + cachedValue.value = defEval(ctx) + } + return cachedValue.value + } + builder.memo.set(def.id, wrappedEval) + return wrappedEval } export function buildDef(builder: Builder, def: LookupDefinition): EvalFunction { From dbda0742dcc6a52ae98dbbff67a0958444c7c9ab Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 27 Aug 2024 18:10:49 -0300 Subject: [PATCH 19/37] Cache constants and fix metadata tracking --- quint/src/runtime/impl/builtins.ts | 14 +++++++------- quint/src/runtime/impl/compiler.ts | 15 +++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index f9a8efa88..2282bb2b8 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -79,16 +79,16 @@ export function lazyBuiltinLambda( case 'actionAny': const app: QuintApp = { id: 0n, kind: 'app', opcode: 'actionAny', args: [] } return (ctx, args) => { - // on `any`, we reset the action taken as the goal is to save the last - // action picked in an `any` call - ctx.varStorage.actionTaken = undefined - ctx.varStorage.nondetPicks.forEach((_, key) => { - ctx.varStorage.nondetPicks.set(key, undefined) - }) - const nextVarsSnapshot = ctx.varStorage.snapshot() const evaluationResults = args.map((arg, i) => { + // on `any`, we reset the action taken as the goal is to save the last + // action picked in an `any` call + ctx.varStorage.actionTaken = undefined + ctx.varStorage.nondetPicks.forEach((_, key) => { + ctx.varStorage.nondetPicks.set(key, undefined) + }) + ctx.recorder.onAnyOptionCall(app, i) const result = arg(ctx).map(result => { // Save vars diff --git a/quint/src/runtime/impl/compiler.ts b/quint/src/runtime/impl/compiler.ts index 32581c527..15fdd149e 100644 --- a/quint/src/runtime/impl/compiler.ts +++ b/quint/src/runtime/impl/compiler.ts @@ -103,7 +103,8 @@ export function buildUnderDefContext( const id = builder.table.get(param.id)!.id const register = builder.registerForConst(id, param.name) - return [register, buildExpr(builder, expr)] + // Build the expr as a pure val def so it gets properly cached + return [register, buildDef(builder, { kind: 'def', qualifier: 'pureval', expr, name: param.name, id: param.id })] }) const namespacesBefore = builder.namespaces @@ -204,7 +205,13 @@ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction const defEval = buildDefCore(builder, def) - if (!(def.kind === 'def' && (def.qualifier === 'pureval' || def.qualifier === 'val') && def.depth === 0)) { + if ( + !( + def.kind === 'def' && + (def.qualifier === 'pureval' || def.qualifier === 'val') && + (def.depth === undefined || def.depth === 0) + ) + ) { builder.memo.set(def.id, defEval) return defEval } @@ -212,10 +219,10 @@ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction // Since we cache things separately per instance, we can cache the value here const cachedValue: CachedValue = { value: undefined } if (def.qualifier === 'val') { - console.log('temp cache', def.name) + // console.log('temp cache', def.name) builder.varStorage.cachesToClear.push(cachedValue) } else { - console.log('perm cache', def.name) + // console.log('perm cache', def.name) } const wrappedEval: EvalFunction = ctx => { if (cachedValue.value === undefined) { From cd3198aa6a9fa3d3e2b0626770e55cbc508f5d0f Mon Sep 17 00:00:00 2001 From: bugarela Date: Tue, 27 Aug 2024 18:58:26 -0300 Subject: [PATCH 20/37] Update unit tests --- quint/test/runtime/compile.test.ts | 330 ++++------------------------- 1 file changed, 47 insertions(+), 283 deletions(-) diff --git a/quint/test/runtime/compile.test.ts b/quint/test/runtime/compile.test.ts index 580168b97..09e174390 100644 --- a/quint/test/runtime/compile.test.ts +++ b/quint/test/runtime/compile.test.ts @@ -1,27 +1,15 @@ import { describe, it } from 'mocha' import { assert } from 'chai' -import { Either, left, right } from '@sweet-monads/either' import { expressionToString } from '../../src/ir/IRprinting' -import { Callable, Computable, ComputableKind, fail, kindName } from '../../src/runtime/runtime' -import { noExecutionListener } from '../../src/runtime/trace' -import { - CompilationContext, - CompilationState, - compile, - compileDecls, - compileExpr, - compileFromCode, - contextNameLookup, - inputDefName, -} from '../../src/runtime/compile' -import { RuntimeValue } from '../../src/runtime/impl/runtimeValue' +import { newTraceRecorder } from '../../src/runtime/trace' import { dedent } from '../textUtils' import { newIdGenerator } from '../../src/idGenerator' -import { Rng, newRng } from '../../src/rng' -import { SourceLookupPath, stringSourceResolver } from '../../src/parsing/sourceResolver' -import { analyzeModules, parse, parseExpressionOrDeclaration, quintErrorToString } from '../../src' -import { flattenModules } from '../../src/flattening/fullFlattener' -import { newEvaluationState } from '../../src/runtime/impl/base' +import { newRng } from '../../src/rng' +import { stringSourceResolver } 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, right } from '@sweet-monads/either' // Use a global id generator, limited to this test suite. const idGen = newIdGenerator() @@ -33,136 +21,64 @@ const idGen = newIdGenerator() // `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 moduleText = `module contextM { ${evalContext} } module __runtime { import contextM.*\n val ${inputDefName} = ${input} }` - const mockLookupPath = stringSourceResolver(new Map()).lookupPath('/', './mock') - const context = compileFromCode( - idGen, - moduleText, - '__runtime', - mockLookupPath, - noExecutionListener, - newRng().next, - false - ) - - assert.isEmpty(context.syntaxErrors, `Syntax errors: ${context.syntaxErrors.map(quintErrorToString).join(', ')}`) - assert.isEmpty(context.compileErrors, `Compile errors: ${context.compileErrors.map(quintErrorToString).join(', ')}`) - - assertInputFromContext(context, expected) + 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 assertInputFromContext(context: CompilationContext, expected: string | undefined) { - contextNameLookup(context.evaluationState.context, inputDefName, 'callable') - .mapLeft(msg => assert(false, `Unexpected error: ${msg}`)) - .mapRight(value => assertComputableAsString(value, expected)) -} +function prepareEvaluator(input: string, evalContext: string): [Evaluator, QuintEx] { + const mockLookupPath = stringSourceResolver(new Map()).lookupPath('/', './mock') + const { resolver, sourceMap } = parse(idGen, '', mockLookupPath, `module contextM { ${evalContext} }`) -function assertComputableAsString(computable: Computable, expected: string | undefined) { - const result = computable - .eval() - .map(r => r.toQuintEx(idGen)) - .map(expressionToString) - .map(s => assert(s === expected, `Expected ${expected}, found ${s}`)) - if (result.isLeft()) { - assert(expected === undefined, `Expected ${expected}, found undefined`) + const parseResult = parseExpressionOrDeclaration(input, '', idGen, sourceMap) + if (parseResult.kind !== 'expr') { + assert.fail(`Expected an expression, found ${parseResult.kind}`) } -} -// Compile an input and evaluate a callback in the context -function evalInContext(input: string, callable: (ctx: CompilationContext) => Either) { - const moduleText = `module __runtime { ${input} }` - const mockLookupPath = stringSourceResolver(new Map()).lookupPath('/', './mock') - const context = compileFromCode( - idGen, - moduleText, - '__runtime', - mockLookupPath, - noExecutionListener, - newRng().next, - false - ) - return callable(context) -} - -// Compile a variable definition and check that the compiled value is defined. -function assertVarExists(kind: ComputableKind, name: string, input: string) { - const callback = (ctx: CompilationContext) => { - return contextNameLookup(ctx.evaluationState.context, `${name}`, kind) - .mapRight(_ => true) - .mapLeft(msg => `Expected a definition for ${name}, found ${msg}, compiled from: ${input}`) + walkExpression(resolver, parseResult.expr) + if (resolver.errors.length > 0) { + assert.fail(`Resolver errors: ${resolver.errors.map(quintErrorToString).join(', ')}`) } - const res = evalInContext(input, callback) - res.mapLeft(m => assert.fail(m)) + const rng = newRng() + const evaluator = new Evaluator(resolver.table, newTraceRecorder(0, rng), rng) + return [evaluator, parseResult.expr] } -// compile a computable for a run definition -function callableFromContext(ctx: CompilationContext, callee: string): Either { - let key = undefined - const lastModule = ctx.compilationState.modules[ctx.compilationState.modules.length - 1] - const def = lastModule.declarations.find(def => def.kind === 'def' && def.name === callee) - if (!def) { - return left(`${callee} definition not found`) - } - key = kindName('callable', def.id) - if (!key) { - return left(`${callee} not found`) - } - const run = ctx.evaluationState.context.get(key) as Callable - if (!run) { - return left(`${callee} not found via ${key}`) - } +// Compile a variable definition and check that the compiled value is defined. +function assertVarExists(name: string, input: string) { + const [evaluator, _] = prepareEvaluator(input, '') - return right(run) + assert.includeDeepMembers([...evaluator.ctx.varStorage.vars.keys()], [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 { - // use a combination of Maybe and Either. - // Recall that left(...) is used for errors, - // whereas right(...) is used for non-errors in sweet monads. - const callback = (ctx: CompilationContext): Either => { - return callableFromContext(ctx, callee).chain(run => { - return run - .eval() - .mapLeft(quintErrorToString) - .chain(res => { - if ((res as RuntimeValue).toBool() === true) { - // extract the value of the state variable - const nextVal = (ctx.evaluationState.context.get(kindName('nextvar', varName)) ?? fail).eval() - if (nextVal.isLeft()) { - return left(`Value of the variable ${varName} is undefined`) - } else { - return right(expressionToString(nextVal.value.toQuintEx(idGen))) - } - } else { - const s = expressionToString(res.toQuintEx(idGen)) - const m = `Callable ${callee} was expected to evaluate to true, found: ${s}` - return left(m) - } - }) - }) - } - - return evalInContext(input, callback) + 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 { - // Recall that left(...) is used for errors, - // whereas right(...) is used for non-errors in sweet monads. - const callback = (ctx: CompilationContext): Either => { - return callableFromContext(ctx, callee).chain(run => { - return run - .eval() - .mapLeft(quintErrorToString) - .chain(res => { - return right(expressionToString(res.toQuintEx(idGen))) - }) - }) - } + const [evaluator, runExpr] = prepareEvaluator(callee, input) + const newEval = evaluator.evaluate(runExpr) - return evalInContext(input, callback) + return newEval.mapLeft(quintErrorToString).map(res => expressionToString(res)) } function assertVarAfterCall(varName: string, expected: string, callee: string, input: string) { @@ -380,7 +296,7 @@ describe('compiling specs to runtime values', () => { describe('compile variables', () => { it('variable definitions', () => { const input = 'var x: int' - assertVarExists('var', 'x', input) + assertVarExists('x', input) }) }) @@ -1133,155 +1049,3 @@ describe('compiling specs to runtime values', () => { }) }) }) - -describe('incremental compilation', () => { - const dummyRng: Rng = { - getState: () => 0n, - setState: (_: bigint) => {}, - next: () => 0n, - } - /* Adds some quint code to the compilation and evaluation state */ - function compileModules(text: string, mainName: string): CompilationContext { - const idGen = newIdGenerator() - const sourceCode: Map = new Map() - const fake_path: SourceLookupPath = { normalizedPath: 'fake_path', toSourceName: () => 'fake_path' } - const { modules, table, sourceMap, errors } = parse(idGen, 'fake_path', fake_path, text, sourceCode) - assert.isEmpty(errors) - - const [analysisErrors, analysisOutput] = analyzeModules(table, modules) - assert.isEmpty(analysisErrors) - - const { flattenedModules, flattenedAnalysis, flattenedTable } = flattenModules( - modules, - table, - idGen, - sourceMap, - analysisOutput - ) - - const state: CompilationState = { - originalModules: modules, - idGen, - modules: flattenedModules, - mainName, - sourceMap, - analysisOutput: flattenedAnalysis, - sourceCode, - } - - const moduleToCompile = flattenedModules[flattenedModules.length - 1] - - return compile( - state, - newEvaluationState(noExecutionListener), - flattenedTable, - dummyRng.next, - false, - moduleToCompile.declarations - ) - } - - describe('compileExpr', () => { - it('should compile a Quint expression', () => { - const { compilationState, evaluationState } = compileModules('module m { pure val x = 1 }', 'm') - - const parsed = parseExpressionOrDeclaration( - 'x + 2', - 'test.qnt', - compilationState.idGen, - compilationState.sourceMap - ) - const expr = parsed.kind === 'expr' ? parsed.expr : undefined - const context = compileExpr(compilationState, evaluationState, dummyRng, false, expr!) - - assert.deepEqual(context.compilationState.analysisOutput.types.get(expr!.id)?.type, { kind: 'int', id: 3n }) - - assertInputFromContext(context, '3') - }) - }) - - describe('compileDef', () => { - it('should compile a Quint definition', () => { - const { compilationState, evaluationState } = compileModules('module m { pure val x = 1 }', 'm') - - const parsed = parseExpressionOrDeclaration( - 'val y = x + 2', - 'test.qnt', - compilationState.idGen, - compilationState.sourceMap - ) - const defs = parsed.kind === 'declaration' ? parsed.decls : undefined - const context = compileDecls(compilationState, evaluationState, dummyRng, false, defs!) - - assert.deepEqual(context.compilationState.analysisOutput.types.get(defs![0].id)?.type, { kind: 'int', id: 3n }) - - const computable = context.evaluationState?.context.get(kindName('callable', defs![0].id))! - assertComputableAsString(computable, '3') - }) - - it('non-exported imports are not visible in subsequent importing modules', () => { - const { compilationState, evaluationState } = compileModules( - 'module m1 { pure val x1 = 1 }' + 'module m2 { import m1.* pure val x2 = x1 }' + 'module m3 { import m2.* }', // m1 shouldn't be acessible inside m3 - 'm3' - ) - - const parsed = parseExpressionOrDeclaration( - 'def x3 = x1', - 'test.qnt', - compilationState.idGen, - compilationState.sourceMap - ) - const decls = parsed.kind === 'declaration' ? parsed.decls : [] - const context = compileDecls(compilationState, evaluationState, dummyRng, false, decls) - - assert.sameDeepMembers(context.syntaxErrors, [ - { - code: 'QNT404', - message: "Name 'x1' not found", - reference: 10n, - data: {}, - }, - ]) - }) - - it('can complile type alias declarations', () => { - const { compilationState, evaluationState } = compileModules('module m {}', 'm') - const parsed = parseExpressionOrDeclaration( - 'type T = int', - 'test.qnt', - compilationState.idGen, - compilationState.sourceMap - ) - const decls = parsed.kind === 'declaration' ? parsed.decls : [] - const context = compileDecls(compilationState, evaluationState, dummyRng, false, decls) - - const typeDecl = decls[0] - assert(typeDecl.kind === 'typedef') - assert(typeDecl.name === 'T') - assert(typeDecl.type!.kind === 'int') - - assert.sameDeepMembers(context.syntaxErrors, []) - }) - - it('can compile sum type declarations', () => { - const { compilationState, evaluationState } = compileModules('module m {}', 'm') - const parsed = parseExpressionOrDeclaration( - 'type T = A(int) | B(str) | C', - 'test.qnt', - compilationState.idGen, - compilationState.sourceMap - ) - const decls = parsed.kind === 'declaration' ? parsed.decls : [] - const context = compileDecls(compilationState, evaluationState, dummyRng, false, decls) - - assert(decls.find(t => t.kind === 'typedef' && t.name === 'T')) - // Sum type declarations are expanded to add an - // operator declaration for each constructor: - assert(decls.find(t => t.kind === 'def' && t.name === 'A')) - assert(decls.find(t => t.kind === 'def' && t.name === 'B')) - assert(decls.find(t => t.kind === 'def' && t.name === 'C')) - - assert.sameDeepMembers(context.syntaxErrors, []) - }) - }) -}) From 0b36c5c44ed01ce851cb859767e1107af79d6f60 Mon Sep 17 00:00:00 2001 From: bugarela Date: Wed, 28 Aug 2024 08:08:30 -0300 Subject: [PATCH 21/37] Ensure REPL still works after name errors --- quint/io-cli-tests.md | 19 +++++++++++++++++++ quint/src/repl.ts | 1 + 2 files changed, 20 insertions(+) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 25caca10e..21c4cd56b 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -1221,6 +1221,25 @@ echo 'q::debug("value:", { foo: 42, bar: "Hello, World!" })' | quint | tail -n + >>> ``` +### REPL continues to work after static analysis errors + + + +``` +echo -e 'inexisting_name\n1 + 1' | quint +``` + + +``` +Quint REPL 0.21.1 +Type ".exit" to exit, or ".help" for more information +>>> static analysis error: error: [QNT404] Name 'inexisting_name' not found +inexisting_name +^^^^^^^^^^^^^^^ + +>>> 2 +>>> +``` ### Errors are reported in the right file diff --git a/quint/src/repl.ts b/quint/src/repl.ts index c26bebc95..e7cfd07d4 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -599,6 +599,7 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { walkExpression(state.nameResolver, parseResult.expr) if (state.nameResolver.errors.length > 0) { printErrorMessages(out, state, 'static analysis error', newInput, state.nameResolver.errors) + state.nameResolver.errors = [] return false } state.evaluator.updateTable(state.nameResolver.table) From 3d618277b79cde0156d97c020b091e0c3c143245 Mon Sep 17 00:00:00 2001 From: bugarela Date: Wed, 28 Aug 2024 18:04:24 -0300 Subject: [PATCH 22/37] Improve error handling in REPL and add tests for that --- quint/io-cli-tests.md | 68 +++++++++++++++++++++++++++++++++++++++++-- quint/src/repl.ts | 50 +++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 21c4cd56b..1dc5474f4 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -1221,15 +1221,15 @@ echo 'q::debug("value:", { foo: 42, bar: "Hello, World!" })' | quint | tail -n + >>> ``` -### REPL continues to work after static analysis errors +### REPL continues to work after missing name errors - + ``` echo -e 'inexisting_name\n1 + 1' | quint ``` - + ``` Quint REPL 0.21.1 Type ".exit" to exit, or ".help" for more information @@ -1241,6 +1241,68 @@ inexisting_name >>> ``` +### REPL continues to work after conflicting definitions + +Regression for https://github.com/informalsystems/quint/issues/434 + + +``` +echo -e 'def farenheit(celsius) = celsius * 9 / 5 + 32\ndef farenheit(celsius) = celsius * 9 / 5 + 32\nfarenheit(1)' | quint +``` + + +``` +Quint REPL 0.21.1 +Type ".exit" to exit, or ".help" for more information +>>> +>>> static analysis error: error: [QNT101] Conflicting definitions found for name 'farenheit' in module '__repl__' +def farenheit(celsius) = celsius * 9 / 5 + 32 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +static analysis error: error: [QNT101] Conflicting definitions found for name 'farenheit' in module '__repl__' +def farenheit(celsius) = celsius * 9 / 5 + 32 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +>>> 33 +>>> +``` + +### REPL continues to work after type errors + + +``` +echo -e 'def foo = 1 + "a"\nfoo\n1 + "a"\n1 + 1' | quint +``` + + +``` +Quint REPL 0.21.1 +Type ".exit" to exit, or ".help" for more information +>>> static analysis error: error: [QNT000] Couldn't unify int and str +Trying to unify int and str +Trying to unify (int, int) => int and (int, str) => _t0 + +def foo = 1 + "a" + ^^^^^^^ + +>>> static analysis error: error: [QNT404] Name 'foo' not found +foo +^^^ + +>>> static analysis error: error: [QNT000] Couldn't unify int and str +Trying to unify int and str +Trying to unify (int, int) => int and (int, str) => _t0 + +1 + "a" +^^^^^^^ + +>>> 2 +>>> +``` + + + ### Errors are reported in the right file File `ImportFileWithError.qnt` has no error, but it imports a module from file `FileWithError.qnt`, which has a type error. The error should be reported only in `FileWithError.qnt`. diff --git a/quint/src/repl.ts b/quint/src/repl.ts index e7cfd07d4..4340e9b17 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -16,7 +16,7 @@ import { left } from '@sweet-monads/either' import chalk from 'chalk' import { format } from './prettierimp' -import { FlatModule, QuintDef, QuintModule } from './ir/quintIr' +import { FlatModule, QuintDef, QuintModule, isDef } from './ir/quintIr' import { createFinders, formatError } from './errorReporter' import { Register } from './runtime/runtime' import { TraceRecorder, newTraceRecorder } from './runtime/trace' @@ -34,7 +34,7 @@ import { QuintError } from './quintError' import { ErrorMessage } from './ErrorMessage' import { Evaluator } from './runtime/impl/evaluator' import { walkDeclaration, walkExpression } from './ir/IRVisitor' -import { AnalysisOutput, analyzeModules } from './quintAnalyzer' +import { AnalysisOutput, analyzeInc, analyzeModules } from './quintAnalyzer' import { NameResolver } from './names/resolver' // tunable settings @@ -604,6 +604,25 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { } state.evaluator.updateTable(state.nameResolver.table) + const [analysisErrors, _analysisOutput] = analyzeInc( + state.compilationState.analysisOutput, + state.nameResolver.table, + [ + { + kind: 'def', + qualifier: 'action', + name: 'q::input', + expr: parseResult.expr, + id: state.compilationState.idGen.nextId(), + }, + ] + ) + + if (analysisErrors.length > 0) { + printErrorMessages(out, state, 'static analysis error', newInput, analysisErrors) + return false + } + const newEval = state.evaluator.evaluate(parseResult.expr) if (newEval.isLeft()) { printErrorMessages(out, state, 'runtime error', newInput, [newEval.value]) @@ -635,8 +654,35 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { if (state.nameResolver.errors.length > 0) { printErrorMessages(out, state, 'static analysis error', newInput, state.nameResolver.errors) out('\n') + parseResult.decls.forEach(decl => { + if (isDef(decl)) { + state.nameResolver.collector.deleteDefinition(decl.name) + } + }) + + state.nameResolver.errors = [] return false } + + const [analysisErrors, analysisOutput] = analyzeInc( + state.compilationState.analysisOutput, + state.nameResolver.table, + parseResult.decls + ) + + if (analysisErrors.length > 0) { + printErrorMessages(out, state, 'static analysis error', newInput, analysisErrors) + parseResult.decls.forEach(decl => { + if (isDef(decl)) { + state.nameResolver.collector.deleteDefinition(decl.name) + } + }) + + return false + } + + state.compilationState.analysisOutput = analysisOutput + out('\n') } From ba55e3860ee68e79fbaac5ebdad2ce03d248d072 Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 29 Aug 2024 12:16:15 -0300 Subject: [PATCH 23/37] Reorganize and delete unused code --- quint/src/repl.ts | 59 +------- quint/src/runtime/impl/base.ts | 70 --------- .../runtime/impl/{compiler.ts => builder.ts} | 0 quint/src/runtime/impl/builtins.ts | 8 +- quint/src/runtime/impl/evaluator.ts | 2 +- quint/src/runtime/impl/runtimeValue.ts | 28 +++- quint/src/runtime/runtime.ts | 140 ------------------ quint/test/runtime/compile.test.ts | 2 +- 8 files changed, 34 insertions(+), 275 deletions(-) delete mode 100644 quint/src/runtime/impl/base.ts rename quint/src/runtime/impl/{compiler.ts => builder.ts} (100%) delete mode 100644 quint/src/runtime/runtime.ts diff --git a/quint/src/repl.ts b/quint/src/repl.ts index 4340e9b17..08f858732 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -11,16 +11,14 @@ import * as readline from 'readline' import { Readable, Writable } from 'stream' import { readFileSync, writeFileSync } from 'fs' -import { Maybe, just, none } from '@sweet-monads/maybe' -import { left } from '@sweet-monads/either' +import { none } from '@sweet-monads/maybe' import chalk from 'chalk' import { format } from './prettierimp' -import { FlatModule, QuintDef, QuintModule, isDef } from './ir/quintIr' +import { FlatModule, QuintModule, isDef } from './ir/quintIr' import { createFinders, formatError } from './errorReporter' -import { Register } from './runtime/runtime' import { TraceRecorder, newTraceRecorder } from './runtime/trace' -import { SourceMap, parse, parseDefOrThrow, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' +import { SourceMap, parse, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' import { prettyQuintEx, terminalWidth } from './graphics' import { verbosity } from './verbosity' import { Rng, newRng } from './rng' @@ -115,7 +113,7 @@ class ReplState { } addReplModule() { - const replModule: FlatModule = { name: '__repl__', declarations: simulatorBuiltins(this.compilationState), id: 0n } + const replModule: FlatModule = { name: '__repl__', declarations: [], id: 0n } this.compilationState.modules.push(replModule) this.compilationState.mainName = '__repl__' this.moduleHist += moduleToString(replModule) @@ -479,55 +477,6 @@ function loadFromFile(out: writer, state: ReplState, filename: string): ReplStat } } -/** - * Moves the nextvars register values into the vars, and clears the nextvars. - * Returns an array of variable names that were not updated. - * @param vars An array of Register objects representing the current state of the variables. - * @param nextvars An array of Register objects representing the next state of the variables. - * @returns An array of variable names that were not updated, or none if all variables were updated. - */ -function saveVars(vars: Register[], nextvars: Register[]): Maybe { - let isAction = false - - const nonUpdated = vars.reduce((acc, varRegister) => { - const nextVarRegister = nextvars.find(v => v.name === varRegister.name) - if (nextVarRegister && nextVarRegister.registerValue.isRight()) { - varRegister.registerValue = nextVarRegister.registerValue - nextVarRegister.registerValue = left({ code: 'QNT501', message: 'var ${nextVarRegiter.name} not set' }) - isAction = true - } else { - // No nextvar for this variable, so it was not updated - acc.push(varRegister.name) - } - - return acc - }, [] as string[]) - - if (isAction) { - // return the names of the variables that have not been updated - return just(nonUpdated) - } else { - return none() - } -} - -// Declarations that are overloaded by the simulator. -// In the future, we will declare them in a separate module. -function simulatorBuiltins(st: CompilationState): QuintDef[] { - return [ - parseDefOrThrow( - `def q::test = (q::nruns, q::nsteps, q::ntraces, q::init, q::next, q::inv) => false`, - st.idGen, - st.sourceMap - ), - parseDefOrThrow( - `def q::testOnce = (q::nsteps, q::ntraces, q::init, q::next, q::inv) => false`, - st.idGen, - st.sourceMap - ), - ] -} - function tryEvalModule(out: writer, state: ReplState, mainName: string): boolean { const modulesText = state.moduleHist const mainPath = fileSourceResolver(state.compilationState.sourceCode).lookupPath(cwd(), 'repl.ts') diff --git a/quint/src/runtime/impl/base.ts b/quint/src/runtime/impl/base.ts deleted file mode 100644 index e3f7db18c..000000000 --- a/quint/src/runtime/impl/base.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* ---------------------------------------------------------------------------------- - * Copyright 2022-2024 Informal Systems - * Licensed under the Apache License, Version 2.0. - * See LICENSE in the project root for license information. - * --------------------------------------------------------------------------------- */ - -/** - * Base types and functions for the runtime implementation. - * - * @author Igor Konnov, Gabriela Moreira - * - * @module - */ - -import { Maybe, just, none } from '@sweet-monads/maybe' -import { ErrorCode, QuintError } from '../../quintError' -import { EvaluationResult } from '../runtime' -import { Either, right } from '@sweet-monads/either' -import { RuntimeValue } from './runtimeValue' - -// Internal names in the compiler, which have special treatment. -// For some reason, if we replace 'q::input' with inputDefName, everything breaks. -// What kind of JS magic is that? -export const specialNames = ['q::input', 'q::runResult', 'q::nruns', 'q::nsteps', 'q::init', 'q::next', 'q::inv'] - -/** - * Creates a new EvaluationState object. - * - * @returns a new EvaluationState object. - */ -export class CompilerErrorTracker { - // messages that are produced during compilation - compileErrors: QuintError[] = [] - // messages that get populated as the compiled code is executed - runtimeErrors: QuintError[] = [] - - addCompileError(reference: bigint, code: ErrorCode, message: string) { - this.compileErrors.push({ code, message, reference }) - } - - addRuntimeError(reference: bigint | undefined, error: QuintError) { - this.runtimeErrors.push({ ...error, reference: error.reference ?? reference }) - } -} - -export function toMaybe(r: Either): Maybe { - if (r.isRight()) { - return just(r.value) - } else { - return none() - } -} - -// make a `Computable` that always returns a given runtime value -export function mkConstComputable(value: RuntimeValue) { - return { - eval: () => { - return right(value) - }, - } -} - -// make a `Computable` that always returns a given runtime value -export function mkFunComputable(fun: () => EvaluationResult) { - return { - eval: () => { - return fun() - }, - } -} diff --git a/quint/src/runtime/impl/compiler.ts b/quint/src/runtime/impl/builder.ts similarity index 100% rename from quint/src/runtime/impl/compiler.ts rename to quint/src/runtime/impl/builder.ts diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index 2282bb2b8..b27f90565 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -10,7 +10,7 @@ import { zerog } from '../../idGenerator' import { QuintApp } from '../../ir/quintIr' import { prettyQuintEx, terminalWidth } from '../../graphics' import { format } from '../../prettierimp' -import { EvalFunction } from './compiler' +import { EvalFunction } from './builder' export function builtinValue(name: string): Either { switch (name) { @@ -76,7 +76,7 @@ export function lazyBuiltinLambda( }) } - case 'actionAny': + case 'actionAny': { const app: QuintApp = { id: 0n, kind: 'app', opcode: 'actionAny', args: [] } return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.snapshot() @@ -115,14 +115,16 @@ export function lazyBuiltinLambda( ctx.recorder.onAnyReturn(args.length, potentialSuccessors[0].index) ctx.varStorage.recoverSnapshot(potentialSuccessors[0].snapshot) return rv.mkBool(true) - default: + default: { const choice = Number(ctx.rand(BigInt(potentialSuccessors.length))) ctx.recorder.onAnyReturn(args.length, potentialSuccessors[choice].index) ctx.varStorage.recoverSnapshot(potentialSuccessors[choice].snapshot) return rv.mkBool(true) + } } }) } + } case 'actionAll': return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.snapshot() diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index c9b731ff2..90a729ecf 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -10,7 +10,7 @@ import { TestResult } from '../testing' import { Rng } from '../../rng' import { zerog } from '../../idGenerator' import { List } from 'immutable' -import { Builder, buildDef, buildExpr, nameWithNamespaces } from './compiler' +import { Builder, buildDef, buildExpr, nameWithNamespaces } from './builder' export class Evaluator { public ctx: Context diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index f7802516f..4264e1ea2 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -66,13 +66,10 @@ import { strict as assert } from 'assert' import { IdGenerator, zerog } from '../../idGenerator' import { expressionToString } from '../../ir/IRprinting' - -import { EvalResult } from '../runtime' import { QuintEx, QuintLambdaParameter, QuintName } from '../../ir/quintIr' import { QuintError, quintErrorToString } from '../../quintError' import { Either, left, mergeInMany, right } from '@sweet-monads/either' -import { toMaybe } from './base' -import { EvalFunction } from './compiler' +import { EvalFunction } from './builder' import { Context, Register } from './Context' /** @@ -371,7 +368,7 @@ export function fromQuintEx(ex: QuintEx): Maybe { * non-iterable ones. Of course, this may lead to ill-typed operations. Type * correctness of the input must be checked by the type checker. */ -export interface RuntimeValue extends EvalResult, ValueObject, Iterable { +export interface RuntimeValue extends ValueObject, Iterable { /** * Can the runtime value behave like a set? Effectively, this means that the * value returns a sequence of elements, when it is iterated over. @@ -514,6 +511,19 @@ export interface RuntimeValue extends EvalResult, ValueObject, Iterable + + /** + * Convert a runtime value to a Quint expression. + * + * This function always returns sets in the normalized representation, + * that is, in `set(elements)`, the elements are ordered according to their + * string representation. As sorting via strings may be slow, we do not + * recommend using `toQuintEx` in computation-intensive code. + * + * @param gen a generator that produces unique ids + * @return this evaluation result converted to Quint expression. + */ + toQuintEx(gen: IdGenerator): QuintEx } /** @@ -1674,3 +1684,11 @@ export class RuntimeValueLambda extends RuntimeValueBase implements RuntimeValue } } } + +function toMaybe(r: Either): Maybe { + if (r.isRight()) { + return just(r.value) + } else { + return none() + } +} diff --git a/quint/src/runtime/runtime.ts b/quint/src/runtime/runtime.ts deleted file mode 100644 index da3921190..000000000 --- a/quint/src/runtime/runtime.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Runtime environment for Quint. This runtime environment is designed for the - * following purposes: - * - * - Give the language user fast feedback via interpretation of expressions. - * - Let the user simulate their specification similar to property-based testing. - * - * Igor Konnov, 2022-2023 - * - * Copyright 2022-2023 Informal Systems - * Licensed under the Apache License, Version 2.0. - * See LICENSE in the project root for license information. - */ - -import { ValueObject } from 'immutable' - -import { QuintEx } from '../ir/quintIr' - -import { IdGenerator } from '../idGenerator' - -import { Either, left } from '@sweet-monads/either' -import { QuintError } from '../quintError' - -/** - * Evaluation result. - * The implementation details are hidden behind this interface. - */ -export interface EvalResult extends ValueObject { - /** - * Convert an evaluation result to Quint. - * - * This function always returns sets in the normalized representation, - * that is, in `set(elements)`, the elements are ordered according to their - * string representation. As sorting via strings may be slow, we do not - * recommend using `toQuintEx` in computation-intensive code. - * - * @param gen a generator that produces unique ids - * @return this evaluation result converted to Quint expression. - */ - toQuintEx(gen: IdGenerator): QuintEx -} - -export type EvaluationResult = Either - -/** - * An object that can be evaluated by the runtime. Normally, it is constructed - * from a Quint expression, but it does not have to. - */ -export interface Computable { - /** - * The simplest form of evaluation. Just compute the value. - * This method may return none, if a computation error happens during - * evaluation. - * - * @param args optional arguments to the computable - */ - eval: (args?: Either[]) => EvaluationResult -} - -/** - * The kind of a computable. - */ -export type ComputableKind = 'var' | 'nextvar' | 'arg' | 'callable' - -/** - * Create a key that encodes its name and kind. This is only useful for - * storing registers in a map, as JS objects are hard to use as keys. - * In a good language, we would use (kind, name), but not in JS. - */ -export const kindName = (kind: ComputableKind, name: string | bigint): string => { - return `${kind}:${name}` -} - -/** - * A computable that evaluates to the value of a readable/writeable register. - */ -export interface Register extends Computable { - // register name - name: string - // register kind - kind: ComputableKind - // register is a placeholder where iterators can put their values - registerValue: Either -} - -/** - * Create an object that implements Register. - */ -export function mkRegister(kind: ComputableKind, registerName: string, initValue: Either): Register { - const reg: Register = { - name: registerName, - kind, - registerValue: initValue, - // first, define a fruitless eval, as we cannot refer to registerValue yet - eval: () => { - return initValue - }, - } - // override `eval`, as we can use `reg` now - reg.eval = () => { - // computing a register just evaluates to the contents that it stores - return reg.registerValue - } - - return reg -} - -/** - * A callable value like an operator definition. Its body is computable. - * One has to pass the values of the parameters to eval. - */ -export interface Callable extends Computable { - /** - * The number of parameters expected by the callable value. - */ - nparams: number -} - -/** - * An implementation of Computable that always fails. - */ -export const fail = { - eval: (): EvaluationResult => { - return left({ code: 'QNT501', message: 'Failed to evaluate something :/' }) - }, -} - -/** - * An error produced during execution. - */ -export interface ExecError { - msg: string - sourceAndLoc: string | undefined -} - -/** - * An error handler that is called on any kind of error that is happening - * during execution. - */ -export type ExecErrorHandler = (_error: ExecError) => void diff --git a/quint/test/runtime/compile.test.ts b/quint/test/runtime/compile.test.ts index 09e174390..4791c1ef6 100644 --- a/quint/test/runtime/compile.test.ts +++ b/quint/test/runtime/compile.test.ts @@ -9,7 +9,7 @@ import { stringSourceResolver } 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, right } from '@sweet-monads/either' +import { Either, left } from '@sweet-monads/either' // Use a global id generator, limited to this test suite. const idGen = newIdGenerator() From 0923a8376676312db8ce8bd79a435bf0bab06e2c Mon Sep 17 00:00:00 2001 From: bugarela Date: Thu, 29 Aug 2024 15:04:39 -0300 Subject: [PATCH 24/37] Fix and update most tests --- quint/src/effects/builtinSignatures.ts | 13 ++- quint/src/names/base.ts | 2 + quint/src/repl.ts | 27 ++++-- quint/src/runtime/impl/VarStorage.ts | 12 ++- quint/src/runtime/impl/builder.ts | 41 +++++--- quint/src/runtime/impl/builtins.ts | 124 +++++++++++++++++-------- quint/src/runtime/impl/evaluator.ts | 28 ++++-- quint/src/types/builtinSignatures.ts | 2 + quint/test/repl.test.ts | 17 +--- quint/test/runtime/compile.test.ts | 21 +++-- 10 files changed, 192 insertions(+), 95 deletions(-) diff --git a/quint/src/effects/builtinSignatures.ts b/quint/src/effects/builtinSignatures.ts index 310f38e40..1a060ad77 100644 --- a/quint/src/effects/builtinSignatures.ts +++ b/quint/src/effects/builtinSignatures.ts @@ -225,7 +225,18 @@ const otherOperators = [ { name: 'fail', effect: propagateComponents(['read', 'update'])(1) }, { name: 'assert', effect: propagateComponents(['read'])(1) }, { name: 'q::debug', effect: propagateComponents(['read'])(2) }, - { name: 'q::lastTrace', effect: parseAndQuantify('Pure') }, // FIXME: Should be in run mode + // FIXME: The following should produce run mode + { name: 'q::lastTrace', effect: parseAndQuantify('Pure') }, + { + name: 'q::test', + effect: parseAndQuantify( + '(Pure, Pure, Pure, Update[u1], Read[r2] & Update[u2], Read[r3]) => Read[r2, r3] & Update[u2]' + ), + }, + { + name: 'q::testOnce', + effect: parseAndQuantify('(Pure, Pure, Update[u1], Read[r2] & Update[u2], Read[r3]) => Read[r2, r3] & Update[u2]'), + }, { name: 'ite', effect: parseAndQuantify('(Read[r1], Read[r2] & Update[u], Read[r3] & Update[u]) => Read[r1, r2, r3] & Update[u]'), diff --git a/quint/src/names/base.ts b/quint/src/names/base.ts index 2ddce97f5..3d30739b8 100644 --- a/quint/src/names/base.ts +++ b/quint/src/names/base.ts @@ -252,4 +252,6 @@ export const builtinNames = [ 'variant', 'q::debug', 'q::lastTrace', + 'q::test', + 'q::testOnce', ] diff --git a/quint/src/repl.ts b/quint/src/repl.ts index 08f858732..e8df784b7 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -19,7 +19,7 @@ import { FlatModule, QuintModule, isDef } from './ir/quintIr' import { createFinders, formatError } from './errorReporter' import { TraceRecorder, newTraceRecorder } from './runtime/trace' import { SourceMap, parse, parseExpressionOrDeclaration } from './parsing/quintParserFrontend' -import { prettyQuintEx, terminalWidth } from './graphics' +import { prettyQuintEx, printExecutionFrameRec, terminalWidth } from './graphics' import { verbosity } from './verbosity' import { Rng, newRng } from './rng' import { version } from './version' @@ -572,13 +572,9 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { return false } - const newEval = state.evaluator.evaluate(parseResult.expr) - if (newEval.isLeft()) { - printErrorMessages(out, state, 'runtime error', newInput, [newEval.value]) - return false - } + const evalResult = state.evaluator.evaluate(parseResult.expr) - newEval.map(ex => { + evalResult.map(ex => { out(format(columns, 0, prettyQuintEx(ex))) out('\n') @@ -593,6 +589,23 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { return ex }) + if (verbosity.hasUserOpTracking(state.verbosity)) { + const trace = state.recorder.currentFrame + if (trace.subframes.length > 0) { + out('\n') + trace.subframes.forEach((f, i) => { + out(`[Frame ${i}]\n`) + printExecutionFrameRec({ width: columns, out }, f, []) + out('\n') + }) + } + } + + if (evalResult.isLeft()) { + printErrorMessages(out, state, 'runtime error', newInput, [evalResult.value]) + return false + } + return true } if (parseResult.kind === 'declaration') { diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 55a20a1cb..60e38ab31 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -15,7 +15,9 @@ interface Snapshot { } // TODO: Add name to error message -const initialRegisterValue: Either = left({ code: 'QNT502', message: 'Variable not set' }) +export function initialRegisterValue(name: string): Either { + return left({ code: 'QNT502', message: `Variable ${name} not set` }) +} export class VarStorage { public vars: ImmutableMap = ImmutableMap() @@ -33,10 +35,10 @@ export class VarStorage { shiftVars() { this.vars.forEach((reg, key) => { - reg.value = this.nextVars.get(key)?.value ?? initialRegisterValue + reg.value = this.nextVars.get(key)?.value ?? initialRegisterValue(reg.name) }) - this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) + this.nextVars.forEach(reg => (reg.value = initialRegisterValue(reg.name))) this.clearCaches() } @@ -62,8 +64,8 @@ export class VarStorage { } reset() { - this.vars.forEach(reg => (reg.value = initialRegisterValue)) - this.nextVars.forEach(reg => (reg.value = initialRegisterValue)) + this.vars.forEach(reg => (reg.value = initialRegisterValue(reg.name))) + this.nextVars.forEach(reg => (reg.value = initialRegisterValue(reg.name))) } snapshot(): Snapshot { diff --git a/quint/src/runtime/impl/builder.ts b/quint/src/runtime/impl/builder.ts index 15fdd149e..012df1a17 100644 --- a/quint/src/runtime/impl/builder.ts +++ b/quint/src/runtime/impl/builder.ts @@ -5,7 +5,7 @@ import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './built import { CachedValue, Context, Register } from './Context' import { QuintApp, QuintEx } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' -import { NamedRegister, VarStorage } from './VarStorage' +import { NamedRegister, VarStorage, initialRegisterValue } from './VarStorage' import { List } from 'immutable' export type EvalFunction = (ctx: Context) => Either @@ -33,8 +33,8 @@ export class Builder { } const varName = nameWithNamespaces(name, this.namespaces) - const register: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } - const nextRegister: NamedRegister = { name: varName, value: left({ code: 'QNT502', message: 'Variable not set' }) } + const register: NamedRegister = { name: varName, value: initialRegisterValue(varName) } + const nextRegister: NamedRegister = { name: varName, value: initialRegisterValue(varName) } this.varStorage.vars = this.varStorage.vars.set(key, register) this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) } @@ -76,8 +76,14 @@ export function buildExpr(builder: Builder, expr: QuintEx): EvalFunction { return builder.memo.get(expr.id)! } const exprEval = buildExprCore(builder, expr) - const wrappedEval: EvalFunction = ctx => - exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) + const wrappedEval: EvalFunction = ctx => { + try { + return exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) + } catch (error) { + const message = error instanceof Error ? error.message : 'unknown error' + return left({ code: 'QNT501', message: message, reference: expr.id }) + } + } builder.memo.set(expr.id, wrappedEval) return wrappedEval } @@ -122,7 +128,7 @@ export function buildUnderDefContext( function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { switch (def.kind) { - case 'def': + case 'def': { if (def.qualifier === 'action') { const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } const body = buildExpr(builder, def.expr) @@ -174,6 +180,7 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { } return cachedValue.value } + } case 'param': { const register = builder.paramRegistry.get(def.id) if (!register) { @@ -190,9 +197,10 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { return register.value } } - case 'const': + case 'const': { const register = builder.registerForConst(def.id, def.name) return _ => register.value + } default: return _ => left({ code: 'QNT000', message: `Not implemented for def kind ${def.kind}` }) } @@ -246,25 +254,27 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { switch (expr.kind) { case 'int': case 'bool': - case 'str': + case 'str': { // These are already values, just return them const value = right(rv.fromQuintEx(expr)) return _ => value - case 'lambda': + } + case 'lambda': { // Lambda is also like a value, but we should construct it with the context const body = buildExpr(builder, expr.expr) const lambda = rv.mkLambda(expr.params, body, builder.paramRegistry) return _ => right(lambda) - case 'name': + } + case 'name': { const def = builder.table.get(expr.id) if (!def) { // TODO: Do we also need to return builtin ops for higher order usage? // Answer: yes, see #1332 - const lambda = builtinValue(expr.name) - return _ => lambda + return builtinValue(expr.name) } return buildDef(builder, def) - case 'app': + } + case 'app': { if (expr.opcode === 'assign') { const varDef = builder.table.get(expr.args[0].id)! return buildUnderDefContext(builder, varDef, () => { @@ -312,8 +322,8 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { } return result } - - case 'let': + } + case 'let': { let cachedValue = builder.scopedCachedValues.get(expr.opdef.id) if (!cachedValue) { cachedValue = { value: undefined } @@ -325,6 +335,7 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { cachedValue!.value = undefined return result } + } } } diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index b27f90565..662170d06 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -4,7 +4,7 @@ import { List, Map, OrderedMap, Range, Set } from 'immutable' import { isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' -import { chunk } from 'lodash' +import { chunk, times } from 'lodash' import { expressionToString } from '../../ir/IRprinting' import { zerog } from '../../idGenerator' import { QuintApp } from '../../ir/quintIr' @@ -12,16 +12,18 @@ import { prettyQuintEx, terminalWidth } from '../../graphics' import { format } from '../../prettierimp' import { EvalFunction } from './builder' -export function builtinValue(name: string): Either { +export function builtinValue(name: string): EvalFunction { switch (name) { case 'Bool': - return right(rv.mkSet([rv.mkBool(false), rv.mkBool(true)])) + return _ => right(rv.mkSet([rv.mkBool(false), rv.mkBool(true)])) case 'Int': - return right(rv.mkInfSet('Int')) + return _ => right(rv.mkInfSet('Int')) case 'Nat': - return right(rv.mkInfSet('Nat')) + return _ => right(rv.mkInfSet('Nat')) + case 'q::lastTrace': + return ctx => right(rv.mkList(ctx.trace.get())) default: - return left({ code: 'QNT404', message: `Unknown builtin ${name}` }) + return _ => left({ code: 'QNT404', message: `Unknown builtin ${name}` }) } } @@ -130,12 +132,16 @@ export function lazyBuiltinLambda( const nextVarsSnapshot = ctx.varStorage.snapshot() for (const action of args) { const result = action(ctx) - if (!isTrue(result)) { + + if (result.isLeft()) { + return result + } + + if (isFalse(result)) { ctx.varStorage.recoverSnapshot(nextVarsSnapshot) - return result.map(_ => rv.mkBool(false)) + return right(rv.mkBool(false)) } } - return right(rv.mkBool(true)) } case 'ite': @@ -221,6 +227,13 @@ export function lazyBuiltinLambda( return result } + if (isFalse(result)) { + return left({ + code: 'QNT513', + message: `Reps loop could not continue after iteration #${i + 1} evaluated to false`, + }) + } + // Don't shift after the last one if (i < Number(n.toInt()) - 1) { ctx.shift() @@ -534,22 +547,38 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) return (ctx, args) => { const set = args[0].toSet() const lam = args[1].toArrow() - const reducer = ([acc, arg]: RuntimeValue[]) => - lam(ctx, [arg]).map(condition => - condition.toBool() === true ? rv.mkSet(acc.toSet().add(arg.normalForm())) : acc - ) - return applyFold('fwd', set, rv.mkSet([]), reducer) + const result = [] + for (const element of set) { + const value = lam(ctx, [element]) + if (value.isLeft()) { + return value + } + if (value.value.toBool()) { + result.push(element) + } + } + + return right(rv.mkSet(result)) } case 'select': return (ctx, args) => { const list = args[0].toList() const lam = args[1].toArrow() - const reducer = ([acc, arg]: RuntimeValue[]) => - lam(ctx, [arg]).map(condition => (condition.toBool() === true ? rv.mkList(acc.toList().push(arg)) : acc)) - return applyFold('fwd', list, rv.mkList([]), reducer) + const result = [] + for (const element of list) { + const value = lam(ctx, [element]) + if (value.isLeft()) { + return value + } + if (value.value.toBool()) { + result.push(element) + } + } + + return right(rv.mkList(result)) } case 'mapBy': @@ -567,11 +596,6 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } return right(rv.fromMap(Map(results))) - - // const reducer = ([acc, arg]: RuntimeValue[]) => - // lambda(ctx, [arg]).map(value => rv.fromMap(acc.toMap().set(arg.normalForm(), value))) - - // return applyFold('fwd', keys, rv.mkMap([]), reducer) } case 'setToMap': @@ -587,6 +611,26 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'assert': return (_, args) => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT508', message: `Assertion failed` })) + + case 'allListsUpTo': + return (_, args) => { + const set = args[0].toSet() + let lists: Set = Set([[]]) + let last_lists: Set = Set([[]]) + times(Number(args[1].toInt())).forEach(_length => { + // Generate all lists of length `length` from the set + const new_lists: Set = set.toSet().flatMap(value => { + // for each value in the set, append it to all lists of length `length - 1` + return last_lists.map(list => list.concat(value)) + }) + + lists = lists.merge(new_lists) + last_lists = new_lists + }) + + return right(rv.mkSet(lists.map(list => rv.mkList(list)).toOrderedSet())) + } + case 'q::debug': return (_, args) => { let columns = terminalWidth() @@ -594,12 +638,27 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) console.log('>', args[0].toStr(), valuePretty.toString()) return right(args[1]) } - // TODO: add missing operators + + // standard unary operators that are not handled by REPL + case 'allLists': + case 'chooseSome': + case 'always': + case 'eventually': + case 'enabled': + return _ => left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${op}'` }) + + // builtin operators that are not handled by REPL + case 'orKeep': + case 'mustChange': + case 'weakFair': + case 'strongFair': + return _ => left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${op}'` }) default: return () => left({ code: 'QNT000', message: `Unknown builtin ${op}` }) } } + export function applyLambdaToSet( ctx: Context, lambda: RuntimeValue, @@ -608,6 +667,8 @@ export function applyLambdaToSet( const f = lambda.toArrow() const elements = set.toSet() const results = [] + + // Apply using a for so we exit early if we get a left for (const element of elements) { const result = f(ctx, [element]) if (result.isLeft()) { @@ -615,23 +676,8 @@ export function applyLambdaToSet( } results.push(result.value) } - return right(Set(results)) - // map(value => f(ctx, [value])) - // const err = results.find(result => result.isLeft()) - // if (err !== undefined && err.isLeft()) { - // return left(err.value) - // } - - // return right( - // results.map(result => { - // if (result.isLeft()) { - // throw new Error(`Impossible, result is left: ${quintErrorToString(result.value)}`) - // } - - // return result.value - // }) - // ) + return right(Set(results)) } function applyFold( diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 90a729ecf..270b220a3 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -1,4 +1,4 @@ -import { Either, left, right } from '@sweet-monads/either' +import { Either, left, mergeInOne, right } from '@sweet-monads/either' import { QuintApp, QuintEx } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { QuintError } from '../../quintError' @@ -53,16 +53,25 @@ export class Evaluator { } evaluate(expr: QuintEx): Either { - if (expr.kind === 'app') { - this.recorder.onUserOperatorCall(expr) + if (expr.kind === 'app' && (expr.opcode == 'q::test' || expr.opcode === 'q::testOnce')) { + return this.evaluateTest(expr) } + const value = buildExpr(this.builder, expr)(this.ctx) - if (expr.kind === 'app') { - this.recorder.onUserOperatorReturn(expr, [], value) - } + return value.map(rv.toQuintEx) } + evaluateTest(expr: QuintApp): Either { + if (expr.opcode === 'q::testOnce') { + const [nsteps, ntraces, init, step, inv] = expr.args + return this.simulate(init, step, inv, 1, toNumber(nsteps), toNumber(ntraces)) + } else { + const [nruns, nsteps, ntraces, init, step, inv] = expr.args + return this.simulate(init, step, inv, toNumber(nruns), toNumber(nsteps), toNumber(ntraces)) + } + } + reset() { this.trace.reset() this.builder.varStorage.reset() @@ -281,3 +290,10 @@ export function isTrue(value: Either): boolean { export function isFalse(value: Either): boolean { return value.isRight() && value.value.toBool() === false } + +function toNumber(value: QuintEx): number { + if (value.kind !== 'int') { + throw new Error('Expected an integer') + } + return Number(value.value) +} diff --git a/quint/src/types/builtinSignatures.ts b/quint/src/types/builtinSignatures.ts index dcd18b477..1f89f68b5 100644 --- a/quint/src/types/builtinSignatures.ts +++ b/quint/src/types/builtinSignatures.ts @@ -129,6 +129,8 @@ const otherOperators = [ { name: 'assert', type: '(bool) => bool' }, { name: 'q::debug', type: '(str, a) => a' }, { name: 'q::lastTrace', type: 'List[a]' }, + { name: 'q::test', type: '(int, int, int, bool, bool, bool) => bool' }, + { name: 'q::testOnce', type: '(int, int, bool, bool, bool) => bool' }, ] function uniformArgsWithResult(argsType: string, resultType: string): Signature { diff --git a/quint/test/repl.test.ts b/quint/test/repl.test.ts index 764fe03e0..feeacadcb 100644 --- a/quint/test/repl.test.ts +++ b/quint/test/repl.test.ts @@ -147,12 +147,6 @@ describe('repl ok', () => { |1 + false |^^^^^^^^^ | - | - |runtime error: error: [QNT501] Expected an integer value - |1 + false - |^^^^^^^^^ - | - | |>>> ` ) await assertRepl(input, output) @@ -215,15 +209,14 @@ describe('repl ok', () => { |>>> .clear | |>>> n * n - |syntax error: error: [QNT404] Name 'n' not found + |static analysis error: error: [QNT404] Name 'n' not found |n * n |^ | - |syntax error: error: [QNT404] Name 'n' not found + |static analysis error: error: [QNT404] Name 'n' not found |n * n | ^ | - | |>>> ` ) await assertRepl(input, output) @@ -273,7 +266,6 @@ describe('repl ok', () => { |div(2, 0) | ^^^^^ | - | |>>> ` ) await assertRepl(input, output) @@ -300,10 +292,6 @@ describe('repl ok', () => { |.verbosity=4 |>>> x' = 0 |true - | - |[Frame 0] - |_ => true - | |>>> action step = x' = x + 1 | |>>> action input1 = step @@ -393,7 +381,6 @@ describe('repl ok', () => { |Set(Int) |^^^^^^^^ | - | |>>> ` ) await assertRepl(input, output) diff --git a/quint/test/runtime/compile.test.ts b/quint/test/runtime/compile.test.ts index 4791c1ef6..e18f1fe44 100644 --- a/quint/test/runtime/compile.test.ts +++ b/quint/test/runtime/compile.test.ts @@ -47,10 +47,16 @@ function prepareEvaluator(input: string, evalContext: string): [Evaluator, Quint } // Compile a variable definition and check that the compiled value is defined. -function assertVarExists(name: string, input: string) { - const [evaluator, _] = prepareEvaluator(input, '') - - assert.includeDeepMembers([...evaluator.ctx.varStorage.vars.keys()], [name], `Input: ${input}`) +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. @@ -295,8 +301,9 @@ describe('compiling specs to runtime values', () => { describe('compile variables', () => { it('variable definitions', () => { - const input = 'var x: int' - assertVarExists('x', input) + const context = 'var x: int' + const input = "x' = 1" + assertVarExists('x', context, input) }) }) @@ -954,7 +961,7 @@ describe('compiling specs to runtime values', () => { 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'")) + .mapLeft(m => assert.equal(m, '[QNT513] Reps loop could not continue after iteration #6 evaluated to false')) }) it('fail', () => { From 017c5d8cbee6bd12ef938f81a29bb10121a69380 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 12:39:30 -0300 Subject: [PATCH 25/37] Document new evaluator's code --- quint/src/runtime/impl/Context.ts | 51 +++++ quint/src/runtime/impl/VarStorage.ts | 85 +++++++- quint/src/runtime/impl/builder.ts | 291 ++++++++++++++++++++----- quint/src/runtime/impl/builtins.ts | 288 +++++++++++++++++++----- quint/src/runtime/impl/evaluator.ts | 113 ++++++++-- quint/src/runtime/impl/runtimeValue.ts | 15 +- 6 files changed, 726 insertions(+), 117 deletions(-) diff --git a/quint/src/runtime/impl/Context.ts b/quint/src/runtime/impl/Context.ts index 0d35e671f..9066eb19a 100644 --- a/quint/src/runtime/impl/Context.ts +++ b/quint/src/runtime/impl/Context.ts @@ -1,3 +1,17 @@ +/* ---------------------------------------------------------------------------------- + * Copyright 2022-2024 Informal Systems + * Licensed under the Apache License, Version 2.0. + * See LICENSE in the project root for license information. + * --------------------------------------------------------------------------------- */ + +/** + * The evaluation context for the Quint evaluator. + * + * @author Gabriela Moreira + * + * @module + */ + import { Either } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue } from './runtimeValue' @@ -5,26 +19,63 @@ import { TraceRecorder } from '../trace' import { VarStorage } from './VarStorage' import { Trace } from './trace' +/** + * A pointer to a value, so we can use the same reference in multiple places, and just update the value. + */ export interface Register { + /** + * The value stored in the register, which can either be a runtime value or an error. + */ value: Either } +/** + * A pointer to an optional value, so we can use the same reference in multiple places, and just update the value. + */ export interface CachedValue { + /** + * The cached value, which can either be a runtime value, an error, or undefined if not cached yet. + */ value: Either | undefined } export class Context { + /** + * Function to generate a random bigint up to a specified maximum value. + */ public rand: (n: bigint) => bigint + + /** + * The recorder for tracing the evaluation process. + */ public recorder: TraceRecorder + + /** + * The trace object for recording variable values at each state in an execution. + */ public trace: Trace = new Trace() + + /** + * Storage for variables at current and next state. + */ public varStorage: VarStorage + /** + * Constructs a new evaluation context. + * + * @param recorder - The trace recorder to use. + * @param rand - Function to generate random bigints. + * @param varStorage - The variable storage to use (should be the same as the builder's) + */ constructor(recorder: TraceRecorder, rand: (n: bigint) => bigint, varStorage: VarStorage) { this.recorder = recorder this.rand = rand this.varStorage = varStorage } + /** + * Shifts the variables in storage and extends the trace with the current variable state. + */ shift() { this.varStorage.shiftVars() this.trace.extend(this.varStorage.asRecord()) diff --git a/quint/src/runtime/impl/VarStorage.ts b/quint/src/runtime/impl/VarStorage.ts index 60e38ab31..d3b16c71d 100644 --- a/quint/src/runtime/impl/VarStorage.ts +++ b/quint/src/runtime/impl/VarStorage.ts @@ -1,38 +1,102 @@ +/* ---------------------------------------------------------------------------------- + * Copyright 2022-2024 Informal Systems + * Licensed under the Apache License, Version 2.0. + * See LICENSE in the project root for license information. + * --------------------------------------------------------------------------------- */ + +/** + * A storage to keep track of Quint state variables in the current and next state. + * + * @author Gabriela Moreira + * + * @module + */ + import { Either, left } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' import { Map as ImmutableMap } from 'immutable' import { CachedValue, Register } from './Context' +/** + * A named pointer to a value, so we can use the same reference in multiple places, and just update the value. + * The name is used for error messages. + */ export interface NamedRegister extends Register { name: string } +/** + * A snapshot of the VarStorage at a given point in time. Stores only information that is needed to backtrack. + */ interface Snapshot { nextVars: ImmutableMap nondetPicks: Map actionTaken: string | undefined } -// TODO: Add name to error message +/** + * Initializes the register value for a given variable name. + * + * @param name - The name of the variable to initialize, to be used in error messages + * @returns a QuintError indicating the variable is not set + */ export function initialRegisterValue(name: string): Either { return left({ code: 'QNT502', message: `Variable ${name} not set` }) } +/** + * A storage to keep track of Quint state variables in the current and next state. + */ export class VarStorage { + /** + * An immutable map with registers for the current state variables. + */ public vars: ImmutableMap = ImmutableMap() + + /** + * An immutable map with registers for the next state variables. + */ public nextVars: ImmutableMap = ImmutableMap() + + /** + * Non-deterministic picks and their values for the current step. + */ public nondetPicks: Map = new Map() + + /** + * The action taken in the current step. + */ public actionTaken: string | undefined + + /** + * Cached values that need to be cleared when shifting. + */ public cachesToClear: CachedValue[] = [] + /** + * Indicates whether to store metadata. + */ private storeMetadata: boolean + /** + * Constructs a new VarStorage instance. + * + * @param storeMetadata - Indicates whether to store metadata. + * @param nondetPicks - Non-deterministic picks and their values for the current step. Should be + the one constructed in the builder. + */ constructor(storeMetadata: boolean, nondetPicks: Map) { this.storeMetadata = storeMetadata this.nondetPicks = nondetPicks } + /** + * Shifts the current state variables to the next state variables. + * This method updates the current state variable registers with the values + * from the next state variable registers, initializes the next state variable + * registers, and clears cached values. + */ shiftVars() { this.vars.forEach((reg, key) => { reg.value = this.nextVars.get(key)?.value ?? initialRegisterValue(reg.name) @@ -42,6 +106,11 @@ export class VarStorage { this.clearCaches() } + /** + * Converts the current state variables and metadata into a RuntimeValue record. + * + * @returns A RuntimeValue representing the current state variables and metadata. + */ asRecord(): RuntimeValue { const map: [string, RuntimeValue][] = this.vars .valueSeq() @@ -63,11 +132,20 @@ export class VarStorage { return rv.mkRecord(map) } + /** + * Resets the current and next state variable registers to their initial values. + * This method sets the value of each register in both the current and next state + * variable maps to an undefined (error) value. + */ reset() { this.vars.forEach(reg => (reg.value = initialRegisterValue(reg.name))) this.nextVars.forEach(reg => (reg.value = initialRegisterValue(reg.name))) } + /** + * Creates a snapshot of the current state of the VarStorage, with the relevant information to backtrack. + * @returns A snapshot of the current state of the VarStorage. + */ snapshot(): Snapshot { return { nextVars: this.nextVars.map(reg => ({ ...reg })), @@ -76,6 +154,11 @@ export class VarStorage { } } + /** + * Recovers the state of the VarStorage from a snapshot. + * + * @param snapshot - the snapshot to recover the state from + */ recoverSnapshot(snapshot: Snapshot) { this.nextVars.forEach((reg, key) => { const snapshotReg = snapshot.nextVars.get(key) diff --git a/quint/src/runtime/impl/builder.ts b/quint/src/runtime/impl/builder.ts index 012df1a17..44fae3a0f 100644 --- a/quint/src/runtime/impl/builder.ts +++ b/quint/src/runtime/impl/builder.ts @@ -1,3 +1,23 @@ +/* ---------------------------------------------------------------------------------- + * Copyright 2022-2024 Informal Systems + * Licensed under the Apache License, Version 2.0. + * See LICENSE in the project root for license information. + * --------------------------------------------------------------------------------- */ + +/** + * A builder to build arrow functions used to evaluate Quint expressions. + * + * Caching and var storage are heavily based on the original `compilerImpl.ts` file written by Igor Konnov. + * The performance of evaluation relies on a lot of memoization done mainly with closures in this file. + * We define registers and cached value data structures that work as pointers, to avoid the most lookups and + * memory usage as possible. This adds a lot of complexity to the code, but it is necessary to achieve feasible + * performance, as the functions built here will be called thousands of times by the simulator. + * + * @author Igor Konnov, Gabriela Moreira + * + * @module + */ + import { Either, left, right } from '@sweet-monads/either' import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' @@ -8,8 +28,18 @@ import { LookupDefinition, LookupTable } from '../../names/base' import { NamedRegister, VarStorage, initialRegisterValue } from './VarStorage' import { List } from 'immutable' +/** + * The type returned by the builder in its methods, which can be called to get the + * evaluation result under a given context. + */ export type EvalFunction = (ctx: Context) => Either +/** + * A builder to build arrow functions used to evaluate Quint expressions. + * It can be understood as a Quint compiler that compiles Quint expressions into + * typescript arrow functions. It is called a builder instead of compiler because + * the compiler term is overloaded. + */ export class Builder { table: LookupTable paramRegistry: Map = new Map() @@ -18,15 +48,28 @@ export class Builder { initialNondetPicks: Map = new Map() memo: Map = new Map() memoByInstance: Map> = new Map() - public namespaces: List = List() - public varStorage: VarStorage - + namespaces: List = List() + varStorage: VarStorage + + /** + * Constructs a new Builder instance. + * + * @param table - The lookup table containing definitions. + * @param storeMetadata - A flag indicating whether to store metadata (`actionTaken` and `nondetPicks`). + */ constructor(table: LookupTable, storeMetadata: boolean) { this.table = table this.varStorage = new VarStorage(storeMetadata, this.initialNondetPicks) } + /** + * Adds a variable to the var storage if it is not there yet. + * + * @param id + * @param name + */ discoverVar(id: bigint, name: string) { + // Keep the key as simple as possible const key = [id, ...this.namespaces].join('#') if (this.varStorage.vars.has(key)) { return @@ -39,6 +82,13 @@ export class Builder { this.varStorage.nextVars = this.varStorage.nextVars.set(key, nextRegister) } + /** + * Gets the register for a variable by its id and the namespaces in scope (tracked by this builder). + * + * @param id + * + * @returns the register for the variable + */ getVar(id: bigint): NamedRegister { const key = [id, ...this.namespaces].join('#') const result = this.varStorage.vars.get(key) @@ -49,6 +99,13 @@ export class Builder { return result } + /** + * Gets the register for the next state of a variable by its id and the namespaces in scope (tracked by this builder). + * + * @param id - The identifier of the variable. + * + * @returns the register for the next state of the variable + */ getNextVar(id: bigint): NamedRegister { const key = [id, ...this.namespaces].join('#') const result = this.varStorage.nextVars.get(key) @@ -59,6 +116,14 @@ export class Builder { return result } + /** + * Gets the register for a constant by its id and the instances in scope (tracked by this builder). + * + * @param id - The identifier of the constant. + * @param name - The constant name to be used in error messages. + * + * @returns the register for the constant + */ registerForConst(id: bigint, name: string): Register { let register = this.constRegistry.get(id) if (!register) { @@ -71,6 +136,21 @@ export class Builder { } } +/* Bulding functionality is given by functions that take a builder instead of Builder methods. + * This should help separating responsability and splitting this into multiple files if ever needed */ + +/** + * Builds an evaluation function for a given Quint expression. + * + * This function first checks if the expression has already been memoized. If it has, + * it returns the memoized evaluation function. If not, it builds the core evaluation + * function for the expression and wraps it to handle errors and memoization. + * + * @param builder - The Builder instance used to construct the evaluation function. + * @param expr - The Quint expression to evaluate. + * + * @returns An evaluation function that takes a context and returns either a QuintError or a RuntimeValue. + */ export function buildExpr(builder: Builder, expr: QuintEx): EvalFunction { if (builder.memo.has(expr.id)) { return builder.memo.get(expr.id)! @@ -78,6 +158,8 @@ export function buildExpr(builder: Builder, expr: QuintEx): EvalFunction { const exprEval = buildExprCore(builder, expr) const wrappedEval: EvalFunction = ctx => { try { + // This is where we add the reference to the error, if it is not already there. + // This way, we don't need to worry about references anywhere else :) return exprEval(ctx).mapLeft(err => (err.reference === undefined ? { ...err, reference: expr.id } : err)) } catch (error) { const message = error instanceof Error ? error.message : 'unknown error' @@ -88,16 +170,59 @@ export function buildExpr(builder: Builder, expr: QuintEx): EvalFunction { return wrappedEval } -export function buildUnderDefContext( +/** + * Builds an evaluation function for a given definition. + * + * This function first checks if the definition has already been memoized. If it has, + * it returns the memoized evaluation function. If the definition is imported from an instance, + * it builds the evaluation function under the context of the instance. Otherwise, it builds the + * core evaluation function for the definition and wraps it to handle errors and memoization. + * + * @param builder - The Builder instance used to construct the evaluation function. + * @param def - The LookupDefinition to evaluate. + * + * @returns An evaluation function that takes a context and returns either a QuintError or a RuntimeValue. + */ +export function buildDef(builder: Builder, def: LookupDefinition): EvalFunction { + if (!def.importedFrom || def.importedFrom.kind !== 'instance') { + return buildDefWithMemo(builder, def) + } + + return buildUnderDefContext(builder, def, () => buildDefWithMemo(builder, def)) +} + +/** + * Given an arrow that builds something, wrap it in modifications over the builder so it has the proper context. + * Specifically, this includes instance overrides in context so that the build function use the right registers + * for the instance if it originated from an instance. + * + * @param builder - The builder instance. + * @param def - The definition for which the context is being built. + * @param buildFunction - The function that builds the EvalFunction. + * + * @returns the result of buildFunction, evaluated under the right context. + */ +function buildUnderDefContext( builder: Builder, def: LookupDefinition, buildFunction: () => EvalFunction ): EvalFunction { if (!def.importedFrom || def.importedFrom.kind !== 'instance') { + // Nothing to worry about if there are no instances involved return buildFunction() } + + // This originates from an instance, so we need to handle overrides const instance = def.importedFrom + + // Save how the builder was before so we can restore it after const memoBefore = builder.memo + const namespacesBefore = builder.namespaces + + // We need separate memos for each instance. + // For example, if N is a constant, the expression N + 1 can have different values for different instances. + // We re-use the same memo for the same instance. So, let's check if there is an existing memo, + // or create and save a new one if (builder.memoByInstance.has(instance.id)) { builder.memo = builder.memoByInstance.get(instance.id)! } else { @@ -105,6 +230,12 @@ export function buildUnderDefContext( builder.memoByInstance.set(instance.id, builder.memo) } + // We also need to update the namespaces to include the instance's namespaces. + // So, if variable x is updated, we update the instance's x, i.e. my_instance::my_module::x + builder.namespaces = List(def.namespaces) + + // Pre-compute as much as possible for the overrides: find the registers and find the expressions to evaluate + // so we don't need to look that up in runtime const overrides: [Register, EvalFunction][] = instance.overrides.map(([param, expr]) => { const id = builder.table.get(param.id)!.id const register = builder.registerForConst(id, param.name) @@ -113,27 +244,41 @@ export function buildUnderDefContext( return [register, buildDef(builder, { kind: 'def', qualifier: 'pureval', expr, name: param.name, id: param.id })] }) - const namespacesBefore = builder.namespaces - builder.namespaces = List(def.namespaces) - + // Here, we have the right context to build the function. That is, all constants are pointing to the right registers, + // and all namespaces are set for unambiguous variable access and update. const result = buildFunction() + + // Restore the builder to its previous state builder.namespaces = namespacesBefore builder.memo = memoBefore + // And then, in runtime, we only need to evaluate the override expressions, update the respective registers + // and then call the function that was built return ctx => { overrides.forEach(([register, evaluate]) => (register.value = evaluate(ctx))) return result(ctx) } } +/** + * Given a lookup definition, build the evaluation function for it, without worring about memoization or error handling. + * + * @param builder - The builder instance. + * @param def - The definition for which the evaluation function is being built. + * + * @returns the evaluation function for the given definition. + */ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { switch (def.kind) { case 'def': { if (def.qualifier === 'action') { + // Create an app to be recorded const app: QuintApp = { id: def.id, kind: 'app', opcode: def.name, args: [] } + const body = buildExpr(builder, def.expr) return (ctx: Context) => { if (def.expr.kind !== 'lambda') { + // Lambdas are recorded when they are called, no need to record them here ctx.recorder.onUserOperatorCall(app) } @@ -150,18 +295,21 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { } } - if (def.expr.kind === 'lambda') { - return buildExpr(builder, def.expr) - } - - if (def.depth === undefined || def.depth === 0) { + if (def.expr.kind === 'lambda' || def.depth === undefined || def.depth === 0) { + // We need to avoid scoped caching in lambdas or top-level expressions + // We still have memoization. This caching is special for scoped defs (let-ins) return buildExpr(builder, def.expr) } - let cachedValue = builder.scopedCachedValues.get(def.id)! + // Else, we are dealing with a scoped value. + // We need to cache it, so every time we access it, it has the same value. + const cachedValue = builder.scopedCachedValues.get(def.id)! const bodyEval = buildExpr(builder, def.expr) if (def.qualifier === 'nondet') { + // Create an entry in the map for this nondet pick, + // as we want the resulting record to be the same at every state. + // Value is optional, and starts with undefined builder.initialNondetPicks.set(def.name, undefined) } @@ -170,18 +318,16 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { cachedValue.value = bodyEval(ctx) if (def.qualifier === 'nondet') { cachedValue.value - .map(value => { - ctx.varStorage.nondetPicks.set(def.name, value) - }) - .mapLeft(_ => { - ctx.varStorage.nondetPicks.set(def.name, undefined) - }) + .map(value => ctx.varStorage.nondetPicks.set(def.name, value)) + .mapLeft(_ => ctx.varStorage.nondetPicks.set(def.name, undefined)) } } return cachedValue.value } } case 'param': { + // Every parameter has a single register, and we just change this register's value before evaluating the body + // So, a reference to a parameter simply evaluates to the value of the register. const register = builder.paramRegistry.get(def.id) if (!register) { const reg: Register = { value: left({ code: 'QNT501', message: `Parameter ${def.name} not set` }) } @@ -192,12 +338,16 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { } case 'var': { + // Every variable has a single register, and we just change this register's value at each state + // So, a reference to a variable simply evaluates to the value of the register. const register = builder.getVar(def.id) return _ => { return register.value } } case 'const': { + // Every constant has a single register, and we just change this register's value when overrides are present + // So, a reference to a constant simply evaluates to the value of the register. const register = builder.registerForConst(def.id, def.name) return _ => register.value } @@ -206,6 +356,16 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { } } +/** + * Builds a definition with memoization and caching. + * - Memoization: use the same built function for the same definition. + * - Caching: for top-level value definitions, cache the resulting value being aware of variable changes. + * + * @param builder - The builder instance. + * @param def - The definition for which the evaluation function is being built. + * + * @returns the evaluation function for the given definition. + */ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction { if (builder.memo.has(def.id)) { return builder.memo.get(def.id)! @@ -213,25 +373,27 @@ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction const defEval = buildDefCore(builder, def) - if ( - !( - def.kind === 'def' && - (def.qualifier === 'pureval' || def.qualifier === 'val') && - (def.depth === undefined || def.depth === 0) - ) - ) { + // For top-level value definitions, we can cache the resulting value, as long as we are careful with state changes. + const statefulCachingCondition = + def.kind === 'def' && + (def.qualifier === 'pureval' || def.qualifier === 'val') && + (def.depth === undefined || def.depth === 0) + + if (!statefulCachingCondition) { + // Only use memo, no runtime caching builder.memo.set(def.id, defEval) return defEval } - // Since we cache things separately per instance, we can cache the value here + // PS: Since we memoize things separately per instance, we can store even values that depend on constants + + // Construct a cached value object (a register with optional value) const cachedValue: CachedValue = { value: undefined } if (def.qualifier === 'val') { - // console.log('temp cache', def.name) + // This definition may use variables, so we need to clear the cache when they change builder.varStorage.cachesToClear.push(cachedValue) - } else { - // console.log('perm cache', def.name) } + // Wrap the evaluation function with caching const wrappedEval: EvalFunction = ctx => { if (cachedValue.value === undefined) { cachedValue.value = defEval(ctx) @@ -242,14 +404,14 @@ function buildDefWithMemo(builder: Builder, def: LookupDefinition): EvalFunction return wrappedEval } -export function buildDef(builder: Builder, def: LookupDefinition): EvalFunction { - if (!def.importedFrom || def.importedFrom.kind !== 'instance') { - return buildDefWithMemo(builder, def) - } - - return buildUnderDefContext(builder, def, () => buildDefWithMemo(builder, def)) -} - +/** + * Given an expression, build the evaluation function for it, without worring about memoization or error handling. + * + * @param builder - The builder instance. + * @param expr - The expression for which the evaluation function is being built. + * + * @returns the evaluation function for the given expression. + */ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { switch (expr.kind) { case 'int': @@ -268,14 +430,16 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { case 'name': { const def = builder.table.get(expr.id) if (!def) { - // TODO: Do we also need to return builtin ops for higher order usage? - // Answer: yes, see #1332 + // FIXME: If this refers to a builtin operator, we need to return it as an arrow (see #1332) return builtinValue(expr.name) } return buildDef(builder, def) } case 'app': { if (expr.opcode === 'assign') { + // Assign is too special, so we handle it separately. + // We need to build things under the context of the variable being assigned, as it may come from an instance, + // and that changed everything const varDef = builder.table.get(expr.args[0].id)! return buildUnderDefContext(builder, varDef, () => { builder.discoverVar(varDef.id, varDef.name) @@ -293,15 +457,17 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { const args = expr.args.map(arg => buildExpr(builder, arg)) - // In these special ops, we don't want to evaluate the arguments before evaluating application + // If the operator is a lazy operator, we can't evaluate the arguments before evaluating application if (lazyOps.includes(expr.opcode)) { const op = lazyBuiltinLambda(expr.opcode) return ctx => op(ctx, args) } - const userDefined = builder.table.has(expr.id) + // Otherwise, this is either a normal (eager) builtin, or an user-defined operator. + // For both, we first evaluate the arguments and then apply the operator. - const op = lambdaForApp(builder, expr) + const operatorFunction = buildApp(builder, expr) + const userDefined = builder.table.has(expr.id) return ctx => { if (userDefined) { @@ -316,7 +482,7 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { argValues.push(argValue.unwrap()) } - const result = op(ctx, argValues) + const result = operatorFunction(ctx, argValues) if (userDefined) { ctx.recorder.onUserOperatorReturn(expr, argValues, result) } @@ -324,14 +490,20 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { } } case 'let': { + // First, we create a cached value (a register with optional value) for the definition in this let expression let cachedValue = builder.scopedCachedValues.get(expr.opdef.id) if (!cachedValue) { + // TODO: check if either this is always the case or never the case. cachedValue = { value: undefined } builder.scopedCachedValues.set(expr.opdef.id, cachedValue) } + // Then, we build the expression for the let body. It will use the lookup table and, every time it needs the value + // for the definition under the let, it will use the cached value (or eval a new value and store it). const bodyEval = buildExpr(builder, expr.expr) return ctx => { const result = bodyEval(ctx) + // After evaluating the whole let expression, we clear the cached value, as it is no longer in scope. + // The next time the whole let expression is evaluated, the definition will be re-evaluated. cachedValue!.value = undefined return result } @@ -339,15 +511,26 @@ function buildExprCore(builder: Builder, expr: QuintEx): EvalFunction { } } -function lambdaForApp( +/** + * Builds the application function for a given Quint application. + * + * This function first checks if the application corresponds to a user-defined operator. + * If it does, it retrieves the corresponding evaluation function. If the operator is a built-in, + * it retrieves the built-in lambda function. The resulting function evaluates the operator + * with the given context and arguments. + * + * @param builder - The Builder instance + * @param app - The Quint application expression to evaluate. + * + * @returns A function that takes a context and arguments, and returns either a QuintError or a RuntimeValue. + */ +function buildApp( builder: Builder, app: QuintApp ): (ctx: Context, args: RuntimeValue[]) => Either { - const { id, opcode } = app - - const def = builder.table.get(id)! + const def = builder.table.get(app.id)! if (!def) { - return builtinLambda(opcode) + return builtinLambda(app.opcode) } const value = buildDef(builder, def) @@ -362,6 +545,16 @@ function lambdaForApp( } } +/** + * Constructs a fully qualified name by combining the given name with the namespaces. + * + * The namespaces are reversed and joined with the name using the "::" delimiter. + * + * @param name - The name to be qualified. + * @param namespaces - A list of namespaces to be included in the fully qualified name. + * + * @returns The fully qualified name as a string. + */ export function nameWithNamespaces(name: string, namespaces: List): string { const revertedNamespaces = namespaces.reverse() return revertedNamespaces.push(name).join('::') diff --git a/quint/src/runtime/impl/builtins.ts b/quint/src/runtime/impl/builtins.ts index 662170d06..ca8ebbfd2 100644 --- a/quint/src/runtime/impl/builtins.ts +++ b/quint/src/runtime/impl/builtins.ts @@ -1,6 +1,22 @@ +/* ---------------------------------------------------------------------------------- + * Copyright 2022-2024 Informal Systems + * Licensed under the Apache License, Version 2.0. + * See LICENSE in the project root for license information. + * --------------------------------------------------------------------------------- */ + +/** + * Definitions on how to evaluate Quint builtin operators and values. + * + * The definitions are heavily based on the original `compilerImpl.ts` file written by Igor Konnov. + * + * @author Igor Konnov, Gabriela Moreira + * + * @module + */ + import { Either, left, mergeInMany, right } from '@sweet-monads/either' import { QuintError } from '../../quintError' -import { List, Map, OrderedMap, Range, Set } from 'immutable' +import { List, Map, Range, Set } from 'immutable' import { isFalse, isTrue } from './evaluator' import { Context } from './Context' import { RuntimeValue, rv } from './runtimeValue' @@ -12,6 +28,25 @@ import { prettyQuintEx, terminalWidth } from '../../graphics' import { format } from '../../prettierimp' import { EvalFunction } from './builder' +/** + * Evaluates the given Quint builtin value by its name. + * + * This function is responsible for handling the evaluation of builtin values + * (operators that do not take parameters). It returns an `EvalFunction` + * which, when executed, provides the corresponding `RuntimeValue` or an error. + * + * The supported builtin values are: + * - 'Bool': Returns a set containing boolean values `true` and `false`. + * - 'Int': Returns an infinite set representing all integers. + * - 'Nat': Returns an infinite set representing all natural numbers. + * - 'q::lastTrace': Returns the list of the last trace from the context. + * + * If the provided name does not match any of the supported builtin values, + * it returns an error indicating the unknown builtin. + * + * @param name - The name of the builtin value to evaluate. + * @returns An `EvalFunction` that evaluates to the corresponding `RuntimeValue` or an error. + */ export function builtinValue(name: string): EvalFunction { switch (name) { case 'Bool': @@ -27,6 +62,15 @@ export function builtinValue(name: string): EvalFunction { } } +/** + * A list of operators that must be evaluated lazily. + * These operators cannot have their arguments evaluated before their own evaluation for various reasons: + * - Short-circuit operators (e.g., `and`, `or`, `implies`) where evaluation stops as soon as the result is determined. + * - Conditional operators (e.g., `ite`, `matchVariant`) where only certain arguments are evaluated based on conditions. + * - Repetition operators (e.g., `reps`) where the number of repetitions is unknown before evaluation. + * - Operators that interact with state variables in a special way (e.g., `assign`, `next`). + * - Operators where we can save resources (e.g., using `#pick()` in `oneOf` instead of enumerating the set). + */ export const lazyOps = [ 'assign', 'actionAny', @@ -43,11 +87,26 @@ export const lazyOps = [ 'expect', ] +/** + * Evaluates the given lazy builtin operator by its name. + * + * This function handles the evaluation of lazy builtin operators, + * which require special handling as described in the `lazyOps` documentation. + * It returns a function that takes a context and a list of evaluation functions, + * and returns the result of evaluating the operator. + * + * If the provided operator does not match any of the supported lazy operators, + * it returns an error indicating the unknown operator. + * + * @param op - The name of the lazy builtin operator to evaluate. + * @returns A function that evaluates the operator with the given context and arguments. + */ export function lazyBuiltinLambda( op: string ): (ctx: Context, args: EvalFunction[]) => Either { switch (op) { case 'and': + // Short-circuit logical AND return (ctx, args) => { for (const arg of args) { const result = arg(ctx) @@ -58,6 +117,7 @@ export function lazyBuiltinLambda( return right(rv.mkBool(true)) } case 'or': + // Short-circuit logical OR return (ctx, args) => { for (const arg of args) { const result = arg(ctx) @@ -68,6 +128,7 @@ export function lazyBuiltinLambda( return right(rv.mkBool(false)) } case 'implies': + // Short-circuit logical implication return (ctx, args) => { return args[0](ctx).chain(l => { if (!l.toBool()) { @@ -79,6 +140,9 @@ export function lazyBuiltinLambda( } case 'actionAny': { + // Executes any of the given actions. + // First, we filter actions so that we only consider those that are enabled. + // Then, we use `rand()` to pick one of the enabled actions. const app: QuintApp = { id: 0n, kind: 'app', opcode: 'actionAny', args: [] } return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.snapshot() @@ -128,6 +192,7 @@ export function lazyBuiltinLambda( } } case 'actionAll': + // Executes all of the given actions, or none of them if any of them results in false. return (ctx, args) => { const nextVarsSnapshot = ctx.varStorage.snapshot() for (const action of args) { @@ -145,6 +210,7 @@ export function lazyBuiltinLambda( return right(rv.mkBool(true)) } case 'ite': + // if-then-else return (ctx, args) => { return args[0](ctx).chain(condition => { return condition.toBool() ? args[1](ctx) : args[2](ctx) @@ -152,6 +218,7 @@ export function lazyBuiltinLambda( } case 'matchVariant': + // Pattern matching on variants return (ctx, args) => { const matchedEx = args[0] return matchedEx(ctx).chain(expr => { @@ -174,6 +241,7 @@ export function lazyBuiltinLambda( } case 'oneOf': + // Randomly selects one element of the set. return (ctx, args) => { return args[0](ctx).chain(set => { const bounds = set.bounds() @@ -200,6 +268,7 @@ export function lazyBuiltinLambda( }) } case 'then': + // Compose two actions, executing the second one only if the first one results in true. return (ctx, args) => { const oldState = ctx.varStorage.asRecord() return args[0](ctx).chain(firstResult => { @@ -218,6 +287,7 @@ export function lazyBuiltinLambda( }) } case 'reps': + // Repeats the given action n times, stopping if the action evaluates to false. return (ctx, args) => { return args[0](ctx).chain(n => { let result: Either = right(rv.mkBool(true)) @@ -277,43 +347,63 @@ export function lazyBuiltinLambda( } } +/** + * Evaluates the given builtin operator by its name. + * + * This function handles the evaluation of builtin operators, + * which require the arguments to be pre-evaluated. It returns + * a function that takes a context and a list of runtime values, + * and returns the result of evaluating the operator. + * + * If the provided operator does not match any of the supported operators, + * it returns an error indicating the unknown operator. + * + * @param op - The name of the builtin operator to evaluate. + * @returns A function that evaluates the operator with the given context and arguments. + */ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) => Either { switch (op) { case 'Set': + // Constructs a set from the given arguments. return (_, args) => right(rv.mkSet(args)) case 'Rec': - return (_, args) => { - const keys = args.filter((e, i) => i % 2 === 0).map(k => k.toStr()) - const map: OrderedMap = keys.reduce((map, key, i) => { - const v = args[2 * i + 1] - return v ? map.set(key, v) : map - }, OrderedMap()) - return right(rv.mkRecord(map)) - } - // right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) + // Constructs a record from the given arguments. Arguments are lists like [key1, value1, key2, value2, ...] + return (_, args) => right(rv.mkRecord(Map(chunk(args, 2).map(([k, v]) => [k.toStr(), v])))) case 'List': + // Constructs a list from the given arguments. return (_, args) => right(rv.mkList(List(args))) case 'Tup': + // Constructs a tuple from the given arguments. return (_, args) => right(rv.mkTuple(List(args))) case 'Map': + // Constructs a map from the given arguments. Arguments are lists like [[key1, value1], [key2, value2], ...] return (_, args) => right(rv.mkMap(args.map(kv => kv.toTuple2()))) case 'variant': + // Constructs a variant from the given arguments. return (_, args) => right(rv.mkVariant(args[0].toStr(), args[1])) case 'not': + // Logical negation return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'iff': + // Logical equivalence/bi-implication return (_, args) => right(rv.mkBool(args[0].toBool() === args[1].toBool())) case 'eq': + // Equality return (_, args) => right(rv.mkBool(args[0].equals(args[1]))) case 'neq': + // Inequality return (_, args) => right(rv.mkBool(!args[0].equals(args[1]))) case 'iadd': + // Integer addition return (_, args) => right(rv.mkInt(args[0].toInt() + args[1].toInt())) case 'isub': + // Integer subtraction return (_, args) => right(rv.mkInt(args[0].toInt() - args[1].toInt())) case 'imul': + // Integer multiplication return (_, args) => right(rv.mkInt(args[0].toInt() * args[1].toInt())) case 'idiv': + // Integer division return (_, args) => { const divisor = args[1].toInt() if (divisor === 0n) { @@ -322,8 +412,10 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) return right(rv.mkInt(args[0].toInt() / divisor)) } case 'imod': + // Integer modulus return (_, args) => right(rv.mkInt(args[0].toInt() % args[1].toInt())) case 'ipow': + // Integer exponentiation return (_, args) => { const base = args[0].toInt() const exp = args[1].toInt() @@ -337,25 +429,32 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) return right(rv.mkInt(base ** exp)) } case 'iuminus': + // Integer unary minus return (_, args) => right(rv.mkInt(-args[0].toInt())) case 'ilt': + // Integer less than return (_, args) => right(rv.mkBool(args[0].toInt() < args[1].toInt())) case 'ilte': + // Integer less than or equal to return (_, args) => right(rv.mkBool(args[0].toInt() <= args[1].toInt())) case 'igt': + // Integer greater than return (_, args) => right(rv.mkBool(args[0].toInt() > args[1].toInt())) case 'igte': + // Integer greater than or equal to return (_, args) => right(rv.mkBool(args[0].toInt() >= args[1].toInt())) case 'item': + // Access a tuple: tuples are 1-indexed, that is, _1, _2, etc. return (_, args) => { - // Access a tuple: tuples are 1-indexed, that is, _1, _2, etc. return getListElem(args[0].toList(), Number(args[1].toInt()) - 1) } case 'tuples': + // A set of all possible tuples from the elements of the respective given sets. return (_, args) => right(rv.mkCrossProd(args)) case 'range': + // Constructs a list of integers from start to end. return (_, args) => { const start = Number(args[0].toInt()) const end = Number(args[1].toInt()) @@ -363,9 +462,11 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'nth': + // List access return (_, args) => getListElem(args[0].toList(), Number(args[1].toInt())) case 'replaceAt': + // Replace an element at a given index in a list. return (_, args) => { const list = args[0].toList() const idx = Number(args[1].toInt()) @@ -377,6 +478,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'head': + // Get the first element of a list. Not allowed in empty lists. return (_, args) => { const list = args[0].toList() if (list.size === 0) { @@ -386,6 +488,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'tail': + // Get the tail (all elements but the head) of a list. Not allowed in empty lists. return (_, args) => { const list = args[0].toList() if (list.size === 0) { @@ -395,6 +498,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'slice': + // Get a sublist of a list from start to end. return (_, args) => { const list = args[0].toList() const start = Number(args[1].toInt()) @@ -410,15 +514,20 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'length': + // The length of a list. return (_, args) => right(rv.mkInt(args[0].toList().size)) case 'append': + // Append an element to a list. return (_, args) => right(rv.mkList(args[0].toList().push(args[1]))) case 'concat': + // Concatenate two lists. return (_, args) => right(rv.mkList(args[0].toList().concat(args[1].toList()))) case 'indices': + // A set with the indices of a list. return (_, args) => right(rv.mkInterval(0n, args[0].toList().size - 1)) case 'field': + // Access a field in a record. return (_, args) => { const field = args[1].toStr() const result = args[0].toOrderedMap().get(field) @@ -426,9 +535,11 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'fieldNames': + // A set with the field names of a record. return (_, args) => right(rv.mkSet(args[0].toOrderedMap().keySeq().map(rv.mkStr))) case 'with': + // Replace a field value in a record. return (_, args) => { const record = args[0].toOrderedMap() const field = args[1].toStr() @@ -442,60 +553,79 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'powerset': + // The powerset of a set. return (_, args) => right(rv.mkPowerset(args[0])) case 'contains': + // Check if a set contains an element. return (_, args) => right(rv.mkBool(args[0].contains(args[1]))) case 'in': + // Check if an element is in a set. return (_, args) => right(rv.mkBool(args[1].contains(args[0]))) case 'subseteq': + // Check if a set is a subset of another set. return (_, args) => right(rv.mkBool(args[0].isSubset(args[1]))) case 'exclude': + // Set difference. return (_, args) => right(rv.mkSet(args[0].toSet().subtract(args[1].toSet()))) case 'union': + // Set union. return (_, args) => right(rv.mkSet(args[0].toSet().union(args[1].toSet()))) case 'intersect': + // Set intersection. return (_, args) => right(rv.mkSet(args[0].toSet().intersect(args[1].toSet()))) case 'size': + // The size of a set. return (_, args) => args[0].cardinality().map(rv.mkInt) case 'isFinite': // at the moment, we support only finite sets, so just return true return _args => right(rv.mkBool(true)) case 'to': + // Construct a set of integers from a to b. return (_, args) => right(rv.mkInterval(args[0].toInt(), args[1].toInt())) case 'fold': + // Fold a set return (ctx, args) => applyFold('fwd', args[0].toSet(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'foldl': + // Fold a list from left to right. return (ctx, args) => applyFold('fwd', args[0].toList(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'foldr': + // Fold a list from right to left. return (ctx, args) => applyFold('rev', args[0].toList(), args[1], arg => args[2].toArrow()(ctx, arg)) case 'flatten': + // Flatten a set of sets. return (_, args) => { const s = args[0].toSet().map(s => s.toSet()) return right(rv.mkSet(s.flatten(1) as Set)) } case 'get': + // Get a value from a map. return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() const value = map.get(key) - return value - ? right(value) - : left({ - code: 'QNT507', - message: `Called 'get' with a non-existing key. Key is ${expressionToString( - key.toQuintEx(zerog) - )}. Map has keys: ${map - .toMap() - .keySeq() - .map(k => expressionToString(k.toQuintEx(zerog))) - .join(', ')}`, - }) + if (value) { + return right(value) + } + + // Else, the key does not exist. Construct an informative error message. + const requestedKey = expressionToString(key.toQuintEx(zerog)) + const existingKeys = map + .toMap() + .keySeq() + .map(k => expressionToString(k.toQuintEx(zerog))) + .join(', ') + + return left({ + code: 'QNT507', + message: `Called 'get' with a non-existing key. Key is ${requestedKey}. Map has keys: ${existingKeys}`, + }) } case 'set': + // Set a value for an existing key in a map. return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() @@ -507,6 +637,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'put': + // Set a value for any key in a map. return (_, args) => { const map = args[0].toMap() const key = args[1].normalForm() @@ -515,11 +646,12 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'setBy': + // Set a value for an existing key in a map using a lambda over the current value. return (ctx, args) => { const map = args[0].toMap() const key = args[1].normalForm() if (!map.has(key)) { - return left({ code: 'QNT507', message: `Called 'setBy' with a non-existing key ${key}` }) + return left({ code: 'QNT507', message: `Called 'setBy' with a non- existing key ${key}` }) } const value = map.get(key)! @@ -528,60 +660,45 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'keys': + // A set with the keys of a map. return (_, args) => right(rv.mkSet(args[0].toMap().keys())) case 'exists': + // Check if a predicate holds for some element in a set. return (ctx, args) => applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.some(v => v.toBool()) === true)) case 'forall': + // Check if a predicate holds for all elements in a set. return (ctx, args) => applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkBool(values.every(v => v.toBool()) === true)) case 'map': + // Map a lambda over a set. return (ctx, args) => { return applyLambdaToSet(ctx, args[1], args[0]).map(values => rv.mkSet(values)) } case 'filter': + // Filter a set using a lambda. return (ctx, args) => { const set = args[0].toSet() const lam = args[1].toArrow() - const result = [] - for (const element of set) { - const value = lam(ctx, [element]) - if (value.isLeft()) { - return value - } - if (value.value.toBool()) { - result.push(element) - } - } - - return right(rv.mkSet(result)) + return filterElementsWithLambda(ctx, set, lam).map(result => rv.mkSet(result)) } case 'select': + // Filter a list using a lambda return (ctx, args) => { const list = args[0].toList() const lam = args[1].toArrow() - const result = [] - for (const element of list) { - const value = lam(ctx, [element]) - if (value.isLeft()) { - return value - } - if (value.value.toBool()) { - result.push(element) - } - } - - return right(rv.mkList(result)) + return filterElementsWithLambda(ctx, list, lam).map(result => rv.mkList(result)) } case 'mapBy': + // Construct a map by applying a lambda to the values of a set. return (ctx, args) => { const lambda = args[1].toArrow() const keys = args[0].toSet() @@ -599,20 +716,25 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'setToMap': + // Convert a set of key-value tuples to a map. return (_, args) => { const set = args[0].toSet() return right(rv.mkMap(Map(set.map(s => s.toTuple2())))) } case 'setOfMaps': + // A set of all possible maps with keys and values from the given sets. return (_, args) => right(rv.mkMapSet(args[0], args[1])) case 'fail': + // Expect a value to be false return (_, args) => right(rv.mkBool(!args[0].toBool())) case 'assert': + // Expect a value to be true, returning a runtime error if it is not return (_, args) => (args[0].toBool() ? right(args[0]) : left({ code: 'QNT508', message: `Assertion failed` })) case 'allListsUpTo': + // Generate all lists of length up to the given number, from a set return (_, args) => { const set = args[0].toSet() let lists: Set = Set([[]]) @@ -632,6 +754,7 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) } case 'q::debug': + // Print a value to the console, and return it return (_, args) => { let columns = terminalWidth() let valuePretty = format(columns, 0, prettyQuintEx(args[1].toQuintEx(zerog))) @@ -645,21 +768,33 @@ export function builtinLambda(op: string): (ctx: Context, args: RuntimeValue[]) case 'always': case 'eventually': case 'enabled': - return _ => left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${op}'` }) + return _ => left({ code: 'QNT501', message: `Runtime does not support the built -in operator '${op}'` }) // builtin operators that are not handled by REPL case 'orKeep': case 'mustChange': case 'weakFair': case 'strongFair': - return _ => left({ code: 'QNT501', message: `Runtime does not support the built-in operator '${op}'` }) + return _ => left({ code: 'QNT501', message: `Runtime does not support the built -in operator '${op}'` }) default: return () => left({ code: 'QNT000', message: `Unknown builtin ${op}` }) } } -export function applyLambdaToSet( +/** + * Applies a lambda function to each element in a set. + * + * If the lambda function returns an error for any element, the function + * will return that error immediately. + * + * @param ctx - The context in which to evaluate the lambda function. + * @param lambda - The lambda function to apply to each element in the set. + * @param set - The set of elements to which the lambda function will be applied. + * @returns A set of the results of applying the lambda function to each element in the set, + * or an error if the lambda function returns an error for any element. + */ +function applyLambdaToSet( ctx: Context, lambda: RuntimeValue, set: RuntimeValue @@ -680,6 +815,49 @@ export function applyLambdaToSet( return right(Set(results)) } +/** + * Filters elements of an iterable using a lambda function. + * + * If the lambda function returns an error for any element, the function + * will return that error immediately. + * + * @param ctx - The context in which to evaluate the lambda function. + * @param elements - The iterable of elements to be filtered. + * @param lam - The lambda function to apply to each element in the iterable. + * @returns An array of elements for which the lambda function returns true, + * or an error if the lambda function returns an error for any element. + */ +function filterElementsWithLambda( + ctx: Context, + elements: Iterable, + lam: (ctx: Context, args: RuntimeValue[]) => Either +): Either { + const result = [] + for (const element of elements) { + const value = lam(ctx, [element]) + if (value.isLeft()) { + return left(value.value) + } + if (value.value.toBool()) { + result.push(element) + } + } + return right(result) +} + +/** + * Applies a fold (reduce) operation to an iterable using a lambda function. + * + * If the lambda function returns an error for any pair of elements, the function + * will return that error immediately. + * + * @param order - The order in which to apply the fold ('fwd' for forward, 'rev' for reverse). + * @param iterable - The iterable of elements to be folded. + * @param initial - The initial value for the fold operation. + * @param lambda - The lambda function to apply to each pair of elements. + * @returns The accumulated result of applying the lambda function to the elements of the iterable, + * or an error if the lambda function returns an error for any pair of elements. + */ function applyFold( order: 'fwd' | 'rev', iterable: Iterable, @@ -704,7 +882,13 @@ function applyFold( } } -// Access a list via an index +/** + * Accesses an element in a list by its index. + * + * @param list - The list of elements. + * @param idx - The index of the element to access. + * @returns The element at the specified index, or an error if the index is out of bounds. + */ function getListElem(list: List, idx: number): Either { if (idx >= 0n && idx < list.size) { const elem = list.get(Number(idx)) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 270b220a3..123d61cc8 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -1,4 +1,20 @@ -import { Either, left, mergeInOne, right } from '@sweet-monads/either' +/* ---------------------------------------------------------------------------------- + * Copyright 2022-2024 Informal Systems + * Licensed under the Apache License, Version 2.0. + * See LICENSE in the project root for license information. + * --------------------------------------------------------------------------------- */ + +/** + * An evaluator for Quint in Node TS runtime. + * + * Testing and simulation are heavily based on the original `compilerImpl.ts` file written by Igor Konnov. + * + * @author Igor Konnov, Gabriela Moreira + * + * @module + */ + +import { Either, left, right } from '@sweet-monads/either' import { QuintApp, QuintEx } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { QuintError } from '../../quintError' @@ -12,12 +28,23 @@ import { zerog } from '../../idGenerator' import { List } from 'immutable' import { Builder, buildDef, buildExpr, nameWithNamespaces } from './builder' +/** + * An evaluator for Quint in Node TS runtime. + */ export class Evaluator { public ctx: Context public recorder: TraceRecorder private rng: Rng private builder: Builder + /** + * Constructs an Evaluator that can be re-used across evaluations. + * + * @param table - The lookup table for definitions. + * @param recorder - The trace recorder to log evaluation traces. + * @param rng - The random number generator to use for evaluation. + * @param storeMetadata - Optional, whether to store `actionTaken` and `nondetPicks`. Default is false. + */ constructor(table: LookupTable, recorder: TraceRecorder, rng: Rng, storeMetadata: boolean = false) { this.recorder = recorder this.rng = rng @@ -25,18 +52,36 @@ export class Evaluator { this.ctx = new Context(recorder, rng.next, this.builder.varStorage) } + /** + * Get the current trace from the context + */ get trace(): Trace { return this.ctx.trace } + /** + * Update the lookup table, if the same table is used for multiple evaluations but there are new definitions. + * + * @param table + */ updateTable(table: LookupTable) { this.builder.table = table } + /** + * Shift the context to the next state. That is, updated variables in the next state are moved to the current state, + * and the trace is extended. + */ shift(): void { this.ctx.shift() } + /** + * Shift the context to the next state. That is, updated variables in the next state are moved to the current state, + * and the trace is extended. + * + * @returns names of the variables that don't have values in the new state. + */ shiftAndCheck(): string[] { const missing = this.ctx.varStorage.nextVars.filter(reg => reg.value.isLeft()) @@ -52,9 +97,15 @@ export class Evaluator { .toArray() } + /** + * Evaluate a Quint expression. + * + * @param expr + * @returns the result of the evaluation, if successful, or an error if the evaluation failed. + */ evaluate(expr: QuintEx): Either { if (expr.kind === 'app' && (expr.opcode == 'q::test' || expr.opcode === 'q::testOnce')) { - return this.evaluateTest(expr) + return this.evaluateSimulation(expr) } const value = buildExpr(this.builder, expr)(this.ctx) @@ -62,21 +113,29 @@ export class Evaluator { return value.map(rv.toQuintEx) } - evaluateTest(expr: QuintApp): Either { - if (expr.opcode === 'q::testOnce') { - const [nsteps, ntraces, init, step, inv] = expr.args - return this.simulate(init, step, inv, 1, toNumber(nsteps), toNumber(ntraces)) - } else { - const [nruns, nsteps, ntraces, init, step, inv] = expr.args - return this.simulate(init, step, inv, toNumber(nruns), toNumber(nsteps), toNumber(ntraces)) - } - } - + /** + * Reset the evaluator to its initial state (in terms of trace and variables) + */ reset() { this.trace.reset() this.builder.varStorage.reset() } + /** + * Simulates the execution of an initial expression followed by a series of step expressions, + * while checking an invariant expression at each step. The simulation is run multiple times, + * up to a specified number of runs, and each run is executed for a specified number of steps. + * The simulation stops if a specified number of traces with errors are found. + * + * @param init - The initial expression to evaluate. + * @param step - The step expression to evaluate repeatedly. + * @param inv - The invariant expression to check after each step. + * @param nruns - The number of simulation runs to perform. + * @param nsteps - The number of steps to perform in each simulation run. + * @param ntraces - The number of error traces to collect before stopping the simulation. + * @returns a boolean expression indicating whether all simulations passed without errors, + or an error if the simulation cannot be completed. + */ simulate( init: QuintEx, step: QuintEx, @@ -92,7 +151,6 @@ export class Evaluator { const stepEval = buildExpr(this.builder, step) const invEval = buildExpr(this.builder, inv) - // TODO: room for improvement here for (let runNo = 0; errorsFound < ntraces && !failure && runNo < nruns; runNo++) { this.recorder.onRunCall() this.reset() @@ -160,6 +218,15 @@ export class Evaluator { return outcome } + /** + * Run a specified test definition a given number of times, and report the result. + * + * @param testDef - The definition of the test to be run. + * @param maxSamples - The maximum number of times to run the test. + * @param onTrace - A callback function to be called with trace information for each test run. + * @returns The result of the test, including its name, status, any errors, the seed used, frames, + and the number of samples run. + */ test( testDef: LookupDefinition, maxSamples: number, @@ -275,12 +342,32 @@ export class Evaluator { } } + /** + * Variable names in context + * @returns the names of all variables in the current context. + */ varNames() { return this.ctx.varStorage.vars .valueSeq() .toArray() .map(v => v.name) } + + /** + * Special case of `evaluate` where the expression is a call to a simulation. + * + * @param expr + * @returns the result of the simulation, or an error if the simulation cannot be completed. + */ + private evaluateSimulation(expr: QuintApp): Either { + if (expr.opcode === 'q::testOnce') { + const [nsteps, ntraces, init, step, inv] = expr.args + return this.simulate(init, step, inv, 1, toNumber(nsteps), toNumber(ntraces)) + } else { + const [nruns, nsteps, ntraces, init, step, inv] = expr.args + return this.simulate(init, step, inv, toNumber(nruns), toNumber(nsteps), toNumber(ntraces)) + } + } } export function isTrue(value: Either): boolean { diff --git a/quint/src/runtime/impl/runtimeValue.ts b/quint/src/runtime/impl/runtimeValue.ts index 4264e1ea2..0957b1dfd 100644 --- a/quint/src/runtime/impl/runtimeValue.ts +++ b/quint/src/runtime/impl/runtimeValue.ts @@ -53,9 +53,9 @@ * whole new layer of abstraction. However, it is deeply rooted in the * semantics of Quint, which, similar to TLA+, heavily utilizes set operators. * - * Igor Konnov, 2022 + * Igor Konnov, Gabriela Moreira 2022-2024 * - * Copyright 2022 Informal Systems + * Copyright 2022-2024 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. */ @@ -264,6 +264,11 @@ export const rv = { return new RuntimeValueLambda(body, registers) }, + /** + * Make a runtime value from a quint expression. + * @param ex - the Quint expression + * @returns a runtime value for the expression + */ fromQuintEx: (ex: QuintEx): RuntimeValue => { const v = fromQuintEx(ex) if (v.isJust()) { @@ -273,6 +278,11 @@ export const rv = { } }, + /** + * Convert a runtime value to a Quint expression. + * @param value - the runtime value to convert + * @returns a Quint expression for the runtime value + */ toQuintEx: (value: RuntimeValue): QuintEx => { return value.toQuintEx(zerog) }, @@ -618,6 +628,7 @@ abstract class RuntimeValueBase implements RuntimeValue { } toTuple2(): [RuntimeValue, RuntimeValue] { + // This is specific for tuples of size 2, as they are expected in many builtins. if (this instanceof RuntimeValueTupleOrList) { const list = this.list From 77a4dde8a69e8587e435b28c4788e6cbfe808d53 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 13:24:10 -0300 Subject: [PATCH 26/37] Revert "Merge pull request #1191 from informalsystems/gabriela/fix-instance-constant-callgraph" This change is no longer necessary as this dependency no longer exists. I've checked that the reported problem doesn't happen after the revert This reverts commit 384c28ed618296fd4f462a74b7ee188723424956, reversing changes made to 576d72b941c0aa54290ee115376c84b49b7e47a6. --- quint/src/static/callgraph.ts | 3 --- quint/test/static/callgraph.test.ts | 7 ------- 2 files changed, 10 deletions(-) diff --git a/quint/src/static/callgraph.ts b/quint/src/static/callgraph.ts index 458d08668..30489f101 100644 --- a/quint/src/static/callgraph.ts +++ b/quint/src/static/callgraph.ts @@ -190,9 +190,6 @@ export class CallGraphVisitor implements IRVisitor { const importedModule = this.context.modulesByName.get(decl.protoName) if (importedModule) { this.graphAddAll(decl.id, Set([importedModule.id])) - decl.overrides.forEach(([_param, expr]) => { - this.graphAddAll(expr.id, Set([decl.id])) - }) } } diff --git a/quint/test/static/callgraph.test.ts b/quint/test/static/callgraph.test.ts index 383800391..18074e0bf 100644 --- a/quint/test/static/callgraph.test.ts +++ b/quint/test/static/callgraph.test.ts @@ -130,7 +130,6 @@ describe('compute call graph', () => { | pure val myM = sqr(3) | import B(M = myM) as B1 | pure val quadM = 2 * B1::doubleM - | pure val constRef = B1::M | export B1.* |}` ) @@ -150,7 +149,6 @@ describe('compute call graph', () => { const importB = findInstance(main, imp => imp.protoName === 'B') const quadM = findDef(main, 'quadM') const doubleM = findDef(B, 'doubleM') - const constRef = findDef(main, 'constRef') const exportB1 = findExport(main, exp => exp.protoName === 'B1') expect(graph.get(importA.id)?.toArray()).eql([A.id]) @@ -158,10 +156,5 @@ describe('compute call graph', () => { expect(graph.get(importB.id)?.toArray()).to.eql([B.id, myM.id]) expect(graph.get(quadM.id)?.toArray()).to.eql([doubleM.id, importB.id]) expect(graph.get(exportB1.id)?.toArray()).to.eql([importB.id]) - - // Find the id for B1::M by checking the dependencies of constRef - // A regression for #1183, ensuring constants like B1::M depend on the instance statements - const B1Mid = graph.get(constRef.id)?.toArray()[0]! - expect(graph.get(B1Mid)?.toArray()).to.eql([importB.id]) }) }) From 2e9d0af933dd26ccb018bb53a3c836fc2286dc07 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 13:31:00 -0300 Subject: [PATCH 27/37] Update test for unsupported scenario This test case describes a scenario where we don't get the desired result. The behavior changed, not for the better nor for the worse. --- quint/test/flattening/flattener.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quint/test/flattening/flattener.test.ts b/quint/test/flattening/flattener.test.ts index 3bf537d38..4c9f86fab 100644 --- a/quint/test/flattening/flattener.test.ts +++ b/quint/test/flattening/flattener.test.ts @@ -143,7 +143,7 @@ describe('flattenModule', () => { const thirdModuleDecls = ['import B.*', 'val a = f(1)'] - const expectedDecls = ['import A(N = 1) as A1', 'def f = ((x) => iadd(x, 1))'] + const expectedDecls = ['import A(N = 1) as A1', 'def f = ((x) => iadd(x, 1))', 'const N: int'] const flattenedDecls = getFlatennedDecls(baseDecls, decls, thirdModuleDecls) assert.deepEqual(flattenedDecls, expectedDecls) From 4ff134768903e740b883a71e1da6c59cc4085028 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 14:17:07 -0300 Subject: [PATCH 28/37] Fix formatting issue --- quint/src/names/collector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quint/src/names/collector.ts b/quint/src/names/collector.ts index 701fe15eb..4c79bf97d 100644 --- a/quint/src/names/collector.ts +++ b/quint/src/names/collector.ts @@ -134,7 +134,7 @@ export class NameCollector implements IRVisitor { // For each override, check if the name exists in the instantiated module and is a constant. // If so, update the value definition to point to the expression being overriden - decl.overrides.forEach(([param, ex]) => { + decl.overrides.forEach(([param, _ex]) => { // Constants are always top-level const constDef = getTopLevelDef(instanceTable, param.name) From 002bd29f75a136f6f55450dae36a456789b3e1c2 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 14:17:17 -0300 Subject: [PATCH 29/37] Fix integration test by making sure the proper table is used in the output --- quint/src/cliCommands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 1b6978653..9cb7e0b87 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -703,7 +703,7 @@ export async function outputCompilationTarget(compiled: CompiledStage): Promise< const verbosityLevel = deriveVerbosity(args) const parsedSpecJson = jsonStringOfOutputStage( - pickOutputStage({ ...compiled, modules: [compiled.mainModule], table: compiled.resolver.table }) + pickOutputStage({ ...compiled, modules: [compiled.mainModule], table: compiled.table }) ) switch ((compiled.args.target as string).toLowerCase()) { case 'json': From b4861855aceb2b272adc09c312b18f7d1b2e8031 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 14:20:20 -0300 Subject: [PATCH 30/37] Update fixtures, consts now keep their proper id in the table --- quint/testFixture/SuperSpec.json | 2 +- quint/testFixture/_1016nonConstOverride.json | 2 +- quint/testFixture/_1031instance.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quint/testFixture/SuperSpec.json b/quint/testFixture/SuperSpec.json index c9cef1d87..ff4a857f5 100644 --- a/quint/testFixture/SuperSpec.json +++ b/quint/testFixture/SuperSpec.json @@ -1 +1 @@ -{"stage":"parsing","warnings":[],"modules":[{"id":11,"name":"Proto","declarations":[{"kind":"var","name":"x","typeAnnotation":{"id":9,"kind":"int"},"id":10,"depth":0},{"kind":"const","name":"N","typeAnnotation":{"id":7,"kind":"int"},"id":8,"depth":0}]},{"id":3,"name":"M1","declarations":[{"id":2,"kind":"def","name":"foo","qualifier":"val","expr":{"id":1,"kind":"int","value":4}}]},{"id":6,"name":"M2","declarations":[{"id":5,"kind":"def","name":"bar","qualifier":"val","expr":{"id":4,"kind":"int","value":4}}]},{"id":530,"name":"withConsts","declarations":[{"id":101,"kind":"def","name":"sub_1_to_2","qualifier":"val","expr":{"id":100,"kind":"app","opcode":"isub","args":[{"id":98,"kind":"int","value":1},{"id":99,"kind":"int","value":2}]}},{"id":105,"kind":"def","name":"mul_2_to_3","qualifier":"val","expr":{"id":104,"kind":"app","opcode":"imul","args":[{"id":102,"kind":"int","value":2},{"id":103,"kind":"int","value":3}]}},{"id":109,"kind":"def","name":"div_2_to_3","qualifier":"val","expr":{"id":108,"kind":"app","opcode":"idiv","args":[{"id":106,"kind":"int","value":2},{"id":107,"kind":"int","value":3}]}},{"id":113,"kind":"def","name":"mod_2_to_3","qualifier":"val","expr":{"id":112,"kind":"app","opcode":"imod","args":[{"id":110,"kind":"int","value":2},{"id":111,"kind":"int","value":3}]}},{"id":117,"kind":"def","name":"pow_2_to_3","qualifier":"val","expr":{"id":116,"kind":"app","opcode":"ipow","args":[{"id":114,"kind":"int","value":2},{"id":115,"kind":"int","value":3}]}},{"id":120,"kind":"def","name":"uminus","qualifier":"val","expr":{"id":119,"kind":"app","opcode":"iuminus","args":[{"id":118,"kind":"int","value":100}]}},{"id":124,"kind":"def","name":"gt_2_to_3","qualifier":"val","expr":{"id":123,"kind":"app","opcode":"igt","args":[{"id":121,"kind":"int","value":2},{"id":122,"kind":"int","value":3}]}},{"id":128,"kind":"def","name":"ge_2_to_3","qualifier":"val","expr":{"id":127,"kind":"app","opcode":"igte","args":[{"id":125,"kind":"int","value":2},{"id":126,"kind":"int","value":3}]}},{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},{"id":132,"kind":"def","name":"lt_2_to_3","qualifier":"val","expr":{"id":131,"kind":"app","opcode":"ilt","args":[{"id":129,"kind":"int","value":2},{"id":130,"kind":"int","value":3}]}},{"id":136,"kind":"def","name":"le_2_to_3","qualifier":"val","expr":{"id":135,"kind":"app","opcode":"ilte","args":[{"id":133,"kind":"int","value":2},{"id":134,"kind":"int","value":3}]}},{"id":140,"kind":"def","name":"eqeq_2_to_3","qualifier":"val","expr":{"id":139,"kind":"app","opcode":"eq","args":[{"id":137,"kind":"int","value":2},{"id":138,"kind":"int","value":3}]}},{"id":144,"kind":"def","name":"ne_2_to_3","qualifier":"val","expr":{"id":143,"kind":"app","opcode":"neq","args":[{"id":141,"kind":"int","value":2},{"id":142,"kind":"int","value":3}]}},{"id":150,"kind":"def","name":"VeryTrue","qualifier":"val","expr":{"id":149,"kind":"app","opcode":"eq","args":[{"id":147,"kind":"app","opcode":"iadd","args":[{"id":145,"kind":"int","value":2},{"id":146,"kind":"int","value":2}]},{"id":148,"kind":"int","value":4}]}},{"id":154,"kind":"def","name":"nat_includes_one","qualifier":"val","expr":{"id":153,"kind":"app","opcode":"in","args":[{"id":151,"kind":"int","value":1},{"id":152,"kind":"name","name":"Nat"}]}},{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},{"id":166,"kind":"def","name":"withType","qualifier":"val","expr":{"id":165,"kind":"app","opcode":"Set","args":[{"id":163,"kind":"int","value":1},{"id":164,"kind":"int","value":2}]},"typeAnnotation":{"id":162,"kind":"set","elem":{"id":161,"kind":"int"}}},{"id":167,"kind":"typedef","name":"PROC","depth":0},{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},{"id":175,"kind":"def","name":"magicNumber","qualifier":"pureval","expr":{"id":174,"kind":"int","value":42}},{"kind":"const","name":"MySet","typeAnnotation":{"id":18,"kind":"set","elem":{"id":17,"kind":"int"}},"id":19,"depth":0},{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},{"kind":"const","name":"MySeq","typeAnnotation":{"id":21,"kind":"list","elem":{"id":20,"kind":"bool"}},"id":22,"depth":0},{"id":240,"kind":"def","name":"test_and","qualifier":"val","expr":{"id":239,"kind":"app","opcode":"and","args":[{"id":237,"kind":"bool","value":false},{"id":238,"kind":"bool","value":true}]}},{"id":244,"kind":"def","name":"test_or","qualifier":"val","expr":{"id":243,"kind":"app","opcode":"or","args":[{"id":241,"kind":"bool","value":false},{"id":242,"kind":"bool","value":true}]}},{"id":248,"kind":"def","name":"test_implies","qualifier":"val","expr":{"id":247,"kind":"app","opcode":"implies","args":[{"id":245,"kind":"bool","value":false},{"id":246,"kind":"bool","value":true}]}},{"kind":"const","name":"MyFun","typeAnnotation":{"id":25,"kind":"fun","arg":{"id":23,"kind":"int"},"res":{"id":24,"kind":"str"}},"id":26,"depth":0},{"id":281,"kind":"def","name":"test_block_and","qualifier":"val","expr":{"id":280,"kind":"app","opcode":"and","args":[{"id":277,"kind":"bool","value":false},{"id":278,"kind":"bool","value":true},{"id":279,"kind":"bool","value":false}]}},{"id":286,"kind":"def","name":"test_action_and","qualifier":"action","expr":{"id":285,"kind":"app","opcode":"actionAll","args":[{"id":282,"kind":"bool","value":false},{"id":283,"kind":"bool","value":true},{"id":284,"kind":"bool","value":false}]}},{"id":291,"kind":"def","name":"test_block_or","qualifier":"val","expr":{"id":290,"kind":"app","opcode":"or","args":[{"id":287,"kind":"bool","value":false},{"id":288,"kind":"bool","value":true},{"id":289,"kind":"bool","value":false}]}},{"id":296,"kind":"def","name":"test_action_or","qualifier":"action","expr":{"id":295,"kind":"app","opcode":"actionAny","args":[{"id":292,"kind":"bool","value":false},{"id":293,"kind":"bool","value":true},{"id":294,"kind":"bool","value":false}]}},{"id":301,"kind":"def","name":"test_ite","qualifier":"val","expr":{"id":300,"kind":"app","opcode":"ite","args":[{"id":297,"kind":"bool","value":true},{"id":298,"kind":"int","value":1},{"id":299,"kind":"int","value":0}]}},{"kind":"var","name":"f1","typeAnnotation":{"id":318,"kind":"fun","arg":{"id":316,"kind":"str"},"res":{"id":317,"kind":"int"}},"id":319,"depth":0},{"kind":"const","name":"MyFunFun","typeAnnotation":{"id":31,"kind":"fun","arg":{"id":29,"kind":"fun","arg":{"id":27,"kind":"int"},"res":{"id":28,"kind":"str"}},"res":{"id":30,"kind":"bool"}},"id":32,"depth":0},{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}}},{"id":340,"kind":"def","name":"oper_in","qualifier":"val","expr":{"id":339,"kind":"app","opcode":"in","args":[{"id":337,"kind":"int","value":1},{"id":338,"kind":"app","opcode":"Set","args":[]}]}},{"kind":"const","name":"MyOperator","typeAnnotation":{"id":36,"kind":"oper","args":[{"id":33,"kind":"int"},{"id":34,"kind":"str"}],"res":{"id":35,"kind":"bool"}},"id":37,"depth":0},{"id":405,"kind":"def","name":"one","qualifier":"val","expr":{"id":404,"kind":"app","opcode":"head","args":[{"id":403,"kind":"app","opcode":"List","args":[{"id":401,"kind":"int","value":1},{"id":402,"kind":"int","value":2}]}]}},{"id":410,"kind":"def","name":"test_tuple","qualifier":"val","expr":{"id":409,"kind":"app","opcode":"Tup","args":[{"id":406,"kind":"int","value":1},{"id":407,"kind":"int","value":2},{"id":408,"kind":"int","value":3}]}},{"id":415,"kind":"def","name":"test_tuple2","qualifier":"val","expr":{"id":414,"kind":"app","opcode":"Tup","args":[{"id":411,"kind":"int","value":1},{"id":412,"kind":"int","value":2},{"id":413,"kind":"int","value":3}]}},{"id":419,"kind":"def","name":"test_pair","qualifier":"val","expr":{"id":418,"kind":"app","opcode":"Tup","args":[{"id":416,"kind":"int","value":4},{"id":417,"kind":"int","value":5}]}},{"kind":"const","name":"MyOperatorWithComma","typeAnnotation":{"id":41,"kind":"oper","args":[{"id":38,"kind":"int"},{"id":39,"kind":"str"}],"res":{"id":40,"kind":"bool"}},"id":42,"depth":0},{"id":424,"kind":"def","name":"test_list","qualifier":"val","expr":{"id":423,"kind":"app","opcode":"List","args":[{"id":420,"kind":"int","value":1},{"id":421,"kind":"int","value":2},{"id":422,"kind":"int","value":3}]}},{"id":429,"kind":"def","name":"test_list2","qualifier":"val","expr":{"id":428,"kind":"app","opcode":"List","args":[{"id":425,"kind":"int","value":1},{"id":426,"kind":"int","value":2},{"id":427,"kind":"int","value":3}]}},{"id":436,"kind":"def","name":"test_list_nth","qualifier":"val","expr":{"id":435,"kind":"app","opcode":"nth","args":[{"id":433,"kind":"app","opcode":"List","args":[{"id":430,"kind":"int","value":2},{"id":431,"kind":"int","value":3},{"id":432,"kind":"int","value":4}]},{"id":434,"kind":"int","value":2}]}},{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]}},{"id":448,"kind":"def","name":"test_record2","qualifier":"val","expr":{"id":447,"kind":"app","opcode":"Rec","args":[{"id":443,"kind":"str","value":"name"},{"id":444,"kind":"str","value":"igor"},{"id":445,"kind":"str","value":"year"},{"id":446,"kind":"int","value":1981}]}},{"id":461,"kind":"def","name":"test_set","qualifier":"val","expr":{"id":460,"kind":"app","opcode":"Set","args":[{"id":457,"kind":"int","value":1},{"id":458,"kind":"int","value":2},{"id":459,"kind":"int","value":3}]}},{"kind":"const","name":"MyTuple","typeAnnotation":{"id":46,"kind":"tup","fields":{"kind":"row","fields":[{"fieldName":"0","fieldType":{"id":43,"kind":"int"}},{"fieldName":"1","fieldType":{"id":44,"kind":"bool"}},{"fieldName":"2","fieldType":{"id":45,"kind":"str"}}],"other":{"kind":"empty"}}},"id":47,"depth":0},{"id":491,"kind":"def","name":"in_2_empty","qualifier":"val","expr":{"id":490,"kind":"app","opcode":"in","args":[{"id":488,"kind":"int","value":2},{"id":489,"kind":"app","opcode":"Set","args":[]}]}},{"id":495,"kind":"def","name":"subseteq_empty","qualifier":"val","expr":{"id":494,"kind":"app","opcode":"subseteq","args":[{"id":492,"kind":"app","opcode":"Set","args":[]},{"id":493,"kind":"app","opcode":"Set","args":[]}]}},{"id":504,"kind":"def","name":"powersets","qualifier":"val","expr":{"id":503,"kind":"app","opcode":"in","args":[{"id":497,"kind":"app","opcode":"Set","args":[{"id":496,"kind":"int","value":1}]},{"id":502,"kind":"app","opcode":"powerset","args":[{"id":501,"kind":"app","opcode":"Set","args":[{"id":498,"kind":"int","value":1},{"id":499,"kind":"int","value":2},{"id":500,"kind":"int","value":3}]}]}]}},{"id":510,"kind":"def","name":"lists","qualifier":"val","expr":{"id":509,"kind":"app","opcode":"allListsUpTo","args":[{"id":507,"kind":"app","opcode":"Set","args":[{"id":505,"kind":"int","value":1},{"id":506,"kind":"int","value":2}]},{"id":508,"kind":"int","value":3}]}},{"kind":"const","name":"MyTupleWithComma","typeAnnotation":{"id":51,"kind":"tup","fields":{"kind":"row","fields":[{"fieldName":"0","fieldType":{"id":48,"kind":"int"}},{"fieldName":"1","fieldType":{"id":49,"kind":"bool"}},{"fieldName":"2","fieldType":{"id":50,"kind":"str"}}],"other":{"kind":"empty"}}},"id":52,"depth":0},{"id":523,"kind":"typedef","name":"INT_SET","type":{"id":522,"kind":"set","elem":{"id":521,"kind":"int"}},"depth":0},{"id":524,"kind":"typedef","name":"UNINTERPRETED_TYPE","depth":0},{"kind":"const","name":"MyRecord","typeAnnotation":{"id":56,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"i","fieldType":{"id":53,"kind":"int"}},{"fieldName":"b","fieldType":{"id":54,"kind":"bool"}},{"fieldName":"s","fieldType":{"id":55,"kind":"str"}}],"other":{"kind":"empty"}}},"id":57,"depth":0},{"kind":"const","name":"MyRecordWithComma","typeAnnotation":{"id":61,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"i","fieldType":{"id":58,"kind":"int"}},{"fieldName":"b","fieldType":{"id":59,"kind":"bool"}},{"fieldName":"s","fieldType":{"id":60,"kind":"str"}}],"other":{"kind":"empty"}}},"id":62,"depth":0},{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},{"kind":"var","name":"i","typeAnnotation":{"id":90,"kind":"int"},"id":91,"depth":0},{"kind":"var","name":"j","typeAnnotation":{"id":92,"kind":"bool"},"id":93,"depth":0},{"id":97,"kind":"def","name":"add_1_to_2","qualifier":"val","expr":{"id":96,"kind":"app","opcode":"iadd","args":[{"id":94,"kind":"int","value":1},{"id":95,"kind":"int","value":2}]}},{"id":160,"kind":"def","name":"there_is_truth","qualifier":"val","expr":{"id":159,"kind":"app","opcode":"exists","args":[{"id":155,"kind":"name","name":"Bool"},{"id":158,"kind":"lambda","params":[{"id":156,"name":"x"}],"qualifier":"def","expr":{"id":157,"kind":"name","name":"x"}}]}},{"id":171,"kind":"def","name":"withUninterpretedType","qualifier":"val","expr":{"id":170,"kind":"app","opcode":"Set","args":[]},"typeAnnotation":{"id":169,"kind":"set","elem":{"id":168,"kind":"const","name":"PROC"}}},{"id":182,"kind":"def","name":"add","qualifier":"puredef","expr":{"id":181,"kind":"lambda","params":[{"id":176,"name":"x"},{"id":177,"name":"y"}],"qualifier":"puredef","expr":{"id":180,"kind":"app","opcode":"iadd","args":[{"id":178,"kind":"name","name":"x"},{"id":179,"kind":"name","name":"y"}]}}},{"id":188,"kind":"def","name":"ofN","qualifier":"def","expr":{"id":187,"kind":"lambda","params":[{"id":183,"name":"factor"}],"qualifier":"def","expr":{"id":186,"kind":"app","opcode":"imul","args":[{"id":184,"kind":"name","name":"factor"},{"id":185,"kind":"name","name":"n"}]}}},{"id":194,"kind":"def","name":"A","qualifier":"action","expr":{"id":193,"kind":"lambda","params":[{"id":189,"name":"x"}],"qualifier":"action","expr":{"id":192,"kind":"app","opcode":"assign","args":[{"id":191,"kind":"name","name":"n"},{"id":190,"kind":"name","name":"x"}]}}},{"id":199,"kind":"def","name":"B","qualifier":"puredef","expr":{"id":198,"kind":"lambda","params":[{"id":195,"name":"x"}],"qualifier":"puredef","expr":{"id":197,"kind":"app","opcode":"not","args":[{"id":196,"kind":"name","name":"x"}]}}},{"id":210,"kind":"def","name":"H","qualifier":"def","expr":{"id":209,"kind":"lambda","params":[{"id":200,"name":"x"},{"id":201,"name":"y"}],"qualifier":"def","expr":{"id":208,"kind":"app","opcode":"iadd","args":[{"id":206,"kind":"name","name":"x"},{"id":207,"kind":"name","name":"y"}]}},"typeAnnotation":{"id":205,"kind":"oper","args":[{"id":202,"kind":"int"},{"id":203,"kind":"int"}],"res":{"id":204,"kind":"int"}}},{"id":217,"kind":"def","name":"Pol","qualifier":"def","expr":{"id":216,"kind":"lambda","params":[{"id":211,"name":"x"}],"qualifier":"def","expr":{"id":215,"kind":"name","name":"x"}},"typeAnnotation":{"id":214,"kind":"oper","args":[{"id":212,"kind":"var","name":"a"}],"res":{"id":213,"kind":"var","name":"a"}}},{"id":223,"kind":"def","name":"asgn","qualifier":"action","expr":{"id":222,"kind":"app","opcode":"assign","args":[{"id":221,"kind":"name","name":"k"},{"id":220,"kind":"int","value":3}]}},{"id":236,"kind":"def","name":"min","qualifier":"puredef","expr":{"id":235,"kind":"lambda","params":[{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"}},{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"}}],"qualifier":"puredef","expr":{"id":234,"kind":"app","opcode":"ite","args":[{"id":231,"kind":"app","opcode":"ilt","args":[{"id":229,"kind":"name","name":"x"},{"id":230,"kind":"name","name":"y"}]},{"id":232,"kind":"name","name":"x"},{"id":233,"kind":"name","name":"y"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":224,"kind":"int"},{"id":226,"kind":"int"}],"res":{"id":228,"kind":"int"}}},{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}}},{"id":315,"kind":"def","name":"test_ite2","qualifier":"def","expr":{"id":314,"kind":"lambda","params":[{"id":302,"name":"x"},{"id":303,"name":"y"}],"qualifier":"def","expr":{"id":313,"kind":"app","opcode":"ite","args":[{"id":306,"kind":"app","opcode":"ilt","args":[{"id":304,"kind":"name","name":"x"},{"id":305,"kind":"int","value":10}]},{"id":309,"kind":"app","opcode":"iadd","args":[{"id":307,"kind":"name","name":"y"},{"id":308,"kind":"int","value":5}]},{"id":312,"kind":"app","opcode":"imod","args":[{"id":310,"kind":"name","name":"y"},{"id":311,"kind":"int","value":5}]}]}}},{"id":323,"kind":"def","name":"funapp","qualifier":"val","expr":{"id":322,"kind":"app","opcode":"get","args":[{"id":320,"kind":"name","name":"f1"},{"id":321,"kind":"str","value":"a"}]}},{"id":332,"kind":"def","name":"oper_app_normal","qualifier":"val","expr":{"id":331,"kind":"app","opcode":"MyOper","args":[{"id":329,"kind":"str","value":"a"},{"id":330,"kind":"int","value":42}]}},{"id":336,"kind":"def","name":"oper_app_ufcs","qualifier":"val","expr":{"id":335,"kind":"app","opcode":"MyOper","args":[{"id":333,"kind":"str","value":"a"},{"id":334,"kind":"int","value":42}]}},{"id":348,"kind":"def","name":"oper_app_dot1","qualifier":"val","expr":{"id":347,"kind":"app","opcode":"filter","args":[{"id":341,"kind":"name","name":"S"},{"id":346,"kind":"lambda","params":[{"id":342,"name":"x"}],"qualifier":"def","expr":{"id":345,"kind":"app","opcode":"igt","args":[{"id":343,"kind":"name","name":"x"},{"id":344,"kind":"int","value":10}]}}]}},{"id":386,"kind":"def","name":"oper_app_dot4","qualifier":"val","expr":{"id":385,"kind":"app","opcode":"filter","args":[{"id":381,"kind":"name","name":"S"},{"id":384,"kind":"lambda","params":[{"id":382,"name":"_"}],"qualifier":"def","expr":{"id":383,"kind":"bool","value":true}}]}},{"id":394,"kind":"def","name":"f","qualifier":"val","expr":{"id":393,"kind":"app","opcode":"mapBy","args":[{"id":387,"kind":"name","name":"S"},{"id":392,"kind":"lambda","params":[{"id":388,"name":"x"}],"qualifier":"def","expr":{"id":391,"kind":"app","opcode":"iadd","args":[{"id":389,"kind":"name","name":"x"},{"id":390,"kind":"int","value":1}]}}]}},{"id":400,"kind":"def","name":"set_difference","qualifier":"val","expr":{"id":399,"kind":"app","opcode":"exclude","args":[{"id":395,"kind":"name","name":"S"},{"id":398,"kind":"app","opcode":"Set","args":[{"id":396,"kind":"int","value":3},{"id":397,"kind":"int","value":4}]}]}},{"id":456,"kind":"def","name":"test_record3","qualifier":"val","expr":{"id":455,"kind":"app","opcode":"with","args":[{"id":454,"kind":"app","opcode":"with","args":[{"id":453,"kind":"name","name":"test_record"},{"id":450,"kind":"str","value":"name"},{"id":449,"kind":"str","value":"quint"}]},{"id":452,"kind":"str","value":"year"},{"id":451,"kind":"int","value":2023}]}},{"id":472,"kind":"def","name":"rec_field","qualifier":"val","expr":{"id":471,"kind":"let","opdef":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]}},"expr":{"id":470,"kind":"app","opcode":"field","args":[{"id":468,"kind":"name","name":"my_rec"},{"id":469,"kind":"str","value":"a"}]}}},{"id":481,"kind":"def","name":"tup_elem","qualifier":"val","expr":{"id":480,"kind":"let","opdef":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]}},"expr":{"id":479,"kind":"app","opcode":"item","args":[{"id":477,"kind":"name","name":"my_tup"},{"id":478,"kind":"int","value":2}]}}},{"id":487,"kind":"def","name":"isEmpty","qualifier":"def","expr":{"id":486,"kind":"lambda","params":[{"id":482,"name":"s"}],"qualifier":"def","expr":{"id":485,"kind":"app","opcode":"eq","args":[{"id":483,"kind":"name","name":"s"},{"id":484,"kind":"app","opcode":"List","args":[]}]}}},{"id":514,"kind":"assume","name":"positive","assumption":{"id":513,"kind":"app","opcode":"igt","args":[{"id":511,"kind":"name","name":"N"},{"id":512,"kind":"int","value":0}]},"depth":0},{"id":518,"kind":"assume","name":"_","assumption":{"id":517,"kind":"app","opcode":"neq","args":[{"id":515,"kind":"name","name":"N"},{"id":516,"kind":"int","value":0}]}},{"id":519,"kind":"import","defName":"foo","protoName":"M1"},{"id":520,"kind":"import","defName":"*","protoName":"M2"},{"kind":"var","name":"S1","typeAnnotation":{"id":525,"kind":"const","name":"INT_SET"},"id":526,"depth":0},{"id":529,"kind":"instance","qualifiedName":"Inst1","protoName":"Proto","overrides":[[{"id":528,"name":"N"},{"id":527,"kind":"name","name":"N"}]],"identityOverride":false},{"id":75,"kind":"def","name":"Circle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":63,"kind":"int"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":74,"kind":"lambda","params":[{"id":71,"name":"__CircleParam"}],"qualifier":"def","expr":{"id":73,"kind":"app","opcode":"variant","args":[{"id":70,"kind":"str","value":"Circle"},{"kind":"name","name":"__CircleParam","id":72}]}}},{"id":81,"kind":"def","name":"Rectangle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":80,"kind":"lambda","params":[{"id":77,"name":"__RectangleParam"}],"qualifier":"def","expr":{"id":79,"kind":"app","opcode":"variant","args":[{"id":76,"kind":"str","value":"Rectangle"},{"kind":"name","name":"__RectangleParam","id":78}]}}},{"id":87,"kind":"def","name":"Dog","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":67,"kind":"str"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":86,"kind":"lambda","params":[{"id":83,"name":"__DogParam"}],"qualifier":"def","expr":{"id":85,"kind":"app","opcode":"variant","args":[{"id":82,"kind":"str","value":"Dog"},{"kind":"name","name":"__DogParam","id":84}]}}},{"kind":"const","name":"MyUnion","typeAnnotation":{"id":88,"kind":"const","name":"MyUnionType"},"id":89,"depth":0},{"id":260,"kind":"def","name":"G","qualifier":"def","expr":{"id":259,"kind":"lambda","params":[{"id":253,"name":"x"}],"qualifier":"def","expr":{"id":258,"kind":"app","opcode":"and","args":[{"id":255,"kind":"app","opcode":"F","args":[{"id":254,"kind":"name","name":"x"}]},{"id":257,"kind":"app","opcode":"not","args":[{"id":256,"kind":"name","name":"x"}]}]}}},{"id":268,"kind":"def","name":"test_and_arg","qualifier":"def","expr":{"id":267,"kind":"lambda","params":[{"id":261,"name":"x"}],"qualifier":"def","expr":{"id":266,"kind":"app","opcode":"and","args":[{"id":263,"kind":"app","opcode":"F","args":[{"id":262,"kind":"name","name":"x"}]},{"id":265,"kind":"app","opcode":"not","args":[{"id":264,"kind":"name","name":"x"}]}]}}},{"id":276,"kind":"def","name":"test_or_arg","qualifier":"def","expr":{"id":275,"kind":"lambda","params":[{"id":269,"name":"x"}],"qualifier":"def","expr":{"id":274,"kind":"app","opcode":"or","args":[{"id":271,"kind":"app","opcode":"F","args":[{"id":270,"kind":"name","name":"x"}]},{"id":273,"kind":"app","opcode":"not","args":[{"id":272,"kind":"name","name":"x"}]}]}}},{"id":368,"kind":"def","name":"tuple_sum","qualifier":"val","expr":{"id":367,"kind":"app","opcode":"map","args":[{"id":351,"kind":"app","opcode":"tuples","args":[{"id":349,"kind":"name","name":"S"},{"id":350,"kind":"name","name":"MySet"}]},{"id":366,"kind":"lambda","params":[{"id":357,"name":"quintTupledLambdaParam357"}],"qualifier":"def","expr":{"id":362,"kind":"let","opdef":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]}},"expr":{"id":358,"kind":"let","opdef":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]}},"expr":{"id":356,"kind":"app","opcode":"iadd","args":[{"id":354,"kind":"name","name":"s"},{"id":355,"kind":"name","name":"mys"}]}}}}]}},{"id":380,"kind":"def","name":"oper_nondet","qualifier":"action","expr":{"id":379,"kind":"let","opdef":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]}},"expr":{"id":378,"kind":"app","opcode":"actionAll","args":[{"id":374,"kind":"app","opcode":"igt","args":[{"id":372,"kind":"name","name":"x"},{"id":373,"kind":"int","value":10}]},{"id":377,"kind":"app","opcode":"assign","args":[{"id":376,"kind":"name","name":"k"},{"id":375,"kind":"name","name":"x"}]}]}}}]}],"table":{"2":{"id":2,"kind":"def","name":"foo","qualifier":"val","expr":{"id":1,"kind":"int","value":4},"depth":0},"5":{"id":5,"kind":"def","name":"bar","qualifier":"val","expr":{"id":4,"kind":"int","value":4},"depth":0},"69":{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},"72":{"id":71,"name":"__CircleParam","kind":"param","depth":1},"75":{"id":75,"kind":"def","name":"Circle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":63,"kind":"int"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":74,"kind":"lambda","params":[{"id":71,"name":"__CircleParam"}],"qualifier":"def","expr":{"id":73,"kind":"app","opcode":"variant","args":[{"id":70,"kind":"str","value":"Circle"},{"kind":"name","name":"__CircleParam","id":72}]}},"depth":0},"78":{"id":77,"name":"__RectangleParam","kind":"param","depth":1},"81":{"id":81,"kind":"def","name":"Rectangle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":80,"kind":"lambda","params":[{"id":77,"name":"__RectangleParam"}],"qualifier":"def","expr":{"id":79,"kind":"app","opcode":"variant","args":[{"id":76,"kind":"str","value":"Rectangle"},{"kind":"name","name":"__RectangleParam","id":78}]}},"depth":0},"84":{"id":83,"name":"__DogParam","kind":"param","depth":1},"87":{"id":87,"kind":"def","name":"Dog","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":67,"kind":"str"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":86,"kind":"lambda","params":[{"id":83,"name":"__DogParam"}],"qualifier":"def","expr":{"id":85,"kind":"app","opcode":"variant","args":[{"id":82,"kind":"str","value":"Dog"},{"kind":"name","name":"__DogParam","id":84}]}},"depth":0},"88":{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},"97":{"id":97,"kind":"def","name":"add_1_to_2","qualifier":"val","expr":{"id":96,"kind":"app","opcode":"iadd","args":[{"id":94,"kind":"int","value":1},{"id":95,"kind":"int","value":2}]},"depth":0},"101":{"id":101,"kind":"def","name":"sub_1_to_2","qualifier":"val","expr":{"id":100,"kind":"app","opcode":"isub","args":[{"id":98,"kind":"int","value":1},{"id":99,"kind":"int","value":2}]},"depth":0},"105":{"id":105,"kind":"def","name":"mul_2_to_3","qualifier":"val","expr":{"id":104,"kind":"app","opcode":"imul","args":[{"id":102,"kind":"int","value":2},{"id":103,"kind":"int","value":3}]},"depth":0},"109":{"id":109,"kind":"def","name":"div_2_to_3","qualifier":"val","expr":{"id":108,"kind":"app","opcode":"idiv","args":[{"id":106,"kind":"int","value":2},{"id":107,"kind":"int","value":3}]},"depth":0},"113":{"id":113,"kind":"def","name":"mod_2_to_3","qualifier":"val","expr":{"id":112,"kind":"app","opcode":"imod","args":[{"id":110,"kind":"int","value":2},{"id":111,"kind":"int","value":3}]},"depth":0},"117":{"id":117,"kind":"def","name":"pow_2_to_3","qualifier":"val","expr":{"id":116,"kind":"app","opcode":"ipow","args":[{"id":114,"kind":"int","value":2},{"id":115,"kind":"int","value":3}]},"depth":0},"120":{"id":120,"kind":"def","name":"uminus","qualifier":"val","expr":{"id":119,"kind":"app","opcode":"iuminus","args":[{"id":118,"kind":"int","value":100}]},"depth":0},"124":{"id":124,"kind":"def","name":"gt_2_to_3","qualifier":"val","expr":{"id":123,"kind":"app","opcode":"igt","args":[{"id":121,"kind":"int","value":2},{"id":122,"kind":"int","value":3}]},"depth":0},"128":{"id":128,"kind":"def","name":"ge_2_to_3","qualifier":"val","expr":{"id":127,"kind":"app","opcode":"igte","args":[{"id":125,"kind":"int","value":2},{"id":126,"kind":"int","value":3}]},"depth":0},"132":{"id":132,"kind":"def","name":"lt_2_to_3","qualifier":"val","expr":{"id":131,"kind":"app","opcode":"ilt","args":[{"id":129,"kind":"int","value":2},{"id":130,"kind":"int","value":3}]},"depth":0},"136":{"id":136,"kind":"def","name":"le_2_to_3","qualifier":"val","expr":{"id":135,"kind":"app","opcode":"ilte","args":[{"id":133,"kind":"int","value":2},{"id":134,"kind":"int","value":3}]},"depth":0},"140":{"id":140,"kind":"def","name":"eqeq_2_to_3","qualifier":"val","expr":{"id":139,"kind":"app","opcode":"eq","args":[{"id":137,"kind":"int","value":2},{"id":138,"kind":"int","value":3}]},"depth":0},"144":{"id":144,"kind":"def","name":"ne_2_to_3","qualifier":"val","expr":{"id":143,"kind":"app","opcode":"neq","args":[{"id":141,"kind":"int","value":2},{"id":142,"kind":"int","value":3}]},"depth":0},"150":{"id":150,"kind":"def","name":"VeryTrue","qualifier":"val","expr":{"id":149,"kind":"app","opcode":"eq","args":[{"id":147,"kind":"app","opcode":"iadd","args":[{"id":145,"kind":"int","value":2},{"id":146,"kind":"int","value":2}]},{"id":148,"kind":"int","value":4}]},"depth":0},"154":{"id":154,"kind":"def","name":"nat_includes_one","qualifier":"val","expr":{"id":153,"kind":"app","opcode":"in","args":[{"id":151,"kind":"int","value":1},{"id":152,"kind":"name","name":"Nat"}]},"depth":0},"157":{"id":156,"name":"x","kind":"param","depth":1},"160":{"id":160,"kind":"def","name":"there_is_truth","qualifier":"val","expr":{"id":159,"kind":"app","opcode":"exists","args":[{"id":155,"kind":"name","name":"Bool"},{"id":158,"kind":"lambda","params":[{"id":156,"name":"x"}],"qualifier":"def","expr":{"id":157,"kind":"name","name":"x"}}]},"depth":0},"166":{"id":166,"kind":"def","name":"withType","qualifier":"val","expr":{"id":165,"kind":"app","opcode":"Set","args":[{"id":163,"kind":"int","value":1},{"id":164,"kind":"int","value":2}]},"typeAnnotation":{"id":162,"kind":"set","elem":{"id":161,"kind":"int"}},"depth":0},"168":{"id":167,"kind":"typedef","name":"PROC","depth":0},"171":{"id":171,"kind":"def","name":"withUninterpretedType","qualifier":"val","expr":{"id":170,"kind":"app","opcode":"Set","args":[]},"typeAnnotation":{"id":169,"kind":"set","elem":{"id":168,"kind":"const","name":"PROC"}},"depth":0},"175":{"id":175,"kind":"def","name":"magicNumber","qualifier":"pureval","expr":{"id":174,"kind":"int","value":42},"depth":0},"178":{"id":176,"name":"x","kind":"param","depth":1,"shadowing":false},"179":{"id":177,"name":"y","kind":"param","depth":1},"182":{"id":182,"kind":"def","name":"add","qualifier":"puredef","expr":{"id":181,"kind":"lambda","params":[{"id":176,"name":"x"},{"id":177,"name":"y"}],"qualifier":"puredef","expr":{"id":180,"kind":"app","opcode":"iadd","args":[{"id":178,"kind":"name","name":"x"},{"id":179,"kind":"name","name":"y"}]}},"depth":0},"184":{"id":183,"name":"factor","kind":"param","depth":1},"185":{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},"188":{"id":188,"kind":"def","name":"ofN","qualifier":"def","expr":{"id":187,"kind":"lambda","params":[{"id":183,"name":"factor"}],"qualifier":"def","expr":{"id":186,"kind":"app","opcode":"imul","args":[{"id":184,"kind":"name","name":"factor"},{"id":185,"kind":"name","name":"n"}]}},"depth":0},"190":{"id":189,"name":"x","kind":"param","depth":1,"shadowing":false},"191":{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},"194":{"id":194,"kind":"def","name":"A","qualifier":"action","expr":{"id":193,"kind":"lambda","params":[{"id":189,"name":"x"}],"qualifier":"action","expr":{"id":192,"kind":"app","opcode":"assign","args":[{"id":191,"kind":"name","name":"n"},{"id":190,"kind":"name","name":"x"}]}},"depth":0},"196":{"id":195,"name":"x","kind":"param","depth":1,"shadowing":false},"199":{"id":199,"kind":"def","name":"B","qualifier":"puredef","expr":{"id":198,"kind":"lambda","params":[{"id":195,"name":"x"}],"qualifier":"puredef","expr":{"id":197,"kind":"app","opcode":"not","args":[{"id":196,"kind":"name","name":"x"}]}},"depth":0},"206":{"id":200,"name":"x","kind":"param","depth":1,"shadowing":false},"207":{"id":201,"name":"y","kind":"param","depth":1,"shadowing":false},"210":{"id":210,"kind":"def","name":"H","qualifier":"def","expr":{"id":209,"kind":"lambda","params":[{"id":200,"name":"x"},{"id":201,"name":"y"}],"qualifier":"def","expr":{"id":208,"kind":"app","opcode":"iadd","args":[{"id":206,"kind":"name","name":"x"},{"id":207,"kind":"name","name":"y"}]}},"typeAnnotation":{"id":205,"kind":"oper","args":[{"id":202,"kind":"int"},{"id":203,"kind":"int"}],"res":{"id":204,"kind":"int"}},"depth":0},"215":{"id":211,"name":"x","kind":"param","depth":1,"shadowing":false},"217":{"id":217,"kind":"def","name":"Pol","qualifier":"def","expr":{"id":216,"kind":"lambda","params":[{"id":211,"name":"x"}],"qualifier":"def","expr":{"id":215,"kind":"name","name":"x"}},"typeAnnotation":{"id":214,"kind":"oper","args":[{"id":212,"kind":"var","name":"a"}],"res":{"id":213,"kind":"var","name":"a"}},"depth":0},"221":{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},"223":{"id":223,"kind":"def","name":"asgn","qualifier":"action","expr":{"id":222,"kind":"app","opcode":"assign","args":[{"id":221,"kind":"name","name":"k"},{"id":220,"kind":"int","value":3}]},"depth":0},"229":{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"230":{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"232":{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"233":{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"236":{"id":236,"kind":"def","name":"min","qualifier":"puredef","expr":{"id":235,"kind":"lambda","params":[{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"}},{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"}}],"qualifier":"puredef","expr":{"id":234,"kind":"app","opcode":"ite","args":[{"id":231,"kind":"app","opcode":"ilt","args":[{"id":229,"kind":"name","name":"x"},{"id":230,"kind":"name","name":"y"}]},{"id":232,"kind":"name","name":"x"},{"id":233,"kind":"name","name":"y"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":224,"kind":"int"},{"id":226,"kind":"int"}],"res":{"id":228,"kind":"int"}},"depth":0},"240":{"id":240,"kind":"def","name":"test_and","qualifier":"val","expr":{"id":239,"kind":"app","opcode":"and","args":[{"id":237,"kind":"bool","value":false},{"id":238,"kind":"bool","value":true}]},"depth":0},"244":{"id":244,"kind":"def","name":"test_or","qualifier":"val","expr":{"id":243,"kind":"app","opcode":"or","args":[{"id":241,"kind":"bool","value":false},{"id":242,"kind":"bool","value":true}]},"depth":0},"248":{"id":248,"kind":"def","name":"test_implies","qualifier":"val","expr":{"id":247,"kind":"app","opcode":"implies","args":[{"id":245,"kind":"bool","value":false},{"id":246,"kind":"bool","value":true}]},"depth":0},"250":{"id":249,"name":"x","kind":"param","depth":1,"shadowing":false},"252":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"254":{"id":253,"name":"x","kind":"param","depth":1,"shadowing":false},"255":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"256":{"id":253,"name":"x","kind":"param","depth":1,"shadowing":false},"260":{"id":260,"kind":"def","name":"G","qualifier":"def","expr":{"id":259,"kind":"lambda","params":[{"id":253,"name":"x"}],"qualifier":"def","expr":{"id":258,"kind":"app","opcode":"and","args":[{"id":255,"kind":"app","opcode":"F","args":[{"id":254,"kind":"name","name":"x"}]},{"id":257,"kind":"app","opcode":"not","args":[{"id":256,"kind":"name","name":"x"}]}]}},"depth":0},"262":{"id":261,"name":"x","kind":"param","depth":1,"shadowing":false},"263":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"264":{"id":261,"name":"x","kind":"param","depth":1,"shadowing":false},"268":{"id":268,"kind":"def","name":"test_and_arg","qualifier":"def","expr":{"id":267,"kind":"lambda","params":[{"id":261,"name":"x"}],"qualifier":"def","expr":{"id":266,"kind":"app","opcode":"and","args":[{"id":263,"kind":"app","opcode":"F","args":[{"id":262,"kind":"name","name":"x"}]},{"id":265,"kind":"app","opcode":"not","args":[{"id":264,"kind":"name","name":"x"}]}]}},"depth":0},"270":{"id":269,"name":"x","kind":"param","depth":1,"shadowing":false},"271":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"272":{"id":269,"name":"x","kind":"param","depth":1,"shadowing":false},"276":{"id":276,"kind":"def","name":"test_or_arg","qualifier":"def","expr":{"id":275,"kind":"lambda","params":[{"id":269,"name":"x"}],"qualifier":"def","expr":{"id":274,"kind":"app","opcode":"or","args":[{"id":271,"kind":"app","opcode":"F","args":[{"id":270,"kind":"name","name":"x"}]},{"id":273,"kind":"app","opcode":"not","args":[{"id":272,"kind":"name","name":"x"}]}]}},"depth":0},"281":{"id":281,"kind":"def","name":"test_block_and","qualifier":"val","expr":{"id":280,"kind":"app","opcode":"and","args":[{"id":277,"kind":"bool","value":false},{"id":278,"kind":"bool","value":true},{"id":279,"kind":"bool","value":false}]},"depth":0},"286":{"id":286,"kind":"def","name":"test_action_and","qualifier":"action","expr":{"id":285,"kind":"app","opcode":"actionAll","args":[{"id":282,"kind":"bool","value":false},{"id":283,"kind":"bool","value":true},{"id":284,"kind":"bool","value":false}]},"depth":0},"291":{"id":291,"kind":"def","name":"test_block_or","qualifier":"val","expr":{"id":290,"kind":"app","opcode":"or","args":[{"id":287,"kind":"bool","value":false},{"id":288,"kind":"bool","value":true},{"id":289,"kind":"bool","value":false}]},"depth":0},"296":{"id":296,"kind":"def","name":"test_action_or","qualifier":"action","expr":{"id":295,"kind":"app","opcode":"actionAny","args":[{"id":292,"kind":"bool","value":false},{"id":293,"kind":"bool","value":true},{"id":294,"kind":"bool","value":false}]},"depth":0},"301":{"id":301,"kind":"def","name":"test_ite","qualifier":"val","expr":{"id":300,"kind":"app","opcode":"ite","args":[{"id":297,"kind":"bool","value":true},{"id":298,"kind":"int","value":1},{"id":299,"kind":"int","value":0}]},"depth":0},"304":{"id":302,"name":"x","kind":"param","depth":1,"shadowing":false},"307":{"id":303,"name":"y","kind":"param","depth":1,"shadowing":false},"310":{"id":303,"name":"y","kind":"param","depth":1,"shadowing":false},"315":{"id":315,"kind":"def","name":"test_ite2","qualifier":"def","expr":{"id":314,"kind":"lambda","params":[{"id":302,"name":"x"},{"id":303,"name":"y"}],"qualifier":"def","expr":{"id":313,"kind":"app","opcode":"ite","args":[{"id":306,"kind":"app","opcode":"ilt","args":[{"id":304,"kind":"name","name":"x"},{"id":305,"kind":"int","value":10}]},{"id":309,"kind":"app","opcode":"iadd","args":[{"id":307,"kind":"name","name":"y"},{"id":308,"kind":"int","value":5}]},{"id":312,"kind":"app","opcode":"imod","args":[{"id":310,"kind":"name","name":"y"},{"id":311,"kind":"int","value":5}]}]}},"depth":0},"320":{"kind":"var","name":"f1","typeAnnotation":{"id":318,"kind":"fun","arg":{"id":316,"kind":"str"},"res":{"id":317,"kind":"int"}},"id":319,"depth":0},"323":{"id":323,"kind":"def","name":"funapp","qualifier":"val","expr":{"id":322,"kind":"app","opcode":"get","args":[{"id":320,"kind":"name","name":"f1"},{"id":321,"kind":"str","value":"a"}]},"depth":0},"328":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"331":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"332":{"id":332,"kind":"def","name":"oper_app_normal","qualifier":"val","expr":{"id":331,"kind":"app","opcode":"MyOper","args":[{"id":329,"kind":"str","value":"a"},{"id":330,"kind":"int","value":42}]},"depth":0},"335":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"336":{"id":336,"kind":"def","name":"oper_app_ufcs","qualifier":"val","expr":{"id":335,"kind":"app","opcode":"MyOper","args":[{"id":333,"kind":"str","value":"a"},{"id":334,"kind":"int","value":42}]},"depth":0},"340":{"id":340,"kind":"def","name":"oper_in","qualifier":"val","expr":{"id":339,"kind":"app","opcode":"in","args":[{"id":337,"kind":"int","value":1},{"id":338,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"341":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"343":{"id":342,"name":"x","kind":"param","depth":1,"shadowing":false},"348":{"id":348,"kind":"def","name":"oper_app_dot1","qualifier":"val","expr":{"id":347,"kind":"app","opcode":"filter","args":[{"id":341,"kind":"name","name":"S"},{"id":346,"kind":"lambda","params":[{"id":342,"name":"x"}],"qualifier":"def","expr":{"id":345,"kind":"app","opcode":"igt","args":[{"id":343,"kind":"name","name":"x"},{"id":344,"kind":"int","value":10}]}}]},"depth":0},"349":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"350":{"kind":"const","name":"MySet","typeAnnotation":{"id":18,"kind":"set","elem":{"id":17,"kind":"int"}},"id":19,"depth":0},"352":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]},"depth":4},"353":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]},"depth":3},"354":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]},"depth":4},"355":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]},"depth":3},"360":{"id":357,"name":"quintTupledLambdaParam357","kind":"param","depth":1},"364":{"id":357,"name":"quintTupledLambdaParam357","kind":"param","depth":1},"368":{"id":368,"kind":"def","name":"tuple_sum","qualifier":"val","expr":{"id":367,"kind":"app","opcode":"map","args":[{"id":351,"kind":"app","opcode":"tuples","args":[{"id":349,"kind":"name","name":"S"},{"id":350,"kind":"name","name":"MySet"}]},{"id":366,"kind":"lambda","params":[{"id":357,"name":"quintTupledLambdaParam357"}],"qualifier":"def","expr":{"id":362,"kind":"let","opdef":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]}},"expr":{"id":358,"kind":"let","opdef":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]}},"expr":{"id":356,"kind":"app","opcode":"iadd","args":[{"id":354,"kind":"name","name":"s"},{"id":355,"kind":"name","name":"mys"}]}}}}]},"depth":0},"369":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"371":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"372":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"375":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"376":{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},"380":{"id":380,"kind":"def","name":"oper_nondet","qualifier":"action","expr":{"id":379,"kind":"let","opdef":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]}},"expr":{"id":378,"kind":"app","opcode":"actionAll","args":[{"id":374,"kind":"app","opcode":"igt","args":[{"id":372,"kind":"name","name":"x"},{"id":373,"kind":"int","value":10}]},{"id":377,"kind":"app","opcode":"assign","args":[{"id":376,"kind":"name","name":"k"},{"id":375,"kind":"name","name":"x"}]}]}},"depth":0},"381":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"386":{"id":386,"kind":"def","name":"oper_app_dot4","qualifier":"val","expr":{"id":385,"kind":"app","opcode":"filter","args":[{"id":381,"kind":"name","name":"S"},{"id":384,"kind":"lambda","params":[{"id":382,"name":"_"}],"qualifier":"def","expr":{"id":383,"kind":"bool","value":true}}]},"depth":0},"387":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"389":{"id":388,"name":"x","kind":"param","depth":1,"shadowing":false},"394":{"id":394,"kind":"def","name":"f","qualifier":"val","expr":{"id":393,"kind":"app","opcode":"mapBy","args":[{"id":387,"kind":"name","name":"S"},{"id":392,"kind":"lambda","params":[{"id":388,"name":"x"}],"qualifier":"def","expr":{"id":391,"kind":"app","opcode":"iadd","args":[{"id":389,"kind":"name","name":"x"},{"id":390,"kind":"int","value":1}]}}]},"depth":0},"395":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"400":{"id":400,"kind":"def","name":"set_difference","qualifier":"val","expr":{"id":399,"kind":"app","opcode":"exclude","args":[{"id":395,"kind":"name","name":"S"},{"id":398,"kind":"app","opcode":"Set","args":[{"id":396,"kind":"int","value":3},{"id":397,"kind":"int","value":4}]}]},"depth":0},"405":{"id":405,"kind":"def","name":"one","qualifier":"val","expr":{"id":404,"kind":"app","opcode":"head","args":[{"id":403,"kind":"app","opcode":"List","args":[{"id":401,"kind":"int","value":1},{"id":402,"kind":"int","value":2}]}]},"depth":0},"410":{"id":410,"kind":"def","name":"test_tuple","qualifier":"val","expr":{"id":409,"kind":"app","opcode":"Tup","args":[{"id":406,"kind":"int","value":1},{"id":407,"kind":"int","value":2},{"id":408,"kind":"int","value":3}]},"depth":0},"415":{"id":415,"kind":"def","name":"test_tuple2","qualifier":"val","expr":{"id":414,"kind":"app","opcode":"Tup","args":[{"id":411,"kind":"int","value":1},{"id":412,"kind":"int","value":2},{"id":413,"kind":"int","value":3}]},"depth":0},"419":{"id":419,"kind":"def","name":"test_pair","qualifier":"val","expr":{"id":418,"kind":"app","opcode":"Tup","args":[{"id":416,"kind":"int","value":4},{"id":417,"kind":"int","value":5}]},"depth":0},"424":{"id":424,"kind":"def","name":"test_list","qualifier":"val","expr":{"id":423,"kind":"app","opcode":"List","args":[{"id":420,"kind":"int","value":1},{"id":421,"kind":"int","value":2},{"id":422,"kind":"int","value":3}]},"depth":0},"429":{"id":429,"kind":"def","name":"test_list2","qualifier":"val","expr":{"id":428,"kind":"app","opcode":"List","args":[{"id":425,"kind":"int","value":1},{"id":426,"kind":"int","value":2},{"id":427,"kind":"int","value":3}]},"depth":0},"436":{"id":436,"kind":"def","name":"test_list_nth","qualifier":"val","expr":{"id":435,"kind":"app","opcode":"nth","args":[{"id":433,"kind":"app","opcode":"List","args":[{"id":430,"kind":"int","value":2},{"id":431,"kind":"int","value":3},{"id":432,"kind":"int","value":4}]},{"id":434,"kind":"int","value":2}]},"depth":0},"442":{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]},"depth":0},"448":{"id":448,"kind":"def","name":"test_record2","qualifier":"val","expr":{"id":447,"kind":"app","opcode":"Rec","args":[{"id":443,"kind":"str","value":"name"},{"id":444,"kind":"str","value":"igor"},{"id":445,"kind":"str","value":"year"},{"id":446,"kind":"int","value":1981}]},"depth":0},"453":{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]},"depth":0},"456":{"id":456,"kind":"def","name":"test_record3","qualifier":"val","expr":{"id":455,"kind":"app","opcode":"with","args":[{"id":454,"kind":"app","opcode":"with","args":[{"id":453,"kind":"name","name":"test_record"},{"id":450,"kind":"str","value":"name"},{"id":449,"kind":"str","value":"quint"}]},{"id":452,"kind":"str","value":"year"},{"id":451,"kind":"int","value":2023}]},"depth":0},"461":{"id":461,"kind":"def","name":"test_set","qualifier":"val","expr":{"id":460,"kind":"app","opcode":"Set","args":[{"id":457,"kind":"int","value":1},{"id":458,"kind":"int","value":2},{"id":459,"kind":"int","value":3}]},"depth":0},"467":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]},"depth":2},"468":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]},"depth":2},"472":{"id":472,"kind":"def","name":"rec_field","qualifier":"val","expr":{"id":471,"kind":"let","opdef":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]}},"expr":{"id":470,"kind":"app","opcode":"field","args":[{"id":468,"kind":"name","name":"my_rec"},{"id":469,"kind":"str","value":"a"}]}},"depth":0},"476":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]},"depth":2},"477":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]},"depth":2},"481":{"id":481,"kind":"def","name":"tup_elem","qualifier":"val","expr":{"id":480,"kind":"let","opdef":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]}},"expr":{"id":479,"kind":"app","opcode":"item","args":[{"id":477,"kind":"name","name":"my_tup"},{"id":478,"kind":"int","value":2}]}},"depth":0},"483":{"id":482,"name":"s","kind":"param","depth":1,"shadowing":false},"487":{"id":487,"kind":"def","name":"isEmpty","qualifier":"def","expr":{"id":486,"kind":"lambda","params":[{"id":482,"name":"s"}],"qualifier":"def","expr":{"id":485,"kind":"app","opcode":"eq","args":[{"id":483,"kind":"name","name":"s"},{"id":484,"kind":"app","opcode":"List","args":[]}]}},"depth":0},"491":{"id":491,"kind":"def","name":"in_2_empty","qualifier":"val","expr":{"id":490,"kind":"app","opcode":"in","args":[{"id":488,"kind":"int","value":2},{"id":489,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"495":{"id":495,"kind":"def","name":"subseteq_empty","qualifier":"val","expr":{"id":494,"kind":"app","opcode":"subseteq","args":[{"id":492,"kind":"app","opcode":"Set","args":[]},{"id":493,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"504":{"id":504,"kind":"def","name":"powersets","qualifier":"val","expr":{"id":503,"kind":"app","opcode":"in","args":[{"id":497,"kind":"app","opcode":"Set","args":[{"id":496,"kind":"int","value":1}]},{"id":502,"kind":"app","opcode":"powerset","args":[{"id":501,"kind":"app","opcode":"Set","args":[{"id":498,"kind":"int","value":1},{"id":499,"kind":"int","value":2},{"id":500,"kind":"int","value":3}]}]}]},"depth":0},"510":{"id":510,"kind":"def","name":"lists","qualifier":"val","expr":{"id":509,"kind":"app","opcode":"allListsUpTo","args":[{"id":507,"kind":"app","opcode":"Set","args":[{"id":505,"kind":"int","value":1},{"id":506,"kind":"int","value":2}]},{"id":508,"kind":"int","value":3}]},"depth":0},"511":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"515":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"525":{"id":523,"kind":"typedef","name":"INT_SET","type":{"id":522,"kind":"set","elem":{"id":521,"kind":"int"}},"depth":0},"527":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"528":{"kind":"const","name":"N","typeAnnotation":{"id":7,"kind":"int"},"id":527,"depth":0,"importedFrom":{"id":529,"kind":"instance","qualifiedName":"Inst1","protoName":"Proto","overrides":[[{"id":528,"name":"N"},{"id":527,"kind":"name","name":"N"}]],"identityOverride":false},"hidden":true,"namespaces":["Inst1","withConsts"]}},"errors":[]} \ No newline at end of file +{"stage":"parsing","warnings":[],"modules":[{"id":11,"name":"Proto","declarations":[{"kind":"var","name":"x","typeAnnotation":{"id":9,"kind":"int"},"id":10,"depth":0},{"kind":"const","name":"N","typeAnnotation":{"id":7,"kind":"int"},"id":8,"depth":0}]},{"id":3,"name":"M1","declarations":[{"id":2,"kind":"def","name":"foo","qualifier":"val","expr":{"id":1,"kind":"int","value":4}}]},{"id":6,"name":"M2","declarations":[{"id":5,"kind":"def","name":"bar","qualifier":"val","expr":{"id":4,"kind":"int","value":4}}]},{"id":530,"name":"withConsts","declarations":[{"id":101,"kind":"def","name":"sub_1_to_2","qualifier":"val","expr":{"id":100,"kind":"app","opcode":"isub","args":[{"id":98,"kind":"int","value":1},{"id":99,"kind":"int","value":2}]}},{"id":105,"kind":"def","name":"mul_2_to_3","qualifier":"val","expr":{"id":104,"kind":"app","opcode":"imul","args":[{"id":102,"kind":"int","value":2},{"id":103,"kind":"int","value":3}]}},{"id":109,"kind":"def","name":"div_2_to_3","qualifier":"val","expr":{"id":108,"kind":"app","opcode":"idiv","args":[{"id":106,"kind":"int","value":2},{"id":107,"kind":"int","value":3}]}},{"id":113,"kind":"def","name":"mod_2_to_3","qualifier":"val","expr":{"id":112,"kind":"app","opcode":"imod","args":[{"id":110,"kind":"int","value":2},{"id":111,"kind":"int","value":3}]}},{"id":117,"kind":"def","name":"pow_2_to_3","qualifier":"val","expr":{"id":116,"kind":"app","opcode":"ipow","args":[{"id":114,"kind":"int","value":2},{"id":115,"kind":"int","value":3}]}},{"id":120,"kind":"def","name":"uminus","qualifier":"val","expr":{"id":119,"kind":"app","opcode":"iuminus","args":[{"id":118,"kind":"int","value":100}]}},{"id":124,"kind":"def","name":"gt_2_to_3","qualifier":"val","expr":{"id":123,"kind":"app","opcode":"igt","args":[{"id":121,"kind":"int","value":2},{"id":122,"kind":"int","value":3}]}},{"id":128,"kind":"def","name":"ge_2_to_3","qualifier":"val","expr":{"id":127,"kind":"app","opcode":"igte","args":[{"id":125,"kind":"int","value":2},{"id":126,"kind":"int","value":3}]}},{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},{"id":132,"kind":"def","name":"lt_2_to_3","qualifier":"val","expr":{"id":131,"kind":"app","opcode":"ilt","args":[{"id":129,"kind":"int","value":2},{"id":130,"kind":"int","value":3}]}},{"id":136,"kind":"def","name":"le_2_to_3","qualifier":"val","expr":{"id":135,"kind":"app","opcode":"ilte","args":[{"id":133,"kind":"int","value":2},{"id":134,"kind":"int","value":3}]}},{"id":140,"kind":"def","name":"eqeq_2_to_3","qualifier":"val","expr":{"id":139,"kind":"app","opcode":"eq","args":[{"id":137,"kind":"int","value":2},{"id":138,"kind":"int","value":3}]}},{"id":144,"kind":"def","name":"ne_2_to_3","qualifier":"val","expr":{"id":143,"kind":"app","opcode":"neq","args":[{"id":141,"kind":"int","value":2},{"id":142,"kind":"int","value":3}]}},{"id":150,"kind":"def","name":"VeryTrue","qualifier":"val","expr":{"id":149,"kind":"app","opcode":"eq","args":[{"id":147,"kind":"app","opcode":"iadd","args":[{"id":145,"kind":"int","value":2},{"id":146,"kind":"int","value":2}]},{"id":148,"kind":"int","value":4}]}},{"id":154,"kind":"def","name":"nat_includes_one","qualifier":"val","expr":{"id":153,"kind":"app","opcode":"in","args":[{"id":151,"kind":"int","value":1},{"id":152,"kind":"name","name":"Nat"}]}},{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},{"id":166,"kind":"def","name":"withType","qualifier":"val","expr":{"id":165,"kind":"app","opcode":"Set","args":[{"id":163,"kind":"int","value":1},{"id":164,"kind":"int","value":2}]},"typeAnnotation":{"id":162,"kind":"set","elem":{"id":161,"kind":"int"}}},{"id":167,"kind":"typedef","name":"PROC","depth":0},{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},{"id":175,"kind":"def","name":"magicNumber","qualifier":"pureval","expr":{"id":174,"kind":"int","value":42}},{"kind":"const","name":"MySet","typeAnnotation":{"id":18,"kind":"set","elem":{"id":17,"kind":"int"}},"id":19,"depth":0},{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},{"kind":"const","name":"MySeq","typeAnnotation":{"id":21,"kind":"list","elem":{"id":20,"kind":"bool"}},"id":22,"depth":0},{"id":240,"kind":"def","name":"test_and","qualifier":"val","expr":{"id":239,"kind":"app","opcode":"and","args":[{"id":237,"kind":"bool","value":false},{"id":238,"kind":"bool","value":true}]}},{"id":244,"kind":"def","name":"test_or","qualifier":"val","expr":{"id":243,"kind":"app","opcode":"or","args":[{"id":241,"kind":"bool","value":false},{"id":242,"kind":"bool","value":true}]}},{"id":248,"kind":"def","name":"test_implies","qualifier":"val","expr":{"id":247,"kind":"app","opcode":"implies","args":[{"id":245,"kind":"bool","value":false},{"id":246,"kind":"bool","value":true}]}},{"kind":"const","name":"MyFun","typeAnnotation":{"id":25,"kind":"fun","arg":{"id":23,"kind":"int"},"res":{"id":24,"kind":"str"}},"id":26,"depth":0},{"id":281,"kind":"def","name":"test_block_and","qualifier":"val","expr":{"id":280,"kind":"app","opcode":"and","args":[{"id":277,"kind":"bool","value":false},{"id":278,"kind":"bool","value":true},{"id":279,"kind":"bool","value":false}]}},{"id":286,"kind":"def","name":"test_action_and","qualifier":"action","expr":{"id":285,"kind":"app","opcode":"actionAll","args":[{"id":282,"kind":"bool","value":false},{"id":283,"kind":"bool","value":true},{"id":284,"kind":"bool","value":false}]}},{"id":291,"kind":"def","name":"test_block_or","qualifier":"val","expr":{"id":290,"kind":"app","opcode":"or","args":[{"id":287,"kind":"bool","value":false},{"id":288,"kind":"bool","value":true},{"id":289,"kind":"bool","value":false}]}},{"id":296,"kind":"def","name":"test_action_or","qualifier":"action","expr":{"id":295,"kind":"app","opcode":"actionAny","args":[{"id":292,"kind":"bool","value":false},{"id":293,"kind":"bool","value":true},{"id":294,"kind":"bool","value":false}]}},{"id":301,"kind":"def","name":"test_ite","qualifier":"val","expr":{"id":300,"kind":"app","opcode":"ite","args":[{"id":297,"kind":"bool","value":true},{"id":298,"kind":"int","value":1},{"id":299,"kind":"int","value":0}]}},{"kind":"var","name":"f1","typeAnnotation":{"id":318,"kind":"fun","arg":{"id":316,"kind":"str"},"res":{"id":317,"kind":"int"}},"id":319,"depth":0},{"kind":"const","name":"MyFunFun","typeAnnotation":{"id":31,"kind":"fun","arg":{"id":29,"kind":"fun","arg":{"id":27,"kind":"int"},"res":{"id":28,"kind":"str"}},"res":{"id":30,"kind":"bool"}},"id":32,"depth":0},{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}}},{"id":340,"kind":"def","name":"oper_in","qualifier":"val","expr":{"id":339,"kind":"app","opcode":"in","args":[{"id":337,"kind":"int","value":1},{"id":338,"kind":"app","opcode":"Set","args":[]}]}},{"kind":"const","name":"MyOperator","typeAnnotation":{"id":36,"kind":"oper","args":[{"id":33,"kind":"int"},{"id":34,"kind":"str"}],"res":{"id":35,"kind":"bool"}},"id":37,"depth":0},{"id":405,"kind":"def","name":"one","qualifier":"val","expr":{"id":404,"kind":"app","opcode":"head","args":[{"id":403,"kind":"app","opcode":"List","args":[{"id":401,"kind":"int","value":1},{"id":402,"kind":"int","value":2}]}]}},{"id":410,"kind":"def","name":"test_tuple","qualifier":"val","expr":{"id":409,"kind":"app","opcode":"Tup","args":[{"id":406,"kind":"int","value":1},{"id":407,"kind":"int","value":2},{"id":408,"kind":"int","value":3}]}},{"id":415,"kind":"def","name":"test_tuple2","qualifier":"val","expr":{"id":414,"kind":"app","opcode":"Tup","args":[{"id":411,"kind":"int","value":1},{"id":412,"kind":"int","value":2},{"id":413,"kind":"int","value":3}]}},{"id":419,"kind":"def","name":"test_pair","qualifier":"val","expr":{"id":418,"kind":"app","opcode":"Tup","args":[{"id":416,"kind":"int","value":4},{"id":417,"kind":"int","value":5}]}},{"kind":"const","name":"MyOperatorWithComma","typeAnnotation":{"id":41,"kind":"oper","args":[{"id":38,"kind":"int"},{"id":39,"kind":"str"}],"res":{"id":40,"kind":"bool"}},"id":42,"depth":0},{"id":424,"kind":"def","name":"test_list","qualifier":"val","expr":{"id":423,"kind":"app","opcode":"List","args":[{"id":420,"kind":"int","value":1},{"id":421,"kind":"int","value":2},{"id":422,"kind":"int","value":3}]}},{"id":429,"kind":"def","name":"test_list2","qualifier":"val","expr":{"id":428,"kind":"app","opcode":"List","args":[{"id":425,"kind":"int","value":1},{"id":426,"kind":"int","value":2},{"id":427,"kind":"int","value":3}]}},{"id":436,"kind":"def","name":"test_list_nth","qualifier":"val","expr":{"id":435,"kind":"app","opcode":"nth","args":[{"id":433,"kind":"app","opcode":"List","args":[{"id":430,"kind":"int","value":2},{"id":431,"kind":"int","value":3},{"id":432,"kind":"int","value":4}]},{"id":434,"kind":"int","value":2}]}},{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]}},{"id":448,"kind":"def","name":"test_record2","qualifier":"val","expr":{"id":447,"kind":"app","opcode":"Rec","args":[{"id":443,"kind":"str","value":"name"},{"id":444,"kind":"str","value":"igor"},{"id":445,"kind":"str","value":"year"},{"id":446,"kind":"int","value":1981}]}},{"id":461,"kind":"def","name":"test_set","qualifier":"val","expr":{"id":460,"kind":"app","opcode":"Set","args":[{"id":457,"kind":"int","value":1},{"id":458,"kind":"int","value":2},{"id":459,"kind":"int","value":3}]}},{"kind":"const","name":"MyTuple","typeAnnotation":{"id":46,"kind":"tup","fields":{"kind":"row","fields":[{"fieldName":"0","fieldType":{"id":43,"kind":"int"}},{"fieldName":"1","fieldType":{"id":44,"kind":"bool"}},{"fieldName":"2","fieldType":{"id":45,"kind":"str"}}],"other":{"kind":"empty"}}},"id":47,"depth":0},{"id":491,"kind":"def","name":"in_2_empty","qualifier":"val","expr":{"id":490,"kind":"app","opcode":"in","args":[{"id":488,"kind":"int","value":2},{"id":489,"kind":"app","opcode":"Set","args":[]}]}},{"id":495,"kind":"def","name":"subseteq_empty","qualifier":"val","expr":{"id":494,"kind":"app","opcode":"subseteq","args":[{"id":492,"kind":"app","opcode":"Set","args":[]},{"id":493,"kind":"app","opcode":"Set","args":[]}]}},{"id":504,"kind":"def","name":"powersets","qualifier":"val","expr":{"id":503,"kind":"app","opcode":"in","args":[{"id":497,"kind":"app","opcode":"Set","args":[{"id":496,"kind":"int","value":1}]},{"id":502,"kind":"app","opcode":"powerset","args":[{"id":501,"kind":"app","opcode":"Set","args":[{"id":498,"kind":"int","value":1},{"id":499,"kind":"int","value":2},{"id":500,"kind":"int","value":3}]}]}]}},{"id":510,"kind":"def","name":"lists","qualifier":"val","expr":{"id":509,"kind":"app","opcode":"allListsUpTo","args":[{"id":507,"kind":"app","opcode":"Set","args":[{"id":505,"kind":"int","value":1},{"id":506,"kind":"int","value":2}]},{"id":508,"kind":"int","value":3}]}},{"kind":"const","name":"MyTupleWithComma","typeAnnotation":{"id":51,"kind":"tup","fields":{"kind":"row","fields":[{"fieldName":"0","fieldType":{"id":48,"kind":"int"}},{"fieldName":"1","fieldType":{"id":49,"kind":"bool"}},{"fieldName":"2","fieldType":{"id":50,"kind":"str"}}],"other":{"kind":"empty"}}},"id":52,"depth":0},{"id":523,"kind":"typedef","name":"INT_SET","type":{"id":522,"kind":"set","elem":{"id":521,"kind":"int"}},"depth":0},{"id":524,"kind":"typedef","name":"UNINTERPRETED_TYPE","depth":0},{"kind":"const","name":"MyRecord","typeAnnotation":{"id":56,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"i","fieldType":{"id":53,"kind":"int"}},{"fieldName":"b","fieldType":{"id":54,"kind":"bool"}},{"fieldName":"s","fieldType":{"id":55,"kind":"str"}}],"other":{"kind":"empty"}}},"id":57,"depth":0},{"kind":"const","name":"MyRecordWithComma","typeAnnotation":{"id":61,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"i","fieldType":{"id":58,"kind":"int"}},{"fieldName":"b","fieldType":{"id":59,"kind":"bool"}},{"fieldName":"s","fieldType":{"id":60,"kind":"str"}}],"other":{"kind":"empty"}}},"id":62,"depth":0},{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},{"kind":"var","name":"i","typeAnnotation":{"id":90,"kind":"int"},"id":91,"depth":0},{"kind":"var","name":"j","typeAnnotation":{"id":92,"kind":"bool"},"id":93,"depth":0},{"id":97,"kind":"def","name":"add_1_to_2","qualifier":"val","expr":{"id":96,"kind":"app","opcode":"iadd","args":[{"id":94,"kind":"int","value":1},{"id":95,"kind":"int","value":2}]}},{"id":160,"kind":"def","name":"there_is_truth","qualifier":"val","expr":{"id":159,"kind":"app","opcode":"exists","args":[{"id":155,"kind":"name","name":"Bool"},{"id":158,"kind":"lambda","params":[{"id":156,"name":"x"}],"qualifier":"def","expr":{"id":157,"kind":"name","name":"x"}}]}},{"id":171,"kind":"def","name":"withUninterpretedType","qualifier":"val","expr":{"id":170,"kind":"app","opcode":"Set","args":[]},"typeAnnotation":{"id":169,"kind":"set","elem":{"id":168,"kind":"const","name":"PROC"}}},{"id":182,"kind":"def","name":"add","qualifier":"puredef","expr":{"id":181,"kind":"lambda","params":[{"id":176,"name":"x"},{"id":177,"name":"y"}],"qualifier":"puredef","expr":{"id":180,"kind":"app","opcode":"iadd","args":[{"id":178,"kind":"name","name":"x"},{"id":179,"kind":"name","name":"y"}]}}},{"id":188,"kind":"def","name":"ofN","qualifier":"def","expr":{"id":187,"kind":"lambda","params":[{"id":183,"name":"factor"}],"qualifier":"def","expr":{"id":186,"kind":"app","opcode":"imul","args":[{"id":184,"kind":"name","name":"factor"},{"id":185,"kind":"name","name":"n"}]}}},{"id":194,"kind":"def","name":"A","qualifier":"action","expr":{"id":193,"kind":"lambda","params":[{"id":189,"name":"x"}],"qualifier":"action","expr":{"id":192,"kind":"app","opcode":"assign","args":[{"id":191,"kind":"name","name":"n"},{"id":190,"kind":"name","name":"x"}]}}},{"id":199,"kind":"def","name":"B","qualifier":"puredef","expr":{"id":198,"kind":"lambda","params":[{"id":195,"name":"x"}],"qualifier":"puredef","expr":{"id":197,"kind":"app","opcode":"not","args":[{"id":196,"kind":"name","name":"x"}]}}},{"id":210,"kind":"def","name":"H","qualifier":"def","expr":{"id":209,"kind":"lambda","params":[{"id":200,"name":"x"},{"id":201,"name":"y"}],"qualifier":"def","expr":{"id":208,"kind":"app","opcode":"iadd","args":[{"id":206,"kind":"name","name":"x"},{"id":207,"kind":"name","name":"y"}]}},"typeAnnotation":{"id":205,"kind":"oper","args":[{"id":202,"kind":"int"},{"id":203,"kind":"int"}],"res":{"id":204,"kind":"int"}}},{"id":217,"kind":"def","name":"Pol","qualifier":"def","expr":{"id":216,"kind":"lambda","params":[{"id":211,"name":"x"}],"qualifier":"def","expr":{"id":215,"kind":"name","name":"x"}},"typeAnnotation":{"id":214,"kind":"oper","args":[{"id":212,"kind":"var","name":"a"}],"res":{"id":213,"kind":"var","name":"a"}}},{"id":223,"kind":"def","name":"asgn","qualifier":"action","expr":{"id":222,"kind":"app","opcode":"assign","args":[{"id":221,"kind":"name","name":"k"},{"id":220,"kind":"int","value":3}]}},{"id":236,"kind":"def","name":"min","qualifier":"puredef","expr":{"id":235,"kind":"lambda","params":[{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"}},{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"}}],"qualifier":"puredef","expr":{"id":234,"kind":"app","opcode":"ite","args":[{"id":231,"kind":"app","opcode":"ilt","args":[{"id":229,"kind":"name","name":"x"},{"id":230,"kind":"name","name":"y"}]},{"id":232,"kind":"name","name":"x"},{"id":233,"kind":"name","name":"y"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":224,"kind":"int"},{"id":226,"kind":"int"}],"res":{"id":228,"kind":"int"}}},{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}}},{"id":315,"kind":"def","name":"test_ite2","qualifier":"def","expr":{"id":314,"kind":"lambda","params":[{"id":302,"name":"x"},{"id":303,"name":"y"}],"qualifier":"def","expr":{"id":313,"kind":"app","opcode":"ite","args":[{"id":306,"kind":"app","opcode":"ilt","args":[{"id":304,"kind":"name","name":"x"},{"id":305,"kind":"int","value":10}]},{"id":309,"kind":"app","opcode":"iadd","args":[{"id":307,"kind":"name","name":"y"},{"id":308,"kind":"int","value":5}]},{"id":312,"kind":"app","opcode":"imod","args":[{"id":310,"kind":"name","name":"y"},{"id":311,"kind":"int","value":5}]}]}}},{"id":323,"kind":"def","name":"funapp","qualifier":"val","expr":{"id":322,"kind":"app","opcode":"get","args":[{"id":320,"kind":"name","name":"f1"},{"id":321,"kind":"str","value":"a"}]}},{"id":332,"kind":"def","name":"oper_app_normal","qualifier":"val","expr":{"id":331,"kind":"app","opcode":"MyOper","args":[{"id":329,"kind":"str","value":"a"},{"id":330,"kind":"int","value":42}]}},{"id":336,"kind":"def","name":"oper_app_ufcs","qualifier":"val","expr":{"id":335,"kind":"app","opcode":"MyOper","args":[{"id":333,"kind":"str","value":"a"},{"id":334,"kind":"int","value":42}]}},{"id":348,"kind":"def","name":"oper_app_dot1","qualifier":"val","expr":{"id":347,"kind":"app","opcode":"filter","args":[{"id":341,"kind":"name","name":"S"},{"id":346,"kind":"lambda","params":[{"id":342,"name":"x"}],"qualifier":"def","expr":{"id":345,"kind":"app","opcode":"igt","args":[{"id":343,"kind":"name","name":"x"},{"id":344,"kind":"int","value":10}]}}]}},{"id":386,"kind":"def","name":"oper_app_dot4","qualifier":"val","expr":{"id":385,"kind":"app","opcode":"filter","args":[{"id":381,"kind":"name","name":"S"},{"id":384,"kind":"lambda","params":[{"id":382,"name":"_"}],"qualifier":"def","expr":{"id":383,"kind":"bool","value":true}}]}},{"id":394,"kind":"def","name":"f","qualifier":"val","expr":{"id":393,"kind":"app","opcode":"mapBy","args":[{"id":387,"kind":"name","name":"S"},{"id":392,"kind":"lambda","params":[{"id":388,"name":"x"}],"qualifier":"def","expr":{"id":391,"kind":"app","opcode":"iadd","args":[{"id":389,"kind":"name","name":"x"},{"id":390,"kind":"int","value":1}]}}]}},{"id":400,"kind":"def","name":"set_difference","qualifier":"val","expr":{"id":399,"kind":"app","opcode":"exclude","args":[{"id":395,"kind":"name","name":"S"},{"id":398,"kind":"app","opcode":"Set","args":[{"id":396,"kind":"int","value":3},{"id":397,"kind":"int","value":4}]}]}},{"id":456,"kind":"def","name":"test_record3","qualifier":"val","expr":{"id":455,"kind":"app","opcode":"with","args":[{"id":454,"kind":"app","opcode":"with","args":[{"id":453,"kind":"name","name":"test_record"},{"id":450,"kind":"str","value":"name"},{"id":449,"kind":"str","value":"quint"}]},{"id":452,"kind":"str","value":"year"},{"id":451,"kind":"int","value":2023}]}},{"id":472,"kind":"def","name":"rec_field","qualifier":"val","expr":{"id":471,"kind":"let","opdef":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]}},"expr":{"id":470,"kind":"app","opcode":"field","args":[{"id":468,"kind":"name","name":"my_rec"},{"id":469,"kind":"str","value":"a"}]}}},{"id":481,"kind":"def","name":"tup_elem","qualifier":"val","expr":{"id":480,"kind":"let","opdef":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]}},"expr":{"id":479,"kind":"app","opcode":"item","args":[{"id":477,"kind":"name","name":"my_tup"},{"id":478,"kind":"int","value":2}]}}},{"id":487,"kind":"def","name":"isEmpty","qualifier":"def","expr":{"id":486,"kind":"lambda","params":[{"id":482,"name":"s"}],"qualifier":"def","expr":{"id":485,"kind":"app","opcode":"eq","args":[{"id":483,"kind":"name","name":"s"},{"id":484,"kind":"app","opcode":"List","args":[]}]}}},{"id":514,"kind":"assume","name":"positive","assumption":{"id":513,"kind":"app","opcode":"igt","args":[{"id":511,"kind":"name","name":"N"},{"id":512,"kind":"int","value":0}]},"depth":0},{"id":518,"kind":"assume","name":"_","assumption":{"id":517,"kind":"app","opcode":"neq","args":[{"id":515,"kind":"name","name":"N"},{"id":516,"kind":"int","value":0}]}},{"id":519,"kind":"import","defName":"foo","protoName":"M1"},{"id":520,"kind":"import","defName":"*","protoName":"M2"},{"kind":"var","name":"S1","typeAnnotation":{"id":525,"kind":"const","name":"INT_SET"},"id":526,"depth":0},{"id":529,"kind":"instance","qualifiedName":"Inst1","protoName":"Proto","overrides":[[{"id":528,"name":"N"},{"id":527,"kind":"name","name":"N"}]],"identityOverride":false},{"id":75,"kind":"def","name":"Circle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":63,"kind":"int"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":74,"kind":"lambda","params":[{"id":71,"name":"__CircleParam"}],"qualifier":"def","expr":{"id":73,"kind":"app","opcode":"variant","args":[{"id":70,"kind":"str","value":"Circle"},{"kind":"name","name":"__CircleParam","id":72}]}}},{"id":81,"kind":"def","name":"Rectangle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":80,"kind":"lambda","params":[{"id":77,"name":"__RectangleParam"}],"qualifier":"def","expr":{"id":79,"kind":"app","opcode":"variant","args":[{"id":76,"kind":"str","value":"Rectangle"},{"kind":"name","name":"__RectangleParam","id":78}]}}},{"id":87,"kind":"def","name":"Dog","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":67,"kind":"str"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":86,"kind":"lambda","params":[{"id":83,"name":"__DogParam"}],"qualifier":"def","expr":{"id":85,"kind":"app","opcode":"variant","args":[{"id":82,"kind":"str","value":"Dog"},{"kind":"name","name":"__DogParam","id":84}]}}},{"kind":"const","name":"MyUnion","typeAnnotation":{"id":88,"kind":"const","name":"MyUnionType"},"id":89,"depth":0},{"id":260,"kind":"def","name":"G","qualifier":"def","expr":{"id":259,"kind":"lambda","params":[{"id":253,"name":"x"}],"qualifier":"def","expr":{"id":258,"kind":"app","opcode":"and","args":[{"id":255,"kind":"app","opcode":"F","args":[{"id":254,"kind":"name","name":"x"}]},{"id":257,"kind":"app","opcode":"not","args":[{"id":256,"kind":"name","name":"x"}]}]}}},{"id":268,"kind":"def","name":"test_and_arg","qualifier":"def","expr":{"id":267,"kind":"lambda","params":[{"id":261,"name":"x"}],"qualifier":"def","expr":{"id":266,"kind":"app","opcode":"and","args":[{"id":263,"kind":"app","opcode":"F","args":[{"id":262,"kind":"name","name":"x"}]},{"id":265,"kind":"app","opcode":"not","args":[{"id":264,"kind":"name","name":"x"}]}]}}},{"id":276,"kind":"def","name":"test_or_arg","qualifier":"def","expr":{"id":275,"kind":"lambda","params":[{"id":269,"name":"x"}],"qualifier":"def","expr":{"id":274,"kind":"app","opcode":"or","args":[{"id":271,"kind":"app","opcode":"F","args":[{"id":270,"kind":"name","name":"x"}]},{"id":273,"kind":"app","opcode":"not","args":[{"id":272,"kind":"name","name":"x"}]}]}}},{"id":368,"kind":"def","name":"tuple_sum","qualifier":"val","expr":{"id":367,"kind":"app","opcode":"map","args":[{"id":351,"kind":"app","opcode":"tuples","args":[{"id":349,"kind":"name","name":"S"},{"id":350,"kind":"name","name":"MySet"}]},{"id":366,"kind":"lambda","params":[{"id":357,"name":"quintTupledLambdaParam357"}],"qualifier":"def","expr":{"id":362,"kind":"let","opdef":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]}},"expr":{"id":358,"kind":"let","opdef":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]}},"expr":{"id":356,"kind":"app","opcode":"iadd","args":[{"id":354,"kind":"name","name":"s"},{"id":355,"kind":"name","name":"mys"}]}}}}]}},{"id":380,"kind":"def","name":"oper_nondet","qualifier":"action","expr":{"id":379,"kind":"let","opdef":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]}},"expr":{"id":378,"kind":"app","opcode":"actionAll","args":[{"id":374,"kind":"app","opcode":"igt","args":[{"id":372,"kind":"name","name":"x"},{"id":373,"kind":"int","value":10}]},{"id":377,"kind":"app","opcode":"assign","args":[{"id":376,"kind":"name","name":"k"},{"id":375,"kind":"name","name":"x"}]}]}}}]}],"table":{"2":{"id":2,"kind":"def","name":"foo","qualifier":"val","expr":{"id":1,"kind":"int","value":4},"depth":0},"5":{"id":5,"kind":"def","name":"bar","qualifier":"val","expr":{"id":4,"kind":"int","value":4},"depth":0},"69":{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},"72":{"id":71,"name":"__CircleParam","kind":"param","depth":1},"75":{"id":75,"kind":"def","name":"Circle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":63,"kind":"int"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":74,"kind":"lambda","params":[{"id":71,"name":"__CircleParam"}],"qualifier":"def","expr":{"id":73,"kind":"app","opcode":"variant","args":[{"id":70,"kind":"str","value":"Circle"},{"kind":"name","name":"__CircleParam","id":72}]}},"depth":0},"78":{"id":77,"name":"__RectangleParam","kind":"param","depth":1},"81":{"id":81,"kind":"def","name":"Rectangle","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":80,"kind":"lambda","params":[{"id":77,"name":"__RectangleParam"}],"qualifier":"def","expr":{"id":79,"kind":"app","opcode":"variant","args":[{"id":76,"kind":"str","value":"Rectangle"},{"kind":"name","name":"__RectangleParam","id":78}]}},"depth":0},"84":{"id":83,"name":"__DogParam","kind":"param","depth":1},"87":{"id":87,"kind":"def","name":"Dog","qualifier":"def","typeAnnotation":{"kind":"oper","args":[{"id":67,"kind":"str"}],"res":{"id":69,"kind":"const","name":"MyUnionType"}},"expr":{"id":86,"kind":"lambda","params":[{"id":83,"name":"__DogParam"}],"qualifier":"def","expr":{"id":85,"kind":"app","opcode":"variant","args":[{"id":82,"kind":"str","value":"Dog"},{"kind":"name","name":"__DogParam","id":84}]}},"depth":0},"88":{"id":68,"kind":"typedef","name":"MyUnionType","type":{"id":68,"kind":"sum","fields":{"kind":"row","fields":[{"fieldName":"Circle","fieldType":{"id":63,"kind":"int"}},{"fieldName":"Rectangle","fieldType":{"id":66,"kind":"rec","fields":{"kind":"row","fields":[{"fieldName":"width","fieldType":{"id":64,"kind":"int"}},{"fieldName":"height","fieldType":{"id":65,"kind":"int"}}],"other":{"kind":"empty"}}}},{"fieldName":"Dog","fieldType":{"id":67,"kind":"str"}}],"other":{"kind":"empty"}}},"depth":0},"97":{"id":97,"kind":"def","name":"add_1_to_2","qualifier":"val","expr":{"id":96,"kind":"app","opcode":"iadd","args":[{"id":94,"kind":"int","value":1},{"id":95,"kind":"int","value":2}]},"depth":0},"101":{"id":101,"kind":"def","name":"sub_1_to_2","qualifier":"val","expr":{"id":100,"kind":"app","opcode":"isub","args":[{"id":98,"kind":"int","value":1},{"id":99,"kind":"int","value":2}]},"depth":0},"105":{"id":105,"kind":"def","name":"mul_2_to_3","qualifier":"val","expr":{"id":104,"kind":"app","opcode":"imul","args":[{"id":102,"kind":"int","value":2},{"id":103,"kind":"int","value":3}]},"depth":0},"109":{"id":109,"kind":"def","name":"div_2_to_3","qualifier":"val","expr":{"id":108,"kind":"app","opcode":"idiv","args":[{"id":106,"kind":"int","value":2},{"id":107,"kind":"int","value":3}]},"depth":0},"113":{"id":113,"kind":"def","name":"mod_2_to_3","qualifier":"val","expr":{"id":112,"kind":"app","opcode":"imod","args":[{"id":110,"kind":"int","value":2},{"id":111,"kind":"int","value":3}]},"depth":0},"117":{"id":117,"kind":"def","name":"pow_2_to_3","qualifier":"val","expr":{"id":116,"kind":"app","opcode":"ipow","args":[{"id":114,"kind":"int","value":2},{"id":115,"kind":"int","value":3}]},"depth":0},"120":{"id":120,"kind":"def","name":"uminus","qualifier":"val","expr":{"id":119,"kind":"app","opcode":"iuminus","args":[{"id":118,"kind":"int","value":100}]},"depth":0},"124":{"id":124,"kind":"def","name":"gt_2_to_3","qualifier":"val","expr":{"id":123,"kind":"app","opcode":"igt","args":[{"id":121,"kind":"int","value":2},{"id":122,"kind":"int","value":3}]},"depth":0},"128":{"id":128,"kind":"def","name":"ge_2_to_3","qualifier":"val","expr":{"id":127,"kind":"app","opcode":"igte","args":[{"id":125,"kind":"int","value":2},{"id":126,"kind":"int","value":3}]},"depth":0},"132":{"id":132,"kind":"def","name":"lt_2_to_3","qualifier":"val","expr":{"id":131,"kind":"app","opcode":"ilt","args":[{"id":129,"kind":"int","value":2},{"id":130,"kind":"int","value":3}]},"depth":0},"136":{"id":136,"kind":"def","name":"le_2_to_3","qualifier":"val","expr":{"id":135,"kind":"app","opcode":"ilte","args":[{"id":133,"kind":"int","value":2},{"id":134,"kind":"int","value":3}]},"depth":0},"140":{"id":140,"kind":"def","name":"eqeq_2_to_3","qualifier":"val","expr":{"id":139,"kind":"app","opcode":"eq","args":[{"id":137,"kind":"int","value":2},{"id":138,"kind":"int","value":3}]},"depth":0},"144":{"id":144,"kind":"def","name":"ne_2_to_3","qualifier":"val","expr":{"id":143,"kind":"app","opcode":"neq","args":[{"id":141,"kind":"int","value":2},{"id":142,"kind":"int","value":3}]},"depth":0},"150":{"id":150,"kind":"def","name":"VeryTrue","qualifier":"val","expr":{"id":149,"kind":"app","opcode":"eq","args":[{"id":147,"kind":"app","opcode":"iadd","args":[{"id":145,"kind":"int","value":2},{"id":146,"kind":"int","value":2}]},{"id":148,"kind":"int","value":4}]},"depth":0},"154":{"id":154,"kind":"def","name":"nat_includes_one","qualifier":"val","expr":{"id":153,"kind":"app","opcode":"in","args":[{"id":151,"kind":"int","value":1},{"id":152,"kind":"name","name":"Nat"}]},"depth":0},"157":{"id":156,"name":"x","kind":"param","depth":1},"160":{"id":160,"kind":"def","name":"there_is_truth","qualifier":"val","expr":{"id":159,"kind":"app","opcode":"exists","args":[{"id":155,"kind":"name","name":"Bool"},{"id":158,"kind":"lambda","params":[{"id":156,"name":"x"}],"qualifier":"def","expr":{"id":157,"kind":"name","name":"x"}}]},"depth":0},"166":{"id":166,"kind":"def","name":"withType","qualifier":"val","expr":{"id":165,"kind":"app","opcode":"Set","args":[{"id":163,"kind":"int","value":1},{"id":164,"kind":"int","value":2}]},"typeAnnotation":{"id":162,"kind":"set","elem":{"id":161,"kind":"int"}},"depth":0},"168":{"id":167,"kind":"typedef","name":"PROC","depth":0},"171":{"id":171,"kind":"def","name":"withUninterpretedType","qualifier":"val","expr":{"id":170,"kind":"app","opcode":"Set","args":[]},"typeAnnotation":{"id":169,"kind":"set","elem":{"id":168,"kind":"const","name":"PROC"}},"depth":0},"175":{"id":175,"kind":"def","name":"magicNumber","qualifier":"pureval","expr":{"id":174,"kind":"int","value":42},"depth":0},"178":{"id":176,"name":"x","kind":"param","depth":1,"shadowing":false},"179":{"id":177,"name":"y","kind":"param","depth":1},"182":{"id":182,"kind":"def","name":"add","qualifier":"puredef","expr":{"id":181,"kind":"lambda","params":[{"id":176,"name":"x"},{"id":177,"name":"y"}],"qualifier":"puredef","expr":{"id":180,"kind":"app","opcode":"iadd","args":[{"id":178,"kind":"name","name":"x"},{"id":179,"kind":"name","name":"y"}]}},"depth":0},"184":{"id":183,"name":"factor","kind":"param","depth":1},"185":{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},"188":{"id":188,"kind":"def","name":"ofN","qualifier":"def","expr":{"id":187,"kind":"lambda","params":[{"id":183,"name":"factor"}],"qualifier":"def","expr":{"id":186,"kind":"app","opcode":"imul","args":[{"id":184,"kind":"name","name":"factor"},{"id":185,"kind":"name","name":"n"}]}},"depth":0},"190":{"id":189,"name":"x","kind":"param","depth":1,"shadowing":false},"191":{"kind":"var","name":"n","typeAnnotation":{"id":172,"kind":"int"},"id":173,"depth":0},"194":{"id":194,"kind":"def","name":"A","qualifier":"action","expr":{"id":193,"kind":"lambda","params":[{"id":189,"name":"x"}],"qualifier":"action","expr":{"id":192,"kind":"app","opcode":"assign","args":[{"id":191,"kind":"name","name":"n"},{"id":190,"kind":"name","name":"x"}]}},"depth":0},"196":{"id":195,"name":"x","kind":"param","depth":1,"shadowing":false},"199":{"id":199,"kind":"def","name":"B","qualifier":"puredef","expr":{"id":198,"kind":"lambda","params":[{"id":195,"name":"x"}],"qualifier":"puredef","expr":{"id":197,"kind":"app","opcode":"not","args":[{"id":196,"kind":"name","name":"x"}]}},"depth":0},"206":{"id":200,"name":"x","kind":"param","depth":1,"shadowing":false},"207":{"id":201,"name":"y","kind":"param","depth":1,"shadowing":false},"210":{"id":210,"kind":"def","name":"H","qualifier":"def","expr":{"id":209,"kind":"lambda","params":[{"id":200,"name":"x"},{"id":201,"name":"y"}],"qualifier":"def","expr":{"id":208,"kind":"app","opcode":"iadd","args":[{"id":206,"kind":"name","name":"x"},{"id":207,"kind":"name","name":"y"}]}},"typeAnnotation":{"id":205,"kind":"oper","args":[{"id":202,"kind":"int"},{"id":203,"kind":"int"}],"res":{"id":204,"kind":"int"}},"depth":0},"215":{"id":211,"name":"x","kind":"param","depth":1,"shadowing":false},"217":{"id":217,"kind":"def","name":"Pol","qualifier":"def","expr":{"id":216,"kind":"lambda","params":[{"id":211,"name":"x"}],"qualifier":"def","expr":{"id":215,"kind":"name","name":"x"}},"typeAnnotation":{"id":214,"kind":"oper","args":[{"id":212,"kind":"var","name":"a"}],"res":{"id":213,"kind":"var","name":"a"}},"depth":0},"221":{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},"223":{"id":223,"kind":"def","name":"asgn","qualifier":"action","expr":{"id":222,"kind":"app","opcode":"assign","args":[{"id":221,"kind":"name","name":"k"},{"id":220,"kind":"int","value":3}]},"depth":0},"229":{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"230":{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"232":{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"233":{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"},"kind":"param","depth":1,"shadowing":false},"236":{"id":236,"kind":"def","name":"min","qualifier":"puredef","expr":{"id":235,"kind":"lambda","params":[{"id":225,"name":"x","typeAnnotation":{"id":224,"kind":"int"}},{"id":227,"name":"y","typeAnnotation":{"id":226,"kind":"int"}}],"qualifier":"puredef","expr":{"id":234,"kind":"app","opcode":"ite","args":[{"id":231,"kind":"app","opcode":"ilt","args":[{"id":229,"kind":"name","name":"x"},{"id":230,"kind":"name","name":"y"}]},{"id":232,"kind":"name","name":"x"},{"id":233,"kind":"name","name":"y"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":224,"kind":"int"},{"id":226,"kind":"int"}],"res":{"id":228,"kind":"int"}},"depth":0},"240":{"id":240,"kind":"def","name":"test_and","qualifier":"val","expr":{"id":239,"kind":"app","opcode":"and","args":[{"id":237,"kind":"bool","value":false},{"id":238,"kind":"bool","value":true}]},"depth":0},"244":{"id":244,"kind":"def","name":"test_or","qualifier":"val","expr":{"id":243,"kind":"app","opcode":"or","args":[{"id":241,"kind":"bool","value":false},{"id":242,"kind":"bool","value":true}]},"depth":0},"248":{"id":248,"kind":"def","name":"test_implies","qualifier":"val","expr":{"id":247,"kind":"app","opcode":"implies","args":[{"id":245,"kind":"bool","value":false},{"id":246,"kind":"bool","value":true}]},"depth":0},"250":{"id":249,"name":"x","kind":"param","depth":1,"shadowing":false},"252":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"254":{"id":253,"name":"x","kind":"param","depth":1,"shadowing":false},"255":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"256":{"id":253,"name":"x","kind":"param","depth":1,"shadowing":false},"260":{"id":260,"kind":"def","name":"G","qualifier":"def","expr":{"id":259,"kind":"lambda","params":[{"id":253,"name":"x"}],"qualifier":"def","expr":{"id":258,"kind":"app","opcode":"and","args":[{"id":255,"kind":"app","opcode":"F","args":[{"id":254,"kind":"name","name":"x"}]},{"id":257,"kind":"app","opcode":"not","args":[{"id":256,"kind":"name","name":"x"}]}]}},"depth":0},"262":{"id":261,"name":"x","kind":"param","depth":1,"shadowing":false},"263":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"264":{"id":261,"name":"x","kind":"param","depth":1,"shadowing":false},"268":{"id":268,"kind":"def","name":"test_and_arg","qualifier":"def","expr":{"id":267,"kind":"lambda","params":[{"id":261,"name":"x"}],"qualifier":"def","expr":{"id":266,"kind":"app","opcode":"and","args":[{"id":263,"kind":"app","opcode":"F","args":[{"id":262,"kind":"name","name":"x"}]},{"id":265,"kind":"app","opcode":"not","args":[{"id":264,"kind":"name","name":"x"}]}]}},"depth":0},"270":{"id":269,"name":"x","kind":"param","depth":1,"shadowing":false},"271":{"id":252,"kind":"def","name":"F","qualifier":"def","expr":{"id":251,"kind":"lambda","params":[{"id":249,"name":"x"}],"qualifier":"def","expr":{"id":250,"kind":"name","name":"x"}},"depth":0},"272":{"id":269,"name":"x","kind":"param","depth":1,"shadowing":false},"276":{"id":276,"kind":"def","name":"test_or_arg","qualifier":"def","expr":{"id":275,"kind":"lambda","params":[{"id":269,"name":"x"}],"qualifier":"def","expr":{"id":274,"kind":"app","opcode":"or","args":[{"id":271,"kind":"app","opcode":"F","args":[{"id":270,"kind":"name","name":"x"}]},{"id":273,"kind":"app","opcode":"not","args":[{"id":272,"kind":"name","name":"x"}]}]}},"depth":0},"281":{"id":281,"kind":"def","name":"test_block_and","qualifier":"val","expr":{"id":280,"kind":"app","opcode":"and","args":[{"id":277,"kind":"bool","value":false},{"id":278,"kind":"bool","value":true},{"id":279,"kind":"bool","value":false}]},"depth":0},"286":{"id":286,"kind":"def","name":"test_action_and","qualifier":"action","expr":{"id":285,"kind":"app","opcode":"actionAll","args":[{"id":282,"kind":"bool","value":false},{"id":283,"kind":"bool","value":true},{"id":284,"kind":"bool","value":false}]},"depth":0},"291":{"id":291,"kind":"def","name":"test_block_or","qualifier":"val","expr":{"id":290,"kind":"app","opcode":"or","args":[{"id":287,"kind":"bool","value":false},{"id":288,"kind":"bool","value":true},{"id":289,"kind":"bool","value":false}]},"depth":0},"296":{"id":296,"kind":"def","name":"test_action_or","qualifier":"action","expr":{"id":295,"kind":"app","opcode":"actionAny","args":[{"id":292,"kind":"bool","value":false},{"id":293,"kind":"bool","value":true},{"id":294,"kind":"bool","value":false}]},"depth":0},"301":{"id":301,"kind":"def","name":"test_ite","qualifier":"val","expr":{"id":300,"kind":"app","opcode":"ite","args":[{"id":297,"kind":"bool","value":true},{"id":298,"kind":"int","value":1},{"id":299,"kind":"int","value":0}]},"depth":0},"304":{"id":302,"name":"x","kind":"param","depth":1,"shadowing":false},"307":{"id":303,"name":"y","kind":"param","depth":1,"shadowing":false},"310":{"id":303,"name":"y","kind":"param","depth":1,"shadowing":false},"315":{"id":315,"kind":"def","name":"test_ite2","qualifier":"def","expr":{"id":314,"kind":"lambda","params":[{"id":302,"name":"x"},{"id":303,"name":"y"}],"qualifier":"def","expr":{"id":313,"kind":"app","opcode":"ite","args":[{"id":306,"kind":"app","opcode":"ilt","args":[{"id":304,"kind":"name","name":"x"},{"id":305,"kind":"int","value":10}]},{"id":309,"kind":"app","opcode":"iadd","args":[{"id":307,"kind":"name","name":"y"},{"id":308,"kind":"int","value":5}]},{"id":312,"kind":"app","opcode":"imod","args":[{"id":310,"kind":"name","name":"y"},{"id":311,"kind":"int","value":5}]}]}},"depth":0},"320":{"kind":"var","name":"f1","typeAnnotation":{"id":318,"kind":"fun","arg":{"id":316,"kind":"str"},"res":{"id":317,"kind":"int"}},"id":319,"depth":0},"323":{"id":323,"kind":"def","name":"funapp","qualifier":"val","expr":{"id":322,"kind":"app","opcode":"get","args":[{"id":320,"kind":"name","name":"f1"},{"id":321,"kind":"str","value":"a"}]},"depth":0},"328":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"331":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"332":{"id":332,"kind":"def","name":"oper_app_normal","qualifier":"val","expr":{"id":331,"kind":"app","opcode":"MyOper","args":[{"id":329,"kind":"str","value":"a"},{"id":330,"kind":"int","value":42}]},"depth":0},"335":{"id":328,"kind":"def","name":"MyOper","qualifier":"def","expr":{"id":327,"kind":"lambda","params":[{"id":324,"name":"a"},{"id":325,"name":"b"}],"qualifier":"def","expr":{"id":326,"kind":"int","value":1}},"depth":0},"336":{"id":336,"kind":"def","name":"oper_app_ufcs","qualifier":"val","expr":{"id":335,"kind":"app","opcode":"MyOper","args":[{"id":333,"kind":"str","value":"a"},{"id":334,"kind":"int","value":42}]},"depth":0},"340":{"id":340,"kind":"def","name":"oper_in","qualifier":"val","expr":{"id":339,"kind":"app","opcode":"in","args":[{"id":337,"kind":"int","value":1},{"id":338,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"341":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"343":{"id":342,"name":"x","kind":"param","depth":1,"shadowing":false},"348":{"id":348,"kind":"def","name":"oper_app_dot1","qualifier":"val","expr":{"id":347,"kind":"app","opcode":"filter","args":[{"id":341,"kind":"name","name":"S"},{"id":346,"kind":"lambda","params":[{"id":342,"name":"x"}],"qualifier":"def","expr":{"id":345,"kind":"app","opcode":"igt","args":[{"id":343,"kind":"name","name":"x"},{"id":344,"kind":"int","value":10}]}}]},"depth":0},"349":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"350":{"kind":"const","name":"MySet","typeAnnotation":{"id":18,"kind":"set","elem":{"id":17,"kind":"int"}},"id":19,"depth":0},"352":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]},"depth":4},"353":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]},"depth":3},"354":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]},"depth":4},"355":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]},"depth":3},"360":{"id":357,"name":"quintTupledLambdaParam357","kind":"param","depth":1},"364":{"id":357,"name":"quintTupledLambdaParam357","kind":"param","depth":1},"368":{"id":368,"kind":"def","name":"tuple_sum","qualifier":"val","expr":{"id":367,"kind":"app","opcode":"map","args":[{"id":351,"kind":"app","opcode":"tuples","args":[{"id":349,"kind":"name","name":"S"},{"id":350,"kind":"name","name":"MySet"}]},{"id":366,"kind":"lambda","params":[{"id":357,"name":"quintTupledLambdaParam357"}],"qualifier":"def","expr":{"id":362,"kind":"let","opdef":{"id":353,"kind":"def","name":"mys","qualifier":"pureval","expr":{"id":363,"kind":"app","opcode":"item","args":[{"id":364,"kind":"name","name":"quintTupledLambdaParam357"},{"id":365,"kind":"int","value":2}]}},"expr":{"id":358,"kind":"let","opdef":{"id":352,"kind":"def","name":"s","qualifier":"pureval","expr":{"id":359,"kind":"app","opcode":"item","args":[{"id":360,"kind":"name","name":"quintTupledLambdaParam357"},{"id":361,"kind":"int","value":1}]}},"expr":{"id":356,"kind":"app","opcode":"iadd","args":[{"id":354,"kind":"name","name":"s"},{"id":355,"kind":"name","name":"mys"}]}}}}]},"depth":0},"369":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"371":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"372":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"375":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]},"depth":2,"shadowing":false},"376":{"kind":"var","name":"k","typeAnnotation":{"id":218,"kind":"int"},"id":219,"depth":0},"380":{"id":380,"kind":"def","name":"oper_nondet","qualifier":"action","expr":{"id":379,"kind":"let","opdef":{"id":371,"kind":"def","name":"x","qualifier":"nondet","expr":{"id":370,"kind":"app","opcode":"oneOf","args":[{"id":369,"kind":"name","name":"S"}]}},"expr":{"id":378,"kind":"app","opcode":"actionAll","args":[{"id":374,"kind":"app","opcode":"igt","args":[{"id":372,"kind":"name","name":"x"},{"id":373,"kind":"int","value":10}]},{"id":377,"kind":"app","opcode":"assign","args":[{"id":376,"kind":"name","name":"k"},{"id":375,"kind":"name","name":"x"}]}]}},"depth":0},"381":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"386":{"id":386,"kind":"def","name":"oper_app_dot4","qualifier":"val","expr":{"id":385,"kind":"app","opcode":"filter","args":[{"id":381,"kind":"name","name":"S"},{"id":384,"kind":"lambda","params":[{"id":382,"name":"_"}],"qualifier":"def","expr":{"id":383,"kind":"bool","value":true}}]},"depth":0},"387":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"389":{"id":388,"name":"x","kind":"param","depth":1,"shadowing":false},"394":{"id":394,"kind":"def","name":"f","qualifier":"val","expr":{"id":393,"kind":"app","opcode":"mapBy","args":[{"id":387,"kind":"name","name":"S"},{"id":392,"kind":"lambda","params":[{"id":388,"name":"x"}],"qualifier":"def","expr":{"id":391,"kind":"app","opcode":"iadd","args":[{"id":389,"kind":"name","name":"x"},{"id":390,"kind":"int","value":1}]}}]},"depth":0},"395":{"kind":"const","name":"S","typeAnnotation":{"id":15,"kind":"set","elem":{"id":14,"kind":"int"}},"id":16,"depth":0},"400":{"id":400,"kind":"def","name":"set_difference","qualifier":"val","expr":{"id":399,"kind":"app","opcode":"exclude","args":[{"id":395,"kind":"name","name":"S"},{"id":398,"kind":"app","opcode":"Set","args":[{"id":396,"kind":"int","value":3},{"id":397,"kind":"int","value":4}]}]},"depth":0},"405":{"id":405,"kind":"def","name":"one","qualifier":"val","expr":{"id":404,"kind":"app","opcode":"head","args":[{"id":403,"kind":"app","opcode":"List","args":[{"id":401,"kind":"int","value":1},{"id":402,"kind":"int","value":2}]}]},"depth":0},"410":{"id":410,"kind":"def","name":"test_tuple","qualifier":"val","expr":{"id":409,"kind":"app","opcode":"Tup","args":[{"id":406,"kind":"int","value":1},{"id":407,"kind":"int","value":2},{"id":408,"kind":"int","value":3}]},"depth":0},"415":{"id":415,"kind":"def","name":"test_tuple2","qualifier":"val","expr":{"id":414,"kind":"app","opcode":"Tup","args":[{"id":411,"kind":"int","value":1},{"id":412,"kind":"int","value":2},{"id":413,"kind":"int","value":3}]},"depth":0},"419":{"id":419,"kind":"def","name":"test_pair","qualifier":"val","expr":{"id":418,"kind":"app","opcode":"Tup","args":[{"id":416,"kind":"int","value":4},{"id":417,"kind":"int","value":5}]},"depth":0},"424":{"id":424,"kind":"def","name":"test_list","qualifier":"val","expr":{"id":423,"kind":"app","opcode":"List","args":[{"id":420,"kind":"int","value":1},{"id":421,"kind":"int","value":2},{"id":422,"kind":"int","value":3}]},"depth":0},"429":{"id":429,"kind":"def","name":"test_list2","qualifier":"val","expr":{"id":428,"kind":"app","opcode":"List","args":[{"id":425,"kind":"int","value":1},{"id":426,"kind":"int","value":2},{"id":427,"kind":"int","value":3}]},"depth":0},"436":{"id":436,"kind":"def","name":"test_list_nth","qualifier":"val","expr":{"id":435,"kind":"app","opcode":"nth","args":[{"id":433,"kind":"app","opcode":"List","args":[{"id":430,"kind":"int","value":2},{"id":431,"kind":"int","value":3},{"id":432,"kind":"int","value":4}]},{"id":434,"kind":"int","value":2}]},"depth":0},"442":{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]},"depth":0},"448":{"id":448,"kind":"def","name":"test_record2","qualifier":"val","expr":{"id":447,"kind":"app","opcode":"Rec","args":[{"id":443,"kind":"str","value":"name"},{"id":444,"kind":"str","value":"igor"},{"id":445,"kind":"str","value":"year"},{"id":446,"kind":"int","value":1981}]},"depth":0},"453":{"id":442,"kind":"def","name":"test_record","qualifier":"val","expr":{"id":441,"kind":"app","opcode":"Rec","args":[{"id":438,"kind":"str","value":"name"},{"id":437,"kind":"str","value":"igor"},{"id":440,"kind":"str","value":"year"},{"id":439,"kind":"int","value":1981}]},"depth":0},"456":{"id":456,"kind":"def","name":"test_record3","qualifier":"val","expr":{"id":455,"kind":"app","opcode":"with","args":[{"id":454,"kind":"app","opcode":"with","args":[{"id":453,"kind":"name","name":"test_record"},{"id":450,"kind":"str","value":"name"},{"id":449,"kind":"str","value":"quint"}]},{"id":452,"kind":"str","value":"year"},{"id":451,"kind":"int","value":2023}]},"depth":0},"461":{"id":461,"kind":"def","name":"test_set","qualifier":"val","expr":{"id":460,"kind":"app","opcode":"Set","args":[{"id":457,"kind":"int","value":1},{"id":458,"kind":"int","value":2},{"id":459,"kind":"int","value":3}]},"depth":0},"467":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]},"depth":2},"468":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]},"depth":2},"472":{"id":472,"kind":"def","name":"rec_field","qualifier":"val","expr":{"id":471,"kind":"let","opdef":{"id":467,"kind":"def","name":"my_rec","qualifier":"val","expr":{"id":466,"kind":"app","opcode":"Rec","args":[{"id":463,"kind":"str","value":"a"},{"id":462,"kind":"int","value":1},{"id":465,"kind":"str","value":"b"},{"id":464,"kind":"str","value":"foo"}]}},"expr":{"id":470,"kind":"app","opcode":"field","args":[{"id":468,"kind":"name","name":"my_rec"},{"id":469,"kind":"str","value":"a"}]}},"depth":0},"476":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]},"depth":2},"477":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]},"depth":2},"481":{"id":481,"kind":"def","name":"tup_elem","qualifier":"val","expr":{"id":480,"kind":"let","opdef":{"id":476,"kind":"def","name":"my_tup","qualifier":"val","expr":{"id":475,"kind":"app","opcode":"Tup","args":[{"id":473,"kind":"str","value":"a"},{"id":474,"kind":"int","value":3}]}},"expr":{"id":479,"kind":"app","opcode":"item","args":[{"id":477,"kind":"name","name":"my_tup"},{"id":478,"kind":"int","value":2}]}},"depth":0},"483":{"id":482,"name":"s","kind":"param","depth":1,"shadowing":false},"487":{"id":487,"kind":"def","name":"isEmpty","qualifier":"def","expr":{"id":486,"kind":"lambda","params":[{"id":482,"name":"s"}],"qualifier":"def","expr":{"id":485,"kind":"app","opcode":"eq","args":[{"id":483,"kind":"name","name":"s"},{"id":484,"kind":"app","opcode":"List","args":[]}]}},"depth":0},"491":{"id":491,"kind":"def","name":"in_2_empty","qualifier":"val","expr":{"id":490,"kind":"app","opcode":"in","args":[{"id":488,"kind":"int","value":2},{"id":489,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"495":{"id":495,"kind":"def","name":"subseteq_empty","qualifier":"val","expr":{"id":494,"kind":"app","opcode":"subseteq","args":[{"id":492,"kind":"app","opcode":"Set","args":[]},{"id":493,"kind":"app","opcode":"Set","args":[]}]},"depth":0},"504":{"id":504,"kind":"def","name":"powersets","qualifier":"val","expr":{"id":503,"kind":"app","opcode":"in","args":[{"id":497,"kind":"app","opcode":"Set","args":[{"id":496,"kind":"int","value":1}]},{"id":502,"kind":"app","opcode":"powerset","args":[{"id":501,"kind":"app","opcode":"Set","args":[{"id":498,"kind":"int","value":1},{"id":499,"kind":"int","value":2},{"id":500,"kind":"int","value":3}]}]}]},"depth":0},"510":{"id":510,"kind":"def","name":"lists","qualifier":"val","expr":{"id":509,"kind":"app","opcode":"allListsUpTo","args":[{"id":507,"kind":"app","opcode":"Set","args":[{"id":505,"kind":"int","value":1},{"id":506,"kind":"int","value":2}]},{"id":508,"kind":"int","value":3}]},"depth":0},"511":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"515":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"525":{"id":523,"kind":"typedef","name":"INT_SET","type":{"id":522,"kind":"set","elem":{"id":521,"kind":"int"}},"depth":0},"527":{"kind":"const","name":"N","typeAnnotation":{"id":12,"kind":"int"},"id":13,"depth":0},"528":{"kind":"const","name":"N","typeAnnotation":{"id":7,"kind":"int"},"id":8,"depth":0,"importedFrom":{"id":529,"kind":"instance","qualifiedName":"Inst1","protoName":"Proto","overrides":[[{"id":528,"name":"N"},{"id":527,"kind":"name","name":"N"}]],"identityOverride":false},"hidden":true,"namespaces":["Inst1","withConsts"]}},"errors":[]} \ No newline at end of file diff --git a/quint/testFixture/_1016nonConstOverride.json b/quint/testFixture/_1016nonConstOverride.json index 16377145c..9050d6c5a 100644 --- a/quint/testFixture/_1016nonConstOverride.json +++ b/quint/testFixture/_1016nonConstOverride.json @@ -1 +1 @@ -{"stage":"parsing","warnings":[],"modules":[{"id":5,"name":"A","declarations":[{"kind":"const","name":"c","typeAnnotation":{"id":1,"kind":"int"},"id":2,"depth":0},{"kind":"var","name":"a","typeAnnotation":{"id":3,"kind":"int"},"id":4,"depth":0}]},{"id":11,"name":"nonConstOverride","declarations":[{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false}]}],"table":{"8":{"kind":"const","name":"c","typeAnnotation":{"id":1,"kind":"int"},"id":6,"depth":0,"importedFrom":{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false},"hidden":true,"namespaces":["A1","nonConstOverride"]},"9":{"kind":"var","name":"a","typeAnnotation":{"id":3,"kind":"int"},"id":4,"depth":0,"importedFrom":{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false},"hidden":true,"namespaces":["A1","nonConstOverride"]}},"errors":[{"explanation":"[QNT406] Instantiation error: 'a' is not a constant in 'A'","locs":[{"source":"mocked_path/testFixture/_1016nonConstOverride.qnt","start":{"line":6,"col":2,"index":70},"end":{"line":6,"col":33,"index":101}}]}]} \ No newline at end of file +{"stage":"parsing","warnings":[],"modules":[{"id":5,"name":"A","declarations":[{"kind":"const","name":"c","typeAnnotation":{"id":1,"kind":"int"},"id":2,"depth":0},{"kind":"var","name":"a","typeAnnotation":{"id":3,"kind":"int"},"id":4,"depth":0}]},{"id":11,"name":"nonConstOverride","declarations":[{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false}]}],"table":{"8":{"kind":"const","name":"c","typeAnnotation":{"id":1,"kind":"int"},"id":2,"depth":0,"importedFrom":{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false},"hidden":true,"namespaces":["A1","nonConstOverride"]},"9":{"kind":"var","name":"a","typeAnnotation":{"id":3,"kind":"int"},"id":4,"depth":0,"importedFrom":{"id":10,"kind":"instance","qualifiedName":"A1","protoName":"A","overrides":[[{"id":8,"name":"c"},{"id":6,"kind":"int","value":1}],[{"id":9,"name":"a"},{"id":7,"kind":"bool","value":false}]],"identityOverride":false},"hidden":true,"namespaces":["A1","nonConstOverride"]}},"errors":[{"explanation":"[QNT406] Instantiation error: 'a' is not a constant in 'A'","locs":[{"source":"mocked_path/testFixture/_1016nonConstOverride.qnt","start":{"line":6,"col":2,"index":70},"end":{"line":6,"col":33,"index":101}}]}]} \ No newline at end of file diff --git a/quint/testFixture/_1031instance.json b/quint/testFixture/_1031instance.json index 7b66f056e..92d6cd850 100644 --- a/quint/testFixture/_1031instance.json +++ b/quint/testFixture/_1031instance.json @@ -1 +1 @@ -{"stage":"parsing","warnings":[],"modules":[{"id":18,"name":"c","declarations":[{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":9,"depth":0},{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":10,"kind":"int"}],"res":{"id":12,"kind":"int"}}}]},{"id":7,"name":"inst","declarations":[{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},{"id":6,"kind":"def","name":"baz","qualifier":"puredef","expr":{"id":5,"kind":"app","opcode":"foo","args":[{"id":4,"kind":"int","value":6}]}}]}],"table":{"2":{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":1,"depth":0,"importedFrom":{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},"hidden":true,"namespaces":["c","inst"]},"5":{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"depth":0,"importedFrom":{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},"hidden":true,"namespaces":["c","inst"]},"6":{"id":6,"kind":"def","name":"baz","qualifier":"puredef","expr":{"id":5,"kind":"app","opcode":"foo","args":[{"id":4,"kind":"int","value":6}]},"depth":0},"13":{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"},"kind":"param","depth":1},"14":{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":9,"depth":0},"17":{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":10,"kind":"int"}],"res":{"id":12,"kind":"int"}},"depth":0}},"errors":[]} \ No newline at end of file +{"stage":"parsing","warnings":[],"modules":[{"id":18,"name":"c","declarations":[{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":9,"depth":0},{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":10,"kind":"int"}],"res":{"id":12,"kind":"int"}}}]},{"id":7,"name":"inst","declarations":[{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},{"id":6,"kind":"def","name":"baz","qualifier":"puredef","expr":{"id":5,"kind":"app","opcode":"foo","args":[{"id":4,"kind":"int","value":6}]}}]}],"table":{"2":{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":9,"depth":0,"importedFrom":{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},"hidden":true,"namespaces":["c","inst"]},"5":{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"depth":0,"importedFrom":{"id":3,"kind":"instance","protoName":"c","overrides":[[{"id":2,"name":"N"},{"id":1,"kind":"int","value":3}]],"identityOverride":true,"fromSource":"./_1030const"},"hidden":true,"namespaces":["c","inst"]},"6":{"id":6,"kind":"def","name":"baz","qualifier":"puredef","expr":{"id":5,"kind":"app","opcode":"foo","args":[{"id":4,"kind":"int","value":6}]},"depth":0},"13":{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"},"kind":"param","depth":1},"14":{"kind":"const","name":"N","typeAnnotation":{"id":8,"kind":"int"},"id":9,"depth":0},"17":{"id":17,"kind":"def","name":"foo","qualifier":"puredef","expr":{"id":16,"kind":"lambda","params":[{"id":11,"name":"i","typeAnnotation":{"id":10,"kind":"int"}}],"qualifier":"puredef","expr":{"id":15,"kind":"app","opcode":"iadd","args":[{"id":13,"kind":"name","name":"i"},{"id":14,"kind":"name","name":"N"}]}},"typeAnnotation":{"kind":"oper","args":[{"id":10,"kind":"int"}],"res":{"id":12,"kind":"int"}},"depth":0}},"errors":[]} \ No newline at end of file From 87dfdfc5a9c2a7e90084ae9696b66fba85057a2c Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 14:46:15 -0300 Subject: [PATCH 31/37] Add missing progress bar update (that got lost in conflict resolution) --- quint/src/runtime/impl/evaluator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 01cff4fbc..175670047 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -269,6 +269,7 @@ export class Evaluator { let nsamples = 1 // run up to maxSamples, stop on the first failure for (; nsamples <= maxSamples; nsamples++) { + progressBar.update(nsamples, { test: name }) // record the seed value seed = this.rng.getState() this.recorder.onRunCall() From 3116a4305fe852e39ca73a3d1980eaf2a05270a0 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 15:29:59 -0300 Subject: [PATCH 32/37] Add CHANGELOG entries --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0186b72b4..d6b1939e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - In the `verify` command, add warning if `--out-itf` option contains `{test}` or `{seq}` as those have no effect since Apalache only produces a single trace (#1485) - The `run` and `test` commands now display a progress bar (#1457) +- Calling `q::test`, `q::testOnce` and `q::lastTrace` on the REPL now works properly (#1495) ### Changed - Performance of incrementally checking types (i.e. in REPL) was improved (#1483). +- Performance of the REPL was drastically improved (#1495) - In the `run` and `test` commands, change placeholders from `{}` to `{test}` and from `{#}` to `{seq}` (#1485) - In the `run` command, auto-append trace sequence number to filename if more than one trace is present and `{seq}` is not specified (#1485) - In the `test` command, rename `--output` to `--out-itf` +- Error reporting was improved for many runtime errors (#1495) ### Deprecated @@ -31,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped GRPC message sizes to 1G (#1480) - Fix format of ITF trace emitted by `verify` command (#1448) +- Sending SIGINT (hitting Ctrl+C) to the run and test commands now actually stops the execution (#1495) ### Security @@ -148,7 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The latest supported node version is now bounded at <= 20, which covers the latest LTS. (#1380) -- Shadowing names are now supported, which means that the same name can be redefined +- Shadowing names are now supported, which means that the same name can be redefined in nested scopes. (#1394) - The canonical unit type is now the empty tuple, `()`, rather than the empty record, `{}`. This should only affect invisible things to do with sum type From 2537fdb397e51df5040ce79ec774a721b4f773e3 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 2 Sep 2024 15:31:29 -0300 Subject: [PATCH 33/37] Remove remainder from old behavior left as comment --- quint/src/names/collector.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/quint/src/names/collector.ts b/quint/src/names/collector.ts index 4c79bf97d..392bb880d 100644 --- a/quint/src/names/collector.ts +++ b/quint/src/names/collector.ts @@ -148,8 +148,6 @@ export class NameCollector implements IRVisitor { return } - // Update the definition to point to the expression being overriden - // constDef.id = ex.id constDef.hidden = false }) From 949408c4ef10896bc3c415d9784f30ca7b798446 Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 9 Sep 2024 09:41:46 -0300 Subject: [PATCH 34/37] Fix hint text --- quint/io-cli-tests.md | 2 +- quint/src/cliCommands.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index c9330995a..2c876ab5b 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -1158,7 +1158,7 @@ exit $exit_code Use --verbosity=3 to show executions. - Further debug with: quint --verbosity=3 testFixture/_1040compileError.qnt + Further debug with: quint test --verbosity=3 testFixture/_1040compileError.qnt error: Tests failed ``` diff --git a/quint/src/cliCommands.ts b/quint/src/cliCommands.ts index 9cb7e0b87..b4daaf930 100644 --- a/quint/src/cliCommands.ts +++ b/quint/src/cliCommands.ts @@ -459,7 +459,7 @@ export async function runTests(prev: TypecheckedStage): Promise Date: Mon, 9 Sep 2024 11:16:26 -0300 Subject: [PATCH 35/37] Improve code quality a bit --- quint/src/runtime/impl/builder.ts | 17 ++++++++--------- quint/src/runtime/impl/evaluator.ts | 3 --- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/quint/src/runtime/impl/builder.ts b/quint/src/runtime/impl/builder.ts index 44fae3a0f..34116ac61 100644 --- a/quint/src/runtime/impl/builder.ts +++ b/quint/src/runtime/impl/builder.ts @@ -241,7 +241,8 @@ function buildUnderDefContext( const register = builder.registerForConst(id, param.name) // Build the expr as a pure val def so it gets properly cached - return [register, buildDef(builder, { kind: 'def', qualifier: 'pureval', expr, name: param.name, id: param.id })] + const purevalEval = buildDef(builder, { kind: 'def', qualifier: 'pureval', expr, name: param.name, id: param.id }) + return [register, purevalEval] }) // Here, we have the right context to build the function. That is, all constants are pointing to the right registers, @@ -530,18 +531,16 @@ function buildApp( ): (ctx: Context, args: RuntimeValue[]) => Either { const def = builder.table.get(app.id)! if (!def) { + // If it is not in the lookup table, it must be a builtin operator return builtinLambda(app.opcode) } - const value = buildDef(builder, def) + const defEval = buildDef(builder, def) return (ctx, args) => { - const lambdaResult = value(ctx) - if (lambdaResult.isLeft()) { - return lambdaResult - } - const arrow = lambdaResult.value.toArrow() - - return arrow(ctx, args) + return defEval(ctx).chain(lambda => { + const arrow = lambda.toArrow() + return arrow(ctx, args) + }) } } diff --git a/quint/src/runtime/impl/evaluator.ts b/quint/src/runtime/impl/evaluator.ts index 175670047..5001d522d 100644 --- a/quint/src/runtime/impl/evaluator.ts +++ b/quint/src/runtime/impl/evaluator.ts @@ -183,9 +183,6 @@ export class Evaluator { errorsFound++ } else { // check all { step, shift(), inv } in a loop - - // FIXME: errorsFound < ntraces is not good, because we continue after invariant violation. - // This is the same in the old version, so I'll fix later. for (let i = 0; errorsFound < ntraces && !failure && i < nsteps; i++) { const stepApp: QuintApp = { id: 0n, From be108774e941e23780a205992e70ebcd6bea6c1d Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 9 Sep 2024 13:34:18 -0300 Subject: [PATCH 36/37] Fix REPL issue when evaluating a variable that was never referenced --- quint/src/repl.ts | 28 +++++++++++++++++----------- quint/src/runtime/impl/builder.ts | 13 +++++++------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/quint/src/repl.ts b/quint/src/repl.ts index e8df784b7..6d7af2651 100644 --- a/quint/src/repl.ts +++ b/quint/src/repl.ts @@ -272,26 +272,28 @@ export function quintRepl( const newState = loadFromFile(out, state, filename) if (!newState) { - return state + return } state.lastLoadedFileAndModule[0] = filename - const moduleNameToLoad = moduleName ?? getMainModuleAnnotation(newState.moduleHist) ?? '__repl__' - if (moduleNameToLoad === '__repl__') { + const moduleNameToLoad = moduleName ?? getMainModuleAnnotation(newState.moduleHist) + if (!moduleNameToLoad) { // No module to load, introduce the __repl__ module newState.addReplModule() } - if (tryEvalModule(out, newState, moduleNameToLoad)) { + if (tryEvalModule(out, newState, moduleNameToLoad ?? '__repl__')) { state.lastLoadedFileAndModule[1] = moduleNameToLoad } else { out(chalk.yellow('Pick the right module name and import it (the file has been loaded)\n')) - return newState + return } if (newState.exprHist) { - newState.exprHist.forEach(expr => { + const expressionsToEvaluate = newState.exprHist + newState.exprHist = [] + expressionsToEvaluate.forEach(expr => { tryEvalAndClearRecorder(out, newState, expr) }) } @@ -441,13 +443,14 @@ export function quintRepl( function saveToFile(out: writer, state: ReplState, filename: string) { // 1. Write the previously loaded modules. - // 2. Write the definitions in the special module called __repl__. + // 2. Write the definitions in the loaded module (or in __repl__ if no module was loaded). // 3. Wrap expressions into special comments. try { - const text = - `// @mainModule ${state.lastLoadedFileAndModule[1]}\n` + - `${state.moduleHist}` + - state.exprHist.map(s => `/*! ${s} !*/\n`).join('\n') + const mainModuleAnnotation = state.moduleHist.startsWith('// @mainModule') + ? '' + : `// @mainModule ${state.lastLoadedFileAndModule[1] ?? '__repl__'}\n` + + const text = mainModuleAnnotation + `${state.moduleHist}` + state.exprHist.map(s => `/*! ${s} !*/\n`).join('\n') writeFileSync(filename, text) out(`Session saved to: ${filename}\n`) @@ -572,6 +575,7 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { return false } + state.exprHist.push(newInput.trim()) const evalResult = state.evaluator.evaluate(parseResult.expr) evalResult.map(ex => { @@ -616,6 +620,7 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { if (state.nameResolver.errors.length > 0) { printErrorMessages(out, state, 'static analysis error', newInput, state.nameResolver.errors) out('\n') + parseResult.decls.forEach(decl => { if (isDef(decl)) { state.nameResolver.collector.deleteDefinition(decl.name) @@ -644,6 +649,7 @@ function tryEval(out: writer, state: ReplState, newInput: string): boolean { } state.compilationState.analysisOutput = analysisOutput + state.moduleHist = state.moduleHist.slice(0, state.moduleHist.length - 1) + newInput + '\n}' // update the history out('\n') } diff --git a/quint/src/runtime/impl/builder.ts b/quint/src/runtime/impl/builder.ts index 34116ac61..b51839663 100644 --- a/quint/src/runtime/impl/builder.ts +++ b/quint/src/runtime/impl/builder.ts @@ -23,7 +23,7 @@ import { QuintError } from '../../quintError' import { RuntimeValue, rv } from './runtimeValue' import { builtinLambda, builtinValue, lazyBuiltinLambda, lazyOps } from './builtins' import { CachedValue, Context, Register } from './Context' -import { QuintApp, QuintEx } from '../../ir/quintIr' +import { QuintApp, QuintEx, QuintVar } from '../../ir/quintIr' import { LookupDefinition, LookupTable } from '../../names/base' import { NamedRegister, VarStorage, initialRegisterValue } from './VarStorage' import { List } from 'immutable' @@ -85,15 +85,16 @@ export class Builder { /** * Gets the register for a variable by its id and the namespaces in scope (tracked by this builder). * - * @param id + * @param def - The variable to get the register for. * * @returns the register for the variable */ - getVar(id: bigint): NamedRegister { - const key = [id, ...this.namespaces].join('#') + getVar(def: QuintVar): NamedRegister { + const key = [def.id, ...this.namespaces].join('#') const result = this.varStorage.vars.get(key) if (!result) { - throw new Error(`Variable not found: ${key}`) + this.discoverVar(def.id, def.name) + return this.varStorage.vars.get(key)! } return result @@ -341,7 +342,7 @@ function buildDefCore(builder: Builder, def: LookupDefinition): EvalFunction { case 'var': { // Every variable has a single register, and we just change this register's value at each state // So, a reference to a variable simply evaluates to the value of the register. - const register = builder.getVar(def.id) + const register = builder.getVar(def) return _ => { return register.value } From c2d27e680d1a8be6031a823ad1a94d35212d717d Mon Sep 17 00:00:00 2001 From: bugarela Date: Mon, 9 Sep 2024 15:47:13 -0300 Subject: [PATCH 37/37] Use `-q` flag in new REPL integration tests to avoid dealing with different version numbers --- quint/io-cli-tests.md | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/quint/io-cli-tests.md b/quint/io-cli-tests.md index 2c876ab5b..27b0cb256 100644 --- a/quint/io-cli-tests.md +++ b/quint/io-cli-tests.md @@ -1226,19 +1226,17 @@ echo 'q::debug("value:", { foo: 42, bar: "Hello, World!" })' | quint | tail -n + ``` -echo -e 'inexisting_name\n1 + 1' | quint +echo -e 'inexisting_name\n1 + 1' | quint -q ``` ``` -Quint REPL 0.21.1 -Type ".exit" to exit, or ".help" for more information ->>> static analysis error: error: [QNT404] Name 'inexisting_name' not found +static analysis error: error: [QNT404] Name 'inexisting_name' not found inexisting_name ^^^^^^^^^^^^^^^ ->>> 2 ->>> +2 + ``` ### REPL continues to work after conflicting definitions @@ -1247,15 +1245,13 @@ Regression for https://github.com/informalsystems/quint/issues/434 ``` -echo -e 'def farenheit(celsius) = celsius * 9 / 5 + 32\ndef farenheit(celsius) = celsius * 9 / 5 + 32\nfarenheit(1)' | quint +echo -e 'def farenheit(celsius) = celsius * 9 / 5 + 32\ndef farenheit(celsius) = celsius * 9 / 5 + 32\nfarenheit(1)' | quint -q ``` ``` -Quint REPL 0.21.1 -Type ".exit" to exit, or ".help" for more information ->>> ->>> static analysis error: error: [QNT101] Conflicting definitions found for name 'farenheit' in module '__repl__' + +static analysis error: error: [QNT101] Conflicting definitions found for name 'farenheit' in module '__repl__' def farenheit(celsius) = celsius * 9 / 5 + 32 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1264,41 +1260,39 @@ def farenheit(celsius) = celsius * 9 / 5 + 32 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ->>> 33 ->>> +33 + ``` ### REPL continues to work after type errors ``` -echo -e 'def foo = 1 + "a"\nfoo\n1 + "a"\n1 + 1' | quint +echo -e 'def foo = 1 + "a"\nfoo\n1 + "a"\n1 + 1' | quint -q ``` ``` -Quint REPL 0.21.1 -Type ".exit" to exit, or ".help" for more information ->>> static analysis error: error: [QNT000] Couldn't unify int and str +static analysis error: error: [QNT000] Couldn't unify int and str Trying to unify int and str Trying to unify (int, int) => int and (int, str) => _t0 def foo = 1 + "a" ^^^^^^^ ->>> static analysis error: error: [QNT404] Name 'foo' not found +static analysis error: error: [QNT404] Name 'foo' not found foo ^^^ ->>> static analysis error: error: [QNT000] Couldn't unify int and str +static analysis error: error: [QNT000] Couldn't unify int and str Trying to unify int and str Trying to unify (int, int) => int and (int, str) => _t0 1 + "a" ^^^^^^^ ->>> 2 ->>> +2 + ```