@informalsystems/quint
Version:
Core tool for the Quint specification language
354 lines • 16.5 kB
JavaScript
;
/* ----------------------------------------------------------------------------------
* 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