UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

354 lines 16.5 kB
"use strict"; /* ---------------------------------------------------------------------------------- * Copyright 2022 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. * --------------------------------------------------------------------------------- */ Object.defineProperty(exports, "__esModule", { value: true }); exports.EffectInferrer = void 0; /** * Inference for effects. Walks through a module and infers effects for all expressions. * See ADR 0004 for additional information * * @author Gabriela Moreira * * @module */ const either_1 = require("@sweet-monads/either"); const IRprinting_1 = require("../ir/IRprinting"); const IRVisitor_1 = require("../ir/IRVisitor"); const base_1 = require("./base"); const substitutions_1 = require("./substitutions"); const errorTree_1 = require("../errorTree"); const builtinSignatures_1 = require("./builtinSignatures"); const FreshVarGenerator_1 = require("../FreshVarGenerator"); const printing_1 = require("./printing"); const util_1 = require("../util"); const namespaces_1 = require("./namespaces"); /* Walks the IR from node to root inferring effects for expressions and * assigning them to the effects attribute map, to be used in upward * expressions. Errors are written to the errors attribute. */ class EffectInferrer { constructor(lookupTable, effects) { this.effects = new Map(); this.errors = new Map(); this.substitutions = []; this.builtinSignatures = (0, builtinSignatures_1.getSignatures)(); // Track location descriptions for error tree traces this.location = ''; // A stack of free effect variables and entity variables for lambda expressions. // Nested lambdas add new entries to the stack, and pop them when exiting. this.freeNames = []; // 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. this.definitionDepth = -1; this.lookupTable = lookupTable; this.freshVarGenerator = new FreshVarGenerator_1.FreshVarGenerator(); if (effects) { this.effects = effects; } } /** * Infers an effect for every expression in a module based on the definitions * table for that module. If there are missing effects in the effect map, * there will be at least one error. * * @param declarations: the list of QuintDeclarations to infer effects for * * @returns a map from expression ids to their effects and a map from expression * ids to the corresponding error for any problematic expressions. */ inferEffects(declarations) { declarations.forEach(decl => { (0, IRVisitor_1.walkDeclaration)(this, decl); }); return [this.errors, this.effects]; } enterExpr(e) { this.location = `Inferring effect for ${(0, IRprinting_1.expressionToString)(e)}`; } exitConst(def) { const pureEffect = { kind: 'concrete', components: [] }; if (def.typeAnnotation.kind === 'oper') { // Operators need to have arrow effects of proper arity // type annotation for c is oper with n args // --------------------------------------------------------(CONST - OPER) // Γ ⊢ const c: propagateComponents(['read', 'temporal'])(n) this.addToResults(def.id, (0, either_1.right)((0, builtinSignatures_1.standardPropagation)(def.typeAnnotation.args.length))); return; } // type annotation for c is not oper // ------------------------------------- (CONST-VAL) // Γ ⊢ const c: Pure this.addToResults(def.id, (0, either_1.right)((0, base_1.toScheme)(pureEffect))); } // -------------------------------------- (VAR) // Γ ⊢ var name: Read[name] exitVar(def) { const effect = { kind: 'concrete', components: [ { kind: 'read', entity: { kind: 'concrete', stateVariables: [{ name: def.name, reference: def.id }] } }, ], }; this.addToResults(def.id, (0, either_1.right)((0, base_1.toScheme)(effect))); } // { identifier: name }: E ∈ Γ // ----------------------------- (NAME) // Γ ⊢ name: E exitName(expr) { if (this.errors.size > 0) { // Don't try to infer application if there are errors with the args return; } this.addToResults(expr.id, this.effectForName(expr.name, expr.id, 2).map(base_1.toScheme)); } // { identifier: op, effect: E } ∈ Γ Γ ⊢ p0:E0 ... Γ ⊢ pn:EN // Eres <- freshVar S = unify(newInstance(E), (E0, ..., EN) => Eres) // ------------------------------------------------------------------- (APP) // Γ ⊢ op(p0, ..., pn): S(Eres) exitApp(expr) { if (this.errors.size > 0) { // Don't try to infer application if there are errors with the args return; } this.location = `Trying to infer effect for operator application in ${(0, IRprinting_1.expressionToString)(expr)}`; const paramsResult = (0, either_1.mergeInMany)(expr.args.map((a) => { return this.fetchResult(a.id).map(e => this.newInstance(e)); })); const resultEffect = { kind: 'variable', name: this.freshVarGenerator.freshVar('_e') }; const arrowEffect = paramsResult .map(params => { const effect = { kind: 'arrow', params, result: resultEffect, }; return effect; }) .chain(e => (0, substitutions_1.applySubstitution)(this.substitutions, e)); this.effectForName(expr.opcode, expr.id, expr.args.length) .mapLeft(err => (0, errorTree_1.buildErrorTree)(this.location, err)) .chain(signature => { const substitution = arrowEffect.chain(effect => (0, substitutions_1.applySubstitution)(this.substitutions, signature).chain(s => (0, base_1.unify)(s, effect))); const resultEffectWithSubs = substitution .chain(s => (0, substitutions_1.compose)(this.substitutions, s)) .chain(s => { this.substitutions = s; paramsResult.map(effects => (0, util_1.zip)(effects, expr.args.map(a => a.id)).forEach(([effect, id]) => { const r = (0, substitutions_1.applySubstitution)(s, effect).map(base_1.toScheme); this.addToResults(id, r); })); // For every free name we are binding in the substitutions, the names occurring in the value of the // substitution have to become free as well. this.addBindingsToFreeNames(s); return (0, substitutions_1.applySubstitution)(s, resultEffect); }); return resultEffectWithSubs; }) .map(effect => { this.addToResults(expr.id, (0, either_1.right)((0, base_1.toScheme)(effect))); }) .mapLeft(err => { this.addToResults(expr.id, (0, either_1.left)(err)); }); } // Literals are always Pure exitLiteral(expr) { this.addToResults(expr.id, (0, either_1.right)((0, base_1.toScheme)({ kind: 'concrete', components: [], }))); } // Γ ⊢ expr: E // ---------------------------------- (OPDEF) // Γ ⊢ (def op(params) = expr): E exitOpDef(def) { if (this.errors.size > 0) { // Don't try to infer let if there are errors with the defined expression return; } this.fetchResult(def.expr.id).map(e => { this.addToResults(def.id, (0, either_1.right)(this.quantify(e.effect))); }); // When exiting top-level definitions, clear the substitutions if (this.definitionDepth === 0) { this.substitutions = []; } } // Γ ⊢ expr: E // ------------------------- (LET) // Γ ⊢ <opdef> { expr }: E exitLet(expr) { if (this.errors.size > 0) { // Don't try to infer let if there are errors with the defined expression return; } const e = this.fetchResult(expr.expr.id); this.addToResults(expr.id, e); } // { kind: 'param', identifier: p, reference: ref } ∈ Γ // ------------------------------------------------------- (LAMBDA-PARAM) // Γ ⊢ p: e_p_ref // // { kind: 'param', identifier: '_', reference: ref } ∈ Γ // e < - freshVar // ------------------------------------------------------- (UNDERSCORE) // Γ ⊢ '_': e enterLambda(expr) { const lastParamNames = this.currentFreeNames(); const paramNames = { effectVariables: new Set(lastParamNames.effectVariables), entityVariables: new Set(lastParamNames.entityVariables), }; expr.params.forEach(p => { const varName = p.name === '_' ? this.freshVarGenerator.freshVar('_e') : `e_${p.name}_${p.id}`; paramNames.effectVariables.add(varName); this.addToResults(p.id, (0, either_1.right)((0, base_1.toScheme)({ kind: 'variable', name: varName }))); }); this.freeNames.push(paramNames); } // Γ ⊢ expr: E // ------------------------------------------------------- (LAMBDA) // Γ ⊢ (p0, ..., pn) => expr: quantify((E0, ..., En) => E) exitLambda(lambda) { if (this.errors.size > 0) { return; } // For every free name we are binding in the substitutions, the names occurring in the value of the substitution // have to become free as well. this.addBindingsToFreeNames(this.substitutions); const exprResult = this.fetchResult(lambda.expr.id); const params = (0, either_1.mergeInMany)(lambda.params.map(p => { const result = this.fetchResult(p.id) .map(e => this.newInstance(e)) .chain(e => (0, substitutions_1.applySubstitution)(this.substitutions, e)); this.addToResults(p.id, result.map(base_1.toScheme)); return result; })); const result = exprResult .chain(resultEffect => { return params.map((ps) => { return { ...resultEffect, effect: { kind: 'arrow', params: ps, result: resultEffect.effect } }; }); }) .map(this.newInstance) .chain(effect => (0, substitutions_1.applySubstitution)(this.substitutions, effect)) .map(effect => { if (effect.kind !== 'arrow') { // Impossible throw new Error(`Arrow effect after substitution should be an arrow: ${(0, printing_1.effectToString)(effect)}`); } if (effect.result.kind == 'arrow') { const error = (0, errorTree_1.buildErrorLeaf)(this.location, `Result cannot be an operator`); // Add result to the lambda body (instead of entire lambda expression) // to make reporting more precise this.addToResults(lambda.expr.id, (0, either_1.left)(error)); } return (0, base_1.toScheme)(effect); }); this.addToResults(lambda.id, result); this.freeNames.pop(); } addToResults(exprId, result) { result .mapLeft(err => this.errors.set(exprId, (0, errorTree_1.buildErrorTree)(this.location, err))) .map(r => this.effects.set(exprId, r)); } fetchResult(id) { const successfulResult = this.effects.get(id); const failedResult = this.errors.get(id); if (failedResult) { return (0, either_1.left)(failedResult); } else if (successfulResult) { return (0, either_1.right)(successfulResult); } else { throw new Error(`Couldn't find any result for id ${id} while ${this.location}`); } } effectForName(name, nameId, arity) { // Assumes a valid number of arguments if (this.builtinSignatures.has(name)) { const signatureFunction = this.builtinSignatures.get(name); const signature = signatureFunction(arity); return (0, either_1.right)(this.newInstance(signature)); } else { const def = this.lookupTable.get(nameId); const id = def?.id; if (!def || !id) { return (0, either_1.left)((0, errorTree_1.buildErrorLeaf)(this.location, `Signature not found for name: ${name}`)); } return this.fetchResult(id).map(e => { const effect = this.newInstance(e); if (def.importedFrom?.kind === 'instance') { // Names imported from instances might have effects that refer to // names that are shared between multiple instances. To properly infer // effects referring to those state variables, they need to be // namespaced in a way that makes them different between different // instances. For that, we use the namespaces attribute from lookup // table definition, which contains the proper namespaces to identify // unique names while flattening. return (0, namespaces_1.addNamespaces)(effect, def.namespaces ?? []); } return effect; }); } } newInstance(effect) { const effectSubs = [...effect.effectVariables].map(name => { return { kind: 'effect', name: name, value: { kind: 'variable', name: this.freshVarGenerator.freshVar('_e') } }; }); const entitySubs = [...effect.entityVariables].map(name => { return { kind: 'entity', name: name, value: { kind: 'variable', name: this.freshVarGenerator.freshVar('_v') } }; }); const result = (0, substitutions_1.compose)(effectSubs, entitySubs).chain(s => (0, substitutions_1.applySubstitution)(s, effect.effect)); if (result.isLeft()) { throw new Error(`Error applying fresh names substitution: ${(0, errorTree_1.errorTreeToString)(result.value)} `); } else { return result.value; } } currentFreeNames() { return (this.freeNames[this.freeNames.length - 1] ?? { effectVariables: new Set(), entityVariables: new Set(), }); } quantify(effect) { const freeNames = this.currentFreeNames(); const nonFreeNames = { effectVariables: new Set([...(0, base_1.effectNames)(effect).effectVariables].filter(name => !freeNames.effectVariables.has(name))), entityVariables: new Set([...(0, base_1.effectNames)(effect).entityVariables].filter(name => !freeNames.entityVariables.has(name))), }; return { ...nonFreeNames, effect: effect }; } addBindingsToFreeNames(substitutions) { // Assumes substitutions are topologically sorted, i.e. [ t0 |-> (t1, t2), t1 |-> (t3, t4) ] substitutions.forEach(s => { switch (s.kind) { case 'effect': this.freeNames .filter(free => free.effectVariables.has(s.name)) .forEach(free => { const names = (0, base_1.effectNames)(s.value); names.effectVariables.forEach(v => free.effectVariables.add(v)); names.entityVariables.forEach(v => free.entityVariables.add(v)); }); return; case 'entity': this.freeNames .filter(free => free.entityVariables.has(s.name)) .forEach(free => (0, base_1.entityNames)(s.value).forEach(v => free.entityVariables.add(v))); return; } }); } } exports.EffectInferrer = EffectInferrer; //# sourceMappingURL=inferrer.js.map