Lurch core classes

source

formula.js


/**
 * A formula is a {@link LogicConcept LogicConcept} that includes symbols
 * intended to be used for substitution, that is, what
 * {@link module:Matching the Matching module} calls
 * {@link module:Matching.metavariable metavariables}.  This namespace
 * provides functions for dealing with formulas, including
 * {@link Formula.from converting} a {@link LogicConcept LogicConcept} into a
 * formula, computing the {@link Formula.domain domain} of a formula, and
 * {@link Formula.instantiate instantiating} a formula.
 * 
 * Import this namespace with code such as
 * `import Formula from './formula.js'` and then make calls such as
 * `Formula.domain( myLC )`.
 * 
 * @namespace Formula
 */

import { MathConcept } from './math-concept.js'
import { LogicConcept } from './logic-concept.js'
import { BindingEnvironment } from './binding-environment.js'
import { BindingExpression } from './binding-expression.js'
import { Declaration } from './declaration.js'
import { Expression } from './expression.js'
import { Environment } from './environment.js'
import { Symbol as LurchSymbol } from './symbol.js'
import Matching from './matching.js'

/**
 * A {@link LogicConcept LogicConcept} can be converted into a formula by
 * replacing any subset of its {@link Symbol Symbols} with metavariables.
 * Recall that a metavariable can be any symbol, but with
 * {@link module:Matching.metavariable a specific attribute} set to "true."
 * 
 * Which {@link Symbol Symbols} should be converted into metavariables?  Any
 * one that is not in the scope of a {@link Declaration Declaration} of that
 * same {@link Symbol}.  For example, if we consider the {@link LogicConcept}
 * indicated by the {@link LogicConcept.fromPutdown putdown notation}
 * `{ :(= a b) (= b a) }` and we assume it is in the scope of a
 * {@link Declaration} of the `=` {@link Symbol}, but not of the `a` or `b`
 * {@link Symbol Symbols}, then converting `{ :(= a b) (= b a) }` to a formula
 * would produce a copy in which each instance of `a` and `b` have had their
 * metavariable attribute set to "true" (via a call to the
 * {@link MathConcept#makeIntoA makeIntoA()} member).
 * 
 * The exception is that bound variables are never marked as metavariables.
 * 
 * @param {LogicConcept} LC - the {@link LogicConcept LogicConcept} to convert
 *   into a formula
 * @param {boolean} [inPlace] - if `true`, the metavariables will be attributed in
 * `LC` itself, and the resulting modified `LC` is returned.  If `false`, it will
 * attribute and return a separate copy of `LC`, leaving the original unchanged.  
 * @returns {LogicConcept} a structural copy of `LC`, except with the relevant
 *   {@link Symbol Symbol} atoms converted into
 *   {@link module:Matching.metavariable metavariables}
 * @memberof Formula
 * @alias Formula.from
 */
const from = (LC, inPlace=false) => {
    // what symbol names were already declared?
    const declared = new Set( LC.accessibles().filter(
        a => a instanceof Declaration
    ).map( d => d.symbols() ).flat().map( s => s.text() ) )
    // add to that the special symbol for empty declaration bodies
    declared.add( Declaration.emptyBody().text() )
    // and what were bound outside of the LC?
    const bound = new Set( LC.ancestors().filter(
        a => a != LC && ( a instanceof BindingEnvironment )
    ).map( be => be.boundSymbolNames() ).flat() )
    // make a copy and change all of its undeclared symbols into metavariables
    const result = (inPlace) ? LC : LC.copy()
    result.descendantsSatisfying( d => d instanceof LurchSymbol )
          .filter( s => !declared.has( s.text() ) && !bound.has( s.text() )
                                                  && s.isFree() )
          .forEach( s => s.makeIntoA( Matching.metavariable ) )
    // return it
    return result
}

/**
 * Once a {@link LogicConcept LogicConcept} has been {@link Formula.from
 * converted into a formula}, it will have zero or more metavariables.  This
 * function returns the set of names of those metavariables.  If the formula
 * has no metavariables, or if a non-formula {@link LogicConcept LogicConcept}
 * was passed as input, this function will return the empty set.
 * 
 * @param {LogicConcept} formula - the {@link LogicConcept LogicConcept} to
 *   view as a formula, and whose metavariables should be investigated to
 *   compute this function's result
 * @returns {Set} the set of names of {@link Symbol Symbols} in the given
 *   `formula` that appear as a metavariable therein
 * @memberof Formula
 * @alias Formula.domain
 */
const domain = formula =>
    new Set( formula.descendantsSatisfying(
        d => ( d instanceof LurchSymbol ) && d.isA( Matching.metavariable )
    ).map( metavar => metavar.text() ) )

/**
 * Instantiating a formula means making a copy of that formula and then
 * replacing each metavariable in it with another {@link LogicConcept
 * LogicConcept}, according to some predetermined mapping of metavariable
 * names to {@link LogicConcept LogicConcepts}, called the "instantiation."
 * This function performs exactly that operation, with two exceptions.
 * 
 * First, this function does not check to ensure that the domain of the given
 * `instantiation` matches the {@link Formula.domain domain} of the given
 * `formula`.  If that is important to the caller, they should verify that
 * themselves in advance, using an {@link Set#equals equality check} between
 * the two sets.
 * 
 * Second, this function also applies {@link module:Matching.fullBetaReduce
 * beta reduction} to the result before returning it (if necessary) because
 * the instantiation may have introduced an explicit expression function in a
 * position where it should be applied to its arguments.  Note the warnings in
 * the beta reduction documentation about infinite loops.
 * 
 * @param {LogicConcept} formula - the {@link LogicConcept LogicConcept} to
 *   instantiate with the given `instantiation`
 * @param {Substitution|Solution|Object|Map} instantiation - either a
 *   {@link Substitution Substitution} instance (which describes a single
 *   metavariable's replacement) or a {@link Solution Solution} instance
 *   (which describes the substitution of zero or more metavariables) or a
 *   JavaScript Object or Map (which should map metavariable names to the
 *   desired instantiations) to be used as the function to apply to each
 *   metavariable in the `formula`
 * @param {String[]} preserve - the list of attributes to preserve while doing
 *   the instantiation.  Any metavariable having these attributes will have the
 *   values of those attributes copied over to its instantiation.  All other
 *   attributes of the metavariable (including, typically, the fact that it is a
 *   metavariable) will be lost during the instantiation.  This defaults to the
 *   list containing just one entry, the "given" type attribute flag.
 * @memberof Formula
 * @alias Formula.instantiate
 */
const instantiate = (
    formula, instantiation,
    preserve = [ MathConcept.typeAttributeKey( 'given' ) ]
) => {
    // handle the atomic case, where .replaceWith() would fail, and where we
    // don't need to check any restrictions on the parent, because there is no
    // parent in this case (since we're making a copy).  Furthermore, in this
    // case, we cannot possibly have created a case of
    // ("LDE EFA" ("LDE lambda" ...) ...), so we do not beta-reduce.
    if ( ( formula instanceof LurchSymbol )
      && formula.isA( Matching.metavariable ) ) {
        const result = lookup( instantiation, formula )
        preserve.forEach( attrKey => {
            if ( formula.hasAttribute( attrKey ) )
                result.setAttribute( attrKey,
                    JSON.copy( formula.getAttribute( attrKey ) ) )
        } )
        return result
    }
    // handle the usual case, where we may have multiple substitutions to make
    // and since each one is inside a parent, that may bring restrictions.
    const result = formula.copy()
    result.descendantsSatisfying(
        d => ( d instanceof LurchSymbol ) && d.isA( Matching.metavariable )
    ).forEach( metavar =>
        replaceIfPossible( metavar, lookup( instantiation, metavar ), preserve )
    )
    // if there are any expression function applications, try beta reducing.
    if ( result instanceof Expression )
        return betaIfNeeded( result )
    result.descendantsSatisfying(
        d => ( d instanceof Expression ) && d.isOutermost()
    ).forEach( d => d.replaceWith( betaIfNeeded( d ) ) )
    return result
}

/**
 * The concept of variable capture in a quantifier (or, more generically, in a
 * {@link BindingExpression}) is a well-known one, and is also documented
 * in functions such as {@link MathConcept#isFree this one}.  But
 * {@link Declaration Declarations} function similarly to bindings, except their
 * scope spans multiple expressions in the same (or inner) environments.  So we
 * can consider a secondary type of symbol capture, one in which we substitute
 * into an expression $E$ in the scope of a declaration $D$, and doing so
 * inserts an instance of a symbol $s$ that is free in $E$ but is declared by
 * $D$.  This function checks to see whether that would happen if we applied an
 * instantiation to a formula.
 * 
 * For example, if we create a formula from the putdown notation
 * `{ :[x] (+ a 1) }`, with both `x` and `a` as metavariables, we could imagine
 * an instantiation of `a` with the expression `(* x 2)`.  No capture would
 * occur from the point of view of binding expressions, but capture would occur
 * from the point of view of the declaration of `x` at the start of the
 * environment, which functions conceptually like a binding.  This function
 * would return true in such a case, because capture of this type would occur.
 * 
 * Note that this function does *not* report on capture of the *other* type.
 * That is, it may be the case that, for some expression `E` inside the formula,
 * if we asked `E.isFreeToReplace(a,b)` where `b` is `(* x 2)`, we would get
 * false, but this function returns true, because it's not checking for capture
 * inside of expressions by a {@link BindingExpression}, but rather it is
 * checking only for capture that happens due to a declaration.
 * @memberof Formula
 * @alias Formula.hasDeclarationCapture
 */
const hasDeclarationCapture = ( formula, instantiation ) => {
    const dbg = foo => foo.toPutdown().replace( / \+\{"_type_LDE MV"\:true\}\n/g, '_' )
    // console.log( 'Formula: ' + dbg( formula ) )
    // console.log( 'Instantiation:' )
    // Object.keys( instantiation ).map( key =>
    //     console.log( `\t${key} => ${dbg(instantiation[key])}` ) )
    const whatMightGetCaptured = subIntoThis => {
        const result = [ ...new Set( subIntoThis.descendantsSatisfying(
            d => ( d instanceof LurchSymbol ) && d.isA( Matching.metavariable )
        ) ) ].map( mv => {
            const image = lookup( instantiation, mv )
            return image == mv ? [ ] :
                image.freeSymbolNames().map( sym => [ mv.text(), sym ] )
        } ).flat()
        // console.log( 'In ' + dbg(subIntoThis) + ', possible captures are: '
        //     + result.join( ' ; ' ) )
        return result
    }
    const expressionsIn = target =>
        target instanceof Expression ? [ target ] :
        target instanceof Array ? target.map( expressionsIn ).flat() :
        expressionsIn( target.children() )
    const expressionScope = target =>
        expressionsIn( target.scope().filter( LC =>
            ( LC instanceof Expression ) && LC.isOutermost() ) )
    return formula.descendantsSatisfying(
        d => d instanceof Declaration
    ).some( d => {
        const declaredNames = d.symbols().map( s => s.text() )
        const declaredImages = d.symbols().map(
            s => lookup( instantiation, s ).text() )
        // console.log( 'Ignore: ' + declaredNames )
        // console.log( 'Seek:   ' + declaredImages )
        return expressionScope( d ).some( expr =>
            whatMightGetCaptured( expr ).some( data =>
                !declaredNames.includes( data[0] )
             && declaredImages.includes( data[1] ) ) )
    } )
}

// Helper function:  Look up a given metavariable in a given instantiation,
// whether that instantiation is a Solution, a Substitution, a Map, or an
// Object, and return a copy of the image, if there is one.
const lookup = ( instantiation, metavar ) => {
    if ( instantiation instanceof Matching.Substitution )
        return instantiation.metavariable.equals( metavar ) ?
            instantiation.expression.copy() : metavar
    if ( instantiation instanceof Matching.Solution ) {
        const mapsTo = instantiation.get( metavar )
        return mapsTo ? mapsTo.copy() : metavar
    }
    if ( instantiation instanceof Map ) {
        if ( !instantiation.has( metavar.text() ) ) return metavar
        const mapsTo = instantiation.get( metavar.text() )
        return mapsTo instanceof LogicConcept ? mapsTo.copy() : metavar
    }
    if ( instantiation instanceof Object ) {
        if ( !instantiation.hasOwnProperty( metavar.text() ) ) return metavar
        const mapsTo = instantiation[metavar.text()]
        return mapsTo instanceof LogicConcept ? mapsTo.copy() : metavar
    }
    throw new Error( `Invalid instantiation: ${instantiation}` )
}

// Helper function:  Check whether it's acceptable to call x.replaceWith( y ),
// in the sense that the resulting LogicConcept hierarchy would still be valid
// (only symbols are bound, no environments inside expressions, etc.).
// The "preserve" attribute is documented above, in instantiate().
const replaceIfPossible = ( target, replacement, preserve ) => {
    const parent = target.parent()
    // bound symbols must be symbols, not anything else
    if ( ( ( parent instanceof BindingEnvironment )
        || ( parent instanceof BindingExpression ) )
      && !( replacement instanceof LurchSymbol )
      && ( target != parent.body() ) )
        throw new Error( 'Cannot replace a bound symbol with a non-symbol' )
    // declared symbols must be symbols, not anything else
    if ( ( parent instanceof Declaration )
      && !( replacement instanceof LurchSymbol )
      && parent.symbols().includes( target ) )
        throw new Error( 'Cannot replace a delcared symbol with a non-symbol' )
    // expressions can contain only other expressions
    if ( ( parent instanceof Expression )
      && !( replacement instanceof Expression ) )
        throw new Error( 'Cannot place a non-expression inside an expression' )
    // no restrictions forbid us, so proceed
    // but note: replacing a declaration's body requires special care
    target.replaceWith( replacement )
    preserve.forEach( attrKey => {
        if ( target.hasAttribute( attrKey ) )
            replacement.setAttribute( attrKey,
                JSON.copy( target.getAttribute( attrKey ) ) )
    } )
}

/**
 * What are all instantiations of the given formula that produce the given
 * candidate?
 * 
 * On the one hand, it may seem like this is a simple application of
 * {@link module:Matching the Matching module}, and this is somewhat true.  The
 * only additional feature provided here is that the formula and its candidate
 * instantiation need not be {@link Expression Expressions}.  If they are not,
 * then they must have isomorphic structure except for any {@link Expression
 * Expressions} inside the two, which will be paired up to construct a matching
 * problem, whose solutions will be returned.
 * 
 * In other words, this function extends matching to support a single pair of
 * inputs that do not need to be {@link Expression Expressions}, and it does
 * exactly what you'd expect in that case.
 * 
 * This function is a generator that yields zero or more ways to instantiation
 * `formula` to produce `candidate`; all possible ways will be enumerated,
 * though it may be that there are no such ways, in which case the enumeration
 * will be empty.
 * 
 * @param {LogicConcept} formula a {@link LogicConcept} that has had
 *   metavariables added to it using {@link Formula.from}
 * @param {LogicConcept} candidate a {@link LogicConcept} that may or may not
 *   be an instantiation of the given `formula`
 * @alias Formula.allPossibleInstantiations
 */
function *allPossibleInstantiations ( formula, candidate ) {
    const problem = problemFromExpressionsWithin( formula, candidate )
    if ( !problem ) return // no isomorphism == no results
    yield* problem.solutions()
}

// Helper function used by allPossibleInstantiations(), above.
// Given two LogicConcepts that are not necessarily expressions, this ensures
// that they have the same structure outside of all expressions, and if so, it
// pairs up the corresponding expressions to produce a matching problem, which
// it then returns.  Otherwise, it returns null (no such pairing possible).
// Third argument is for internal use only; clients provide just the first two.
const problemFromExpressionsWithin = ( formula, candidate, result = null ) => {
    // create a problem if we were not passed one
    if ( result == null ) result = new Matching.Problem()
    // Case 1: The formula is an expression
    if ( formula instanceof Expression ) {
        if ( ( candidate instanceof Expression )
          && ( formula.isA( 'given' ) == candidate.isA( 'given' ) ) ) {
            // If the candidate is, they pair up in the matching problem
            result.add( formula.copy().unmakeIntoA( 'given' ),
                        candidate.copy().unmakeIntoA( 'given' ) )
            return result
        } else {
            return null // otherwise there can be no possible instantiation
        }
    } else { // Case 2: the formula is not an expression
        if ( ( candidate instanceof Expression )
          || ( formula.constructor.className
            != candidate.constructor.className )
          || ( formula.numChildren() != candidate.numChildren() )
          || ( formula.isA( 'given' ) != candidate.isA( 'given' ) ) ) {
            return null // candidate has diff. structure; no pairing possible
        } else {
            // Candidate has the same shape; proceed recursively
            for ( let i = 0 ; i < formula.numChildren() ; i++ ) {
                result = problemFromExpressionsWithin(
                    formula.child( i ), candidate.child( i ), result )
                if ( !result ) return null // if any child fails, all fails
            }
            return result // return the recursively-produced result
        }
    }
}

// Helper function:  Beta reduce an Expression only if necessary.
const betaIfNeeded = expr => Matching.fullBetaReduce( expr, false )

/**
 * To facilitate marking some {@link LogicConcept LogicConcepts} as cached
 * instantiations of formulas, we declare a string constant that can be used
 * with the {@link MathConcept#isA isA()} and {@link MathConcept#asA asA()} and
 * {@link MathConcept#makeIntoA makeIntoA()} functions in the
 * {@link MathConcept MathConcept} class.
 * 
 * This is used when we have a formula `F` and the inferences done in the scope
 * of that formula cite the formula and create instantiations of it.  Some of
 * our algorithms then insert those instantiations as next siblings following
 * `F` so that they are accessible at any location at which `F` is accessible.
 * But in order to distinguish those algorithmically-created instantiations
 * from content that was authored by the user, we mark algorithmically-created
 * instantiations with an attribute.  Specifically, for such an instantiation
 * `I`, we would call `I.makeIntoA( cachedInstantiation )`, and can then test
 * that later with `I.isA( cachedInstantiation )`.
 * 
 * @see {@link Formula.addCachedInstantiation addCachedInstantiation()}
 * @see {@link Formula.allCachedInstantiations allCachedInstantiations()}
 * @see {@link Formula.clearCachedInstantiations clearCachedInstantiations()}
 * @alias Formula.cachedInstantiation
 */
const cachedInstantiation = 'LDE CI'

/**
 * As described in {@link Formula.cachedInstantiation this documentation}, we
 * can insert after any formula instantiations of it.  This function adds a new
 * instantiation to that cache.  It does not first check to be sure that the
 * given instantiation is actually an instantiation of the given formula; the
 * client is in charge of ensuring that.  This function inserts the given
 * instantiation at the end of the given formula's instantiation cache and also
 * marks it with {@link Formula.cachedInstantiation the appropriate attribute}
 * so that it can be clearly identified as part of the cache.
 * 
 * If the formula has no parent, it can have no siblings, and thus this
 * function cannot do its job and will instead throw an error.
 * 
 * @param {LogicConcept} formula add a cached instantiation of this formula
 * @param {LogicConcept} instantiation the instantiation to add to the cache
 * @see {@link Formula.allCachedInstantiations allCachedInstantiations()}
 * @see {@link Formula.clearCachedInstantiations clearCachedInstantiations()}
 * @alias Formula.addCachedInstantiation
 */
const addCachedInstantiation = ( formula, instantiation ) => {
    if ( !formula.parent() )
        throw new Error(
            'Cannot insert cached instantiation: formula has no parent' )
    const existing = allCachedInstantiations( formula )
    const insertAfter = existing.length > 0 ? existing.last() : formula
    insertAfter.parent().insertChild(
        instantiation, insertAfter.indexInParent() + 1 )
    instantiation.makeIntoA( cachedInstantiation )
}

/**
 * As described in {@link Formula.cachedInstantiation this documentation}, we
 * can insert after any formula instantiations of it.  The sequence of next
 * siblings after a formula that have been marked as instantiations of it
 * (using {@link Formula.cachedInstantiation this constant}) are that formula's
 * instantiation cache.  This function returns that cache as a JavaScript
 * array, in the same order that the siblings appear following the formula.  It
 * may have zero or more entries, but will always be an array.
 * 
 * If the given formula has no parent, it will have no siblings and thus no
 * instantiation cache, and this function will return an empty array.
 * 
 * @param {LogicConcept} formula the formula whose cache should be computed
 * @returns {LogicConcept[]} the array of instantiations cached for the given
 *   formula
 * @see {@link Formula.addCachedInstantiation addCachedInstantiation()}
 * @see {@link Formula.clearCachedInstantiations clearCachedInstantiations()}
 * @alias Formula.allCachedInstantiations
 */
const allCachedInstantiations = formula => {
    const parent = formula.parent()
    if ( !parent ) return [ ]
    const result = [ ]
    const children = parent.children()
    let i = formula.indexInParent() + 1
    while ( i < children.length && children[i].isA( cachedInstantiation ) )
        result.push( children[i++] )
    return result
}

/**
 * As described in {@link Formula.cachedInstantiation this documentation}, we
 * can insert after any formula instantiations of it.  This function removes
 * all entries from that cache.  Because the cache is defined to be the
 * sequence of siblings following the formula that have the {@link
 * Formula.cachedInstantiation appropriate attribute}, this function actually
 * removes those {@link LogicConcept LogicConcepts} from their parent (which is
 * also the parent of the given formula).
 * 
 * If the given formula has no parent, it will have no instantiation cache, and
 * so this function will do nothing.
 * 
 * @param {LogicConcept} formula add a cached instantiation of this formula
 * @param {LogicConcept} instantiation the instantiation to add to the cache
 * @see {@link Formula.allCachedInstantiations allCachedInstantiations()}
 * @see {@link Formula.addCachedInstantiation addCachedInstantiation()}
 * @alias Formula.clearCachedInstantiations
 */
const clearCachedInstantiations = formula => {
    allCachedInstantiations( formula ).forEach(
        instantiation => instantiation.remove() )
}

// Helper function used by possibleSufficientInstantiations(), below.
// Traverses any given LogicConcept and returns an object of the form
// { positives : [ ... ], negatives : [ ... ] }, where the two arrays are full
// of the outermost expressions in the given LogicConcept, in the same order
// they appear in the tree, but classified by parity.  There is also a "both"
// member in the result, which just concatenates the two arrays.
const classifyByParity = ( LC, parity = 1 ) => {
    // Base case: this is an Expression; classify by the given parity
    if ( LC instanceof Expression ) {
        const claim = LC.copy().unmakeIntoA( 'given' )
        claim.original = LC
        return parity == 1 ?
            { positives : [ claim ], negatives : [ ], both : [ claim ] }
          : { positives : [ ], negatives : [ claim ], both : [ claim ] }
    }
    // Other base case: some type of LC we cannot handle; don't classify it
    const result = { positives : [ ], negatives : [ ], both : [ ] }
    if ( !( LC instanceof Environment ) ) return result
    // Inductive case: recur on children, gather their results, in order
    LC.children().forEach( child => {
        const childResult = classifyByParity( child,
            parity * ( child.isA( 'given' ) ? -1 : 1 ) )
        result.positives = result.positives.concat( childResult.positives )
        result.negatives = result.negatives.concat( childResult.negatives )
        result.both = result.both.concat( childResult.both )
    } )
    return result
}

// Utility function used by possibleSufficientInstantiations(), below.
// Takes a list [ LC_1, ..., LC_n ] of LCs and returns a sublist such that no
// two entries are .equals() with one another.
const duplicateLCsRemoved = listOfLCs => {
    const result = [ ]
    listOfLCs.forEach( LC => {
        if ( !result.some( earlierLC => earlierLC.equals( LC ) ) )
            result.push( LC )
    } )
    return result
}

// Utility function used by possibleSufficientInstantiations(), below.
// Takes a pattern list [ p_1, ..., p_n ] and a list of lists of candidate
// matches, [ L_1, ..., L_n ] (each L_i being a list of LCs) and an index of
// the first optional match.
// Modifies the first two arguments in place and returns a new index of the
// first optional match, ensuring that no p_i.equals( p_j ), but all constraints
// on the same pattern have been united into one list of candidate matches.
const removeDuplicatePatterns = (
    patterns, candidateLists, firstOptionalIndex
) => {
    // Drop any optional patterns that are also required patterns (and thus
    // appearing earlier in the patterns list, as a requirement).
    const requiredPatterns = patterns.slice( 0, firstOptionalIndex )
    for ( let i = patterns.length - 1 ; i >= firstOptionalIndex ; i-- ) {
        if ( requiredPatterns.some(
                required => required.equals( patterns[i] ) ) ) {
            patterns.splice( i, 1 )
            candidateLists.splice( i, 1 )
        }
    }
    // For any two requirements that share the same pattern, merge it into a
    // single requirement for that pattern, with the candidate list being the
    // intersection of the original two candidate lists (since both must match).
    const LCListIntersection = ( LCList1, LCList2 ) =>
        LCList1.filter( LC1 => LCList2.some( LC2 => LC1.equals( LC2 ) ) )
    for ( let i = firstOptionalIndex - 1 ; i >= 0 ; i-- ) {
        const earlierIndex = patterns.slice( 0, i ).findIndex(
            earlierPattern => earlierPattern.equals( patterns[i] ) )
        if ( earlierIndex > -1 ) {
            candidateLists[earlierIndex] = LCListIntersection(
                candidateLists[earlierIndex], candidateLists[i] )
            patterns.splice( i, 1 )
            candidateLists.splice( i, 1 )
            firstOptionalIndex-- // because we removed a required pattern
        }
    }
    // Now do the same thing for optional patterns as well.
    for ( let i = patterns.length - 1 ; i >= firstOptionalIndex ; i-- ) {
        const earlierIndex = patterns.slice( firstOptionalIndex, i ).findIndex(
            earlierPattern => earlierPattern.equals( patterns[i] ) )
        if ( earlierIndex > -1 ) {
            candidateLists[earlierIndex] = LCListIntersection(
                candidateLists[earlierIndex], candidateLists[i] )
            patterns.splice( i, 1 )
            candidateLists.splice( i, 1 )
        }
    }
    // Done--return new firstOptionalIndex
    return firstOptionalIndex
}

// Utility function used by possibleSufficientInstantiations(), below.
// Given a pattern P and a list L of possible instantiations of the pattern,
// this function returns a (typically smaller) list L' that contains only those
// entries from L that have the same top-level signature as P.
// P and Q have the same top-level signature iff:
// - P is a single metavariable, meaning it could match anything, or
// - P is a compound expression whose operator and number of operands match
//   those of Q
const filterBySignature = ( P, L ) => {
    // a single metavariable matches anything
    if ( P.isAtomic() ) return L
    // filter first by number of children
    L = L.filter( entry => entry.numChildren() == P.numChildren() )
    // filter next by operator, unless that operator is a metavariable
    const operator = P.firstChild()
    if ( !operator.isA( Matching.metavariable ) )
        L = L.filter( entry => entry.firstChild().equals( operator ) )
    return L
}

/**
 * Given a sequent and a formula, is there an instantiation of the formula that,
 * if added as another premise to the sequent, would make the sequent true?  The
 * answer to that question depends upon which deductive system is providing the
 * meaning of "true" for sequents, but in any reasonable case, finding exactly
 * the set of instantiations that would make a sequent true will typically be at
 * least as difficult as validating the instantiated sequent, perhaps moreso.
 * Consequently, this function does not attempt to find the exact correct answer
 * to that question, but rather finds a superset of the answer, guaranteeing
 * that any instantiation that would make the sequent hold will be among the
 * results.  Those results can be filtered further, if desired, by the client,
 * by running them through the validation algorithm for the deductive system in
 * play.
 * 
 * The default is to search for instantiations that will make the sequent true
 * when validated by {@link ValidationTools.classicalPropositionalValidator
 * classical propositional logic}.  However, if the client will be using
 * {@link ValidationTools.intuitionisticPropositionalValidator intuitionistic
 * propositional logic} instead, the results can be filterted further; simply
 * set `intuitionistic: true` in the options object.  This will not only return
 * fewer results, but also speed up the search for those results.
 * 
 * The default is to search for instantiations that will make the sequent true
 * even if the formula itself is only an intermediate step in a larger chain of
 * inferences.  However, it is often the case that the client wants the formula
 * to be the final step that achieves the sequent's conclusion (say, if a
 * student should be expected to cite a formula directly relevant to the
 * statement they're justifying).  This will significantly narrow the search
 * space and the result set and will consequently speed up the search.  Simply
 * set `direct: true` in the options object.
 * 
 * The default is to do the work silently, not generating any debugging output.
 * But if debugging a complex problem, it may help to see the inner workings of
 * this function.  Set `debug: true` in the options object to print copious
 * debugging output to the console.
 * 
 * This function is a generator that yields a sequence of objects in the same
 * format as those returned by {@link module:Matching.allInstantiations
 * allInstantiations()}; see its documentation for details.
 * 
 * @param {Sequent} sequent the sequent whose conclusion the client hopes to
 *   justify by instantiating the formula
 * @param {Formula} formula the formula whose possible instantiations are to be
 *   explored
 * @param {Object} options a dictionary of options, which default to
 *   `{ direct: false, intuitionistic: false, debug: false }` and whose meaning
 *   is given above
 * @alias Formula.possibleSufficientInstantiations
 */
function *possibleSufficientInstantiations (
    sequent, formula, options = { }
) {
    // Assign default options
    options = Object.assign( {
        direct : false, intuitionistic : false, debug : false
    }, options )
    if ( options.debug ) {
        console.log( 'Sequent: ' + sequent.toPutdown() )
        console.log( 'Formula: ' + formula.toPutdown() )
    }
    // Compute and classify the outermost expressions in the sequent.
    const sequentOEs = classifyByParity( sequent )
    // Now view the formula as a set of formulas, one for each of its
    // conclusions.  The conditionalForm() function is ideal for this.
    for ( let innerFormula of formula.conditionalForm() ) {

        // Compute outermost expres in the formula; classifying not used here.
        const classified = classifyByParity( innerFormula )
        let patterns = classified.both.slice()
        patterns.unshift( patterns.pop() )
        // Candidates to match with each are all outermost expressions of the
        // sequent, so we make many copies of that for use below.
        let candidates = Array( patterns.length ).fill( sequentOEs.both )

        // Now process the options object.
        let numRequired = 0
        if ( options.direct ) {
            // If direct = true, then the final conclusion of the formula must
            // match the final conclusion of the sequent, so we can simplify:
            candidates[0] = [ sequentOEs.both.last() ]
            numRequired = 1
        } else if ( options.intuitionistic ) {
            // If direct = false, but intuitionistics = true, the formula
            // conclusion must still match something in positive position.
            candidates[0] = sequentOEs.positives
            numRequired = 1
        }
        // If intuitionistics = true, we can also require any formulaOE that is
        // the last child of a top-level premise to match a negative sequentOE.
        if ( options.intuitionistic ) {
            const isLastChildOfTopLevelPremise = x => x.parent() && ( (
                // either :{ ... x } is a child of the innerFormula but not the last:
                x == x.parent().lastChild()
             && x.parent().parent() == innerFormula
             && x.parent() != innerFormula.lastChild()
            ) || (
                // or :x is a child of the innerFormula but not the last:
                x.parent() == innerFormula
             && x != innerFormula.lastChild()
            ) )
            const toMatchNegatives = patterns.slice( 1 ).filter(
                x => isLastChildOfTopLevelPremise( x.original ) )
            const toMatchAnything = patterns.slice( 1 ).filter(
                x => !isLastChildOfTopLevelPremise( x.original ) )
            patterns = [ patterns[0], ...toMatchNegatives, ...toMatchAnything ]
            candidates = [ candidates[0],
                ...Array( toMatchNegatives.length ).fill( sequentOEs.negatives ),
                ...Array( toMatchAnything.length ).fill( sequentOEs.both ) ]
            numRequired = 1 + toMatchNegatives.length
        }

        if ( options.debug ) {
            console.log( `Calling Matching w/j=${numRequired} and:` )
            for ( let i = 0 ; i < patterns.length ; i++ ) {
                console.log( 'Pair '+i+':' )
                console.log( patterns[i].toPutdown() )
                for ( let j = 0 ; j < candidates[i].length ; j++ )
                    console.log( '\t' + candidates[i][j].toPutdown() )
            }
        }

        // Now yield all instantiations for this innerFormula
        // But fix their expression indices to match the tree order, because
        // the way we've permuted them will not make sense to the caller.
        const origPatternIndex = pattern => classified.both.indexOf( pattern )
        const unpermute = anyArray => {
            const result = Array( anyArray ).fill( 0 )
            for ( let i = 0 ; i < anyArray.length ; i++ )
                result[origPatternIndex( patterns[i] )] = anyArray[i]
            return result
        }
        // Make the optional multi-matching problem we're about to run as
        // efficient as possible, by removing duplicate candidates, combining
        // constraints where possible, and applying signature filtering.
        candidates = candidates.map( duplicateLCsRemoved )
        numRequired = removeDuplicatePatterns(
            patterns, candidates, numRequired )
        for ( let i = 0 ; i < patterns.length ; i++ )
            candidates[i] = filterBySignature( patterns[i], candidates[i] )
        // Now prepare to run the optional multi-matching algorithm.
        if ( options.debug ) {
            console.log( 'Patterns:   [ '
                       + patterns.map( x => x.toPutdown() ).join( ', ' ) + ' ]' )
            console.log( 'Candidates: [ '
                       + candidates.map( x => '[ '
                       + x.map( y => y.toPutdown() ).join( ', ' ) + ' ]' ) + ' ]' )
            console.log( 'Classified: [ '
                       + classified.both.map( x => x.toPutdown() ).join( ', ' )
                       + ' ]' )
            console.log( 'sequentOEs: [ '
                       + sequentOEs.both.map( x => x.toPutdown() ).join( ', ' )
                       + ' ]' )
        }
        const generator = Matching.allOptionalInstantiations(
            patterns, candidates, numRequired )
        // Before returning its results, convert the expression indices back to
        // ones the client will expect, since we have permuted the client's
        // inputs as needed for matching purposes.
        for ( let solObj of generator ) {
            if ( options.debug )
                console.log( 'Before: '
                           + JSON.stringify( solObj.expressionIndices ) + ' '
                           + solObj.solution.toString() )
            solObj.expressionIndices =
                solObj.expressionIndices.map(
                    ( exprInd, patInd ) => sequentOEs.both.findIndex(
                        x => candidates[patInd][exprInd]
                          && x.original == candidates[patInd][exprInd].original ) )
            if ( options.debug )
                console.log( 'Half:   '
                           + JSON.stringify( solObj.expressionIndices ) + ' '
                           + solObj.solution.toString() )
            solObj.expressionIndices = unpermute(
                solObj.expressionIndices )
            if ( options.debug )
                console.log( 'After:  '
                           + JSON.stringify( solObj.expressionIndices ) + ' '
                           + solObj.solution.toString() )
            yield solObj
        }
    }
}

export default {
    from, domain, instantiate,
    allPossibleInstantiations, possibleSufficientInstantiations,
    cachedInstantiation, addCachedInstantiation,
    allCachedInstantiations, clearCachedInstantiations,
    hasDeclarationCapture
}