#charset "us-ascii"

/* 
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
 *   
 *   TADS 3 Library: parser
 *   
 *   This modules defines the language-independent parts of the command
 *   parser.
 *   
 *   Portions based on xiny.t, copyright 2002 by Steve Breslin and
 *   incorporated by permission.  
 */

#include "adv3.h"
#include "tok.h"
#include <dict.h>
#include <gramprod.h>
#include <strcomp.h>


/* ------------------------------------------------------------------------ */
/* 
 *   property we add to ResolveInfo to store the remaining items from an
 *   ambiguous list 
 */
property extraObjects;


/* ------------------------------------------------------------------------ */
/*
 *   ResolveResults - an instance of this class is passed to the
 *   resolveNouns() routine to receive the results of the resolution.
 *   
 *   This class's main purpose is to virtualize the handling of error or
 *   warning conditions during the resolution process.  The ResolveResults
 *   object is created by the initiator of the resolution, so it allows
 *   the initiator to determine how errors are to be handled without
 *   having to pass flags down through the match tree.  
 */
class ResolveResults: object
    /*
     *   Instances must provide the following methods:
     *   
     *   noVocabMatch(action, txt) - there are no objects in scope matching
     *   the given noun phrase vocabulary.  This is "worse" than noMatch(),
     *   in the sense that this indicates that the unqualified noun phrase
     *   simply doesn't refer to any objects in scope, whereas noMatch()
     *   means that some qualification applied to the vocabulary ruled out
     *   any matches.  'txt' is the original text of the noun phrase.
     *   
     *   noMatch(action, txt) - there are no objects in scope matching the
     *   noun phrase.  This is used in cases where we eliminate all
     *   possible matches because of qualifications or constraints, so it's
     *   possible that the noun phrase vocabulary, taken by itself, does
     *   match some object; it's just that when the larger noun phrase
     *   context is considered, there's nothing that matches.
     *   
     *   noMatchPossessive(action, txt) - same as noMatch, but the
     *   unmatched phrase is qualified with a possessive phrase.  For
     *   ranking matches, the possessive version ranks ahead of treating
     *   the possessive words as raw vocabulary when neither matches, since
     *   it's more informative to report that we can't even match the
     *   underlying qualified noun phrase, let alone the whole phrase with
     *   qualification.
     *   
     *   noMatchForAll() - there's nothing matching "all"
     *   
     *   noMatchForAllBut() - there's nothing matching "all except..."
     *   (there might be something matching all, but there's nothing left
     *   when the exception list is applied)
     *   
     *   noMatchForListBut() - there's nothing matching "<list> except..."
     *   
     *   noteEmptyBut() - a "but" (or "except") list matches nothing.  We
     *   don't consider this an error, but we rate an interpretation with a
     *   non-empty "but" list and complete exclusion of the "all" or "any"
     *   phrase whose objects are excluded by the "but" higher than one
     *   with an empty "but" list and a non-empty all/any list - we do this
     *   because it probably means that we came up with an incorrect
     *   interpretation of the "but" phrase in the empty "but" list case
     *   and failed to exclude things we should have excluded.
     *   
     *   noMatchForPronoun(typ, txt) - there's nothing matching a pronoun.
     *   'typ' is one of the PronounXxx constants, and 'txt' is the text of
     *   the word used in the command.
     *   
     *   allNotAllowed() - the command contained the word ALL (or a
     *   synonym), but the verb doesn't allow ALL to be used in its noun
     *   phrases.  
     *   
     *   reflexiveNotAllowed(typ, txt) - the reflexive pronoun isn't
     *   allowed in this context.  This usually means that the pronoun was
     *   used with an intransitive or single-object action.  'typ' is one
     *   of the PronounXxx constants, and 'txt' is the text of the word
     *   used.
     *   
     *   wrongReflexive(typ, txt) - the reflexive pronoun doesn't agree
     *   with its referent in gender, number, or some other way.
     *   
     *   noMatchForPossessive(owner, txt) - there's nothing matching the
     *   phrase 'txt' owned by the resolved possessor object 'owner'.  Note
     *   that 'owner' is a list, since we can have plural possessive
     *   qualifier phrases.
     *   
     *   noMatchForLocation(loc, txt) - there's nothing matching 'txt' in
     *   the location object 'loc'.  This is used when a noun phrase is
     *   explicitly qualified by location ("the book on the table").
     *   
     *   noteBadPrep() - we have a noun phrase or other phrase
     *   incorporating an invalid prepositional phrase structure.  This is
     *   called from "badness" rules that are set up to match phrases with
     *   embedded prepositions, as a last resort when no valid
     *   interpretation can be found.
     *   
     *   nothingInLocation(loc) - there's nothing in the given location
     *   object.  This is used when we try to select the one object or all
     *   of the objects in a given container, but the container doesn't
     *   actually have any contents.
     *   
     *   ambiguousNounPhrase(keeper, asker, txt, matchLst, fullMatchList,
     *   scopeList, requiredNum, resolver) - an ambiguous noun phrase was
     *   entered: the noun phrase matches multiple objects that are all
     *   equally qualified, but we only want the given exact number of
     *   matches.  'asker' is a ResolveAsker object that we'll use to
     *   generate any prompts; if no customization is required, simply pass
     *   the base ResolveAsker.  'txt' is the original text of the noun
     *   list in the command, which the standard prompt messages can use to
     *   generate their questions.  'matchLst' is a list of the qualified
     *   objects matching the phrase, with only one object included for
     *   each set of equivalents in the original full list; 'fullMatchList'
     *   is the full list of matches, including each copy of equivalents;
     *   'scopeList' is the list of everything in scope that matched the
     *   original phrase, including illogical items.  If it's desirable to
     *   interact with the user at this point, prompt the user to resolve
     *   the list, and return a new list with the results.  If no prompting
     *   is desired, the original list can be returned.  If it isn't
     *   possible to determine the final set of objects, and a final set of
     *   objects is required (this is up to the subclass to determine), a
     *   parser exception should be thrown to stop further processing of
     *   the command.  'keeper' is an AmbigResponseKeeper object, which is
     *   usually simply the production object itself; each time we parse an
     *   interactive response (if we are interactive at all), we'll call
     *   addAmbigResponse() on the calling production object to add it to
     *   the saved list of responses, and we'll call getAmbigResponses() to
     *   find previous answers to the same question, in case of
     *   re-resolving the phrase with 'again' or the like.
     *   
     *   unknownNounPhrase(match, resolver) - a noun phrase that doesn't
     *   match any known noun phrase syntax was entered.  'match' is the
     *   production match tree object for the unknown phrase.  Returns a
     *   list of the resolved objects for the noun phrase, if possible.  If
     *   it is not possible to resolve the phrase, and a resolution is
     *   required (this is up to the subclass to determine), a parser
     *   exception should be thrown.
     *   
     *   getImpliedObject(np, resolver) - a noun phrase was left out
     *   entirely.  'np' is the noun phrase production standing in for the
     *   missing noun phrase; this is usually an EmptyNounPhraseProd or a
     *   subclass.  If an object is implicit in the command, or a
     *   reasonable default can be assumed, return the implicit or default
     *   object or objects.  If not, the routine can return nil or can
     *   throw an error.  The result is a ResolveInfo list.
     *   
     *   askMissingObject(asker, resolver, responseProd) - a noun phrase
     *   was left out entirely, and no suitable default can be found
     *   (getImpliedObject has already been called, and that returned nil).
     *   If it is possible to ask the player interactively to fill in the
     *   missing object, ask the player.  If it isn't possible to resolve
     *   an object, an error can be thrown, or an empty list can be
     *   returned.  'asker' is a ResolveAsker object, which can be used to
     *   customize the prompt (if any) that we show; pass the base
     *   ResolveAsker if no customization is needed.  'responseProd' is the
     *   production to use to parse the response.  The return value is the
     *   root match tree object of the player's interactive response, with
     *   its 'resolvedObjects' property set to the ResolveInfo list from
     *   resolving the player's response.  (The routine returns the match
     *   tree for the player's response so that, if we must run resolution
     *   again on another pass, we can re-resolve the same response without
     *   asking the player the same question again.)
     *   
     *   noteLiteral(txt) - note the text of a literal phrase.  When
     *   selecting among alternative interpretations of a phrase, we'll
     *   favor shorter literals, since treating fewer tokens as literals
     *   means that we're actually interpreting more tokens.
     *   
     *   askMissingLiteral(action, which) - a literal phrase was left out
     *   entirely.  If possible, prompt interactively for a player response
     *   and return the result.  If it's not possible to ask for a
     *   response, an error can be thrown, or nil can be returned.  The
     *   return value is simply the text string the player enters.
     *   
     *   emptyNounPhrase(resolver) - a noun phrase involving only
     *   qualifiers was entered ('take the').  In most cases, an exception
     *   should be thrown.  If the empty phrase can be resolved to an
     *   object or list of objects, the resolved list should be returned.
     *   
     *   zeroQuantity(txt) - a noun phrase referred to a zero quantity of
     *   something ("take zero books").
     *   
     *   insufficientQuantity(txt, matchList, requiredNumber) - a noun
     *   phrase is quantified with a number exceeding the number of objects
     *   available: "take five books" when only two books are in scope.
     *   
     *   uniqueObjectRequired(txt, matchList) - a noun phrase yields more
     *   than one object, but only one is allowed.  For example, we'll call
     *   this if the user attempts to use more than one result for a
     *   single-noun phrase (such as by answering a disambiguation question
     *   with 'all').
     *   
     *   singleObjectRequired(txt) - a noun phrase list was used where a
     *   single noun phrase is required.
     *   
     *   noteAdjEnding() - a noun phrase ends in an adjective.  This isn't
     *   normally an error, but is usually less desirable than interpreting
     *   the same noun phrase as ending in a noun (in other words, if a
     *   word can be used as both an adjective and a noun, it is usually
     *   better to interpret the word as a noun rather than as an adjective
     *   when the word appears at the end of a noun phrase, as long as the
     *   noun interpretation matches an object in scope).
     *   
     *   noteIndefinite() - a noun phrase is phrased as an indefinite ("any
     *   book", "one book"), meaning that we can arbitrarily choose any
     *   matching object in case of ambiguity.  Sometimes, an object will
     *   have explicit vocabulary that could be taken to be indefinite: a
     *   button labeled "1" could be a "1 button", for example, or a subway
     *   might have an "A train".  By noting the indefinite interpretation,
     *   we can give priority to the alternative definite interpretation.
     *   
     *   noteMatches(matchList) - notifies the results object that the
     *   given list of objects is being matched.  This allows the results
     *   object to inspect the object list for its strength: for example,
     *   by noting the presence of truncated words.  This should only be
     *   called after the nouns have been resolved to the extent possible,
     *   so any disambiguation or selection that is to be performed should
     *   be performed before this routine is called.
     *   
     *   noteMiscWordList(txt) - a noun phrase is made up of miscellaneous
     *   words.  A miscellaneous word list as a noun phrase has non-zero
     *   badness, so we will never use a misc word list unless we can't
     *   find any more structured interpretation.  In most cases, a
     *   miscellaneous word list indicates an invalid phrasing, but in some
     *   cases objects might use this unstructured type of noun phrase in
     *   conjunction with matchName() to perform dynamic or special-case
     *   parsing.
     *   
     *   notePronoun() - a noun phrase is matching as a pronoun.  In
     *   general, we prefer a match to an object's explicit vocabulary
     *   words to a match to a pronoun phrase: if the game goes to the
     *   trouble of including a word explicitly among an object's
     *   vocabulary, that's a better match than treating the same word as a
     *   generic pronoun.
     *   
     *   notePlural() - a plural phrase is matching.  In some cases we
     *   require a single object match, in which case a plural phrase is
     *   undesirable.  (A plural phrase might still match just one object,
     *   though, so it can't be ruled out on structural grounds alone.)
     *   
     *   beginSingleObjSlot() and endSingleObjSlot() are used to bracket
     *   resolution of a noun phrase that needs to be resolved as a single
     *   object.  We use these to explicitly lower the ranking for plural
     *   structural phrasings within these slots.
     *   
     *   beginTopicSlot() and endTopicSlot() are used to bracket resolution
     *   of a noun phrase that's being used as a conversation topic.  These
     *   are of the *form* of single nouns, but singular and plural words
     *   are equivalent, so when we're in a topic we don't consider a
     *   plural to be a minus the way we would for an ordinary object noun
     *   phrase.
     *   
     *   incCommandCount() - increment the command counter.  This is used
     *   to keep track of how many subcommands are in a command tree.
     *   
     *   noteActorSpecified() - note that the command is explicitly
     *   addressed to an actor.
     *   
     *   noteNounSlots(cnt) - note the number of "noun slots" in the verb
     *   phrase.  This is the number of objects the verb takes: an
     *   intransitive verb has no noun slots; a transitive verb with a
     *   direct object only has one; a verb with a direct and indirect
     *   object has two.  Note this has nothing to do with the number of
     *   objects specified in a noun list - TAKE BOX, BOOK, AND BELL has
     *   only one noun slot (a direct object) even though the slot is
     *   occupied by a list with three objects.
     *   
     *   noteWeakPhrasing(level) - note that the phrasing is "weak," with
     *   the given weakness level - higher is weaker.  The exact meaning of
     *   the weakness levels is up to the language module to define.  The
     *   English module, for example, considers VERB IOBJ DOBJ phrasing
     *   (with no preposition, as in GIVE BOB BOOK) to be weak when the
     *   DOBJ part doesn't have a grammatical marker that clarifies that
     *   it's really a separate noun phrase (an article serves this purpose
     *   in English: GIVE BOB THE BOOK).
     *   
     *   allowActionRemapping - returns true if we can remap the action
     *   during noun phrase resolution.  Remapping is usually allowed only
     *   during the actual execution phase, not during the ranking phase.  
     *   
     *   allowEquivalentFiltering - returns true if we can filter an
     *   ambiguous resolution list by making an arbitrary choice among
     *   equivalent objects.  This is normally allowed only during a final
     *   resolution phase, not during a tentative resolution phase.  
     */
;

/* ------------------------------------------------------------------------ */
/*
 *   Noun phrase resolver "asker."  This type of object can be passed to
 *   certain ResolveResults methods in order to customize the messages
 *   that the parser generates for interactive prompting.  
 */
class ResolveAsker: object
    /*
     *   Ask for help disambiguating a noun phrase.  This asks which of
     *   several possible matching objects was intended.  This method has
     *   the same parameter list as the equivalent message object method.  
     */
    askDisambig(targetActor, promptTxt, curMatchList, fullMatchList,
                requiredNum, askingAgain, dist)
    {
        /* let the target actor's parser message object handle it */
        targetActor.getParserMessageObj().askDisambig(
            targetActor, promptTxt, curMatchList, fullMatchList,
            requiredNum, askingAgain, dist);
    }

    /*
     *   Ask for a missing object.  This prompts for an object that's
     *   structurally required for an action, but which was omitted from
     *   the player's command.  
     */
    askMissingObject(targetActor, action, which)
    {
        /* let the target actor's parser message object handle it */
        targetActor.getParserMessageObj().askMissingObject(
            targetActor, action, which);
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   The resolveNouns() method returns a list of ResolveInfo objects
 *   describing the objects matched to the noun phrase.  
 */
class ResolveInfo: object
    construct(obj, flags, np = nil)
    {
        /* remember the members */
        obj_ = obj;
        flags_ = flags;
        np_ = np;
    }

    /*
     *   Look for a ResolveInfo item in a list of ResolveInfo items that
     *   is equivalent to us.  Returns true if we find such an item, nil
     *   if not.
     *   
     *   Another ResolveInfo is equivalent to us if it refers to the same
     *   underlying game object that we do, or if it refers to a game
     *   object that is indistinguishable from our underlying game object.
     */
    isEquivalentInList(lst)
    {
        /* 
         *   if we can find our exact item in the list, or we can find an
         *   equivalent object, we have an equivalent 
         */
        return (lst.indexWhich({x: x.obj_ == obj_}) != nil
                || lst.indexWhich(
                       {x: x.obj_.isVocabEquivalent(obj_)}) != nil);
    }

    /*
     *   Look for a ResolveInfo item in a list of ResolveInfo items that
     *   is equivalent to us according to a particular Distinguisher. 
     */
    isDistEquivInList(lst, dist)
    {
        /* 
         *   if we can find our exact item in the list, or we can find an
         *   equivalent object, we have an equivalent 
         */
        return (lst.indexWhich({x: x.obj_ == obj_}) != nil
                || lst.indexWhich(
                       {x: !dist.canDistinguish(x.obj_, obj_)}) != nil);
    }
    
    /* the object matched */
    obj_ = nil

    /* flags describing how we matched the object */
    flags_ = 0

    /* 
     *   By default, each ResolveInfo counts as one object, for the
     *   purposes of a quantity specifier (as in "five coins" or "both
     *   hats").  However, in some cases, a single resolved object might
     *   represent a collection of discrete objects and thus count as more
     *   than one for the purposes of the quantifier.  
     */
    quant_ = 1

    /*
     *   The possessive ranking, if applicable.  If this object is
     *   qualified by a possessive phrase ("my books"), we'll set this to
     *   a value indicating how strongly the possession applies to our
     *   object: 2 indicates that the object is explicitly owned by the
     *   object indicated in the possessive phrase, 1 indicates that it's
     *   directly held by the named possessor but not explicitly owned,
     *   and 0 indicates all else.  In cases where there's no posessive
     *   qualifier, this will simply be zero.  
     */
    possRank_ = 0

    /* the pronoun type we matched, if any (as a PronounXxx enum) */
    pronounType_ = nil

    /* the noun phrase we parsed to come up with this object */
    np_ = nil

    /*
     *   The pre-calculated multi-object announcement text for this object.
     *   When we iterate over the object list in a command with multiple
     *   direct or indirect objects (TAKE THE BOOK, BELL, AND CANDLE), we
     *   calculate the little announcement messages ("book:") for the
     *   objects BEFORE we execute the actual commands.  We then use the
     *   pre-calculated announcements during our iteration.  This ensures
     *   consistency in the basis for choosing the names, which is
     *   important in cases where the names include state-dependent
     *   information for the purposes of distinguishing one object from
     *   another.  The relevant state can change over the course of
     *   executing the command on the objects in the iteration, so if we
     *   calculated the names on the fly we could end up with inconsistent
     *   naming.  The user thinks of the objects in terms of their state at
     *   the start of the command, so the pre-calculation approach is not
     *   only more internally consistent, but is also more consistent with
     *   the user's perspective.  
     */
    multiAnnounce = nil
;

/*
 *   Intersect two resolved noun lists, returning a list consisting only
 *   of the unique objects from the two lists.  
 */
intersectNounLists(lst1, lst2)
{
    /* we don't have any results yet */
    local ret = [];
    
    /* 
     *   run through each element of the first list to see if it has a
     *   matching object in the second list 
     */
    foreach(local cur in lst1)
    {
        local other;
        /* 
         *   if this element's object occurs in the second list, we can
         *   include it in the result list 
         */
        if ((other = lst2.valWhich({x: x.obj_ == cur.obj_})) != nil)
        {
            /* if one or the other has a noun phrase, keep it */
            local np = cur.np_ ?? other.np_;
            
            /* 
             *   include this one in the result list, with the combined
             *   flags from the two original entries 
             */
            ret += new ResolveInfo(cur.obj_, cur.flags_ | other.flags_, np);
        }
    }

    /* return the result list */
    return ret;
}

/*
 *   Extract the objects from a list obtained with resolveNouns().
 *   Returns a list composed only of the objects in the resolution
 *   information list.  
 */
getResolvedObjects(lst)
{
    /* 
     *   return a list composed only of the objects from the ResolveInfo
     *   structures 
     */
    return lst.mapAll({x: x.obj_});
}


/* ------------------------------------------------------------------------ */
/*
 *   The basic production node base class.  We'll use this as the base
 *   class for all of our grammar rule match objects.  
 */
class BasicProd: object
    /* get the original text of the command for this match */
    getOrigText()
    {
        /* if we have no token list, return an empty string */
        if (tokenList == nil)
            return '';
        
        /* build the string based on my original token list */
        return cmdTokenizer.buildOrigText(getOrigTokenList());
    }

    /* get my original token list, in canonical tokenizer format */
    getOrigTokenList()
    {
        /* 
         *   return the subset of the full token list from my first token
         *   to my last token
         */
        return nilToList(tokenList).sublist(
            firstTokenIndex, lastTokenIndex - firstTokenIndex + 1);
    }

    /* 
     *   Set my original token list.  This replaces the actual token list
     *   we matched from the parser with a new token list provided by the
     *   caller. 
     */
    setOrigTokenList(toks)
    {
        tokenList = toks;
        firstTokenIndex = 1;
        lastTokenIndex = toks.length();
    }

    /*
     *   Can this object match tree resolve to the given object?  We'll
     *   resolve the phrase as though it were a topic phrase, then look for
     *   the object among the matches.  
     */
    canResolveTo(obj, action, issuingActor, targetActor, which)
    {
        /* set up a topic resolver */
        local resolver = new TopicResolver(
            action, issuingActor, targetActor, self, which,
            action.createTopicQualifierResolver(issuingActor, targetActor));

        /* 
         *   set up a results object - use a tentative results object,
         *   since we're only looking at the potential matches, not doing a
         *   full resolution pass 
         */
        local results = new TentativeResolveResults(issuingActor, targetActor);
        
        /* resolve the phrase as a topic */
        local match = resolveNouns(resolver, results);

        /* 
         *   make sure it's packaged in the canonical topic form, as a
         *   ResolvedTopic object 
         */
        match = resolver.packageTopicList(match, self);

        /* if the topic can match it, it's a possible resolution */
        return (match != nil
                && match.length() > 0
                && match[1].obj_.canMatchObject(obj));
    }

    /*
     *   Is this match a match to the special syntax for a custom missing
     *   object query?  This returns true if the match has a wording that
     *   strongly distinguishes it from an ordinary new command.  In the
     *   English parser, for example, this returns true for the
     *   PrepSingleTopicProd matches (e.g., inSingleNoun) if the phrase
     *   starts with the preposition for the match.
     *   
     *   This property is used when we ask a missing object question:
     *   
     *.    >dig
     *.    What do you want to dig in?
     *.  
     *.    >in the dirt
     *   
     *   In English, the DIG command sets up to receive a response phrased
     *   as "in <noun phrase>" - that's done by setting the response
     *   production to inSingleNoun.  In this case, "in the dirt" would
     *   return true, since that's pretty clearly a match to the expected
     *   inSingleNoun syntax.  In contrast, "the dirt" would return false,
     *   since that's just a noun phrase without the special wording for
     *   this particular verb.
     */
    isSpecialResponseMatch = nil

    /*
     *   Grammar match objects that come from a GrammarProd.parseTokens()
     *   call will always have a set of properties indicating which tokens
     *   from the input matched the grammar rule.  However, we sometimes
     *   synthesize match trees internally rather than getting them from
     *   parser input; for synthesized trees, the parser obviously won't
     *   supply those properties for us, so we need to define suitable
     *   defaults that synthesized match tree nodes can inherit.
     */
    firstTokenIndex = 0
    lastTokenIndex = 0
;


/* ------------------------------------------------------------------------ */
/*
 *   Basic disambiguation production class 
 */
class DisambigProd: BasicProd
    /*
     *   Remove the "ambiguous" flags from a result list.  This can be
     *   used to mark the response to a disambiguation query as no longer
     *   ambiguous.  
     */
    removeAmbigFlags(lst)
    {
        /* remove the "unclear disambig" flag from each result item */
        foreach (local cur in lst)
            cur.flags_ &= ~UnclearDisambig;

        /* return the list */
        return lst;
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Base class for "direction" productions.  Each direction (the compass
 *   directions, the vertical directions, the shipboard directions, and so
 *   on) must have an associated grammar rule, which must produce one of
 *   these.  This should be subclassed with grammar rules like this:
 *   
 *   grammar directionName: 'north' | 'n' : DirectionProd
 *.      dir = northDirection
 *.  ;
 */
class DirectionProd: BasicProd
    /*
     *   Each direction-specific grammar rule subclass must set this
     *   property to the associated direction object (northDirection,
     *   etc). 
     */
    dir = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   The base class for commands.  A command is the root of the grammar
 *   match tree for a single action.  A command line can consist of a
 *   number of commands joined with command separators; in English,
 *   command separators are things like periods, semicolons, commas, and
 *   the words "and" and "then".  
 */
class CommandProd: BasicProd
    hasTargetActor()
    {
        /* 
         *   By default, a command production does not include a
         *   specification of a target actor.  Command phrases that
         *   contain syntax specifically directing the command to a target
         *   actor should return true here. 
         */
        return nil;
    }

    /* 
     *   Get the match tree for the target actor phrase, if any.  By
     *   default, we have no target actor phrase, so just return nil.  
     */
    getActorPhrase = nil

    /* 
     *   "Execute" the actor phrase.  This lets us know that the parser
     *   has decided to use our phrasing to specify the target actor.
     *   We're not required to do anything here; it's just a notification
     *   for subclass use.  Since we don't have a target actor phrase at
     *   all, we obviously don't need to do anything here.  
     */
    execActorPhrase(issuingActor) { }
;

/*
 *   A first-on-line command.  The first command on a command line can
 *   optionally start with an actor specification, to give orders to the
 *   actor.  
 */
class FirstCommandProd: CommandProd
    countCommands(results)
    {
        /* count commands in the underlying command */
        cmd_.countCommands(results);
    }

    getTargetActor()
    {
        /* 
         *   we have no actor specified explicitly, so it's the current
         *   player character 
         */
        return libGlobal.playerChar;
    }

    /* 
     *   The tokens of the entire command except for the target actor
     *   specification.  By default, we take all of the tokens starting
     *   with the first command's first token and running to the end of
     *   the token list.  This assumes that the target actor is specified
     *   at the beginning of the command - languages that use some other
     *   word ordering must override this accordingly.  
     */
    getCommandTokens()
    {
        return tokenList.sublist(cmd_.firstTokenIndex);
    }

    /*
     *   Resolve my first action.  This returns an instance of a subclass
     *   of Action that represents the resolved action.  We'll ask our
     *   first subcommand to resolve its action.  
     */
    resolveFirstAction(issuingActor, targetActor)
    {
        return cmd_.resolveFirstAction(issuingActor, targetActor);
    }

    /* resolve nouns in the command */
    resolveNouns(issuingActor, targetActor, results)
    {
        /* resolve nouns in the underlying command */
        cmd_.resolveNouns(issuingActor, targetActor, results);

        /* count our commands */
        countCommands(results);
    }

    /*
     *   Does this command end a sentence?  The exact meaning of a
     *   sentence may vary by language; in English, a sentence ends with
     *   certain punctuation marks (a period, a semicolon, an exclamation
     *   point).  
     */
    isEndOfSentence()
    {
        /* ask the underlying command phrase */
        return cmd_.isEndOfSentence();
    }

    /*
     *   Get the token index of the first command separator token.  This
     *   is the first token that is not part of the underlying command. 
     */
    getCommandSepIndex()
    {
        /* get the separator index from the underlying command */
        return cmd_.getCommandSepIndex();
    }

    /* 
     *   get the token index of the next command - this is the index of
     *   the next token after our conjunction if we have one, or after our
     *   command if we don't have a conjunction 
     */
    getNextCommandIndex()
    {
        /* get the next command index from the underlying command */
        return cmd_.getNextCommandIndex();
    }
;

/* 
 *   Define the simplest grammar rule for a first-on-line command phrase,
 *   which is just an ordinary command phrase.  The language-specific
 *   grammar must define any other alternatives; specifically, the
 *   language might provide an "actor, command" syntax to direct a command
 *   to a particular actor.  
 */
grammar firstCommandPhrase(commandOnly): commandPhrase->cmd_
    : FirstCommandProd
;

/*
 *   A command with an actor specification.  This should be instantiated
 *   with grammar rules in a language-specific module.
 *   
 *   Instantiating grammar rules should set property actor_ to the actor
 *   match tree, and cmd_ to the underlying 'commandPhrase' production
 *   match tree.  
 */
class CommandProdWithActor: CommandProd
    hasTargetActor()
    {
        /* this command explicitly specifies an actor */
        return true;
    }
    getTargetActor()
    {
        /* return my resolved actor object */
        return resolvedActor_;
    }
    getActorPhrase()
    {
        /* return the actor phrase production */
        return actor_;
    }

    /*
     *   Execute the target actor phrase.  This is a notification, for use
     *   by subclasses; we don't have anything we need to do in this base
     *   class implementation. 
     */
    execActorPhrase(issuingActor) { }

    /*
     *   Resolve nouns.  We'll resolve only the nouns in the target actor
     *   phrase; we do not resolve nouns in the command phrase, because we
     *   must re-parse the command phrase after we've finished resolving
     *   the actor phrase, and thus we can't resolve nouns in the command
     *   phrase until after the re-parse is completed.  
     */
    resolveNouns(issuingActor, targetActor, results)
    {
        local lst;
        
        /* 
         *   Resolve the actor, then we're done.  Note that we do not
         *   attempt to resolve anything in the underlying command yet,
         *   because we can only resolve the command after we've fully
         *   processed the actor clause.  
         */
        lst = actor_.resolveNouns(getResolver(issuingActor), results);

        /* 
         *   there are no noun phrase slots, since we're not looking at the
         *   verb 
         */
        results.noteNounSlots(0);

        /* 
         *   if we have an object in the list, use it - note that our
         *   actor phrase is a single-noun production, and hence should
         *   never yield more than a single entry in its resolution list 
         */
        if (lst.length() > 0)
        {
            /* remember the resolved actor */
            resolvedActor_ = lst[1].obj_;

            /* note in the results that an actor is specified */
            results.noteActorSpecified();
        }

        /* count our commands */
        countCommands(results);
    }

    /* get or create my actor resolver */
    getResolver(issuingActor)
    {
        /* if I don't already have a resolver, create one and cache it */
        if (resolver_ == nil)
            resolver_ = new ActorResolver(issuingActor);

        /* return our cached resolver */
        return resolver_;
    }

    /* my resolved actor object */
    resolvedActor_ = nil

    /* my actor resolver object */
    resolver_ = nil
;

/*
 *   First-on-line command with target actor.  As with
 *   CommandProdWithActor, instantiating grammar rules must set the
 *   property actor_ to the actor match tree, and cmd_ to the underlying
 *   commandPhrase match.  
 */
class FirstCommandProdWithActor: CommandProdWithActor, FirstCommandProd
;


/* ------------------------------------------------------------------------ */
/*
 *   The 'predicate' production is the grammar rule for all individual
 *   command phrases.  We don't define anything about the predicate
 *   grammar here, since it is completely language-dependent, but we do
 *   *use* the predicate production as a sub-production of our
 *   commandPhrase rules.
 *   
 *   The language-dependent implementation of the 'predicate' production
 *   must provide the following methods:
 *   
 *   resolveAction(issuingActor, targetActor): This method returns a
 *   newly-created instance of the Action subclass that the command refers
 *   to.  This method must generally interpret the phrasing of the command
 *   to determine what noun phrases are present and what roles they serve,
 *   and what verb phrase is present, then create an appropriate Action
 *   subclass to represent the verb phrase as applied to the noun phrases
 *   in their determined roles.
 *   
 *   resolveNouns(issuingActor, targetActor, results): This method
 *   resolves all of the noun phrases in the predicate, using the
 *   'results' object (an object of class ResolveResults) to store
 *   information about the status of the resolution.  Generally, this
 *   routine should collect information about the roles of the noun phrase
 *   production matches, plug these into the Action via appropriate
 *   properties (dobjMatch for the direct object of a transitive verb, for
 *   example), resolve the Action, and then call the Action to do the
 *   resolution.  This method has no return value, since the Action is
 *   responsible for keeping track of the results of the resolution.
 *   
 *   For languages like English which encode most information about the
 *   phrase roles in word ordering, a good way to design a predicate
 *   grammar is to specify the syntax of each possible verb phrase as a
 *   'predicate' rule, and base the match object for that rule on the
 *   corresponding Action subclass.  For example, the English grammar has
 *   this rule to define the syntax for the verb 'take':
 *   
 *   VerbRule
 *.     ('take' | 'pick' 'up' | 'get') dobjList
 *.     | 'pick' dobjList 'up'
 *.     : TakeAction
 *.  ;
 *   
 *   Since English encodes everything in this command positionally, it's
 *   straightforward to write grammar rules for the possible syntax
 *   variations of a given action, hence it's easy to associate command
 *   syntax directly with its associated action.  When each 'predicate'
 *   grammar maps to an Action subclass for its match object,
 *   resolveAction() is especially easy to implement: it simply returns
 *   'self', since the grammar match and the Action are the same object.
 *   It's also easy to plug each noun phrase into its appropriate property
 *   slot in the Action subclass: the parser can be told to plug in each
 *   noun phrase directly using the "->" notation in the grammar.
 *   
 *   Many languages encode the roles of noun phrases with case markers or
 *   other syntactic devices, and as a result do not impose such strict
 *   rules as English does on word order.  For such languages, it is
 *   usually better to construct the 'predicate' grammar separately from
 *   any single action, so that the various acceptable phrase orderings
 *   are enumerated, and the verb phrase is just another phrase that plugs
 *   into these top-level orderings.  In such grammars, the predicate must
 *   do some programmatic work in resolveAction(): it must figure out
 *   which Action subclass is involved based on the verb phrase sub-match
 *   object and the noun phrases present, then must create an instance of
 *   that Action subclass.  Furthermore, since we can't plug the noun
 *   phrases into the Action using the "->" notation in the grammar, the
 *   resolveAction() routine must pick out the sub-match objects
 *   representing the noun phrases and plug them into the Action itself.
 *   
 *   Probably the easiest way to accomplish all of this is by designing
 *   each verb phrase match object so that it is associated with one or
 *   more Action subclasses, according to the specific noun phrases
 *   present.  The details of this mapping are obviously specific to each
 *   language, but it should be possible in most cases to build a base
 *   class for all verb phrases that uses parameters to create the Action
 *   and noun phrase associations.  For example, each verb phrase grammar
 *   match object might have a list of possible Action matches, such as
 *   this example from a purely hypothetical English grammar based on this
 *   technique
 *   
 *   grammar verbPhrase: 'take' | 'pick' 'up': VerbProd
 *.      actionMap =
 *.      [
 *.          [TakeAction, BasicProd, &dobjMatch],
 *.          [TakeWithAction, BasicProd, &dobjMatch, WithProd, &iobjMatch]
 *.      ]
 *.  ;
 *   
 *   The idea is that the base VerbProd class looks through the list given
 *   by the actionMap property for a template that matches the number and
 *   type of noun phrases present in the predicate; when it finds a match,
 *   it creates an instance of the Action subclass given in the template,
 *   then plugs the noun phrases into the listed properties of the new
 *   Action instance.
 *   
 *   Note that our verbPhrase example above is not an example of actual
 *   working code, because there is no such thing in the
 *   language-independent library as a VerbProd base class that reads
 *   actionMap properites - this is a purely hypothetical bit of code to
 *   illustrate how such a construction might work in a language that
 *   requires it, and it is up to the language-specific module to define
 *   such a mechanism for its own use.  
 */


/* ------------------------------------------------------------------------ */
/*
 *   Base classes for grammar matches for full commands.
 *   
 *   There are two kinds of command separators: ambiguous and unambiguous.
 *   Unambiguous separators are those that can separate only commands, such
 *   as "then" and periods.  Ambiguous separators are those that can
 *   separate nouns in a noun list as well as commands; for example "and"
 *   and commas.
 *   
 *   First, CommandProdWithDefiniteConj, which is for a single full
 *   command, unambiguously terminated with a separator that can only mean
 *   that a new command follows.  We parse one command at a time when a
 *   command line includes several commands, since we can only resolve
 *   objects - and thus can only choose structural interpretations - when
 *   we reach the game state in which a given command is to be executed.
 *   
 *   Second, CommandProdWithAmbiguousConj, which is for multiple commands
 *   separated by a conjunction that does not end a sentence, but could
 *   just as well separate two noun phrases.  In this case, we parse both
 *   commands, to ensure that we actually have a well-formed command
 *   following the conjunction; this allows us to try interpreting the part
 *   after the conjunction as a command and also as a noun phrase, to see
 *   which interpretation looks better.
 *   
 *   When we find an unambiguous command separator, we can simply use the
 *   '*' grammar match to put off parsing everything after the separator
 *   until later, reducing the complexity of the grammar tree.  When we
 *   find an ambiguous separator, we can't put off parsing the rest with
 *   '*', because we need to know if the part after the separator can
 *   indeed be construed as a new command.  During resolution, we'll take a
 *   noun list interpretation of 'and' over a command list whenever doing
 *   so would give us resolvable noun phrases.  
 */

/*
 *   Match class for a command phrase that is separated from anything that
 *   follows with an unambiguous conjunction. 
 *   
 *   Grammar rules based on this match class must set property cmd_ to the
 *   underlying 'predicate' production.  
 */
class CommandProdWithDefiniteConj: CommandProd
    resolveNouns(issuingActor, targetActor, results)
    {
        /* resolve nouns */
        cmd_.resolveNouns(issuingActor, targetActor, results);

        /* count commands */
        countCommands(results);
    }
    countCommands(results)
    {
        /* we have only one subcommand */
        if (cmdCounted_++ == 0)
            results.incCommandCount();
    }

    /* counter: have we counted our command in the results object yet? */
    cmdCounted_ = 0

    /* resolve my first action */
    resolveFirstAction(issuingActor, targetActor)
    {
        return cmd_.resolveAction(issuingActor, targetActor);
    }

    /* does this command end a sentence */
    isEndOfSentence()
    {
        /* 
         *   it's the end of the sentence if our predicate encompasses all
         *   of the tokens in the command, or the conjunction is a
         *   sentence-ending conjunction 
         */
        return conj_ == nil || conj_.isEndOfSentence();
    }

    /*
     *   Get the token index of the first command separator token.  This
     *   is the first token that is not part of the underlying command. 
     */
    getCommandSepIndex()
    {
        /* 
         *   if we have a conjunction, return the first token index of the
         *   conjunction; otherwise, return the index of the next token
         *   after the command itself 
         */
        if (conj_ != nil)
            return conj_.firstTokenIndex;
        else
            return cmd_.lastTokenIndex + 1;
    }

    /* 
     *   get the token index of the next command - this is the index of
     *   the next token after our conjunction if we have one, or after our
     *   command if we don't have a conjunction 
     */
    getNextCommandIndex()
    {
        return (conj_ == nil ? cmd_ : conj_).lastTokenIndex + 1;
    }
;

/*
 *   Match class for two command phrases separated by an ambiguous
 *   conjunction (i.e., a conjunction that could also separate two noun
 *   phrases).  Grammar rules based on this class must set the properties
 *   'cmd1_' to the underlying 'predicate' production match of the first
 *   command, and 'cmd2_' to the underlying 'commandPhrase' production
 *   match of the second command.  
 */
class CommandProdWithAmbiguousConj: CommandProd
    resolveNouns(issuingActor, targetActor, results)
    {
        /* 
         *   Resolve nouns in the first subcommand only.  Do NOT resolve
         *   nouns in any of the additional subcommands (there might be
         *   more than one, since cmd2_ can be a list of subcommands, not
         *   just a single subcommand), because we cannot assume that the
         *   current scope will continue to be valid after executing the
         *   first subcommand - the first command could take us to a
         *   different location, or change the lighting conditions, or add
         *   or remove objects from the location, or any number of other
         *   things that would invalidate the current scope.
         */
        cmd1_.resolveNouns(issuingActor, targetActor, results);

        /* count our commands */
        countCommands(results);
    }
    countCommands(results)
    {
        /* count our first subcommand (cmd1_) */
        if (cmdCounted_++ == 0)
            results.incCommandCount();

        /* count results in the rest of the list (cmd2_ and its children) */
        cmd2_.countCommands(results);
    }

    /* counter: have we counted our command in the results object yet? */
    cmdCounted_ = 0

    /* resolve my first action */
    resolveFirstAction(issuingActor, targetActor)
    {
        return cmd1_.resolveAction(issuingActor, targetActor);
    }

    /* does this command end a sentence */
    isEndOfSentence()
    {
        /*
         *   it's the end of the sentence if the conjunction is a
         *   sentence-ending conjunction 
         */
        return conj_.isEndOfSentence();
    }

    /*
     *   Get the token index of the first command separator token.  This
     *   is the first token that is not part of the underlying command. 
     */
    getCommandSepIndex()
    {
        /* return the conjunction's first token index */
        return conj_.firstTokenIndex;
    }

    /* 
     *   get the token index of the next command - this is simply the
     *   starting index for our second subcommand tree 
     */
    getNextCommandIndex() { return cmd2_.firstTokenIndex; }
;

/*
 *   The basic grammar rule for an unambiguous end-of-sentence command.
 *   This matches either a predicate with nothing following, or a predicate
 *   with an unambiguous command-only conjunction following.  
 */
grammar commandPhrase(definiteConj):
    predicate->cmd_
    | predicate->cmd_ commandOnlyConjunction->conj_ *
    : CommandProdWithDefiniteConj
;

/*
 *   the basic grammar rule for a pair of commands with an ambiguous
 *   separator (i.e., a separator that could separate commands or
 *   conjunctions) 
 */
grammar commandPhrase(ambiguousConj):
    predicate->cmd1_ commandOrNounConjunction->conj_ commandPhrase->cmd2_
    : CommandProdWithAmbiguousConj
;

/* ------------------------------------------------------------------------ */
/*
 *   We leave almost everything about the grammatical rules of noun
 *   phrases to the language-specific implementation, but we provide a set
 *   of base classes for implementing several conceptual structures that
 *   have equivalents in most languages.  
 */


/*
 *   Basic noun phrase production class. 
 */
class NounPhraseProd: BasicProd
    /*
     *   Determine whether this kind of noun phrase prefers to keep a
     *   collective or the collective's individuals when filtering.  If
     *   this is true, we'll keep a collective and discard its individuals
     *   when filtering a resolution list; otherwise, we'll drop the
     *   collective and keep the individuals.  
     */
    filterForCollectives = nil

    /*
     *   Filter a "verify" result list for the results we'd like to keep
     *   in the final resolution of the noun phrase.  This is called after
     *   we've run through the verification process on the list of
     *   candidate matches, so 'lst' is a list of ResolveInfo objects,
     *   sorted in descending order of logicalness.
     *   
     *   By default, we keep only the items that are equally as logical as
     *   the best item in the results.  Since the items are sorted in
     *   descending order of goodness, the best item is always the first
     *   item.  
     */
    getVerifyKeepers(results)
    {
        local best;
        
        /* 
         *   Reduce the list to the most logical elements - in other
         *   words, keep only the elements that are exactly as logical as
         *   the first element, which we know to have the best logicalness
         *   ranking in the list by virtue of having sorted the list in
         *   descending order of logicalness.  
         */
        best = results[1];
        return results.subset({x: x.compareTo(best) == 0});
    }

    /*
     *   Filter a match list of results for truncated matches.  If we have
     *   a mix of truncated matches and exact matches, we'll keep only the
     *   exact matches.  If we have only truncated matches, though, we'll
     *   return the list unchanged, as we don't have a better offer going.
     */
    filterTruncations(lst, resolver)
    {
        local exactLst;

        /*
         *   If we're in "global scope," it means that we're resolving
         *   something like a topic phrase, and we're not limited to the
         *   current physical scope but can choose objects from the entire
         *   game world.  In this case, don't apply any truncation
         *   filtering.  The reason is that we could have several unrelated
         *   objects in the game with vocabulary that differs only in
         *   truncation, but the user has forgotten about the ones with the
         *   longer names and is thinking of something she's referred to
         *   with truncation in the past, when the longer-named objects
         *   weren't around.  We're aware of all of those longer-named
         *   objects, but it's not helpful to the player, since they might
         *   currently be out of sight.
         */
        if (resolver.isGlobalScope)
            return lst;

        /* get the list of exact (i.e., untruncated) matches */
        exactLst = lst.subset(
            {x: (x.flags_ & (VocabTruncated | PluralTruncated)) == 0});

        /* 
         *   if we have any exact matches, use only the exact matches; if
         *   we don't, use the full list as is 
         */
        return (exactLst.length() != 0 ? exactLst : lst);
    }
;

/*
 *   Basic noun phrase list production class. 
 */
class NounListProd: BasicProd
;

/*
 *   Basic "layered" noun phrase production.  It's often useful to define
 *   a grammar rule that simply defers to an underlying grammar rule; we
 *   make this simpler by defining this class that automatically delegates
 *   resolveNouns to the underlying noun phrase given by the property np_. 
 */
class LayeredNounPhraseProd: NounPhraseProd
    resolveNouns(resolver, results)
    {
        /* let the underlying match tree object do the work */
        return np_.resolveNouns(resolver, results);
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   A single noun is sometimes required where, structurally, a list is
 *   not allowed.  Single nouns should not be used to prohibit lists where
 *   there is no structural reason for the prohibition - these should be
 *   used only where it doesn't make sense to use a list structurally.  
 */
class SingleNounProd: NounPhraseProd
    resolveNouns(resolver, results)
    {
        /* tell the results object we're resolving a single-object slot */
        results.beginSingleObjSlot();

        /* resolve the underlying noun phrase */
        local lst = np_.resolveNouns(resolver, results);

        /* tell the results object we're done with the single-object slot */
        results.endSingleObjSlot();

        /* make sure the list has only one element */
        if (lst.length() > 1)
            results.uniqueObjectRequired(getOrigText(), lst);

        /* return the results */
        return lst;
    }
;

/*
 *   A user could attempt to use a noun list where a single noun is
 *   required.  This is not a grammatical error, so we accept it
 *   grammatically; however, for disambiguation purposes we score it lower
 *   than a singleNoun production with only one noun phrase, and if we try
 *   to resolve it, we'll fail with an error.  
 */
class SingleNounWithListProd: NounPhraseProd
    resolveNouns(resolver, results)
    {
        /* note the problem */
        results.singleObjectRequired(getOrigText());

        /* 
         *   In case we have any unknown words or other detrimental
         *   aspects, resolve the underlying noun list as well, so we can
         *   score even lower.  Note that doing this after noting our
         *   single-object-required problem will avoid any interactive
         *   resolution (such as asking for disambiguation information),
         *   since once we get to the point where we're ready to do
         *   anything interactive, the single-object-required results
         *   notification will fail with an error, so we won't make it to
         *   this point during interactive resolution.  
         */
        np_.resolveNouns(resolver, results);

        /* we have no resolution */
        return [];
    }
;

/*
 *   A topic is a noun phrase used in commands like "ask <person> about
 *   <topic>."  For our purposes, this works as an ordinary single noun
 *   production.  
 */
class TopicProd: SingleNounProd
    /* get the original text and tokens from the underlying phrase */
    getOrigTokenList() { return np_.getOrigTokenList(); }
    getOrigText() { return np_.getOrigText(); }

    resolveNouns(resolver, results)
    {
        /* note that we're entering a topic slot */
        results.beginTopicSlot();

        /* do the inherited work */
        local ret = inherited(resolver, results);

        /* we're leaving the topic slot */
        results.endTopicSlot();

        /* return the resolved list */
        return ret;
    }
;

/*
 *   A literal is a string enclosed in quotes. 
 */
class LiteralProd: BasicProd
;

/*
 *   Basic class for pronoun phrases.  The specific pronouns are
 *   language-dependent; each instance should define its pronounType
 *   property to an appropriate PronounXxx constant.
 */
class PronounProd: NounPhraseProd
    resolveNouns(resolver, results)
    {
        local lst;

        /* 
         *   check for a valid anaphoric binding (i.e., a back-reference to
         *   an object mentioned earlier in the current command: ASK BOB
         *   ABOUT HIMSELF) 
         */
        lst = checkAnaphoricBinding(resolver, results);

        /* 
         *   if we didn't get an anaphoric binding, find an antecedent from
         *   a previous command 
         */
        if (lst == nil)
        {
            /* ask the resolver to get the antecedent */
            lst = resolver.resolvePronounAntecedent(
                pronounType, self, results, isPossessive);

            /* if there are no results, note the error */
            if (lst == [])
                results.noMatchForPronoun(pronounType,
                                          getOrigText().toLower());

            /* remember the pronoun type in each ResolveInfo */
            lst.forEach({x: x.pronounType_ = pronounType});

            /* 
             *   If the pronoun is singular, but we got multiple potential
             *   antecedents, it means that the previous command had
             *   multiple noun slots (as in UNLOCK DOOR WITH KEY) and
             *   didn't want to decide a priori which one is the antecedent
             *   for future pronouns.  So, we have to decide now which of
             *   the potential antecedents to use as the actual antecedent.
             *   Choose the most logical, if there's a clearly more logical
             *   one.  
             */
            if (!isPlural && lst.length() > 1)
            {
                /* filter the phrase using the normal disambiguation */
                lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
                
                /* 
                 *   if that leaves more than one item, pick the first one
                 *   and mark it as unclearly disambiguated 
                 */
                if (lst.length() > 1)
                {
                    /* arbitrarily keep the first item only */
                    lst = lst.sublist(1, 1);
                    
                    /* mark it as an arbitrary choice */
                    lst[1].flags_ |= UnclearDisambig;
                }
            }
        }

        /* note that we have phrase involving a pronoun */
        results.notePronoun();

        /* return the result list */
        return lst;
    }

    /* 
     *   our pronoun specifier - this must be set in each rule instance to
     *   one of the PronounXxx constants to specify which pronoun to use
     *   when resolving the pronoun phrase 
     */
    pronounType = nil

    /* is this a possessive usage? */
    isPossessive = nil

    /* 
     *   Is this pronoun a singular or a plural?  A pronoun like "it" or
     *   "he" is singular, because it refers to a single antecedent; "them"
     *   is plural.  Language modules that define their own custom pronoun
     *   subclasses should override this as needed.  
     */
    isPlural = nil

    /*
     *   Check for an anaphoric binding.  Returns a list (which is allowed
     *   to be empty) if this can refer back to an earlier noun phrase in
     *   the same command, nil if not.  By default, we consider pronouns
     *   to be non-anaphoric, meaning they refer to something from a
     *   previous sentence, not something in this same sentence.  In most
     *   languages, pronouns don't refer to objects in other noun phrases
     *   within the same predicate unless they're reflexive.  
     */
    checkAnaphoricBinding(resolver, results) { return nil; }
;

/*
 *   For simplicity, define subclasses of PronounProd for the basic set of
 *   pronouns found in most languages.  Language-specific grammar
 *   definitions can choose to use these or not, and can add their own
 *   extra subclasses as needed for types of pronouns we don't define
 *   here.  
 */
class ItProd: PronounProd
    pronounType = PronounIt
;

class ThemProd: PronounProd
    pronounType = PronounThem
    isPlural = true
;

class HimProd: PronounProd
    pronounType = PronounHim
;

class HerProd: PronounProd
    pronounType = PronounHer
;

class MeProd: PronounProd
    pronounType = PronounMe
;

class YouProd: PronounProd
    pronounType = PronounYou
;

/*
 *   Third-person anaphoric reflexive pronouns.  These refer to objects
 *   that appeared earlier in the sentence: "ask bob about himself."  
 */
class ReflexivePronounProd: PronounProd
    resolveNouns(resolver, results)
    {
        /* ask the resolver for the reflexive pronoun binding */
        local lst = resolver.getReflexiveBinding(pronounType);

        /* 
         *   if the result is empty, the verb will provide the binding
         *   later, on a second pass 
         */
        if (lst == [])
            return lst;

        /* 
         *   If the result is nil, the verb is saying that the reflexive
         *   pronoun makes no sense internally within the predicate
         *   structure.  In this case, or if we did get a list that
         *   doesn't agree with the pronoun type (in number or gender, for
         *   example), consider the reflexive to refer back to the actor,
         *   for a construct such as TELL BOB TO HIT HIMSELF.  However,
         *   only do this if the issuer and target actor aren't the same,
         *   since we generally don't refer to the PC in the third person.
         */
        if ((lst == nil || !checkAgreement(lst))
            && resolver.actor_ != resolver.issuer_)
        {
            /* try treating the actor as the reflexive pronoun */
            lst = [new ResolveInfo(resolver.actor_, 0, self)];
        }

        /* if the list is nil, it means reflexives aren't allowed here */
        if (lst == nil)
        {
            results.reflexiveNotAllowed(pronounType, getOrigText.toLower());
            return [];
        }

        /*
         *   Check the list for agreement (in gender, number, and so on).
         *   Don't bother if the list is empty, as this is the action's
         *   way of telling us that it doesn't have a binding for us yet
         *   but will provide one on a subsequent attempt at re-resolving
         *   this phrase.  
         */
        if (!checkAgreement(lst))
            results.wrongReflexive(pronounType, getOrigText().toLower());

        /* return the result list */
        return lst;
    }

    /*
     *   Check that the binding we found for our reflexive pronoun agrees
     *   with the pronoun in gender, number, and anything else that it has
     *   to agree with.  This must be defined by each concrete subclass.
     *   Note that language-specific subclasses can and *should* override
     *   this to test agreement for the local language's rules.
     *   
     *   This should return true if we agree, nil if not.  
     */
    checkAgreement(lst) { return true; }
;

class ItselfProd: ReflexivePronounProd
    pronounType = PronounIt
;

class ThemselvesProd: ReflexivePronounProd
    pronounType = PronounThem
;

class HimselfProd: ReflexivePronounProd
    pronounType = PronounHim
;

class HerselfProd: ReflexivePronounProd
    pronounType = PronounHer
;

/*
 *   The special 'all' constructions are full noun phrases. 
 */
class EverythingProd: BasicProd
    resolveNouns(resolver, results)
    {
        local lst;

        /* check to make sure 'all' is allowed */
        if (!resolver.allowAll())
        {
            /* it's not - flag an error and give up */
            results.allNotAllowed();
            return [];
        }

        /* get the 'all' list */
        lst = resolver.getAll(self);

        /* 
         *   set the "always announce" flag for each item - the player
         *   didn't name these items specifically, so always show what we
         *   chose 
         */
        foreach (local cur in lst)
            cur.flags_ |= AlwaysAnnounce | MatchedAll;

        /* make sure there's something in it */
        if (lst.length() == 0)
            results.noMatchForAll();

        /* return the list */
        return lst;
    }

    /* match Collective objects instead of their individuals */
    filterForCollectives = true
;


/* ------------------------------------------------------------------------ */
/*
 *   Basic exclusion list ("except the silver one") production base class. 
 */
class ExceptListProd: BasicProd
;

/*
 *   Basic "but" rule, which selects a list of plurals minus a list of
 *   specifically excepted objects.  This can be used to construct more
 *   specific production classes for things like "everything but the book"
 *   and "all books except the red ones".
 *   
 *   In each grammar rule based on this class, the 'except_' property must
 *   be set to a suitable noun phrase for the exception list.  We'll
 *   resolve this list and remove the objects in it from our main list.
 */
class ButProd: NounPhraseProd
    resolveNouns(resolver, results)
    {
        local mainList;
        local butList;
        local butRemapList;
        local action;
        local role;
        local remapProp;

        /* get our main list of items to include */
        mainList = getMainList(resolver, results);

        /* filter out truncated matches if we have any exact matches */
        mainList = filterTruncations(mainList, resolver);

        /* 
         *   resolve the 'except' list - use an 'except' resolver based on
         *   our list so that we resolve these objects in the scope of our
         *   main list 
         */
        butList = except_.resolveNouns(
            new ExceptResolver(mainList, getOrigText(), resolver),
            new ExceptResults(results));

        /* if the exception list is empty, tell the results about it */
        if (butList == [])
            results.noteEmptyBut();

        /* 
         *   Get the remapping property for this object role for this
         *   action.  This property applies to each of the objects we're
         *   resolving, and tells us if the resolved object is going to
         *   remap its handling of this action when the object is used in
         *   this role.  For example, the Take action's remapping property
         *   for the direct object would usually be remapDobjTake, so
         *   book.remapDobjTake would tell us if TAKE BOOK were going to
         *   be remapped. 
         */
        action = resolver.getAction();
        role = resolver.whichObject;
        remapProp = action.getRemapPropForRole(role);

        /* get the list of simple synonym remappings for the 'except' list */
        butRemapList = butList.mapAll(
            {x: action.getSimpleSynonymRemap(x.obj_, role, remapProp)});

        /* 
         *   scan the 'all' list, and remove each item that appears in the
         *   'except' list 
         */
        for (local i = 1, local len = mainList.length() ; i <= len ; ++i)
        {
            local curRemap;
            
            /* get the current 'all' list element */
            local cur = mainList[i].obj_;

            /* get the simple synonym remapping for this item, if any */
            curRemap = action.getSimpleSynonymRemap(cur, role, remapProp);
            
            /* 
             *   If this item appears in the 'except' list, remove it.
             *   
             *   Similarly, if this item is remapped to something that
             *   appears in the 'except' list, remove it.
             *   
             *   Similarly, if something in the 'except' list is remapped
             *   to this item, remove this item. 
             */
            if (butList.indexWhich({x: x.obj_ == cur}) != nil
                || butRemapList.indexWhich({x: x == cur}) != nil
                || butList.indexWhich({x: x.obj_ == curRemap}) != nil)
            {
                /* remove it and adjust our loop counters accordingly */
                mainList = mainList.removeElementAt(i);
                --i;
                --len;
            }
        }

        /* if that doesn't leave anything, it's an error */
        if (mainList == [])
            flagAllExcepted(resolver, results);

        /* perform the final filtering on the list for our subclass */
        mainList = filterFinalList(mainList);

        /* add any flags to the result list that our subclass indicates */
        foreach (local cur in mainList)
            cur.flags_ |= addedFlags;

        /* note the matched objects in the results */
        results.noteMatches(mainList);

        /* return whatever we have left after the exclusions */
        return mainList;
    }

    /* get my main list, which is the list of items to include */
    getMainList(resolver, results) { return []; }

    /* flag an error - everything has been excluded by the 'but' list */
    flagAllExcepted(resolver, results) { }

    /* filter the final list - by default we just return the same list */
    filterFinalList(lst) { return lst; }

    /* by default, add no extra flags to our resolved object list */
    addedFlags = 0
;


/* ------------------------------------------------------------------------ */
/*
 *   Base class for "all but" rules, which select everything available
 *   except for objects in a specified list of exceptions; for example, in
 *   English, "take everything but the book".  
 */
class EverythingButProd: ButProd
    /* our main list is given by the "all" list */
    getMainList(resolver, results)
    {
        /* check to make sure 'all' is allowed */
        if (!resolver.allowAll())
        {
            /* it's not - flag an error and give up */
            results.allNotAllowed();
            return [];
        }

        /* return the 'all' list */
        return resolver.getAll(self);
    }

    /* flag an error - our main list has been completely excluded */
    flagAllExcepted(resolver, results)
    {
        results.noMatchForAllBut();
    }

    /*         
     *   set the "always announce" flag for each item - the player didn't
     *   name the selected items specifically, so always show what we
     *   chose 
     */
    addedFlags = AlwaysAnnounce

    /* match Collective objects instead of their individuals */
    filterForCollectives = true
;

/*
 *   Base class for "list but" rules, which select everything in an
 *   explicitly provided list minus a set of exceptions; for example, in
 *   English, "take all of the books except the red ones".
 *   
 *   Subclasses defining grammar rules must set the 'np_' property to the
 *   main noun list; we'll resolve this list to find the objects to be
 *   included before exclusions are applied.  
 */
class ListButProd: ButProd
    /* our main list is given by the 'np_' subproduction */
    getMainList(resolver, results)
    {
        return np_.resolveNouns(resolver, results);
    }

    /* flag an error - everything has been excluded */
    flagAllExcepted(resolver, results)
    {
        results.noMatchForListBut();
    }

    /* 
     *   set the "unclear disambig" flag in our results, so we provide an
     *   indication of which object we chose 
     */
    addedFlags = UnclearDisambig
;
    

/* ------------------------------------------------------------------------ */
/*
 *   Pre-resolved phrase production.  This isn't normally used in any
 *   actual grammar; instead, this is for use when building actions
 *   programmatically.  This allows us to fill in an action tree when we
 *   already know the object we want to resolve.  
 */
class PreResolvedProd: BasicProd
    construct(obj)
    {
        /* if it's not a collection, we need to make it a list */
        if (!obj.ofKind(Collection))
        {
            /* if it's not already a ResolveInfo, wrap it in a ResolveInfo */
            if (!obj.ofKind(ResolveInfo))
                obj = new ResolveInfo(obj, 0, self);

            /* the resolved object list is simply the one ResolveInfo */
            obj = [obj];
        }

        /* store the new ResolveInfo list */
        obj_ = obj;
    }

    /* resolve the nouns: this is easy, since we started out resolved */
    resolveNouns(resolver, results)
    {
        /* return our pre-resolved object */
        return obj_;
    }

    /* 
     *   Our pre-resolved object result.  This is a list containing a
     *   single ResolveInfo representing our resolved object, since this is
     *   the form required by callers of resolveNouns.  
     */
    obj_ = nil
;

/*
 *   A pre-resolved *ambiguous* noun phrase.  This is used when the game
 *   or library wants to suggest a specific set of objects for a new
 *   action, then ask which one to use.  
 */
class PreResolvedAmbigProd: DefiniteNounProd
    construct(objs, asker, phrase)
    {
        /* remember my list of possible objects as a resolved object list */
        objs_ = objs.mapAll({x: new ResolveInfo(x, 0, nil)});

        /* remember the ResolveAsker to use */
        asker_ = asker;

        /* remember the noun phrase to use in disambiguation questions */
        phrase_ = phrase;
    }

    resolveNouns(resolver, results)
    {
        /* resolve our list using definite-phrase rules */
        return resolveDefinite(asker_, phrase_, objs_,
                               self, resolver, results);
    }

    /* my pre-resolved list of ambiguous objects */
    objs_ = nil

    /* the noun phrase to use in disambiguation questions */
    phrase_ = nil

    /* the ResolveAsker to use when prompting for the selection */
    asker_ = nil
;

/*
 *   Pre-resolved literal phrase production
 */
class PreResolvedLiteralProd: BasicProd
    construct(txt)
    {
        /*
         *   If the argument is a ResolveInfo, assume its obj_ property
         *   gives the literal string, and retrieve the string.  
         */
        if (txt.ofKind(ResolveInfo))
            txt = txt.obj_;

        /* save the text */
        text_ = txt;
    }

    /* get the text */
    getLiteralText(results, action, which) { return text_; }
    getTentativeLiteralText() { return text_; }

    /* our underlying text */
    text_ = nil
;

/*
 *   A token list production is an internally generated placeholder when we
 *   synthesize a production rather than matching grammar, and we want to
 *   keep track of the token list that triggered the node.
 */
class TokenListProd: BasicProd
    construct(toks)
    {
        tokenList = toks;
        firstTokenIndex = 1;
        lastTokenIndex = toks.length();
    }

    /* the token list */
    tokenList = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Mix-in class for noun phrase productions that use
 *   ResolveResults.ambiguousNounPhrase().  This mix-in provides the
 *   methods that ambiguousNounPhrase() uses to keep track of past
 *   responses to the disambiguation question.  
 */
class AmbigResponseKeeper: object
    addAmbigResponse(resp)
    {
        /* add an ambiguous response to our list */
        ambigResponses_ += resp;
    }

    getAmbigResponses()
    {
        /* return our list of past interactive disambiguation responses */
        return ambigResponses_;
    }

    /* our list of saved interactive disambiguation responses */
    ambigResponses_ = []
;


/* ------------------------------------------------------------------------ */
/*
 *   Base class for noun phrase productions with definite articles. 
 */
class DefiniteNounProd: NounPhraseProd, AmbigResponseKeeper
    resolveNouns(resolver, results)
    {
        /* resolve our underlying noun phrase using definite rules */
        return resolveDefinite(ResolveAsker, np_.getOrigText(),
                               np_.resolveNouns(resolver, results),
                               self, resolver, results);
    }

    /*
     *   Resolve an underlying phrase using definite noun phrase rules. 
     */
    resolveDefinite(asker, origText, lst, responseKeeper, resolver, results)
    {
        local scopeList;
        local fullList;

        /* filter the list to remove truncations if we have exact matches */
        lst = filterTruncations(lst, resolver);

        /* 
         *   Remember the current list, before filtering for logical
         *   matches and before filtering out equivalents, as our full
         *   scope list.  If we have to ask for clarification, this is the
         *   scope of the clarification.  
         */
        scopeList = lst;

        /* filter for possessive qualification strength */
        lst = resolver.filterPossRank(lst, 1);

        /* filter out the obvious mismatches */
        lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);

        /* 
         *   remember the current list as the full match list, before
         *   filtering equivalents 
         */
        fullList = lst;

        /* 
         *   reduce the list to include only one of each set of
         *   equivalents; do this only if the results object allows this
         *   kind of filtering 
         */
        if (results.allowEquivalentFiltering)
            lst = resolver.filterAmbiguousEquivalents(lst, self);

        /* do any additional subclass-specific filtering on the list */
        lst = reduceDefinite(lst, resolver, results);

        /* 
         *   This is (explicitly or implicitly) a definite noun phrase, so
         *   we must resolve to exactly one object.  If the list has more
         *   than one object, we must disambiguate it.  
         */
        if (lst.length() == 0)
        {
            /* there are no objects matching the phrase */
            results.noMatch(resolver.getAction(), origText);
        }
        else if (lst.length() > 1)
        {
            /* 
             *   the noun phrase is ambiguous - pass in the full list
             *   (rather than the list with the redundant equivalents
             *   weeded out) so that we can display the appropriate
             *   choices for multiple equivalent items 
             */
            lst = results.ambiguousNounPhrase(
                responseKeeper, asker, origText, lst, fullList,
                scopeList, 1, resolver);
        }

        /* note the matched objects in the results */
        results.noteMatches(lst);

        /* return the results */
        return lst;
    }

    /*
     *   Do any additional subclass-specific filtering to further reduce
     *   the list before we decide whether or not we have sufficient
     *   specificity.  We call this just before deciding whether or not to
     *   prompt for more information ("which book do you mean...?").  By
     *   default, this simply returns the same list unchanged; subclasses
     *   that have some further way of narrowing down the options can use
     *   this opportunity to apply that extra narrowing.  
     */
    reduceDefinite(lst, resolver, results) { return lst; }
;

/*
 *   Base class for a plural production 
 */
class PluralProd: NounPhraseProd
    /*
     *   Basic plural noun resolution.  We'll retrieve the matching objects
     *   and filter them using filterPluralPhrase.  
     */
    basicPluralResolveNouns(resolver, results)
    {
        local lst;
        
        /* resolve the underlying noun phrase */
        lst = np_.resolveNouns(resolver, results);

        /* if there's nothing in it, it's an error */
        if (lst.length() == 0)
            results.noMatch(resolver.getAction(), np_.getOrigText());

        /* filter out truncated matches if we have any exact matches */
        lst = filterTruncations(lst, resolver);

        /* filter the plurals for the logical subset and return the result */
        lst = resolver.filterPluralPhrase(lst, self);

        /* if we have a list, sort it by pluralOrder */
        if (lst != nil)
            lst = lst.sort(SortAsc,
                           {a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});

        /* note the matches */
        results.noteMatches(lst);

        /* note the plural in the results */
        results.notePlural();

        /* return the result */
        return lst;
    }

    /*
     *   Get the verify "keepers" for a plural phrase.
     *   
     *   If the "filter plural matches" configuration flag is set to true,
     *   we'll return the subset of items which are logical for this
     *   command.  If the filter flag is nil, we'll simply return the full
     *   set of vocabulary matches without any filtering.  
     */
    getVerifyKeepers(results)
    {
        /* check the global "filter plural matches" configuration flag */
        if (gameMain.filterPluralMatches)
        {
            local sub;
            
            /* get the subset of items that don't exclude plurals */
            sub = results.subset({x: !x.excludePluralMatches});

            /* 
             *   if that's non-empty, use it as the result; otherwise, just
             *   use the original list 
             */
            return (sub.length() != 0 ? sub : results);
        }
        else
        {
            /* we don't want any filtering */
            return results;
        }
    }
;

/*
 *   A plural phrase that explicitly selects all of matching objects.  For
 *   English, this would be a phrase like "all of the marbles".  This type
 *   of phrase matches all of the objects that match the name in the
 *   plural, except that if we have a collective object and we also have
 *   objects that are part of the collective (such as a bag of marbles and
 *   some individual marbles), we'll omit the collective, and match only
 *   the individual items.
 *   
 *   Grammar rule instantiations in language modules should set property
 *   np_ to the plural phrase match tree.  
 */
class AllPluralProd: PluralProd
    resolveNouns(resolver, results)
    {
        /* return the basic plural resolution */
        return basicPluralResolveNouns(resolver, results);
    }

    /* 
     *   since the player explicitly told us to use ALL of the matching
     *   objects, keep everything in the verify list, logical or not 
     */
    getVerifyKeepers(results) { return results; }

    /* prefer to keep individuals over collectives */
    filterForCollectives = nil
;

/*
 *   A plural phrase qualified by a definite article ("the books").  This
 *   type of phrasing doesn't specify anything about the specific number of
 *   items involved, except that they number more than one.
 *   
 *   In most cases, we take this to imply that all of the matching objects
 *   are intended to be included, with one exception: when we have an
 *   object that can serve as a collective for some of the other objects,
 *   we match only the collective but not the other objects.  For example,
 *   if we type "take marbles," and we have five marbles and a bag of
 *   marbles that serves as a collective object for three of the five
 *   marbles, we'll match the bag and two marbles not in the bag, but NOT
 *   the marbles that are in the bag.  This is usually desirable when
 *   there's a collective object, since it applies the command to the
 *   object standing in for the group rather than applying the command one
 *   by one to each of the individuals in the group.  
 */
class DefinitePluralProd: PluralProd
    resolveNouns(resolver, results)
    {
        /* return the basic plural resolution */
        return basicPluralResolveNouns(resolver, results);
    }

    /* prefer to keep collectives instead of their individuals */
    filterForCollectives = true
;

/*
 *   Quantified plural phrase.  
 */
class QuantifiedPluralProd: PluralProd
    /* 
     *   Resolve the main noun phrase.  By default, we simply resolve np_,
     *   but we make this separately overridable to allow this class to be
     *   subclassed for quantifying other types of plural phrases.
     *   
     *   If this is unable to resolve the list, it can flag an appropriate
     *   error via the results object and return nil.  If this routine
     *   returns nil, our main resolver will simply return an empty list
     *   without further flagging of any errors.  
     */
    resolveMainPhrase(resolver, results)
    {
        /* resolve the main noun phrase */
        return np_.resolveNouns(resolver, results);
    }

    /* 
     *   get the quantity specified - by default, this comes from the
     *   quantifier phrase in "quant_" 
     */
    getQuantity() { return quant_.getval(); }

    /* resolve the noun phrase */
    resolveNouns(resolver, results)
    {
        local lst;
        local num;

        /* resolve the underlying noun phrase */
        if ((lst = resolveMainPhrase(resolver, results)) == nil)
            return [];

        /* filter out truncated matches if we have any exact matches */
        lst = filterTruncations(lst, resolver);

        /* sort the list in ascending order of pluralOrder */
        if (lst != nil)
            lst = lst.sort(SortAsc,
                           {a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});

        /* get the quantity desired */
        num = getQuantity();

        /* 
         *   if we have at least the desired number, arbitrarily choose
         *   the desired number; otherwise, it's an error 
         */
        if (num == 0)
        {
            /* zero objects makes no sense */
            results.zeroQuantity(np_.getOrigText());
        }
        else
        {
            local qsum;
            
            /* if we have too many, disambiguate */
            if (lst.length() >= num)
            {
                local scopeList;

                /* remember the list of everything in scope that matches */
                scopeList = lst;

                /* filter for possessive qualifier strength */
                lst = resolver.filterPossRank(lst, num);

                /*
                 *   Use the normal disambiguation ranking to find the best
                 *   set of possibilities.  
                 */
                lst = resolver.filterAmbiguousNounPhrase(lst, num, self);

                /* 
                 *   If that left us with more than we're looking for, call
                 *   our selection routine to select the subset.  If it
                 *   left us with too few, note it in the results.  
                 */
                if (lst.length() > num)
                {
                    /* select the desired exact count */
                    lst = selectExactCount(lst, num, scopeList,
                                           resolver, results);
                }
            }

            /* 
             *   Check to make sure we have enough items matching.  Go by
             *   the 'quant_' property of the ResolveInfo entries, since we
             *   might have a single ResolveInfo object that represents a
             *   quantity of objects from the player's perspective. 
             */
            qsum = 0;
            lst.forEach({x: qsum += x.quant_});
            if (qsum < num)
            {
                /* note in the results that there aren't enough matches */
                results.insufficientQuantity(np_.getOrigText(), lst, num);
            }
        }

        /* note the matched objects in the results */
        results.noteMatches(lst);

        /* return the results */
        return lst;
    }

    /*
     *   Select the desired number of matches from what the normal
     *   disambiguation filtering leaves us with.
     *   
     *   Note that this will never be called with 'num' larger than the
     *   number in the current list.  This is only called to select a
     *   smaller subset than we currently have.
     *   
     *   By default, we'll simply select an arbitrary subset, since we
     *   simply want any 'num' of the matches.  This can be overridden if
     *   other behaviors are needed.  
     */
    selectExactCount(lst, num, scopeList, resolver, results)
    {
        /* 
         *   If we want less than what we actually got, arbitrarily pick
         *   the first 'num' elements; otherwise, return what we have.  
         */
        if (lst.length() > num)
            return lst.sublist(1, num);
        else
            return lst;
    }

    /* 
     *   Since the player explicitly told us to use a given number of
     *   matching objects, keep the required number, logical or not.  
     */
    getVerifyKeepers(results)
    {
        /* get the quantity desired */
        local num = getQuantity();

        /* 
         *   if we have at least the number required, arbitrarily choose
         *   the initial subset of the desired length; otherwise, use them
         *   all 
         */
        if (results.length() > num)
            return results.sublist(1, num);
        else
            return results;
    }
;

/*
 *   Exact quantified plural phrase.  This is similar to the normal
 *   quantified plural, but has the additional requirement of matching an
 *   unambiguous set of the exact given number ("the five books" means
 *   that we expect to find exactly five books matching the phrase - no
 *   fewer, and no more).  
 */
class ExactQuantifiedPluralProd: QuantifiedPluralProd, AmbigResponseKeeper
    /*
     *   Select the desired number of matches.  Since we want an exact set
     *   of matches, we'll run disambiguation on the set.  
     */
    selectExactCount(lst, num, scopeList, resolver, results)
    {
        local fullList;
        
        /* remember the list before filtering for redundant equivalents */
        fullList = lst;

        /* reduce the list by removing redundant equivalents, if allowed */
        if (results.allowEquivalentFiltering)
            lst = resolver.filterAmbiguousEquivalents(lst, self);

        /* 
         *   if the reduced list has only one element, everything in the
         *   original list must have been equivalent, so arbitrarily pick
         *   the desired number of items from the original list
         */
        if (lst.length() == 1)
            return fullList.sublist(1, num);
        
        /* we still have too many items, so disambiguate the results */
        return results.ambiguousNounPhrase(
            self, ResolveAsker, np_.getOrigText(),
            lst, fullList, scopeList, num, resolver);
    }

    /* get the keepers in the verify stage */
    getVerifyKeepers(results)
    {
        /* 
         *   keep everything: we want an exact quantity, so we want the
         *   keepers to match the required quantity on their own, without
         *   any arbitrary paring down 
         */
        return results;
    }
;

/*
 *   Noun phrase with an indefinite article 
 */
class IndefiniteNounProd: NounPhraseProd
    /* 
     *   resolve the main phrase - this is separately overridable to allow
     *   subclassing 
     */
    resolveMainPhrase(resolver, results)
    {
        /* by default, resolve the main noun phrase */
        return np_.resolveNouns(resolver, results);
    }
    
    resolveNouns(resolver, results)
    {
        local lst;
        local allEquiv = nil;
        
        /* resolve the underlying list */
        if ((lst = resolveMainPhrase(resolver, results)) == nil)
            return [];

        /* filter out truncated matches if we have any exact matches */
        lst = filterTruncations(lst, resolver);

        /* see what we found */
        if (lst.length() == 0)
        {
            /* it turned up nothing - note the problem */
            results.noMatch(resolver.getAction(), np_.getOrigText());
        }
        else if (lst.length() > 1)
        {
            /* 
             *   There are multiple objects, but the phrase is indefinite,
             *   which means that it doesn't refer to a specific matching
             *   object but could refer to any of them.  
             */

            /* start by noting if the choices are all equivalent */
            allEquiv = areAllEquiv(lst);

            /*   
             *   Filter using possessive qualifier strength and then normal
             *   disambiguation ranking to find the best set of
             *   possibilities, then pick which we want.  
             */
            lst = resolver.filterPossRank(lst, 1);
            lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);
            lst = selectFromList(resolver, results, lst);
        }

        /* 
         *   Set the "unclear disambiguation" flag on the item we picked -
         *   our selection was arbitrary, so it's polite to let the player
         *   know which we chose.  However, don't do this if the possible
         *   matches were all equivalent to start with, as the player's
         *   input must already have been as specific as we can be in
         *   reporting the choice.  
         */
        if (lst.length() == 1 && !allEquiv)
            lst[1].flags_ |= UnclearDisambig;

        /* note the matched objects in the results */
        results.noteMatches(lst);

        /* note that this is an indefinite phrasing */
        results.noteIndefinite();

        /* return the results */
        return lst;
    }

    /* are all of the items in the resolve list equivalents? */
    areAllEquiv(lst)
    {
        local first = lst[1].obj_;
        
        /* check each item to see if it's equivalent to the first */
        for (local i = 2, local cnt = lst.length() ; i <= cnt ; ++i)
        {
            /* 
             *   if this one isn't equivalent to the first, then they're
             *   not all equivalent 
             */
            if (!first.isVocabEquivalent(lst[i].obj_))
                return nil;
        }

        /* we didn't find any non-equivalents, so they're all equivalents */
        return true;
    }

    /*
     *   Select an item from the list of potential matches, given the list
     *   sorted from most likely to least likely (according to the
     *   resolver's ambiguous match filter).  We'll ask the resolver to
     *   make the selection, because indefinite noun phrases can mean
     *   different things in different contexts.  
     */
    selectFromList(resolver, results, lst)
    {
        /* ask the resolver to select */
        return resolver.selectIndefinite(results, lst, 1);
    }
    
;

/*
 *   Noun phrase explicitly asking us to choose an object arbitrarily
 *   (with a word like "any").  This is similar to the indefinite noun
 *   phrase, but differs in that this phrase is *explicitly* arbitrary,
 *   rather than merely indefinite.  
 */
class ArbitraryNounProd: IndefiniteNounProd
    /*
     *   Select an object from a list of potential matches.  Since the
     *   choice is explicitly arbitrary, we simply choose the first
     *   (they're in order from most likely to least likely, so this will
     *   choose the most likely).  
     */
    selectFromList(resolver, results, lst)
    {
        /* simply select the first item */
        return lst.sublist(1, 1);
    }
;

/*
 *   Noun phrase with an indefinite article and an exclusion ("any of the
 *   books except the red one") 
 */
class IndefiniteNounButProd: ButProd
    /* resolve our main phrase */
    resolveMainPhrase(resolver, results)
    {
        /* note that this is an indefinite phrasing */
        results.noteIndefinite();

        /* by default, simply resolve the underlying noun phrase */
        return np_.resolveNouns(resolver, results);
    }

    /* get our main list */
    getMainList(resolver, results)
    {
        local lst;
        
        /* resolve the underlying list */
        if ((lst = resolveMainPhrase(resolver, results)) == nil)
            return [];

        /* filter for possessive qualifier strength */
        lst = resolver.filterPossRank(lst, 1);

        /* filter it to pick the most likely objects */
        lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);

        /* return the filtered list */
        return lst;
    }

    /* flag an error - everything has been excluded */
    flagAllExcepted(resolver, results)
    {
        results.noMatchForListBut();
    }

    /* filter the final list */
    filterFinalList(lst)
    {
        /* we want to keep only one item - arbitrarily take the first one */
        return (lst.length() == 0 ? [] : lst.sublist(1, 1));
    }

    /* 
     *   set the "unclear disambig" flag in our results, so we provide an
     *   indication of which object we chose 
     */
    addedFlags = UnclearDisambig
;

/*
 *   A qualified plural phrase explicitly including two objects (such as,
 *   in English, "both books").  
 */
class BothPluralProd: ExactQuantifiedPluralProd
    /* the quantity specified by a "both" phrase is 2 */
    getQuantity() { return 2; }
;

/* ------------------------------------------------------------------------ */
/*
 *   Possessive adjectives
 */

class PossessivePronounAdjProd: PronounProd
    /*
     *   Possessive pronouns can refer to the earlier noun phrases of the
     *   same predicate, which is to say that they're anaphoric.  For
     *   example, in GIVE BOB HIS BOOK, 'his' refers to Bob.  
     */
    checkAnaphoricBinding(resolver, results)
    {
        local lst;

        /* if we simply can't be an anaphor, there's no binding */
        if (!canBeAnaphor)
            return nil;

        /* ask the resolver for the reflexive binding, if any */
        lst = resolver.getReflexiveBinding(pronounType);

        /*
         *   If there's no binding from the verb, or it doesn't match in
         *   number and gender, try an anaphoric binding from the actor. 
         */
        if (lst == nil || (lst != [] && !checkAnaphorAgreement(lst)))
        {
            /* get the actor's anaphoric possessive binding */
            local obj = resolver.actor_.getPossAnaphor(pronounType);

            /* if we got an object or a list, make a resolve list */
            if (obj != nil)
            {
                if (obj.ofKind(Collection))
                    lst = obj.mapAll({x: new ResolveInfo(x, 0, self)});
                else
                    lst = [new ResolveInfo(obj, 0, self)];
            }
        }

        /* 
         *   If we got a binding, make sure we agree in number and gender;
         *   if not, don't use the anaphoric form.  This isn't an error;
         *   it just means we're falling back on the regular antecedent
         *   binding.  If we have an empty list, it means the action isn't
         *   ready to tell us the binding yet, so we can't verify it yet.  
         */
        if (lst != nil && (lst == [] || checkAnaphorAgreement(lst)))
            return lst;

        /* don't use an anaphoric binding */
        return nil;
    }

    /* this is a possessive usage of the pronoun */
    isPossessive = true

    /*
     *   Can we be an anaphor?  By default, we consider third-person
     *   possessive pronouns to be anaphoric, and others to be
     *   non-anaphoric.  For example, in GIVE BOB MY BOOK, MY always refers
     *   to the speaker, so it's clearly not anaphoric within the sentence.
     */
    canBeAnaphor = true

    /* 
     *   Check agreement to a given anaphoric pronoun binding.  The
     *   language module should override this for each pronoun type to
     *   ensure that the actual contents of the list agree in number and
     *   gender with this type of pronoun.  If so, return true; if not,
     *   return nil.  It's not an error or a ranking demerit if we don't
     *   agree; it just means that we'll fall back on the regular pronoun
     *   antecedent rather than trying to use an anaphoric binding.  
     */
    checkAnaphorAgreement(lst) { return true; }

    /* 
     *   By default, the "main text" of a possessive pronoun is the same as
     *   the actual token text.  Languages can override this as needed> 
     */
    getOrigMainText() { return getOrigText(); }
;

class ItsAdjProd: PossessivePronounAdjProd
    pronounType = PronounIt
;

class HisAdjProd: PossessivePronounAdjProd
    pronounType = PronounHim
;

class HerAdjProd: PossessivePronounAdjProd
    pronounType = PronounHer
;

class TheirAdjProd: PossessivePronounAdjProd
    pronounType = PronounThem
;

class YourAdjProd: PossessivePronounAdjProd
    pronounType = PronounYou
    canBeAnaphor = nil
;

class MyAdjProd: PossessivePronounAdjProd
    pronounType = PronounMe
    canBeAnaphor = nil
;

/*
 *   Possessive nouns 
 */
class PossessivePronounNounProd: PronounProd
    /* this is a possessive usage of the pronoun */
    isPossessive = true
;

class ItsNounProd: PossessivePronounNounProd
    pronounType = PronounIt
;

class HisNounProd: PossessivePronounNounProd
    pronounType = PronounHim
;

class HersNounProd: PossessivePronounNounProd
    pronounType = PronounHer
;

class TheirsNounProd: PossessivePronounNounProd
    pronounType = PronounThem
;

class YoursNounProd: PossessivePronounNounProd
    pronounType = PronounYou
;

class MineNounProd: PossessivePronounNounProd
    pronounType = PronounMe
;

/*
 *   Basic possessive phrase.  The grammar rules for these phrases must map
 *   the possessive qualifier phrase to poss_, and the noun phrase being
 *   qualified to np_.  We are based on DefiniteNounProd because we resolve
 *   the possessive qualifier as though it had a definite article.
 *   
 *   The possessive production object poss_ must define the method
 *   getOrigMainText() to return the text of its noun phrase in a format
 *   suitable for disambiguation prompts or error messages.  In English,
 *   for example, this means that the getOrigMainText() must omit the
 *   apostrophe-S suffix if present.  
 */
class BasicPossessiveProd: DefiniteNounProd
    /*
     *   To allow this class to be mixed with other classes that have
     *   mixed-in ambiguous response keepers, create a separate object to
     *   hold our ambiguous response keeper for the possessive phrase.  We
     *   will never use our own ambiguous response keeper properties, so
     *   those are available to any other production class we're mixed
     *   into.  
     */
    construct()
    {
        /* create an AmbigResponseKeeper for the possessive phrase */
        npKeeper = new AmbigResponseKeeper;
    }

    /*
     *   Resolve the possessive, and perform preliminary resolution of the
     *   qualified noun phrase.  We find the owner object and reduce the
     *   resolved objects for the qualified phrase to those owned by the
     *   owner.
     *   
     *   If we fail, we return nil.  Otherwise, we return a list of the
     *   tentatively resolved objects.  The caller can further resolve
     *   this list as needed.  
     */
    resolvePossessive(resolver, results, num)
    {
        /* resolve the underlying noun phrase being qualified */
        local lst = np_.resolveNouns(resolver, results);

        /* if we found no matches for the noun phrase, so note */
        if (lst.length() == 0)
        {
            results.noMatchPossessive(resolver.getAction(), np_.getOrigText());
            return nil;
        }

        /* 
         *   resolve the possessive phrase and reduce the list to select
         *   only the items owned by the possessor 
         */
        lst = selectWithPossessive(resolver, results, lst,
                                   np_.getOrigText(), num);

        /* return the tentative resolution list for the qualified phrase */
        return lst;
    }

    /*
     *   Resolve the possessive, and reduce the given match list by
     *   selecting only those items owned by the resolution of the
     *   possessive phrase.
     *   
     *   'num' is the number of objects we want to select.  If the noun
     *   phrase being qualified is singular, this will be 1; if it's
     *   plural, this will be nil, to indicate that there's no specific
     *   target quantity; if the phrase is something like "bob's five
     *   books," the the number will be the qualifying quantity (5, in this
     *   case).  
     */
    selectWithPossessive(resolver, results, lst, lstOrigText, num)
    {
        /* 
         *   Create the possessive resolver.  Note that we resolve the
         *   possessive phrase in the context of the resolver's indicated
         *   qualifier resolver, which might not be the same as the
         *   resolver for the overall phrase.  
         */
        local possResolver = resolver.getPossessiveResolver();
        
        /* enter a single-object slot for the possessive phrase */
        results.beginSingleObjSlot();
            
        /* resolve the underlying possessive */
        local possLst = poss_.resolveNouns(possResolver, results);

        /* perform the normal resolve list filtering */
        possLst = resolver.getAction().finishResolveList(
            possLst, resolver.whichObject, self, nil);

        /* done with the single-object slot */
        results.endSingleObjSlot();

        /*
         *   If that resolved to an empty list, return now with an empty
         *   list.  The underlying possessive resolver will have noted an
         *   error if this is indeed an error; if it's not an error, it
         *   means that we're pending resolution of the other noun phrase
         *   to resolve an anaphor in the possessive phrase.  
         */
        if (possLst == [])
        {
            /* 
             *   we must have a pending anaphor to resolve - simply return
             *   the current list without any possessive filtering 
             */
            return lst;
        }

        /*
         *   If the possessive phrase itself is singular, treat the
         *   possessive phrase as a definite phrase, requiring an
         *   unambiguous referent.  If it's plural ("the men's books"),
         *   leave it as it is, taking it to mean that we want to select
         *   things that are owned by any/all of the possessors.
         *   
         *   Note that the possessive phrase has no qualifier - any
         *   qualifier applies to the noun phrase our possessive is also
         *   qualifying, not to the possessive phrase itself.  
         */
        local owner;
        if (poss_.isPluralPossessive)
        {
            /* 
             *   The possessive phrase is plural, so don't reduce its match
             *   to a single object; instead, select all of the objects
             *   owned by any of the possessors.  The owner is anyone in
             *   the list.  
             */
            owner = possLst.mapAll({x: x.obj_});
        }
        else
        {
            /* 
             *   the possessive phrase is singular, so resolve the
             *   possessive qualifier as a definite noun 
             */
            possLst = resolveDefinite(
                ResolveAsker, poss_.getOrigMainText(), possLst,
                npKeeper, possResolver, results);

            /* 
             *   if we didn't manage to find a single resolution to the
             *   possessive phrase, we can't resolve the rest of the phrase
             *   yet 
             */
            if (possLst.length() != 1)
            {
                /* if we got more than one object, it's a separate error */
                if (possLst.length() > 1)
                    results.uniqueObjectRequired(poss_.getOrigMainText(),
                        possLst);
                
                /* we can't go on */
                return [];
            }

            /* get the resolved owner object */
            owner = [possLst[1].obj_];
        }

        /* select the objects owned by any of the owners */
        local newLst = lst.subset({x: owner.indexWhich(
            {y: x.obj_.isOwnedBy(y)}) != nil});

        /*
         *   If that didn't leave any results, try one more thing: if the
         *   owner is itself in the list of possessed objects, keep the
         *   owner.  This allows for sequences like this:
         *   
         *   >take book
         *.  Which book?
         *.  >the man's
         *.  Which man, Bob or Dave?
         *.  >bob's
         *   
         *   In this case, the qualified object list will be [bob,dave],
         *   and the owner will be [bob], so we want to keep [bob] as the
         *   result list.
         */
        if (newLst == [])
            newLst = lst.subset({x: owner.indexOf(x.obj_) != nil});

        /* use the new list we found */
        lst = newLst;

        /* 
         *   Give each item an ownership priority ranking, since the items
         *   are being qualified by owner: explicitly owned items have the
         *   highest ranking, items directly held but not explicitly owned
         *   have the second ranking, and items merely held have the
         *   lowest ranking.  If we deem the list to be ambiguous later,
         *   we'll apply the ownership priority ranking first in trying to
         *   disambiguate.  
         */
        foreach (local cur in lst)
        {
            /* 
             *   give the item a ranking: explicitly owned, directly held,
             *   or other 
             */
            if (owner.indexOf(cur.obj_.owner) != nil)
                cur.possRank_ = 2;
            else if (owner.indexWhich({x: cur.obj_.isDirectlyIn(x)}) != nil)
                cur.possRank_ = 1;
        }

        /* if we found nothing, mention it */
        if (lst.length() == 0)
        {
            results.noMatchForPossessive(owner, lstOrigText);
            return [];
        }

        /* return the reduced list */
        return lst;
    }

    /* our ambiguous response keeper */
    npKeeper = nil
;

/*
 *   Possessive phrase + singular noun phrase.  The language grammar rule
 *   must map poss_ to the possessive production and np_ to the noun
 *   phrase being qualified.
 */
class PossessiveNounProd: BasicPossessiveProd
    resolveNouns(resolver, results)
    {
        local lst;

        /* 
         *   perform the initial qualification; if that fails, give up now
         *   and return an empty list 
         */
        if ((lst = resolvePossessive(resolver, results, 1)) == nil)
            return [];

        /* now resolve the underlying list definitely */
        return resolveDefinite(ResolveAsker, np_.getOrigText(), lst, self,
                               resolver, results);
    }

    /* our AmbigResponseKeeper for the qualified noun phrase */
    npKeeper = nil
;

/*
 *   Possessive phrase + plural noun phrase.  The grammar rule must set
 *   poss_ to the possessive and np_ to the plural.  
 */
class PossessivePluralProd: BasicPossessiveProd
    resolveNouns(resolver, results)
    {
        local lst;
        
        /* perform the initial qualifier resolution */
        if ((lst = resolvePossessive(resolver, results, nil)) == nil)
            return [];

        /* filter out truncated matches if we have any exact matches */
        lst = filterTruncations(lst, resolver);

        /* filter the plurals for the logical subset */
        lst = resolver.filterPluralPhrase(lst, self);

        /* if we have a list, sort it by pluralOrder */
        if (lst != nil)
            lst = lst.sort(SortAsc,
                           {a, b: a.obj_.pluralOrder - b.obj_.pluralOrder});

        /* note the matched objects in the results */
        results.noteMatches(lst);

        /* we want everything in the list, so return what we found */
        return lst;
    }
;

/*
 *   Possessive plural with a specific quantity that must be exact
 */
class ExactQuantifiedPossessivePluralProd:
    ExactQuantifiedPluralProd, BasicPossessiveProd

    /* 
     *   resolve the main noun phrase - this is the possessive-qualified
     *   plural phrase 
     */
    resolveMainPhrase(resolver, results)
    {
        return resolvePossessive(resolver, results, getQuantity());
    }
;

/*
 *   Possessive noun used in an exclusion list.  This is for things like
 *   the "mine" in a phrase like "take keys except mine".  
 */
class ButPossessiveProd: BasicPossessiveProd
    resolveNouns(resolver, results)
    {
        /* 
         *   resolve the possessive phrase, and use it to reduce the
         *   resolver's main list (this is the list before the "except"
         *   from which are choosing items to exclude) to those items
         *   owned by the object indicated in the possessive phrase 
         */
        return selectWithPossessive(resolver, results, resolver.mainList,
                                    resolver.mainListText, nil);
    }
;

/*
 *   Possessive phrase production for disambiguation.  This base class can
 *   be used for grammar productions that match possessive phrases in
 *   disambiguation prompt ("which book do you mean...?") responses. 
 */
class DisambigPossessiveProd: BasicPossessiveProd, DisambigProd
    resolveNouns(resolver, results)
    {
        local lst;

        /* 
         *   Remember the original qualified list (this is the list of
         *   objects from which we're trying to choose on the basis of the
         *   possessive phrase we're resolving now).  We can feed the
         *   qualified-object list back into the selection process for the
         *   qualifier itself, because we're looking for a qualifier that
         *   makes sense when combined with one of the qualified objects.  
         */
        qualifiedList_ = resolver.matchList;

        /* select from the match list using the possessive phrase */
        lst = selectWithPossessive(resolver, results,
                                   resolver.matchList, resolver.matchText, 1);

        /* 
         *   if the list has more than one entry, treat the result as still
         *   ambiguous - a simple possessive response to a disambiguation
         *   query is implicitly definite, so we must select a single
         *   object 
         */
        if (lst != nil && lst.length() > 1)
            lst = results.ambiguousNounPhrase(
                self, ResolveAsker, resolver.matchText,
                lst, resolver.fullMatchList, lst, 1, resolver);

        /* 
         *   if we failed to resolve it, return an empty list; otherwise
         *   return the list 
         */
        return (lst == nil ? [] : lst);
    }

    /*
     *   Do extra filter during disambiguation.  Since we have a list of
     *   objects we're trying to qualify, we can look at that list to see
     *   if some of the possible matches for the qualifier phrase are
     *   owners of things in the qualified list.  
     */
    reduceDefinite(lst, resolver, results)
    {
        local newLst;
        
        /* 
         *   try reducing the list to owners of objects that appear in the
         *   qualified object list 
         */
        newLst = lst.subset({x: qualifiedList_.indexWhich(
            {y: y.obj_.isOwnedBy(x.obj_)}) != nil});

        /* if there's anything in that list, keep only the subset */
        if (newLst.length() > 0)
            lst = newLst;

        /* return the result */
        return lst;
    }

    /* 
     *   the list of objects being qualified - this is the list of books,
     *   for example, in "bob's books" 
     */
    qualifiedList_ = []
;

/* ------------------------------------------------------------------------ */
/*
 *   A noun phrase with explicit containment.  Grammar rules based on this
 *   class must set the property np_ to the main noun phrase, and cont_ to
 *   the noun phrase giving the container.
 *   
 *   We're based on the definite noun phrase production, because we need
 *   to resolve the underlying container phrase to a singe, unambiguous
 *   object.  
 */
class ContainerNounPhraseProd: DefiniteNounProd
    resolveNouns(resolver, results)
    {
        local lst;
        local cRes;
        local cLst;
        local cont;
        
        /*
         *   We have two separate noun phrases to resolve: the qualified
         *   noun phrase in np_, and the locational qualifier in cont_.
         *   We then want to filter the object matches for np_ to select
         *   the subset that is contained in cont_.
         *   
         *   We must resolve cont_ to a single, unambiguous object.
         *   However, we want to be smart about it by limiting the range
         *   of choices to objects that actually contain something that
         *   could match the possible resolutions of np_.  So, tentatively
         *   resolve np_ first, to get the range of possible matches.
         */
        lst = np_.resolveNouns(resolver, results);

        /* 
         *   We have the tentative resolution of the main noun phrase, so
         *   we can now resolve the locational qualifier phrase.  Use our
         *   special container resolver for this step, since this will try
         *   to pick objects that contain something in the tentative
         *   results for the main list.  Resolve the container as a
         *   definite noun phrase, since we want a single, unambiguous
         *   match.
         *   
         *   Note that we must base our special container resolver on the
         *   qualifier resolver, not on the main resolver.  The location is
         *   a qualifying phrase, so it's resolved in the scope of any
         *   other qualifying phrase.
         *   
         *   The container phrase has to be a single object, so note in the
         *   results that we're working on a single-object slot.  
         */
        results.beginSingleObjSlot();
        
        cRes = new ContainerResolver(lst, np_.getOrigText(),
                                     resolver.getQualifierResolver());
        cLst = resolveDefinite(ResolveAsker, cont_.getOrigText(),
                               cont_.resolveNouns(cRes, results),
                               self, cRes, results);
        
        results.endSingleObjSlot();

        /* 
         *   We need a single object in the container list.  If we have
         *   no objects, or more than one object, it's an error. 
         */
        if (cLst.length() != 1)
        {
            /* it's a separate error if we got more than one object */
            if (cLst.length() > 1)
                results.uniqueObjectRequired(cont_.getOrigText(), cLst);

            /* we can't go on */
            return [];
        }

        /* we have a unique item, so it's the container */
        cont = cLst[1].obj_;

        /* reduce the list to those objects inside the container */
        lst = lst.subset({x: x.obj_.isNominallyIn(cont)});

        /*
         *   If we have some objects directly in the container, and other
         *   objects indirectly in the container, filter the list to
         *   include only the directly contained items. 
         */
        if (lst.indexWhich({x: x.obj_.isDirectlyIn(cont)}) != nil)
            lst = lst.subset({x: x.obj_.isDirectlyIn(cont)});

        /* if that leaves nothing, mention it */
        if (lst.length() == 0)
        {
            results.noMatchForLocation(cont, np_.getOrigText());
            return [];
        }

        /* return the list */
        return lst;
    }
;

/*
 *   Basic container resolver.  This is a common subclass for the standard
 *   container resolver and the "vague" container resolver. 
 */
class BasicContainerResolver: ProxyResolver
    /* we're a sub-phrase resolver */
    isSubResolver = true

    /* resolve any qualifiers in the main scope */
    getQualifierResolver() { return origResolver; }

    /* filter an ambiguous noun phrase */
    filterAmbiguousNounPhrase(lst, requiredNum, np)
    {
        local outer;
        local lcl;
        
        /* do the normal filtering first */
        lst = inherited(lst, requiredNum, np);

        /* 
         *   get the subset that includes only local objects - that is,
         *   objects within the same outermost room as the target actor 
         */
        outer = actor_.getOutermostRoom();
        lcl = lst.subset({x: x.obj_.isIn(outer)});

        /* if there's a local subset, take the subset */
        if (lcl.length() != 0)
            lst = lcl;

        /* return the result */
        return lst;
    }
;    

/*
 *   Container Resolver.  This is a proxy for the main qualifier resolver
 *   that prefers to match objects that are plausible in the sense that
 *   they contain something in the tentative resolution of the main list.  
 */
class ContainerResolver: BasicContainerResolver
    construct(mainList, mainText, origResolver)
    {
        /* inherit base handling */
        inherited(origResolver);

        /* remember my tentative main match list */
        self.mainList = mainList;
        self.mainListText = mainText;
    }

    /* filter ambiguous equivalents */
    filterAmbiguousEquivalents(lst, np)
    {
        local vec;
        
        /*
         *   Check to see if any of the objects in the list are plausible
         *   containers for objects in our main list.  If we can find any
         *   plausible entries, keep only the plausible ones. 
         */
        vec = new Vector(lst.length());
        foreach (local cur in lst)
        {
            /* if this item is plausible, add it to our result vector */
            if (mainList.indexWhich(
                {x: x.obj_.isNominallyIn(cur.obj_)}) != nil)
                vec.append(cur);
        }

        /* 
         *   if we found anything plausible, return only the plausible
         *   subset; otherwise, return the full original list, since
         *   they're all equally implausible 
         */
        if (vec.length() != 0)
            return vec.toList();
        else
            return lst;
    }

    /* the tentative match list for the main phrase we're qualifying */
    mainList = nil

    /* the text of the main phrase we're qualifying */
    mainListText = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   A "vague" container noun phrase.  This is a phrase that specifies a
 *   container but nothing else: "the one in the box", "the ones in the
 *   box", "everything in the box".  
 */
class VagueContainerNounPhraseProd: DefiniteNounProd
    resolveNouns(resolver, results)
    {
        local cRes;
        local cLst;
        local lst;
        local cont;
        
        /* resolve the container as a single-object slot */
        results.beginSingleObjSlot();

        /*
         *   Resolve the container.  Use a special resolver that prefers
         *   objects that have any contents.  
         */
        cRes = new VagueContainerResolver(resolver.getQualifierResolver());
        cLst = resolveDefinite(ResolveAsker, cont_.getOrigText(),
                               cont_.resolveNouns(cRes, results),
                               self, cRes, results);

        /* done with the single-object slot */
        results.endSingleObjSlot();

        /* make sure we resolved to a unique container */
        if (cLst.length() != 1)
        {
            /* it's a separate error if we got more than one object */
            if (cLst.length() > 1)
                results.uniqueObjectRequired(cont_.getOrigText(), cLst);

            /* we can't go on */
            return [];
        }

        /* we have a unique item, so it's the container */
        cont = cLst[1].obj_;

        /* 
         *   If the container is the nominal drop destination for the
         *   actor's location, then use the actor's location as the actual
         *   container.  This way, if we try to get "all on floor" or the
         *   like, we'll correctly get objects that are directly in the
         *   room. 
         */
        if (cont == resolver.actor_.location.getNominalDropDestination())
            cont = resolver.actor_.location;

        /* get the contents */
        lst = cont.getAllForTakeFrom(resolver.getScopeList());

        /* keep only visible objects */
        lst = lst.subset({x: resolver.actor_.canSee(x)});

        /* map the contents to a resolved object list */
        lst = lst.mapAll({x: new ResolveInfo(x, 0, self)});

        /* make sure the list isn't empty */
        if (lst.length() == 0)
        {
            /* there's nothing in this container */
            results.nothingInLocation(cont);
            return [];
        }

        /* make other subclass-specific checks on the list */
        lst = checkContentsList(resolver, results, lst, cont);

        /* note the matches */
        results.noteMatches(lst);

        /* return the list */
        return lst;
    }

    /* check a contents list */
    checkContentsList(resolver, results, lst, cont)
    {
        /* by default, just return the list */
        return lst;
    }
;

/*
 *   "All in container" 
 */
class AllInContainerNounPhraseProd: VagueContainerNounPhraseProd
    /* check a contents list */
    checkContentsList(resolver, results, lst, cont)
    {
        /* keep only items that aren't hidden from "all" */
        lst = lst.subset({x: !x.obj_.hideFromAll(resolver.getAction())});

        /* set the "matched all" and "always announce as multi" flags */
        foreach (local cur in lst)
            cur.flags_ |= AlwaysAnnounce | MatchedAll;

        /* if that emptied the list, so note */
        if (lst.length() == 0)
            results.nothingInLocation(cont);

        /* return the result list */
        return lst;
    }
;

/*
 *   A definite vague container phrase.  This selects a single object in a
 *   given container ("the one in the box").  If more than one object is
 *   present, we'll try to disambiguate it.
 *   
 *   Grammar rules instantiating this class must set the property
 *   'mainPhraseText' to the text to display for a disambiguation prompt
 *   involving the main phrase.  
 */
class VagueContainerDefiniteNounPhraseProd: VagueContainerNounPhraseProd
    construct()
    {
        /* create a disambiguator for the main phrase */
        npKeeper = new AmbigResponseKeeper();
    }

    /* check a contents list */
    checkContentsList(resolver, results, lst, cont)
    {
        /* 
         *   This production type requires a single object in the
         *   container (since it's for phrases like "the one in the box").
         *   If we have more than one object, try disambiguating.
         */
        if (lst.length() > 1)
        {
            local scopeList;
            local fullList;
            
            /* 
             *   There's more than one object in this container.  First,
             *   try filtering it by possessive qualifier strength and
             *   then the normal disambiguation ranking.  
             */
            scopeList = lst;
            lst = resolver.filterPossRank(lst, 1);
            lst = resolver.filterAmbiguousNounPhrase(lst, 1, self);

            /* try removing redundant equivalents */
            fullList = lst;
            if (results.allowEquivalentFiltering)
                lst = resolver.filterAmbiguousEquivalents(lst, self);

            /* if we still have too many objects, it's ambiguous */
            if (lst.length() > 1)
            {
                /* ask the results object to handle the ambiguous phrase */
                lst = results.ambiguousNounPhrase(
                    npKeeper, ResolveAsker, mainPhraseText,
                    lst, fullList, scopeList, 1, resolver);
            }
        }
        
        /* return the contents of the container */
        return lst;
    }

    /* our disambiguation result keeper */
    npKeeper = nil
;

/*
 *   An indefinite vague container phrase.  This selects a single object,
 *   choosing arbitrarily if multiple objects are in the container. 
 */
class VagueContainerIndefiniteNounPhraseProd: VagueContainerNounPhraseProd
    /* check a contents list */
    checkContentsList(resolver, results, lst, cont)
    {
        /* choose one object arbitrarily */
        if (lst.length() > 1)
            lst = lst.sublist(1, 1);

        /* return the (possibly trimmed) list */
        return lst;
    }
;


/*
 *   Container Resolver for vaguely-specified containment phrases.  We'll
 *   select for objects that have contents, but that's about as much as we
 *   can do, since the main phrase is bounded only by the container in
 *   vague containment phrases (and thus provides no information that
 *   would help us narrow down the container itself).  
 */
class VagueContainerResolver: BasicContainerResolver
    /* filter ambiguous equivalents */
    filterAmbiguousEquivalents(lst, np)
    {
        local vec;
        
        /* prefer objects with contents */
        vec = new Vector(lst.length());
        foreach (local cur in lst)
        {
            /* if this item has any contents, add it to the new list */
            if (cur.obj_.contents.length() > 0)
                vec.append(cur);
        }

        /* 
         *   if we found anything plausible, return only the plausible
         *   subset; otherwise, return the full original list, since
         *   they're all equally implausible 
         */
        if (vec.length() != 0)
            return vec.toList();
        else
            return lst;
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   Noun phrase with vocabulary resolution.  This is a base class for the
 *   various noun phrases that match adjective, noun, and plural tokens.
 *   This class provides dictionary resolution of a vocabulary word into a
 *   list of objects.
 *   
 *   In non-declined languages such as English, the parts of speech of our
 *   words are usually simply 'adjective' and 'noun'.  A language
 *   "declines" its noun phrases if the words in a noun phrase have
 *   different forms that depend on the function of the noun phrase in a
 *   sentence; for example, in German, adjectives take suffixes that
 *   depend upon the gender of the noun being modified and the function of
 *   the noun phrase in the sentence (subject, direct object, etc).  In a
 *   declined language, it might be desirable to use separate parts of
 *   speech for separate declined adjective and noun forms.  
 */
class NounPhraseWithVocab: NounPhraseProd
    /*
     *   Get a list of the matches in the main dictionary for the given
     *   token as the given part of speech (&noun, &adjective, &plural, or
     *   others as appropriate for the local language) that are in scope
     *   according to the resolver.  
     */
    getWordMatches(tok, partOfSpeech, resolver, flags, truncFlags)
    {
        local lst;

        /* start with the dictionary matches */
        lst = cmdDict.findWord(tok, partOfSpeech);

        /* filter it to eliminate redundant matches */
        lst = filterDictMatches(lst);

        /* add the objects that match the full dictionary word */
        lst = inScopeMatches(lst, flags, flags | truncFlags, resolver);

        /* return the combined results */
        return lst;
    }

    /*
     *   Combine two word match lists.  This simply adds each entry from
     *   the second list that doesn't already have a corresponding entry
     *   in the first list, returning the combined list.  
     */
    combineWordMatches(aLst, bLst)
    {
        /* create a vector copy of the original 'a' list */
        aLst = new Vector(aLst.length(), aLst);
        
        /* 
         *   add each entry from the second list whose object isn't
         *   already represented in the first list 
         */
        foreach (local b in bLst)
        {
            local ia;

            /* look for an existing entry for this object in the 'a' list */
            ia = aLst.indexWhich({aCur: aCur.obj_ == b.obj_});

            /* 
             *   If this 'b' entry isn't already in the 'a' list, simply
             *   add the 'b' entry.  If both lists have this entry, combine
             *   the flags into the existing entry.  
             */
            if (ia == nil)
            {
                /* it's not already in the 'a' list, so simply add it */
                aLst += b;
            }
            else
            {
                /* 
                 *   Both lists have the entry, so keep the existing 'a'
                 *   entry, but merge the flags.  Note that the 
                 *   original 'a' might still be interesting to the 
                 *   caller separately, so create a copy of it before 
                 *   modifying it.
                 */
                local a = aLst[ia];
                aLst[ia] = a = a.createClone();
                combineWordMatchItems(a, b);
            }
        }

        /* return the combined list */
        return aLst;
    }

    /*
     *   Combine the given word match entries.  We'll merge the flags of
     *   the two entries to produce a single merged entry in 'a'.  
     */
    combineWordMatchItems(a, b)
    {
        /*
         *   If one item was matched with truncation and the other wasn't,
         *   remove the truncation flag entirely.  This will make the
         *   combined entry reflect the fact that we were able to match the
         *   word without any truncation.  The fact that we also matched it
         *   with truncation isn't relevant, since matching without
         *   truncation is the stronger condition.  
         */
        if ((b.flags_ & VocabTruncated) == 0)
            a.flags_ &= ~VocabTruncated;

        /* likewise for plural truncation */
        if ((b.flags_ & PluralTruncated) == 0)
            a.flags_ &= ~PluralTruncated;

        /*
         *   Other flags generally are set for the entire list of matching
         *   objects for a production, rather than at the level of
         *   individual matching objects, so we don't have to worry about
         *   combining them - they'll naturally be the same at this point. 
         */
    }

    /*
     *   Filter a dictionary match list.  This is called to clean up the
     *   raw match list returned from looking up a vocabulary word in the
     *   dictionary.
     *   
     *   The main purpose of this routine is to eliminate unwanted
     *   redundancy from the dictionary matches; in particular, the
     *   dictionary might have multiple matches for a given word at a given
     *   object, due to truncation, upper/lower folding, accent removal,
     *   and so on.  In general, we want to keep only the single strongest
     *   match from the dictionary for a given word matching a given
     *   object.
     *   
     *   The meaning of "stronger" and "exact" matches is
     *   language-dependent, so we abstract these with the separate methods
     *   dictMatchIsExact() and dictMatchIsStronger().
     *   
     *   Keep in mind that the raw dictionary match list has alternating
     *   entries: object, comparator flags, object, comparator flags, etc.
     *   The return list should be in the same format.  
     */
    filterDictMatches(lst)
    {
        local ret;

        /* set up a vector to hold the result */
        ret = new Vector(lst.length());
        
        /* 
         *   check each inexact element of the list for another entry with
         *   a stronger match; keep only the ones without a stronger match 
         */
    truncScan:
        /* scan for a stronger match */
        for (local i = 1, local len = lst.length() ; i < len ; i += 2)
        {
            /* get the current object and its flags */
            local curObj = lst[i];
            local curFlags = lst[i+1];
            
            /* if it's not an exact match, check for a stronger match */
            if (!dictMatchIsExact(curFlags))
            {
                /* scan for an equal or stronger match later in the list */
                for (local j = i + 2 ; j < len ; j += 2)
                {
                    /* 
                     *   if entry j is stronger than or identical to the
                     *   current entry, omit the current entry in favor of
                     *   entry j
                     */
                    local jObj = lst[j], jFlags = lst[j+1];
                    if (jObj == curObj
                        && (jFlags == curFlags
                            || dictMatchIsStronger(jFlags, curFlags)))
                    {
                        /* there's a better entry; omit the current one */
                        continue truncScan;
                    }
                }
            }
            
            /* keep this entry - add it to the result vector */
            ret.append(curObj);
            ret.append(curFlags);
        }

        /* return the result vector */
        return ret;
    }

    /*
     *   Check a dictionary match's string comparator flags to see if the
     *   match is "exact."  The exact meaning of "exact" is dependent on
     *   the language's lexical rules; this generic base version considers
     *   a match to be exact if it doesn't have any string comparator flags
     *   other than the base "matched" flag and the case-fold flag.  This
     *   should be suitable for most languages, as (1) case folding usually
     *   doesn't improve match strength, and (2) any additional comparator
     *   flags usually indicate some kind of inexact match status.
     *   
     *   A language that depends on upper/lower case as a marker of match
     *   strength will need to override this to consider the case-fold flag
     *   as significant in determining match exactness.  In addition, a
     *   language that uses additional string comparator flags to indicate
     *   better (rather than worse) matches will have to override this to
     *   require the presence of those flags.  
     */
    dictMatchIsExact(flags)
    {
        /* 
         *   the match is exact if it doesn't have any qualifier flags
         *   other than the basic "yes I matched" flag and the case-fold
         *   flag 
         */
        return ((flags & ~(StrCompMatch | StrCompCaseFold)) == 0);
    }

    /*
     *   Compare two dictionary matches for the same object and determine
     *   if the first one is stronger than the second.  Both are for the
     *   same object; the only difference is the string comparator flags.
     *   
     *   Language modules might need to override this to supplement the
     *   filtering with their own rules.  This generic base version
     *   considers truncation: an untruncated match is stronger than a
     *   truncated match.  Non-English languages might want to consider
     *   other lexical factors in the match strength, such as whether we
     *   matched the exact accented characters or approximated with
     *   unaccented equivalents - this information will, of course, need to
     *   be coordinated with the dictionary's string comparator, and
     *   reflected in the comparator match flags.  It's the comparator
     *   match flags that we're looking at here.  
     */
    dictMatchIsStronger(flags1, flags2)
    {
        /* 
         *   if the first match (flags1) indicates no truncation, and the
         *   second (flags2) was truncated, then the first match is
         *   stronger; otherwise, there's no distinction as far as we're
         *   concerned 
         */
        return ((flags1 & StrCompTrunc) == 0
                && (flags2 & StrCompTrunc) != 0);
    }

    /*
     *   Get a list of the matches in the main dictionary for the given
     *   token, intersecting the resulting list with the given list. 
     */
    intersectWordMatches(tok, partOfSpeech, resolver, flags, truncFlags, lst)
    {
        local newList;
        
        /* get the matches to the given word */
        newList = getWordMatches(tok, partOfSpeech, resolver,
                                 flags, truncFlags);

        /* intersect the result with the other list */
        newList = intersectNounLists(newList, lst);

        /* return the combined results */
        return newList;
    }

    /*
     *   Given a list of dictionary matches to a given word, construct a
     *   list of ResolveInfo objects for the matches that are in scope.
     *   For regular resolution, "in scope" means the resolver thinks the
     *   object is in scope.  
     */
    inScopeMatches(dictionaryMatches, flags, truncFlags, resolver)
    {
        local ret;

        /* set up a vector to hold the results */
        ret = new Vector(dictionaryMatches.length());

        /* 
         *   Run through the list and include only the words that are in
         *   scope.  Note that the list of dictionary matches has
         *   alternating objects and match flags, so we must scan it two
         *   elements at a time.  
         */
        for (local i = 1, local cnt = dictionaryMatches.length() ;
             i <= cnt ; i += 2)
        {
            /* get this object */
            local cur = dictionaryMatches[i];

            /* if it's in scope, add a ResolveInfo for this object */
            if (resolver.objInScope(cur))
            {
                local curResults;
                local curFlags;

                /* get the comparator match results for this object */
                curResults = dictionaryMatches[i+1];
                
                /* 
                 *   If the results indication truncation, use the the
                 *   truncated flags; otherwise use the ordinary flags.
                 */
                if (dataType(curResults) == TypeInt
                    && (curResults & StrCompTrunc) != 0)
                    curFlags = truncFlags;
                else
                    curFlags = flags;

                /* add the item to the results */
                ret.append(new ResolveInfo(cur, curFlags, self));
            }
        }

        /* return the results as a list */
        return ret.toList();
    }

    /*
     *   Resolve the objects.
     */
    resolveNouns(resolver, results)
    {
        local matchList;
        local starList;
        
        /* 
         *   get the preliminary match list - this is simply the set of
         *   objects that match all of the words in the noun phrase 
         */
        matchList = getVocabMatchList(resolver, results, 0);

        /* 
         *   get a list of any in-scope objects that match '*' for nouns -
         *   these are objects that want to do all of their name parsing
         *   themselves 
         */
        starList = inScopeMatches(cmdDict.findWord('*', &noun),
                                  0, 0, resolver);

        /* combine the lists */
        matchList = combineWordMatches(matchList, starList);

        /* run the results through matchName */
        return resolveNounsMatchName(results, resolver, matchList);
    }

    /*
     *   Run a set of resolved objects through matchName() or a similar
     *   routine.  Returns the filtered results.  
     */
    resolveNounsMatchName(results, resolver, matchList)
    {
        local origTokens;
        local adjustedTokens;
        local objVec;
        local ret;

        /* get the original token list for the command */
        origTokens = getOrigTokenList();

        /* get the adjusted token list for the command */
        adjustedTokens = getAdjustedTokens();

        /* set up to receive about the same number of results as inputs */
        objVec = new Vector(matchList.length());

        /* consider each preliminary match */
        foreach (local cur in matchList)
        {
            /* ask this object if it wants to be included */
            local newObj = resolver.matchName(
                cur.obj_, origTokens, adjustedTokens);

            /* check the result */
            if (newObj == nil)
            {
                /* 
                 *   it's nil - this means it's not a match for the name
                 *   after all, so leave it out of the results 
                 */
            }
            else if (newObj.ofKind(Collection))
            {
                /* 
                 *   it's a collection of some kind - add each element to
                 *   the result list, using the same flags as the original 
                 */
                foreach (local curObj in newObj)
                    objVec.append(new ResolveInfo(curObj, cur.flags_, self));
            }
            else
            {
                /* 
                 *   it's a single object - add it ito the result list,
                 *   using the same flags as the original 
                 */
                objVec.append(new ResolveInfo(newObj, cur.flags_, self));
            }
        }

        /* convert the result vector to a list */
        ret = objVec.toList();

        /* if our list is empty, note it in the results */
        if (ret.length() == 0)
        {
            /* 
             *   If the adjusted token list contains any tokens of type
             *   "miscWord", send the phrase to the results object for
             *   further consideration.  
             */
            if (adjustedTokens.indexOf(&miscWord) != nil)
            {
                /* 
                 *   we have miscWord tokens, so this is a miscWordList
                 *   match - let the results object process it specially.  
                 */
                ret = results.unknownNounPhrase(self, resolver);
            }

            /* 
             *   if the list is empty, note that we have a noun phrase
             *   whose vocabulary words don't match anything in the game 
             */
            if (ret.length() == 0)
                results.noVocabMatch(resolver.getAction(), getOrigText());
        }

        /* return the result list */
        return ret;
    }

    /*
     *   Each subclass must override getAdjustedTokens() to provide the
     *   appropriate set of tokens used to match the object.  This is
     *   usually simply the original set of tokens, but in some cases it
     *   may differ; for example, spelled-out numbers normally adjust to
     *   the numeral form of the number.
     *   
     *   For each adjusted token, the list must have two entries: the
     *   first is a string giving the token text, and the second is the
     *   property giving the part of speech for the token.  
     */
    getAdjustedTokens() { return nil; }

    /*
     *   Get the vocabulary match list.  This is simply the set of objects
     *   that match all of the words in the noun phrase.  Each rule
     *   subclass must override this to return an appropriate list.  Note
     *   that subclasses should use getWordMatches() and
     *   intersectWordMatches() to build the list.  
     */
    getVocabMatchList(resolver, results, extraFlags) { return nil; }
;


/* ------------------------------------------------------------------------ */
/*
 *   An empty noun phrase production is one that matches, typically with
 *   non-zero badness value, as a placeholder when a command is missing a
 *   noun phrase where one is required.
 *   
 *   Each grammar rule instance of this rule class must define the
 *   property 'responseProd' to be the production that should be used to
 *   parse any response to an interactive prompt for the missing object.  
 */
class EmptyNounPhraseProd: NounPhraseProd
    /* customize the way we generate the prompt and parse the response */
    setPrompt(response, asker)
    {
        /* remember the new response production and ResolveAsker */
        responseProd = response;
        asker_ = asker;
    }
    
    /* resolve the empty phrase */
    resolveNouns(resolver, results)
    {
        /* 
         *   if we've filled in our missing phrase already, return the
         *   resolution of that list 
         */
        if (newMatch != nil)
            return newMatch.resolveNouns(resolver, results);
        
        /* 
         *   The noun phrase was left out entirely, so try to get an
         *   implied object.
         */
        local match = getImpliedObject(resolver, results);

        /* if that succeeded, return the result */
        if (match != nil)
            return match;

        /* 
         *   Parse the next input with our own (subclassed) responseProd if
         *   we have one.  If we don't (i.e., it's nil), get one from the
         *   action. 
         */
        local rp = responseProd;
        if (rp == nil && resolver.getAction() != nil)
            rp = resolver.getAction().getObjResponseProd(resolver);

        /* as a last resort, use the fallback response */
        if (rp == nil)
            rp = fallbackResponseProd;

        /* 
         *   There is no implied object, so try to get a result
         *   interactively.  Use the production that our rule instance
         *   specifies via the responseProd property to parse the
         *   interactive response.  
         */
        match = results.askMissingObject(asker_, resolver, rp);
        
        /* if we didn't get a match, we have nothing to return */
        if (match == nil)
            return [];
        
        /* we got a match - remember it as a proxy for our noun phrase */
        newMatch = match;
        
        /* return the resolved noun phrase from the proxy match */
        return newMatch.resolvedObjects;
    }

    /*
     *   Get an implied object to automatically fill in for the missing
     *   noun phrase.  By default, we simply ask the 'results' object for
     *   the missing object.  
     */
    getImpliedObject(resolver, results)
    {
        /* ask the 'results' object for the information */
        return results.getImpliedObject(self, resolver);
    }

    /*
     *   Get my tokens.  If I have a new match tree, return the tokens
     *   from the new match tree.  Otherwise, we don't have any tokens,
     *   since we're empty.  
     */
    getOrigTokenList()
    {
        return (newMatch != nil ? newMatch.getOrigTokenList() : []);
    }

    /* 
     *   Get my original text.  If I have a new match tree, return the
     *   text from the new match tree.  Otherwise, we have no original
     *   text, since we're an empty phrase.  
     */
    getOrigText()
    {
        return (newMatch != nil ? newMatch.getOrigText() : '');
    }

    /*
     *   I'm an empty noun phrase, unless I already have a new match
     *   object. 
     */
    isEmptyPhrase { return newMatch == nil; }

    /*
     *   the new match, when we get an interactive response to a query for
     *   the missing object 
     */
    newMatch = nil

    /* 
     *   Our "response" production - this is the production we use to parse
     *   the player's input in response to our disambiguation prompt.  A
     *   subclass can leave this as nil, in which case we'll attempt to get
     *   the appropriate response production from the action.
     */
    responseProd = nil

    /* 
     *   Our fallback response production - if responseProd is nil, this
     *   must be supplied for cases where we can't get the production from
     *   the action.  This is ignored if responseProd is non-nil.
     */
    fallbackResponseProd = nil

    /* 
     *   The ResolveAsker we use to generate our prompt.  Use the base
     *   ResolveAsker by default; this can be overridden when the prompt
     *   is to be customized. 
     */
    asker_ = ResolveAsker
;

/*
 *   An empty noun phrase production for verb phrasings that imply an
 *   actor, but don't actually include one by name.
 *   
 *   This is similar to EmptyNounPhraseProd, but has an important
 *   difference: if the actor carrying out the command has a current or
 *   implied conversation partner, then we choose the conversation partner
 *   as the implied object.  This is important in that we don't count the
 *   noun phrase as technically missing in this case, for the purposes of
 *   command ranking.  This is useful for phrasings that inherently imply
 *   an actor strongly enough that there should be no match-strength
 *   penalty for leaving it out.  
 */
class ImpliedActorNounPhraseProd: EmptyNounPhraseProd
    /* get my implied object */
    getImpliedObject(resolver, results)
    {
        /* 
         *   If the actor has a default interlocutor, use that, bypassing
         *   the normal implied object search. 
         */
        local actor = resolver.actor_.getDefaultInterlocutor();
        if (actor != nil)
            return [new ResolveInfo(actor, DefaultObject, nil)];

        /* ask the 'results' object for the information */
        return results.getImpliedObject(self, resolver);
    }
;

/*
 *   Empty literal phrase - this serves a purpose similar to that of
 *   EmptyNounPhraseProd, but can be used where literal phrases are
 *   required. 
 */
class EmptyLiteralPhraseProd: LiteralProd
    getLiteralText(results, action, which)
    {
        local toks;
        local prods;
        
        /* if we already have an interactive response, return it */
        if (newText != nil)
            return newText;

        /* 
         *   ask for the missing phrase and remember it for the next time
         *   we're asked for our text 
         */
        newText = results.askMissingLiteral(action, which);

        /*
         *   Parse the text (if any) as a literal phrase.  In most cases,
         *   anything can be parsed as a literal phrase, so this might
         *   seem kind of pointless; however, this is useful when the
         *   language-specific library defines rules for things like
         *   removing quotes from a quoted string.  
         */
        if (newText != nil)
        {
            try
            {
                /* tokenize the input */
                toks = cmdTokenizer.tokenize(newText);
            }
            catch (TokErrorNoMatch exc)
            {
                /* note the token error */
                gLibMessages.invalidCommandToken(exc.curChar_.htmlify());

                /* treat the command as empty */
                throw new ReplacementCommandStringException(nil, nil, nil);
            }

            /* parse the input as a literal phrase */
            prods = literalPhrase.parseTokens(toks, cmdDict);

            /* if we got a match, use it */
            if (prods.length() > 0)
            {
                /* 
                 *   we got a match - this will be a LiteralProd, so ask
                 *   the matching LiteralProd for its literal text, and
                 *   use that as our new literal text 
                 */
                newText = prods[1].getLiteralText(results, action, which);
            }
        }

        /* return the text */
        return newText;
    }

    /* 
     *   Tentatively get my literal text.  This is used for pre-resolution
     *   when we have another phrase we want to resolve first, but we want
     *   to provide a tentative form of the text in the meantime.  We won't
     *   attempt to ask for more information interactively, but we'll
     *   return any information we do have.  
     */
    getTentativeLiteralText()
    {
        /* 
         *   if we have a result from a previous interaactive request,
         *   return it; otherwise we have no tentative text 
         */
        return newText;
    }

    /* I'm an empty phrase, unless I already have a text response */
    isEmptyPhrase { return newText == nil; }

    /* the response to a previous interactive query */
    newText = nil
;

/*
 *   Empty topic phrase production.  This is the topic equivalent of
 *   EmptyNounPhraseProd. 
 */
class EmptyTopicPhraseProd: TopicProd
    resolveNouns(resolver, results)
    {
        local match;

        /* 
         *   if we've filled in our missing phrase already, return the
         *   resolution of that list 
         */
        if (newMatch != nil)
            return newMatch.resolveNouns(resolver, results);
        
        /* ask for a topic interactively, using our responseProd */
        match = results.askMissingObject(asker_, resolver, responseProd);
        
        /* if we didn't get a match, we have nothing to return */
        if (match == nil)
            return [];
        
        /* we got a match - remember it as a proxy for our noun phrase */
        newMatch = match;
        
        /* return the resolved noun phrase from the proxy match */
        return newMatch.resolvedObjects;
    }

    /* we're an empty phrase if we don't have a new topic yet */
    isEmptyPhrase { return newMatch = nil; }

    /* get my tokens - use the underlying new match tree if we have one */
    getOrigTokenList()
    {
        return (newMatch != nil ? newMatch.getOrigTokenList() : inherited());
    }

    /* get my original text - use the new match tree if we have one */
    getOrigText()
    {
        return (newMatch != nil ? newMatch.getOrigText() : inherited());
    }

    /* our new underlying topic phrase */
    newMatch = nil

    /* 
     *   by default, parse our interactive response as an ordinary topic
     *   phrase 
     */
    responseProd = topicPhrase

    /* our ResolveAsker object - this is for customizing the prompt */
    asker_ = ResolveAsker
;


/* ------------------------------------------------------------------------ */
/*
 *   Look for an undefined word in a list of tokens, and give the player a
 *   chance to correct a typo with "OOPS" if appropriate.
 *   
 *   If we find an unknown word and we can prompt for interactive
 *   resolution, we'll do so, and we'll throw an appropriate exception to
 *   handle the response.  If we can't resolve the missing word
 *   interactively, we'll throw a parse failure exception.
 *   
 *   If there are no undefined words in the command, we'll simply return.
 *   
 *   tokList is the list of tokens under examination; this is a subset of
 *   the full command token list.  cmdTokenList is the full command token
 *   list, in the usual tokenizer format.  firstTokenIndex is the index of
 *   the first token in tokList within cmdTokenList.
 *   
 *   cmdType is an rmcXxx code giving the type of input we're reading.  
 */
tryOops(tokList, issuingActor, targetActor,
        firstTokenIndex, cmdTokenList, cmdType)
{
    /* run the main "oops" processor in the player character sense context */
    callWithSenseContext(nil, sight,
                         {: tryOopsMain(tokList, issuingActor, targetActor,
                                        firstTokenIndex, cmdTokenList,
                                        cmdType) });
}

/* main "oops" processor */
tryOopsMain(tokList, issuingActor, targetActor,
            firstTokenIndex, cmdTokenList, cmdType)
{
    local str;
    local unknownIdx;
    local oopsMatch;
    local toks;
    local w;

    /*
     *   Look for a word not in the dictionary. 
     */
    for (unknownIdx = nil, local i = 1, local len = tokList.length() ;
         i <= len ; ++i)
    {
        local cur;
        local typ;
        
        /* get the token value for this word */
        cur = getTokVal(tokList[i]);
        typ = getTokType(tokList[i]);
        
        /* check to see if this word is defined in the dictionary */
        if (typ == tokWord && !cmdDict.isWordDefined(cur))
        {
            /* note that we found an unknown word */
            unknownIdx = i;
            
            /* no need to look any further */
            break;
        }
    }

    /* 
     *   if we didn't find an unknown word, there's no need to offer the
     *   user a chance to correct a typo - simply return without any
     *   further processing 
     */
    if (unknownIdx == nil)
        return;

    /*
     *   We do have an unknown word, but check one more thing: if we were
     *   asking some kind of follow-up question, such as a missing-object
     *   or disambiguation query, check to see if the new entry would parse
     *   successfully as a new command.  It's possible that the new entry
     *   is a brand new command rather than a response to our question, and
     *   that the unknown word is valid in the context of a new command -
     *   it could be part of a literal-phrase, for example.  
     */
    if (cmdType != rmcCommand)
    {
        /* parse the command */
        local lst = firstCommandPhrase.parseTokens(cmdTokenList, cmdDict);

        /* resolve actions */
        lst = lst.subset(
            {x: x.resolveFirstAction(issuingActor, targetActor) != nil});

        /* if we managed to match something, treat it as a new command */
        if (lst.length() != 0)
            throw new ReplacementCommandStringException(
                cmdTokenizer.buildOrigText(cmdTokenList), nil, nil);
    }
            

    /* get the unknown word, in presentable form */
    w = getTokOrig(tokList[unknownIdx]).htmlify();

    /* 
     *   Give them a chance to correct a typo via OOPS if the player
     *   issued the command.  If the command came from an actor other than
     *   the player character, simply fail the command.  
     */
    if (!issuingActor.isPlayerChar())
    {
        /* we can't do this interactively - treat it as a failure */
        throw new ParseFailureException(&wordIsUnknown, w);
    }
    
    /* 
     *   tell the player about the unknown word, implicitly asking for
     *   an OOPS to fix it 
     */
    targetActor.getParserMessageObj().askUnknownWord(targetActor, w);
    
    /* 
     *   Prompt for a new command.  We'll use the main command prompt,
     *   because we want to pretend that we're asking for a brand new
     *   command, which we'll accept.  However, if the player enters
     *   an OOPS command, we'll process it specially. 
     */
getResponse:
    str = readMainCommandTokens(rmcOops);

    /* re-enable the transcript, if we have one */
    if (gTranscript)
        gTranscript.activate();

    /* 
     *   if the command reader fully processed the input via preparsing,
     *   we have nothing more to do here - simply throw a replace-command
     *   exception with the nil string 
     */
    if (str == nil)
        throw new ReplacementCommandStringException(nil, nil, nil);
    
    /* extract the tokens and string from the result */
    toks = str[2];
    str = str[1];
    
    /* try parsing it as an "oops" command */
    oopsMatch = oopsCommand.parseTokens(toks, cmdDict);

    /* 
     *   if we found a match, process the OOPS command; otherwise,
     *   treat it as a brand new command 
     */
    if (oopsMatch.length() != 0)
    {
        local badIdx;

        /* 
         *   if they typed in just OOPS without any tokens, show an error
         *   and ask again 
         */
        if (oopsMatch[1].getNewTokens() == nil)
        {
            /* tell them they need to supply the missing word */
            gLibMessages.oopsMissingWord;

            /* go back and try reading another response */
            goto getResponse;
        }
            
        /* 
         *   Build a new token list by removing the errant token, and
         *   splicing the new tokens into the original token list to
         *   replace the errant one.
         *   
         *   Note that we'll arbitrarily take the first "oops" match,
         *   even if there are several.  There should be no ambiguity
         *   in the "oops" grammar rule, but even if there is, it
         *   doesn't matter, since ultimately all we care about is the
         *   list of tokens after the "oops".  
         */
        badIdx = firstTokenIndex + unknownIdx - 1;
        cmdTokenList = spliceList(cmdTokenList, badIdx,
                                  oopsMatch[1].getNewTokens());

        /*
         *   Turn the token list back into a string.  Since we've edited
         *   the original text, we want to start over with the new input,
         *   including running pre-parsing on the text.  
         */
        str = cmdTokenizer.buildOrigText(cmdTokenList);

        /* 
         *   run it through pre-parsing as the same kind of input the
         *   caller was reading 
         */
        str = StringPreParser.runAll(str, cmdType);

        /* 
         *   if it came back nil, it means the preparser fully handled it;
         *   in this case, simply throw a nil replacement command string 
         */
        if (str == nil)
            throw new ReplacementCommandStringException(nil, nil, nil);

        /* re-tokenize the result of pre-parsing */
        cmdTokenList = cmdTokenizer.tokenize(str);

        /* retry parsing the edited token list */
        throw new RetryCommandTokensException(cmdTokenList);
    }
    else
    {
        /* 
         *   they didn't enter something that looks like an "OOPS", so
         *   it must be a brand new command - parse it as a new
         *   command by throwing a replacement command exception with
         *   the new string 
         */
        throw new ReplacementCommandStringException(str, nil, nil);
    }
}

/* 
 *   splice a new sublist into a list, replacing the item at 'idx' 
 */
spliceList(lst, idx, newItems)
{
    return (lst.sublist(1, idx - 1)
            + newItems
            + lst.sublist(idx + 1));
}


/* ------------------------------------------------------------------------ */
/*
 *   Try reading a response to a missing object question.  If we
 *   successfully read a noun phrase that matches the given production
 *   rule, we'll resolve it, stash the resolved list in the
 *   resolvedObjects_ property of the match tree, and return the match
 *   tree.  If they enter something that doesn't look like a response to
 *   the question at all, we'll throw a new-command exception to process
 *   it.  
 */
tryAskingForObject(issuingActor, targetActor,
                   resolver, results, responseProd)
{
    /* 
     *   Prompt for a new command.  We'll use the main command prompt,
     *   because we want to pretend that we're asking for a brand new
     *   command, which we'll accept.  However, if the player enters
     *   something that looks like a response to the missing-object query,
     *   we'll handle it as an answer rather than as a new command.  
     */
    local str = readMainCommandTokens(rmcAskObject);

    /* re-enable the transcript, if we have one */
    if (gTranscript)
        gTranscript.activate();

    /* 
     *   if it came back nil, it means that the preparser fully processed
     *   the input, which means we have nothing more to do here - simply
     *   treat this is a nil replacement command 
     */
    if (str == nil)
        throw new ReplacementCommandStringException(nil, nil, nil);
    
    /* extract the input line and tokens */
    local toks = str[2];
    str = str[1];

    /* keep going as long as we get replacement token lists */
    for (;;)
    {    
        /* try parsing it as an object list */
        local matchList = responseProd.parseTokens(toks, cmdDict);
        
        /* 
         *   if we didn't find any match at all, it's probably a brand new
         *   command - go process it as a replacement for the current
         *   command 
         */
        if (matchList == [])
        {
            /* 
             *   they didn't enter something that looks like a valid
             *   response, so assume it's a brand new command - parse it
             *   as a new command by throwing a replacement command
             *   exception with the new string 
             */
            throw new ReplacementCommandStringException(str, nil, nil);
        }

        /* if we're in debug mode, show the interpretations */
        dbgShowGrammarList(matchList);
        
        /* create an interactive sub-resolver for resolving the response */
        local ires = new InteractiveResolver(resolver);

        /* 
         *   rank them using our response ranker - use the original
         *   resolver to resolve the object list 
         */
        local rankings = MissingObjectRanking.sortByRanking(matchList, ires);

        /*
         *   If the best item has unknown words, try letting the user
         *   correct typos with OOPS.  
         */
        if (rankings[1].nonMatchCount != 0
            && rankings[1].unknownWordCount != 0)
        {
            try
            {
                /* 
                 *   complain about the unknown word and look for an OOPS
                 *   reply 
                 */
                tryOops(toks, issuingActor, targetActor,
                        1, toks, rmcAskObject);
            }
            catch (RetryCommandTokensException exc)
            {
                /* get the new token list */
                toks = exc.newTokens_;

                /* replace the string as well */
                str = cmdTokenizer.buildOrigText(toks);

                /* go back for another try at parsing the response */
                continue;
            }
        }

        /*
         *   If the best item we could find has no matches, check to see
         *   if it has miscellaneous noun phrases - if so, it's probably
         *   just a new command, since it doesn't have anything we
         *   recognize as a noun phrase.  
         */
        if (rankings[1].nonMatchCount != 0
            && rankings[1].miscWordListCount != 0)
        {
            /* 
             *   it's probably not an answer at all - treat it as a new
             *   command 
             */
            throw new ReplacementCommandStringException(str, nil, nil);
        }

        /* the highest ranked object is the winner */
        local match = rankings[1].match;

        /* 
         *   Check to see if this looks like an ordinary new command as
         *   well as a noun phrase.  For example, "e" looks like the noun
         *   phrase "east wall," in that "e" is a synonym for the adjective
         *   "east".  But "e" also looks like an ordinary new command.
         *   
         *   We'd have to be able to read the user's mind to know which
         *   they mean in such cases, so we have to make some assumptions
         *   to deal with the ambiguity.  In particular, if the phrasing
         *   has special syntax that makes it look like a particularly
         *   close match to the query phrase, assume it's a query response;
         *   otherwise, assume it's a new command.  For example:
         *   
         *.    >dig
         *.    What do you want to dig in?  [sets up for "in <noun>" reply]
         *   
         *   If the user answers "IN THE DIRT", we have a match to the
         *   special syntax of the reply (the "in <noun>" phrasing), so we
         *   will assume this is a reply to the query, even though it also
         *   matches a valid new command phrasing for ENTER DIRT.  If the
         *   user answers "E" or "EAST", we *don't* have a match to the
         *   special syntax, but merely an ordinary noun phrase match, so
         *   we'll assume this is an ordinary GO EAST command.
         */
        local cmdMatchList = firstCommandPhrase.parseTokens(toks, cmdDict);
        if (cmdMatchList != [])
        {
            /* 
             *   The phrasing looks like it's a valid new command as well
             *   as a noun phrase reply.  Check the query reply match for
             *   special syntax that would distinguish it from a new
             *   command; if it doesn't match any special syntax, assume
             *   that it is indeed a new command instead of a query reply.
             */
            if (!match.isSpecialResponseMatch)
                throw new ReplacementCommandStringException(str, nil, nil);
        }

        /* show our winning interpretation */
        dbgShowGrammarWithCaption('Missing Object Winner', match);

        /* 
         *   actually resolve the response to objects, using the original
         *   results and resolver objects 
         */
        local objList = match.resolveNouns(ires, results);

        /* stash the resolved object list in a property of the match tree */
        match.resolvedObjects = objList;

        /* return the match tree */
        return match;
    }
}

/* 
 *   a property we use to hold the resolved objects of a match tree in
 *   tryAskingForObject 
 */
property resolvedObjects;

/*
 *   Missing-object response ranker. 
 */
class MissingObjectRanking: CommandRanking
    /* 
     *   missing-object responses have no verb, so they won't count any
     *   noun slots; we just need to give these an arbitrary value so that
     *   we can compare them (and find them equal) 
     */
    nounSlotCount = 0
;

/* ------------------------------------------------------------------------ */
/*
 *   An implementation of ResolveResults for normal noun resolution.
 */
class BasicResolveResults: ResolveResults
    /*
     *   The target and issuing actors for the command being resolved. 
     */
    targetActor_ = nil
    issuingActor_ = nil

    /* set up the actor parameters */
    setActors(target, issuer)
    {
        targetActor_ = target;
        issuingActor_ = issuer;
    }

    /*
     *   Results gatherer methods
     */

    noVocabMatch(action, txt)
    {
        /* indicate that we couldn't match the phrase */
        throw new ParseFailureException(&noMatch,
                                        action, txt.toLower().htmlify());
    }

    noMatch(action, txt)
    {
        /* show an error */
        throw new ParseFailureException(&noMatch,
                                        action, txt.toLower().htmlify());
    }

    noMatchPossessive(action, txt)
    {
        /* use the basic noMatch handling */
        noMatch(action, txt);
    }

    allNotAllowed()
    {
        /* show an error */
        throw new ParseFailureException(&allNotAllowed);
    }

    noMatchForAll()
    {
        /* show an error */
        throw new ParseFailureException(&noMatchForAll);
    }

    noteEmptyBut()
    {
        /* this isn't an error - ignore it */
    }

    noMatchForAllBut()
    {
        /* show an error */
        throw new ParseFailureException(&noMatchForAllBut);
    }

    noMatchForListBut()
    {
        /* show an error */
        throw new ParseFailureException(&noMatchForListBut);
    }

    noMatchForPronoun(typ, txt)
    {
        /* show an error */
        throw new ParseFailureException(&noMatchForPronoun,
                                        typ, txt.toLower().htmlify());
    }

    reflexiveNotAllowed(typ, txt)
    {
        /* show an error */
        throw new ParseFailureException(&reflexiveNotAllowed,
                                        typ, txt.toLower().htmlify());
    }

    wrongReflexive(typ, txt)
    {
        /* show an error */
        throw new ParseFailureException(&wrongReflexive,
                                        typ, txt.toLower().htmlify());
    }

    noMatchForPossessive(owner, txt)
    {
        /* if the owner is a list, it's a plural owner */
        if (owner.length() > 1)
        {
            /* it's a plural owner */
            throw new ParseFailureException(&noMatchForPluralPossessive, txt);
        }
        else
        {
            /* let the (singular) owner object generate the error */
            owner[1].throwNoMatchForPossessive(txt.toLower().htmlify());
        }
    }

    noMatchForLocation(loc, txt)
    {
        /* let the location object generate the error */
        loc.throwNoMatchForLocation(txt.toLower().htmlify());
    }

    nothingInLocation(loc)
    {
        /* let the location object generate the error */
        loc.throwNothingInLocation();
    }

    noteBadPrep()
    {
        /* 
         *   bad prepositional phrase structure - assume that the
         *   preposition was intended as part of the verb phrase, so
         *   indicate that the entire command is not understood 
         */
        throw new ParseFailureException(&commandNotUnderstood);
    }

    /*
     *   Service routine - determine if we can interactively resolve a
     *   need for more information.  If the issuer is the player, and the
     *   target actor can talk to the player, then we can resolve a
     *   question interactively; otherwise, we cannot.
     *   
     *   We can't interactively resolve a question if the issuer isn't the
     *   player, because there's no interactive user to prompt for more
     *   information.
     *   
     *   We can't interactively resolve a question if the target actor
     *   can't talk to the player, because the question to the player
     *   would be coming out of thin air.
     */
    canResolveInteractively(action)
    {
        /* 
         *   If this is an implied action, and the target actor is in
         *   "NPC" mode, we can't resolve interactively.  Note that if
         *   there's no action, it can't be implicit (we won't have an
         *   action if we're resolving the actor to whom the action is
         *   targeted).  
         */
        if (action != nil
            && action.isImplicit
            && targetActor_.impliedCommandMode() == ModeNPC)
            return nil;

        /* 
         *   if the issuer is the player, and either the target is the
         *   player or the target can talk to the player, we can resolve
         *   interactively 
         */
        return (issuingActor_.isPlayerChar()
                && (targetActor_ == issuingActor_
                    || targetActor_.canTalkTo(issuingActor_)));
    }

    /*
     *   Handle an ambiguous noun phrase.  We'll first check to see if we
     *   can find a Distinguisher that can tell at least some of the
     *   matches apart; if we fail to do that, we'll just pick the required
     *   number of objects arbitrarily, since we have no way to distinguish
     *   any of them.  Once we've chosen a Distinguisher, we'll ask the
     *   user for help interactively if possible.  
     */
    ambiguousNounPhrase(keeper, asker, txt,
                        matchList, fullMatchList, scopeList,
                        requiredNum, resolver)
    {
        /* put the match list in disambigPromptOrder order */
        matchList = matchList.sort(
            SortAsc, {a, b: (a.obj_.disambigPromptOrder
                             - b.obj_.disambigPromptOrder) });

        /* 
         *   Get the token list for the original phrase.  Since the whole
         *   point is to disambiguate a phrase that matched multiple
         *   objects, all of the matched objects came from the same phrase,
         *   so we can just grab the tokens from the first item. 
         */
        local np = matchList[1].np_;
        local npToks = np ? np.getOrigTokenList() : [];

        /* for the prompt, use the lower-cased version of the input text */
        local promptTxt = txt.toLower().htmlify();

        /* ask the response keeper for its list of past responses, if any */
        local pastResponses = keeper.getAmbigResponses();

        /* 
         *   set up a results object - use the special disambiguation
         *   results object instead of the basic resolver type 
         */
        local disambigResults = new DisambigResults(self);

        /* 
         *   start out with an empty still-to-resolve list - we have only
         *   the one list to resolve so far 
         */
        local stillToResolve = [];

        /* we have nothing in the result list yet */
        local resultList = [];

        /* determine if we can ask for clarification interactively */
        if (!canResolveInteractively(resolver.getAction()))
        {
            /* 
             *   don't attempt to resolve this interactively - just treat
             *   it as a resolution failure 
             */
            throw new ParseFailureException(
                &ambiguousNounPhrase, txt.htmlify(),
                matchList, fullMatchList);
        }

        /* we're asking for the first time */
        local everAsked = nil;
        local askingAgain = nil;

        /* 
         *   Keep going until we run out of things left to resolve.  Each
         *   time an answer looks valid but doesn't completely
         *   disambiguate the results, we'll queue it for another question
         *   iteration, so we must continue until we run out of queued
         *   questions.  
         */
    queryLoop:
        for (local pastIdx = 1 ;; )
        {
            local str;
            local toks;
            local dist;
            local curMatchList = [];

            /*
             *   Find the first distinguisher that can tell one or more of
             *   these objects apart.  Work through the distinguishers in
             *   priority order, which is the order they appear in an
             *   object's 'distinguishers' property.
             *   
             *   If these objects aren't all equivalents, then we'll
             *   immediately find that the basic distinguisher can tell
             *   them all apart.  Every object's first distinguisher is
             *   the basic distinguisher.  If these objects are all
             *   equivalents, then they'll all have the same set of
             *   distinguishers.  In either case, we don't have to worry
             *   about what set of objects we have, because we're
             *   guaranteed to have a suitable set of distinguishers in
             *   common - so just arbitrarily pick an object and work
             *   through its distinguishers.  
             */
            foreach (dist in matchList[1].obj_.distinguishers)
            {
                /* filter the match list using this distinguisher only */
                curMatchList = filterWithDistinguisher(matchList, dist);

                /* 
                 *   if this list has more than the required number of
                 *   results, then this distinguisher is capable of
                 *   telling apart enough of these objects, so use this
                 *   distinguisher for now 
                 */
                if (curMatchList.length() > requiredNum)
                    break;
            }

            /*
             *   If we didn't find any distinguishers that could tell
             *   apart enough of the objects, then we've exhausted our
             *   ability to choose among these objects, so return an
             *   arbitrary set of the desired size.
             */
            if (curMatchList.length() <= requiredNum)
                return fullMatchList.sublist(1, requiredNum);
            
            /*
             *   If we have past responses, try reusing the past responses
             *   before asking for new responses. 
             */
            if (pastIdx <= pastResponses.length())
            {
                /* we have another past response - use the next one */
                str = pastResponses[pastIdx++];

                /* tokenize the new command */
                toks = cmdTokenizer.tokenize(str);
            }
            else
            {
                /* 
                 *   Try filtering the options with the basic
                 *   distinguisher, to see if all of the remaining options
                 *   are basic equivalents - if they are, then we can
                 *   refer to them by the object name, since every one has
                 *   the same object name.  
                 */
                local basicDistList = filterWithDistinguisher(
                    matchList, basicDistinguisher);

                /* 
                 *   if we filtered to one object, everything remaining is
                 *   a basic equivalent of that one object, so they all
                 *   have the same name, so we can use that name 
                 */
                if (basicDistList.length() == 1)
                    promptTxt = basicDistList[1].obj_.disambigEquivName;
                
                /* 
                 *   let the distinguisher know we're prompting for more
                 *   information based on this distinguisher 
                 */
                dist.notePrompt(curMatchList);

                /*
                 *   We don't have any special insight into which object
                 *   the user might have intended, and we have no past
                 *   responses to re-use, so simply ask for clarification.
                 *   Ask using the full match list, so that we correctly
                 *   indicate where there are multiple equivalents.  
                 */
                asker.askDisambig(targetActor_, promptTxt,
                                  curMatchList, fullMatchList, requiredNum,
                                  everAsked && askingAgain, dist);

                /* ask for a new command */
                str = readMainCommandTokens(rmcDisambig);

                /* re-enable the transcript, if we have one */
                if (gTranscript)
                    gTranscript.activate();

                /* note that we've asked for input interactively */
                everAsked = true;

                /* 
                 *   if it came back nil, the input was fully processed by
                 *   the preparser, so throw a nil replacement string
                 *   exception 
                 */
                if (str == nil)
                    throw new ReplacementCommandStringException(
                        nil, nil, nil);

                /* extract the tokens and the string result */
                toks = str[2];
                str = str[1];
            }

            /* presume we won't have to ask again */
            askingAgain = nil;

            /* 
             *   Add the new tokens to the noun phrase token list, at the
             *   end, enclosed in parentheses.  The effect is that each
             *   time we answer a disambiguation question, the answer
             *   appears as a parenthetical at the end of the phrase: "take
             *   the box (cardboard) (the big one)".
             */
            npToks = npToks.append(['(', tokPunct, '(']);
            npToks += toks;
            npToks = npToks.append([')', tokPunct, ')']);

        retryParse:
            /*
             *   Check for narrowing syntax.  If the command doesn't
             *   appear to match a narrowing syntax, then treat it as an
             *   entirely new command.  
             */
            local prodList = mainDisambigPhrase.parseTokens(toks, cmdDict);

            /* 
             *   if we didn't get any structural matches for a
             *   disambiguation response, it must be an entirely new
             *   command 
             */
            if (prodList == [])
                throw new ReplacementCommandStringException(str, nil, nil);

            /* if we're in debug mode, show the interpretations */
            dbgShowGrammarList(prodList);

            /* create a disambiguation response resolver */
            local disResolver = new DisambigResolver(
                txt, matchList, fullMatchList, fullMatchList, resolver, dist);

            /* 
             *   create a disambiguation resolver that uses the full scope
             *   list - we'll use this as a fallback if we can't match any
             *   objects with the narrowed possibility list 
             */
            local scopeDisResolver = new DisambigResolver(
                txt, matchList, fullMatchList, scopeList, resolver, dist);

            /*
             *   Run the alternatives through the disambiguation response
             *   ranking process.  
             */
            local rankings =
                DisambigRanking.sortByRanking(prodList, disResolver);

            /*
             *   If the best item has unknown words, try letting the user
             *   correct typos with OOPS.  
             */
            if (rankings[1].nonMatchCount != 0
                && rankings[1].unknownWordCount != 0)
            {
                try
                {
                    /* 
                     *   complain about the unknown word and look for an
                     *   OOPS reply 
                     */
                    tryOops(toks, issuingActor_, targetActor_,
                            1, toks, rmcDisambig);
                }
                catch (RetryCommandTokensException exc)
                {
                    /* get the new token list */
                    toks = exc.newTokens_;
                    
                    /* replace the string as well */
                    str = cmdTokenizer.buildOrigText(toks);

                    /* go back for another try at parsing the response */
                    goto retryParse;
                }
            }

            /*
             *   If the best item we could find has no matches, check to
             *   see if it has miscellaneous noun phrases - if so, it's
             *   probably just a new command, since it doesn't have
             *   anything we recognize as a noun phrase. 
             */
            if (rankings[1].nonMatchCount != 0
                && rankings[1].miscWordListCount != 0)
            {
                /* 
                 *   it's probably not an answer to our disambiguation
                 *   question, so treat it as a whole new command -
                 *   abandon the current command and start over with the
                 *   new string 
                 */
                throw new ReplacementCommandStringException(str, nil, nil);
            }

            /* if we're in debug mode, show the winning intepretation */
            dbgShowGrammarWithCaption('Disambig Winner', rankings[1].match);

            /* get the response list */
            local respList = rankings[1].match.getResponseList();

            /* 
             *   Select the objects for each response in our winning list.
             *   The user can select more than one of the objects we
             *   offered, so simply take each one they specify here
             *   separately.  
             */
            foreach (local resp in respList)
            {
                try
                {
                    try
                    {
                        /* 
                         *   select the objects for this match, and add
                         *   them into the matches so far 
                         */
                        local newObjs = resp.resolveNouns(
                            disResolver, disambigResults);

                        /* set the new token list in the new matches */
                        for (local n in newObjs)
                        {
                            if (n.np_ != nil)
                                n.np_.setOrigTokenList(npToks);
                        }

                        /* add them to the matches so far */
                        resultList += newObjs;
                    }
                    catch (UnmatchedDisambigException udExc)
                    {
                        /*
                         *   The response didn't match anything in the
                         *   narrowed list.  Try again with the full scope
                         *   list, in case they actually wanted to apply
                         *   the command to something that's in scope but
                         *   which didn't make the cut for the narrowed
                         *   list we originally offered. 
                         */
                        resultList +=
                            resp.resolveNouns(scopeDisResolver,
                                              disambigResults);
                    }
                }
                catch (StillAmbiguousException saExc)
                {
                    /* 
                     *   Get the new "reduced" list from the exception.
                     *   Use our original full list, and reduce it to
                     *   include only the elements in the reduced list -
                     *   this will ensure that the items in the new list
                     *   are in the same order as they were in the
                     *   original list, which keeps the next iteration's
                     *   question in the same order.  
                     */
                    local newList = new Vector(saExc.matchList_.length());
                    foreach (local cur in fullMatchList)
                    {
                        /* 
                         *   If this item from the original list has an
                         *   equivalent in the reduced list, include it in
                         *   the new match list.  
                         */
                        if (cur.isDistEquivInList(saExc.matchList_, dist))
                            newList.append(cur);
                    }

                    /* convert it to a list */
                    newList = newList.toList();

                    /*
                     *   If that left us with nothing, just keep the
                     *   original list unchanged.  This can occasionally
                     *   happen, such as when a sub-phrase (a locational
                     *   qualifier, for example) is itself ambiguous. 
                     */
                    if (newList == [])
                        newList = matchList;

                    /*
                     *   Generate the new "full" list.  This is a list of
                     *   all of the items from our current "full" list
                     *   that either are directly in the new match list,
                     *   or are indistinguishable from items in the new
                     *   reduced list.
                     *   
                     *   The exception thrower is not capable of providing
                     *   us with a new full list, because it only knows
                     *   about the reduced list, as the reduced list is
                     *   its scope for narrowing the results.  
                     */
                    local newFullList = new Vector(fullMatchList.length());
                    foreach (local cur in fullMatchList)
                    {
                        /* 
                         *   if this item is in the new reduced list, or
                         *   has an equivalent in the new match list,
                         *   include it in the new full list 
                         */
                        if (cur.isDistEquivInList(newList, dist))
                        {
                            /* 
                             *   we have this item or something
                             *   indistinguishable - include it in the
                             *   revised full match list 
                             */
                            newFullList.append(cur);
                        }
                    }

                    /* convert it to a list */
                    newFullList = newFullList.toList();
                    
                    /* 
                     *   They answered, but with insufficient specificity.
                     *   Add the given list to the still-to-resolve list,
                     *   so that we try again with this new list.  
                     */
                    stillToResolve +=
                        new StillToResolveItem(newList, newFullList,
                                               saExc.origText_);
                }
                catch (DisambigOrdinalOutOfRangeException oorExc)
                {
                    /* 
                     *   Explain the problem (note that if we didn't want
                     *   to offer another chance here, we could simply
                     *   throw this message as a ParseFailureException
                     *   instead of continuing with the query loop).
                     *   
                     *   If we've never asked interactively for input
                     *   (because we've obtained all of our input from
                     *   past responses to a command we're repeating),
                     *   don't show this message, because it makes no
                     *   sense when they haven't answered any questions in
                     *   the first place.  
                     */
                    if (everAsked)
                        targetActor_.getParserMessageObj().
                            disambigOrdinalOutOfRange(
                                targetActor_, oorExc.ord_, txt.htmlify());

                    /* go back to the outer loop to ask for a new response */
                    askingAgain = true;
                    continue queryLoop;
                }
                catch (UnmatchedDisambigException udExc)
                {
                    local newList;
                    
                    /*
                     *   They entered something that looked like a
                     *   disambiguation response, but didn't refer to any
                     *   of our objects.  Try parsing the input as though
                     *   it were a new command, and if it matches any
                     *   command syntax, treat it as a new command.  
                     */
                    newList = firstCommandPhrase.parseTokens(toks, cmdDict);
                    if (newList.length() != 0)
                    {
                        /* 
                         *   it appears syntactically to be a new command
                         *   - treat it as such by throwing a command
                         *   replacement exception 
                         */
                        throw new ReplacementCommandStringException(
                            str, nil, nil);
                    }

                    /* 
                     *   Explain the problem (note that if we didn't want
                     *   to continue with the query loop, we could simply
                     *   throw this message as a ParseFailureException).
                     *   
                     *   Don't show any error if we've never asked
                     *   interactively on this turn (which can only be
                     *   because we're using interactive responses from a
                     *   previous turn that we're repeating), because it
                     *   makes no sense when we haven't apparently asked
                     *   for anything yet.  
                     */
                    if (everAsked)
                        targetActor_.getParserMessageObj().
                            noMatchDisambig(targetActor_, txt.htmlify(),
                                            udExc.resp_);

                    /* go back to the outer loop to ask for a new response */
                    askingAgain = true;
                    continue queryLoop;
                }
            }

            /*
             *   If we got this far, the last input we parsed was actually
             *   a response to our question, as opposed to a new command
             *   or something unintelligible.  
             *   
             *   If this was an interactive response, add the response to
             *   the response keeper's list of past responses.  This will
             *   allow the production to re-use the same list if it has to
             *   re-resolve the phrase in the future, without asking the
             *   user to answer the same questions again 
             */
            if (everAsked)
                keeper.addAmbigResponse(str);

            /* 
             *   if there's nothing left in the still-to-resolve list, we
             *   have nothing left to do, so we can stop working 
             */
            if (stillToResolve.length() == 0)
                break;

            /* 
             *   set up for the next iteration with the first item in the
             *   still-to-resolve list 
             */
            matchList = stillToResolve[1].matchList;
            fullMatchList = stillToResolve[1].fullMatchList;
            txt = stillToResolve[1].origText;

            /* remove the first item, since we're going to process it now */
            stillToResolve = stillToResolve.sublist(2);
        }

        /* success - return the final match list */
        return resultList;
    }

    /*
     *   filter a match list with a specific Distinguisher
     */
    filterWithDistinguisher(lst, dist)
    {
        local result;

        /* create a vector for the result list */
        result = new Vector(lst.length());

        /* scan the list */
        foreach (local cur in lst)
        {
            /*  
             *   if we have no equivalent of the current item (for the
             *   purposes of our Distinguisher) in the result list, add
             *   the current item to the result list 
             */
            if (!cur.isDistEquivInList(result, dist))
                result.append(cur);
        }

        /* return the result list */
        return result.toList();
    }

    /* 
     *   handle a noun phrase that doesn't match any legal grammar rules
     *   for noun phrases 
     */
    unknownNounPhrase(match, resolver)
    {
        local wordList;
        local ret;
        
        /* 
         *   ask the resolver to handle it - if it gives us a resolved
         *   object list, simply return it 
         */
        wordList = match.getOrigTokenList();
        if ((ret = resolver.resolveUnknownNounPhrase(wordList)) != nil)
            return ret;

        /*
         *   The resolver doesn't handle unknown words, so we can't
         *   resolve the phrase.  Look for an undefined word and give the
         *   player a chance to correct typos with OOPS.  
         */
        tryOops(wordList, issuingActor_, targetActor_,
                match.firstTokenIndex, match.tokenList, rmcCommand);

        /*
         *   If we didn't find any unknown words, it means that they used
         *   a word that's in the dictionary in a way that makes no sense
         *   to us.  Simply return an empty list and let the resolver
         *   proceed with its normal handling for unmatched noun phrases.  
         */
        return [];
    }

    getImpliedObject(np, resolver)
    {
        /* ask the resolver to supply an implied default object */
        return resolver.getDefaultObject(np);
    }

    askMissingObject(asker, resolver, responseProd)
    {
        /* if we can't resolve this interactively, fail with an error */
        if (!canResolveInteractively(resolver.getAction()))
        {
            /* interactive resolution isn't allowed - fail */
            throw new ParseFailureException(&missingObject,
                                            resolver.getAction(),
                                            resolver.whichMessageObject);
        }

        /* have the action show objects already defaulted */
        resolver.getAction().announceAllDefaultObjects(nil);

        /* ask for a default object */
        asker.askMissingObject(targetActor_, resolver.getAction(),
                               resolver.whichMessageObject);

        /* try reading an object response */
        return tryAskingForObject(issuingActor_, targetActor_,
                                  resolver, self, responseProd);
    }

    noteLiteral(txt)
    {
        /* 
         *   there's nothing to do with a literal at this point, since
         *   we're not ranking anything 
         */
    }

    askMissingLiteral(action, which)
    {
        local ret;
        
        /* if we can't resolve this interactively, fail with an error */
        if (!canResolveInteractively(action))
        {
            /* interactive resolution isn't allowed - fail */
            throw new ParseFailureException(&missingLiteral, action, which);
        }

        /* ask for the missing literal */
        targetActor_.getParserMessageObj().
            askMissingLiteral(targetActor_, action, which);

        /* read the response */
        ret = readMainCommand(rmcAskLiteral);

        /* re-enable the transcript, if we have one */
        if (gTranscript)
            gTranscript.activate();

        /* return the response */
        return ret;
    }

    emptyNounPhrase(resolver)
    {
        /* abort with an error */
        throw new ParseFailureException(&emptyNounPhrase);
    }

    zeroQuantity(txt)
    {
        /* abort with an error */
        throw new ParseFailureException(&zeroQuantity,
                                        txt.toLower().htmlify());
    }

    insufficientQuantity(txt, matchList, requiredNum)
    {
        /* abort with an error */
        throw new ParseFailureException(
            &insufficientQuantity, txt.toLower().htmlify(),
            matchList, requiredNum);
    }

    uniqueObjectRequired(txt, matchList)
    {
        /* abort with an error */
        throw new ParseFailureException(
            &uniqueObjectRequired, txt.toLower().htmlify(), matchList);
    }

    singleObjectRequired(txt)
    {
        /* abort with an error */
        throw new ParseFailureException(
            &singleObjectRequired, txt.toLower().htmlify());
    }

    noteAdjEnding()
    {
        /* we don't care about adjective-ending noun phrases at this point */
    }

    noteIndefinite()
    {
        /* we don't care about indefinites at this point */
    }

    noteMiscWordList(txt)
    {
        /* we don't care about unstructured noun phrases at this point */
    }

    notePronoun()
    {
        /* we don't care about pronouns right now */
    }

    noteMatches(matchList)
    {
        /* we don't care about the matches just now */
    }

    notePlural()
    {
        /* we don't care about these right now */
    }

    beginSingleObjSlot() { }
    endSingleObjSlot() { }

    beginTopicSlot() { }
    endTopicSlot() { }

    incCommandCount()
    {
        /* we don't care about how many subcommands there are */
    }

    noteActorSpecified()
    {
        /* 
         *   we don't care about this during execution - it only matters
         *   for determining the strength of the command during the
         *   ranking process 
         */
    }

    noteNounSlots(cnt)
    {
        /* 
         *   we don't care about this during execution; it only matters
         *   for the ranking process 
         */
    }

    noteWeakPhrasing(level)
    {
        /* ignore this during execution; it only matters during ranking */
    }

    /* allow remapping the action */
    allowActionRemapping = true

    /* allow making an arbitrary choice among equivalents */
    allowEquivalentFiltering = true
;


/*
 *   List entry for the still-to-resolve list 
 */
class StillToResolveItem: object
    construct(lst, fullList, txt)
    {
        /* remember the equivalent-reduced and full match lists */
        matchList = lst;
        fullMatchList = fullList;

        /* note the text */
        origText = txt;
    }

    /* the reduced (equivalent-eliminated) match list */
    matchList = []

    /* full (equivalent-inclusive) match list */
    fullMatchList = []

    /* the original command text being disambiguated */
    origText = ''
;

/* ------------------------------------------------------------------------ */
/*
 *   Specialized noun-phrase resolution results gatherer for resolving a
 *   command actor (i.e., the target actor of a command).
 */
class ActorResolveResults: BasicResolveResults
    construct()
    {
        /* do the inherited work */
        inherited();

        /* 
         *   set the initial actor context to the PC - this type of
         *   resolver is set up to determine the actor context, so we don't
         *   usually know the actual actor context yet when setting up this
         *   resolver 
         */
        targetActor_ = issuingActor_ = gPlayerChar;
    }
    
    getImpliedObject(np, resolver)
    {
        /* 
         *   there's no default for the actor - it's usually simply a
         *   syntax error when the actor is omitted 
         */
        throw new ParseFailureException(&missingActor);
    }

    uniqueObjectRequired(txt, matchList)
    {
        /* an actor phrase must address a single actor */
        throw new ParseFailureException(&singleActorRequired);
    }

    singleObjectRequired(txt)
    {
        /* an actor phrase must address a single actor */
        throw new ParseFailureException(&singleActorRequired);
    }

    /* don't allow action remapping while resolving the actor */
    allowActionRemapping = nil
;

/*
 *   A results object for resolving an actor in a command with an unknown
 *   word or invalid phrasing in the predicate.  For this type of
 *   resolution, we're trying to interpret the actor portion of the command
 *   as a noun phrase referring to an actor, but it could also just be
 *   another command.  E.g., we could have "bob, asdf" or "east, asdf".
 *   Since we're only tentatively interpreting the phrase as a noun phrase,
 *   to see if that interpretation goes anywhere, we don't want to throw
 *   any errors on failures; instead we simply allow empty match lists.
 */
class TryAsActorResolveResults: ResolveResults
    noVocabMatch(action, txt) { }
    noMatch(action, txt) { }
    noMatchPossessive(action, txt) { }
    noMatchForAll() { }
    noMatchForAllBut() { }
    noMatchForListBut() { }
    noteEmptyBut() { }
    noMatchForPronoun() { }
    allNotAllowed() { }
    reflexiveNotAllowed() { }
    wrongReflexive(typ, txt) { }
    noMatchForPossessive(owner, txt) { }
    noMatchForLocation(loc, txt) { }
    noteBadPrep() { }
    nothingInLocation(loc) { }
    ambiguousNounPhrase(keeper, askwer, txt, matchLst, fullMatchLst,
                        scopeList, requiredNum, resolver) { }
    unknownNounPhrase(match, resolver) { }
    getImpliedObject(np, resolver) { return []; }
    askMissingObject(asker, resolver, responseProd) { return []; }
    noteLiteral(txt) { }
    askMissingLiteral(action, which) { return nil; }
    emptyNounPhrase(resolver) { }
    zeroQuantity(txt) { }
    insufficientQuantity(txt, matchList, requiredNum) { }
    uniqueObjectRequired(txt, matchList) { }
    singleObjectRequired(txt) { }
    noteAdjEnding() { }
    noteIndefinite() { }
    noteMatches(matchList) { }
    noteMiscWord(txt) { }
    notePronoun() { }
    notePlural() { }
    beginSingleObjSlot() { }
    endSingleObjSlot() { }
    beginTopicSlot() { }
    endTopicSlot() { }
    incCommandCount() { }
    noteActorSpecified() { }
    noteNounSlots(cnt) { }
    noteWeakPhrasing() { }
    allowActionRemapping = true
    allowEquivalentFiltering = true
;


/* ------------------------------------------------------------------------ */
/*
 *   Command ranking criterion.  This is used by the CommandRanking class
 *   to represent one criterion for comparing two parse trees.
 *   
 *   Rankings are performed in two passes.  The first pass is the rough,
 *   qualitative pass, meant to determine if one parse tree has big,
 *   obvious differences from another.  In most cases, this means that one
 *   tree has a particular type of problem or special advantage that the
 *   other doesn't have at all.
 *   
 *   The second pass is the fine-grained pass.  We only reach the second
 *   pass if we can't find any coarse differences on the first rough pass.
 *   In most cases, the second pass compares the magnitude of problems or
 *   advantages to determine if one tree is slightly better than the other.
 */
class CommandRankingCriterion: object
    /*
     *   Compare two CommandRanking objects on the basis of this criterion,
     *   for the first, coarse-grained pass.  Returns a positive number if
     *   a is better than b, 0 if they're indistinguishable, or -1 if a is
     *   worse than b.  
     */
    comparePass1(a, b) { return 0; }

    /* compare two rankings for the second, fine-grained pass */
    comparePass2(a, b) { return 0; }
;

/* 
 *   A command ranking criterion that measures a "problem" by a count of
 *   occurrences stored in a property of the CommandRanking object.  For
 *   example, we could count the number of noun phrases that don't resolve
 *   to any objects.
 *   
 *   On the first, coarse-grained pass, we measure only the presence or
 *   absence of our problem.  That is, if one parse tree has zero
 *   occurrences of the problem and the other has a non-zero number of
 *   occurrences of the problem (as measured by our counting property),
 *   then we'll prefer the one with zero occurrences.  If both have no
 *   occurrences, or both have a non-zero number of occurrences, we'll
 *   consider the two equivalent for the first pass, since we only care
 *   about the presence or absence of the problem.
 *   
 *   On the second, fine-grained pass, we measure the actual number of
 *   occurrences of the problem, and choose the parse tree with the lower
 *   number.  
 */
class CommandRankingByProblem: CommandRankingCriterion
    /* 
     *   our ranking property - this is a property of the CommandRanking
     *   object that gives us a count of the number of times our "problem"
     *   has occurred in the ranking object's parse tree 
     */
    prop_ = nil

    /* first pass - compare by presence or absence of the problem */
    comparePass1(a, b)
    {
        local acnt = a.(self.prop_);
        local bcnt = b.(self.prop_);

        /* if b has the problem but a doesn't, a is better */
        if (acnt == 0 && bcnt != 0)
            return 1;

        /* if a has the problem but b doesn't, b is better */
        if (acnt != 0 && bcnt == 0)
            return -1;

        /* we can't tell the difference at this stage */
        return 0;
    }

    /* second pass - compare by number of occurrences of the problem */
    comparePass2(a, b)
    {
        /* 
         *   Return the difference in the problem counts.  We want to
         *   return >0 if a has fewer problems, <0 if b has fewer problems:
         *   so compute (a-b) and negate it, which is the same as computing
         *   (a-b). 
         */
        return b.(self.prop_) - a.(self.prop_);
    }
;

/*
 *   A "weakness" criterion.  This is similar to the rank-by-problem
 *   criterion, but rather than ranking on an actual structural problem, it
 *   ranks on a structural weakness.  This is suitable for things like
 *   adjective endings and truncations, where the weakness isn't on the
 *   same order as a "problem" but where we'd still rather avoid the
 *   weakness if we can.
 *   
 *   The point of the separate "weakness" criterion is that we only allow
 *   weaknesses to come into play on pass 2, after we've already
 *   discriminated based on problems.  If we can discriminate based on
 *   problems, we'll do so in pass 1 and won't even get to pass 2; we'll
 *   only discriminate based on weakness if we can't tell the difference
 *   based on real problems.  
 */
class CommandRankingByWeakness: CommandRankingCriterion
    /* on pass 1, ignore weaknesses */
    comparePass1(a, b) { return 0; }

    /* on pass 2, compare based on weaknesses */
    comparePass2(a, b) { return b.(self.prop_) - a.(self.prop_); }

    /* our command-ranking property */
    prop_ = nil
;

/* 
 *   command-ranking-by-problem and by-weakness objects for the pre-defined
 *   ranking criteria 
 */
rankByVocabNonMatch: CommandRankingByProblem prop_ = &vocabNonMatchCount;
rankByNonMatch: CommandRankingByProblem prop_ = &nonMatchCount;
rankByInsufficient: CommandRankingByProblem prop_ = &insufficientCount;
rankByListForSingle: CommandRankingByProblem prop_ = &listForSingle;
rankByEmptyBut: CommandRankingByProblem prop_ = &emptyButCount;
rankByAllExcluded: CommandRankingByProblem prop_ = &allExcludedCount;
rankByActorSpecified: CommandRankingByProblem prop_ = &actorSpecifiedCount;
rankByMiscWordList: CommandRankingByProblem prop_ = &miscWordListCount;
rankByPluralTrunc: CommandRankingByWeakness prop_ = &pluralTruncCount;
rankByEndAdj: CommandRankingByWeakness prop_ = &endAdjCount;
rankByIndefinite: CommandRankingByProblem prop_ = &indefiniteCount;
rankByTrunc: CommandRankingByWeakness prop_ = &truncCount;
rankByMissing: CommandRankingByProblem prop_ = &missingCount;
rankByPronoun: CommandRankingByWeakness prop_ = &pronounCount;
rankByWeakness: CommandRankingByWeakness prop_ = &weaknessLevel;
rankByUnwantedPlural: CommandRankingByProblem prop_ = &unwantedPluralCount;

/*
 *   Rank by unmatched possessive-qualified phrases.  If we have two
 *   unknown phrases, one with a possessive qualifier and one without, and
 *   other things being equal, prefer the one with the possessive
 *   qualifier.
 *   
 *   We prefer the qualified version because it lets us report a smaller
 *   phrase that we can't match.  For example, in X BOB'S WALLET, if we
 *   can't match WALLET all by itself, it's more useful to report that "you
 *   see no wallet" than to report that you see no "bob's wallet", because
 *   the latter incorrectly implies that there might still be a wallet in
 *   scope as long as it's not Bob's we're looking for.  
 */
rankByNonMatchPoss: CommandRankingCriterion
    /* 
     *   ignore on pass 1 - this only counts if other factors are equal, so
     *   we want to consider all of the other factors on pass 1 before
     *   taking this criterion into account 
     */
    comparePass1(a, b) { return 0; }

    /* pass 2 - more possessives are better */
    comparePass2(a, b)
    {
        /* 
         *   if we don't have the same underlying non-match count,
         *   possessive qualification is irrelevant 
         */
        if (a.nonMatchCount != b.nonMatchCount)
            return 0;

        /* more possessives are better */
        return a.nonMatchPossCount - b.nonMatchPossCount;
    }
;        

/*
 *   Command ranking by literal phrase length.  We prefer interpretations
 *   that treat less text as uninterpreted literal text.  By "less text,"
 *   we simply mean that one has a shorter string treated as literal text
 *   than the other.  (We prefer shorter literals because when the parser
 *   matches a string of literal text, it's essentially throwing up its
 *   hands and admitting it can't parse the text; so the less text is
 *   contained in literals, the more text the parser is actually parsing,
 *   and more parsed is better.)
 */
rankByLiteralLength: CommandRankingCriterion
    /* first pass */
    comparePass1(a, b)
    {
        /* 
         *   Compare our lengths.  We want to return >0 if a is shorter and
         *   <0 if a is longer (and, of course, 0 if they're the same
         *   length).  So, we can just compute (a-b) and negate the result,
         *   which is the same as computing (b-a).
         *   
         *   The CommandRanking objects keep track of the length of text in
         *   literals in their literalLength properties.  
         */
        return b.literalLength - a.literalLength;
    }

    /* 
     *   Second pass - we use our full powers of discrimination on the
     *   first pass, so if we make it to the second pass, we couldn't tell
     *   a difference on the first pass and thus can't tell a difference
     *   now.  So, just inherit the default implementation, which simply
     *   returns 0 to indicate that there's no difference.  
     */
;

/*
 *   Command ranking by subcommand count: we prefer the match with fewer
 *   subcommands.  If one has fewer subcommands than the other, it means
 *   that we were able to interpret ambiguous conjunctions (such as "and")
 *   as noun phrase conjunctions rather than as command conjunctions; other
 *   things being equal, we'd rather take the interpretation that gives us
 *   noun phrases than the one that involves more separate commands.  
 */
rankBySubcommands: CommandRankingCriterion
    /* first pass - compare subcommand counts */
    comparePass1(a, b)
    {
        /* 
         *   if a has fewer subcommands, return <0, and if b has fewer
         *   subcommands, return >0: so we can just return the negative of
         *   (a-b), or (b-a) 
         */
        return b.commandCount - a.commandCount;
    }

    /* second pass - do nothing, as we do all of our work on the first pass */
;

/*
 *   Rank by token count.  Other things being equal, we'd rather pick a
 *   longer match.  If one match is shorter than the other in terms of the
 *   number of tokens it encompasses, then it means that the shorter match
 *   left more tokens at the end of the command to be interpreted as
 *   separate commands.  If we have an interpretation that can take more of
 *   those tokens and parse them as part of the current command, that
 *   interpretation is probably better. 
 */
rankByTokenCount: CommandRankingCriterion
    /* first pass - compare token counts */
    comparePass1(a, b)
    {
        /* choose the one that matched more tokens */
        return a.tokCount - b.tokCount;
    }

    /* first pass - we do all our work on the first pass */
;

/*
 *   Rank by "verb structure."  This gives more weight to an
 *   interpretation that has more structural noun phrases in the verb.
 *   For example, "DETACH dobj FROM iobj" is given more weight than
 *   "DETACH dobj", because the former has two structural noun phrases
 *   whereas the latter has only one.  This will make us prefer to treat
 *   DETACH WIRE FROM BOX as a two-object action, for example, even if we
 *   could treat WIRE FROM BOX as a single "locational" noun phrase.  
 */
rankByVerbStructure: CommandRankingCriterion
    comparePass2(a, b)
    {
        /* take the one with more structural noun slots in the verb phrase */
        return a.nounSlotCount - b.nounSlotCount;
    }
;

/* 
 *   Rank by ambiguous noun phrases.  We apply this criterion on the second
 *   pass only, because it's a weak test: we might end up narrowing things
 *   down through automatic "logicalness" tests during the noun resolution
 *   process, so ambiguity at this stage in the parsing process doesn't
 *   necessarily indicate that there's real ambiguity in the command.
 *   However, if we can already tell that one interpretation is unambiguous
 *   and another is ambiguous, and the two interpretations are otherwise
 *   equally good, pick the one that's already unambiguous: the ambiguous
 *   interpretation might or might not stay ambiguous, but the unambiguous
 *   interpretation will definitely stay unambiguous.  
 */
rankByAmbiguity: CommandRankingCriterion
    /* 
     *   Do nothing on the first pass, because we want any first-pass
     *   criterion to prevail over our weak test.  Instead, check for a
     *   difference in ambiguity only on the second pass. 
     */
    comparePass2(a, b)
    {
        /* the one with lower ambiguity is better */
        return b.ambigCount - a.ambigCount;
    }
;


/*
 *   Production match ranking object.  We create one of these objects for
 *   each match tree that we wish to rank.
 *   
 *   This class is generally not instantiated by client code - instead,
 *   clients use the sortByRanking() class method to rank a list of
 *   production matches.  
 */
class CommandRanking: ResolveResults
    /*
     *   Sort a list of productions, as returned from
     *   GrammarProd.parseTokens(), in descending order of command
     *   strength.  We return a list of CommandRanking objects whose first
     *   element is the best command interpretation.
     *   
     *   Note that this can be used as a class-level method.  
     */
    sortByRanking(lst, [resolveArguments])
    {
        /* 
         *   create a vector to hold the ranking information - we
         *   need one ranking item per match 
         */
        local rankings = new Vector(lst.length());
        
        /* get the ranking information for each command */
        foreach(local cur in lst)
        {
            local curRank;
            
            /* create a ranking item for the entry */
            curRank = self.createInstance(cur);
            
            /* rank this entry */
            curRank.calcRanking(resolveArguments);
            
            /* add this to our ranking list */
            rankings.append(curRank);
        }
        
        /* sort the entries by descending ranking, and return the results */
        return rankings.sort(SortDesc, {x, y: x.compareRanking(y)});
    }

    /* create a new entry */
    construct(match)
    {
        /* remember the match object */
        self.match = match;

        /* remember the number of tokens in the match */
        tokCount = match.lastTokenIndex - match.firstTokenIndex + 1;
    }

    /* calculate my ranking */
    calcRanking(resolveArguments)
    {
        /* 
         *   Ask the match tree to resolve nouns, using this ranking
         *   object as the resolution results receiver - when an error or
         *   warning occurs during resolution, we'll merely note the
         *   condition rather than say anything about it.
         *   
         *   Note that 'self' is the results object, because the point of
         *   this resolution pass is to gather statistics into this
         *   results object.  
         */
        match.resolveNouns(resolveArguments..., self);
    }

    /*
     *   Compare two production list entries for ranking purposes.  Returns
     *   a negative number if this one ranks worse than the other, 0 if
     *   they have the same ranking, or a positive number if this one ranks
     *   better than the other one.
     *   
     *   This routine is designed to run entirely off of our
     *   rankingCriteria property.  In most cases, subclasses should be
     *   able to customize the ranking system simply by overriding the
     *   rankingCriteria property to provide a customized list of criteria
     *   objects.  
     */
    compareRanking(other)
    {
        local ret;

        /* 
         *   Run through our ranking criteria and apply the first pass to
         *   each one.  Return the indication of the first criterion that
         *   can tell a difference.  
         */
        foreach (local cur in rankingCriteria)
        {
            /* if the rankings differ in this criterion, return the result */
            if ((ret = cur.comparePass1(self, other)) != 0)
                return ret;
        }

        /*
         *   We couldn't tell any difference on the first pass, so try
         *   again with the finer-grained second pass. 
         */
        foreach (local cur in rankingCriteria)
        {
            /* run the second pass */
            if ((ret = cur.comparePass2(self, other)) != 0)
                return ret;
        }

        /* we couldn't tell any difference between the two */
        return 0;
    }

    /*
     *   Our list of ranking criteria.  This is a list of
     *   CommandRankingCriterion objects.  The list is given in order of
     *   importance: the first criterion is the most important, so if it
     *   can discriminate the two match trees, we use its result; if the
     *   first criterion can't tell any difference, then we move on to the
     *   second criterion; and so on through the list.
     *   
     *   The most important thing is whether or not we have irresolvable
     *   noun phrases (vocabNonMatchCount).  If one of us has a noun phrase
     *   that refers to nothing anywhere in the game, it's not as good as a
     *   phrase that at least matches something somewhere.  
     *   
     *   Next, if one of us has noun phrases that cannot be resolved to
     *   something in scope (nonMatchCount), and the other can successfully
     *   resolve its noun phrases, the one that can resolve the phrases is
     *   preferred.
     *   
     *   Next, check for insufficient numbers of matches to counted phrases
     *   (insufficientCount).
     *   
     *   Next, check for noun lists in single-noun-only slots
     *   (listForSingle).  
     *   
     *   Next, if we have an empty "but" list in one but not the other,
     *   take the one with the non-empty "but" list (emptyButCount).  We
     *   prefer a non-empty "but" list with an empty "all" even to a
     *   non-empty "all" list with an empty "but", because in the latter
     *   case we probably failed to exclude anything because we
     *   misinterpreted the noun phrase to be excluded.  
     *   
     *   Next, if we have an empty "all" or "any" phrase due to "but"
     *   exclusion, take the one that's not empty (allExcludedCount).
     *   
     *   Next, prefer a command that addresses an actor
     *   (actorSpecifiedCount) - if the actor name looks like a command (we
     *   have someone named "Open Bob," maybe?), we'd prefer to interpret
     *   the name appearing as a command prefix as an actor name.
     *   
     *   Next, prefer no unstructured word lists as noun phrases
     *   (miscWordList phrases) (miscWordListCount).  
     *   
     *   Next, prefer interpretations that treat less text as uninterpreted
     *   literal text.  By "less text," we simply mean that one has a
     *   shorter string treated as a literal than the other.
     *   
     *   Prefer no indefinite noun phrases (indefiniteCount).
     *   
     *   Prefer no truncated plurals (pluralTruncCount).
     *   
     *   Prefer no noun phrases ending in adjectives (endAdjCount).
     *   
     *   Prefer no truncated words of any kind (truncCount).
     *   
     *   Prefer fewer pronouns.  If we have an interpretation that matches
     *   a word to explicit vocabulary, take it over matching a word as a
     *   pronoun: if a word is given explicitly as vocabulary for an
     *   object, use it if possible.
     *   
     *   Prefer no missing phrases (missingCount).
     *   
     *   Prefer the one with fewer subcommands - if one has fewer
     *   subcommands than the other, it means that we were able to
     *   interpret ambiguous conjunctions (such as "and") as noun phrase
     *   conjunctions rather than as command conjunctions; since we know by
     *   now that we both either have or don't have unresolved noun
     *   phrases, we'd rather take the interpretation that gives us noun
     *   phrases than the one that involves more separate commands.
     *   
     *   Prefer the tree that matches more tokens.
     *   
     *   Prefer the one with more structural noun phrases in the verb.  For
     *   example, if we have one interpretation that's DETACH (X FROM Y)
     *   (where X FROM Y is a 'locational' phrase that we treat as the
     *   direct object), and one that's DETACH X FROM Y (where X is the
     *   direct object and Y is in the indirect object), prefer the latter,
     *   because it has both direct and indirect object phrases, whereas
     *   the former has only a direct object phrase.  English speakers
     *   almost always try to put prepositions into a structural role in
     *   the verb phrase like this when they could be either in the verb
     *   phrase or part of a noun phrase.
     *   
     *   If all else fails, prefer the one that is initially less
     *   ambiguous.  Ambiguity is a weak test at this point, since we might
     *   end up narrowing things down through automatic "logicalness" tests
     *   later, but it's slightly better to have the match be less
     *   ambiguous now, all other things being equal.  
     */
    rankingCriteria = [rankByVocabNonMatch,
                       rankByNonMatch,
                       rankByNonMatchPoss,
                       rankByInsufficient,
                       rankByListForSingle,
                       rankByEmptyBut,
                       rankByAllExcluded,
                       rankByActorSpecified,
                       rankByUnwantedPlural,
                       rankByMiscWordList,
                       rankByWeakness,
                       rankByLiteralLength,
                       rankByIndefinite,
                       rankByPluralTrunc,
                       rankByEndAdj,
                       rankByTrunc,
                       rankByPronoun,
                       rankByMissing,
                       rankBySubcommands,
                       rankByTokenCount,
                       rankByVerbStructure,
                       rankByAmbiguity]

    /* the match tree I'm ranking */
    match = nil

    /* the number of tokens my match tree consumes */
    tokCount = 0

    /*
     *   Ranking information.  calcRanking() fills in these members, and
     *   compareRanking() uses these to calculate the relative ranking.  
     */

    /* 
     *   The number of structural "noun phrase slots" in the verb.  An
     *   intransitive verb has no noun phrase slots; a transitive verb
     *   with a direct object has one; a verb with a direct and indirect
     *   object has two slots. 
     */
    nounSlotCount = nil

    /* number of noun phrases matching nothing anywhere in the game */
    vocabNonMatchCount = 0

    /* number of noun phrases matching nothing in scope */
    nonMatchCount = 0

    /* 
     *   Number of possessive-qualified noun phrases matching nothing in
     *   scope.  For example, "bob's desk" when there's no desk in scope
     *   (Bob's or otherwise).
     */
    nonMatchPossCount = 0

    /* number of phrases requiring quantity higher than can be fulfilled */
    insufficientCount = 0

    /* number of noun lists in single-noun slots */
    listForSingle = 0

    /* number of empty "but" lists */
    emptyButCount = 0

    /* number of "all" or "any" lists totally excluded by "but" */
    allExcludedCount = 0

    /* missing phrases (structurally omitted, as in "put book") */
    missingCount = 0

    /* number of truncated plurals */
    pluralTruncCount = 0

    /* number of phrases ending in adjectives */
    endAdjCount = 0

    /* number of phrases with indefinite noun phrase structure */
    indefiniteCount = 0

    /* number of miscellaneous word lists as noun phrases */
    miscWordListCount = 0

    /* number of truncated words overall */
    truncCount = 0

    /* number of ambiguous noun phrases */
    ambigCount = 0

    /* number of subcommands in the command */
    commandCount = 0

    /* an actor is specified */
    actorSpecifiedCount = 0

    /* unknown words */
    unknownWordCount = 0

    /* total character length of literal text phrases */
    literalLength = 0

    /* number of pronoun phrases */
    pronounCount = 0

    /* weakness level (for noteWeakPhrasing) */
    weaknessLevel = 0

    /* number of plural phrases encountered in single-object slots */
    unwantedPluralCount = 0

    /* -------------------------------------------------------------------- */
    /*
     *   ResolveResults implementation.  We use this results receiver when
     *   we're comparing the semantic strengths of multiple structural
     *   matches, so we merely note each error condition without showing
     *   any message to the user or asking the user for any input.  Once
     *   we've ranked all of the matches, we'll choose the one with the
     *   best attributes and then resolve it for real, at which point if
     *   we chose one with any errors, we'll finally get around to showing
     *   the errors to the user.  
     */

    noVocabMatch(action, txt)
    {
        /* note the unknown phrase */
        ++vocabNonMatchCount;
    }

    noMatch(action, txt)
    {
        /* note that we have a noun phrase that matches nothing */
        ++nonMatchCount;
    }

    noMatchPossessive(action, txt)
    {
        /* note that we have an unmatched possessive-qualified noun phrase */
        ++nonMatchCount;
        ++nonMatchPossCount;
    }

    allNotAllowed()
    {
        /* treat this as a non-matching noun phrase */
        ++nonMatchCount;
    }

    noMatchForAll()
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    noteEmptyBut()
    {
        /* note it */
        ++emptyButCount;
    }

    noMatchForAllBut()
    {
        /* count the total exclusion */
        ++allExcludedCount;
    }

    noMatchForListBut()
    {
        /* treat this as any other noun phrase that matches nothing */
        ++allExcludedCount;
    }

    noMatchForPronoun(typ, txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    reflexiveNotAllowed(typ, txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    wrongReflexive(typ, txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    noMatchForPossessive(owner, txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    noMatchForLocation(loc, txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    noteBadPrep()
    {
        /* don't do anything at this point */
    }

    nothingInLocation(txt)
    {
        /* treat this as any other noun phrase that matches nothing */
        ++nonMatchCount;
    }

    ambiguousNounPhrase(keeper, asker, txt,
                        matchList, fullMatchList, scopeList,
                        requiredNum, resolver)
    {
        local lst;
        
        /* note the ambiguity */
        ++ambigCount;

        /* 
         *   There's no need to disambiguate the list at this stage, since
         *   we're only testing the strength of the structure.
         *   
         *   As a tentative approximation of the results, return a list
         *   consisting of the required number only, but stash away the
         *   remainder of the full list as a property of the first element
         *   of the return list so we can find the full list again later.  
         */
        lst = matchList.sublist(1, requiredNum);
        if (matchList.length() > requiredNum && lst.length() >= 1)
            lst[1].extraObjects = matchList.sublist(requiredNum + 1);

        /* return the abbreviated list */
        return lst;
    }

    unknownNounPhrase(match, resolver)
    {
        local wordList;
        local ret;
        
        /* 
         *   if the resolver can handle this set of unknown words, treat
         *   it as a good noun phrase; otherwise, treat it as an unmatched
         *   noun phrase 
         */
        wordList = match.getOrigTokenList();
        if ((ret = resolver.resolveUnknownNounPhrase(wordList)) == nil)
        {
            /* count the unmatchable phrase */
            ++nonMatchCount;

            /* count the unknown word */
            ++unknownWordCount;

            /* 
             *   since this is only a ranking pass, resolve to an empty
             *   list for now 
             */
            ret = [];
        }

        /* return the results */
        return ret;
    }

    getImpliedObject(np, resolver)
    {
        /* count the missing object phrase */
        ++missingCount;
        return nil;
    }

    askMissingObject(asker, resolver, responseProd)
    {
        /* 
         *   no need to do anything here - we'll count the missing object
         *   in getImpliedObject, and we don't want to ask for anything
         *   interactively at this point 
         */
        return nil;
    }

    noteLiteral(txt)
    {
        /* add the length of this literal to the total literal length */
        literalLength += txt.length();
    }

    emptyNounPhrase(resolver)
    {
        /* treat this as a non-matching noun phrase */
        ++nonMatchCount;
        return [];
    }

    zeroQuantity(txt)
    {
        /* treat this as a non-matching noun phrase */
        ++nonMatchCount;
    }

    insufficientQuantity(txt, matchList, requiredNum)
    {
        /* treat this as a non-matching noun phrase */
        ++insufficientCount;
    }

    singleObjectRequired(txt)
    {
        /* treat this as a non-matching noun phrase */
        ++listForSingle;
    }

    uniqueObjectRequired(txt, matchList)
    {
        /* 
         *   ignore this for now - we might get a unique object via
         *   disambiguation during the execution phase 
         */
    }

    noteAdjEnding()
    {
        /* count it */
        ++endAdjCount;
    }

    noteIndefinite()
    {
        /* count it */
        ++indefiniteCount;
    }

    noteMiscWordList(txt)
    {
        /* note the presence of an unstructured noun phrase */
        ++miscWordListCount;

        /* count this as a literal as well */
        noteLiteral(txt);
    }

    notePronoun()
    {
        /* note the presence of a pronoun */
        ++pronounCount;
    }

    noteMatches(matchList)
    {
        /* 
         *   Run through the match list and note each weak flag.  Note
         *   that each element of the match list is a ResolveInfo
         *   instance. 
         */
        foreach (local cur in matchList)
        {
            /* if this object was matched with a truncated word, note it */
            if ((cur.flags_ & VocabTruncated) != 0)
                ++truncCount;

            /* if this object was matched with a truncated plural, note it */
            if ((cur.flags_ & PluralTruncated) != 0)
                ++pluralTruncCount;
        }
    }

    beginSingleObjSlot() { ++inSingleObjSlot; }
    endSingleObjSlot() { --inSingleObjSlot; }
    inSingleObjSlot = 0

    beginTopicSlot() { ++inTopicSlot; }
    endTopicSlot() { --inTopicSlot; }
    inTopicSlot = 0

    notePlural()
    {
        /* 
         *   If we're resolving a single-object slot, we want to avoid
         *   plurals, since they could resolve to multiple objects as
         *   though we'd typed a list of objects here.  This isn't a
         *   problem for topics, though, since a topic slot isn't iterated
         *   for execution.  
         */
        if (inSingleObjSlot && !inTopicSlot)
            ++unwantedPluralCount;
    }

    incCommandCount()
    {
        /* increase our subcommand counter */
        ++commandCount;
    }

    noteActorSpecified()
    {
        /* note it */
        ++actorSpecifiedCount;
    }

    noteNounSlots(cnt)
    {
        /* 
         *   If this is the first noun slot count we've received, remember
         *   it.  If we already have a count, ignore the new one - we only
         *   want to consider the first verb phrase if there are multiple
         *   verb phrases, since we'll reconsider the next verb phrase when
         *   we're ready to execute it. 
         */
        if (nounSlotCount == nil)
            nounSlotCount = cnt;
    }

    noteWeakPhrasing(level)
    {
        /* note the weak phrasing level */
        weaknessLevel = level;
    }

    /* don't allow action remapping while ranking */
    allowActionRemapping = nil
;

/*
 *   Another preliminary results gatherer that does everything the way the
 *   CommandRanking results object does, except that we perform
 *   interactive resolution of unknown words via OOPS.  
 */
class OopsResults: CommandRanking
    construct(issuingActor, targetActor)
    {
        /* remember the actors */
        issuingActor_ = issuingActor;
        targetActor_ = targetActor;
    }

    /*
     *   handle a phrase with unknown words 
     */
    unknownNounPhrase(match, resolver)
    {
        
        /* 
         *   if the resolver can handle this set of unknown words, treat
         *   it as a good noun phrase
         */
        local wordList = match.getOrigTokenList();
        local ret = resolver.resolveUnknownNounPhrase(wordList);
        if (ret != nil)
            return ret;
        
        /* 
         *   we still can't resolve it; try prompting for a correction of
         *   any misspelled words in the phrase 
         */
        tryOops(wordList, issuingActor_, targetActor_,
                match.firstTokenIndex, match.tokenList, rmcCommand);

        /* 
         *   if we got this far, we still haven't resolved it; resolve to
         *   an empty phrase 
         */
        return [];
    }

    /* the command's issuing actor */
    issuingActor_ = nil

    /* the command's target actor */
    targetActor_ = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Exception list resolver.  We use this type of resolution for noun
 *   phrases in the "but" list of an "all but" construct.
 *   
 *   We scope the "all but" list to the objects in the "all" list, since
 *   there's no point in excluding objects that aren't in the "all" list.
 *   In addition, if a phrase in the exclusion list matches more than one
 *   object in the "all" list, we consider it a match to all of those
 *   objects, even if it's a definite phrase - this means that items in
 *   the "but" list are never ambiguous.  
 */
class ExceptResolver: ProxyResolver
    construct(mainList, mainListText, resolver)
    {
        /* invoke the base class constructor */
        inherited(resolver);
        
        /* remember the main list, from which we're excluding items */
        self.mainList = mainList;
        self.mainListText = mainListText;
    }

    /* we're a sub-phrase resolver */
    isSubResolver = true

    /* 
     *   match an object's name - we'll use the disambiguation name
     *   resolver, so that they can give us partial names just like in
     *   answer to a disambiguation question
     */
    matchName(obj, origTokens, adjustedTokens)
    {
        return obj.matchNameDisambig(origTokens, adjustedTokens);
    }

    /*
     *   Resolve qualifiers in the enclosing main scope, since qualifier
     *   phrases are not part of the narrowed list - qualifiers apply to
     *   the main phrase from which we're excluding, not to the exclusion
     *   list itself.  
     */
    getQualifierResolver() { return origResolver; }

    /* 
     *   determine if an object is in scope - it's in scope if it's in the
     *   original main list 
     */
    objInScope(obj)
    {
        return mainList.indexWhich({x: x.obj_ == obj}) != nil;
    }

    /* for 'all', simply return the whole original list */
    getAll(np)
    {
        return mainList;
    }

    /* filter ambiguous equivalents */
    filterAmbiguousEquivalents(lst, np)
    {
        /* 
         *   keep all of the equivalent items in an exception list,
         *   because we want to exclude all of the equivalent items from
         *   the main list 
         */
        return lst;
    }

    /* filter an ambiguous noun list */
    filterAmbiguousNounPhrase(lst, requiredNum, np)
    {
        /*
         *   noun phrases in an exception list are never ambiguous,
         *   because they implicitly refer to everything they match -
         *   simply return the full matching list 
         */
        return lst;
    }

    /* filter a plural noun list */
    filterPluralPhrase(lst, np)
    {
        /* return all of the original plural matches */
        return lst;
    }

    /* the main list from which we're excluding things */
    mainList = nil

    /* the original text for the main list */
    mainListText = ''

    /* the original underlying resolver */
    origResolver = nil
;

/*
 *   Except list results object 
 */
class ExceptResults: object
    construct(results)
    {
        /* remember the original results object */
        origResults = results;
    }

    /* 
     *   ignore failed matches in the exception list - if they try to
     *   exclude something that's not in the original list, the object is
     *   excluded to begin with 
     */
    noMatch(action, txt) { }
    noMatchPoss(action, txt) { }
    noVocabMatch(action, txt) { }

    /* ignore failed matches for possessives in the exception list */
    noMatchForPossessive(owner, txt) { }

    /* ignore failed matches for location in the exception list */
    noMatchForLocation(loc, txt) { }

    /* ignore failed matches for location in the exception list */
    nothingInLocation(loc) { }

    /* 
     *   in case of ambiguity, simply keep everything and treat it as
     *   unambiguous - if they say "take coin except copper", we simply
     *   want to treat "copper" as unambiguously excluding every copper
     *   coin in the original list 
     */
    ambiguousNounPhrase(keeper, asker, txt,
                        matchList, fullMatchList, scopeList,
                        requiredNum, resolver)
    {
        /* return the full match list - exclude everything that matches */
        return fullMatchList;
    }

    /* proxy anything we don't override to the underlying results object */
    propNotDefined(prop, [args])
    {
        return origResults.(prop)(args...);
    }

    /* my original underlying results object */
    origResults = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Base class for parser exceptions
 */
class ParserException: Exception
;

/*
 *   Terminate Command exception - when the parser encounters an error
 *   that makes it impossible to go any further processing a command, we
 *   throw this error to abandon the current command and proceed to the
 *   next.  This indicates a syntax error or semantic resolution error
 *   that renders the command meaningless or makes it impossible to
 *   proceed.
 *   
 *   When this exception is thrown, all processing of the current command
 *   termintes immediately.  No further action processing is performed; we
 *   don't continue iterating the command on any additional objects for a
 *   multi-object command; and we discard any remaining commands on the
 *   same command line.  
 */
class TerminateCommandException: ParserException
;

/*
 *   Cancel Command Line exception.  This is used to cancel any *remaining*
 *   commands on a command line after finishing execution of one command on
 *   the line.  For example, if the player types "TAKE BOX AND GO NORTH",
 *   the handler for TAKE BOX can throw this exception to cancel everything
 *   later on the command line (in this case, the GO NORTH part).
 *   
 *   This is handled almost identically to TerminateCommandException.  The
 *   only difference is that some games might want to alert the player with
 *   an explanation that extra commands are being ignored.
 */
class CancelCommandLineException: TerminateCommandException
;
    

/*
 *   Parsing failure exception.  This exception is parameterized with
 *   message information describing the failure, and can be used to route
 *   the failure notification to the issuing actor.  
 */
class ParseFailureException: ParserException
    construct(messageProp, [args])
    {
        /* remember the message property and the parameters */
        message_ = messageProp;
        args_ = args;
    }

    /* notify the issuing actor of the problem */
    notifyActor(targetActor, issuingActor)
    {
        /* 
         *   Tell the target actor to notify the issuing actor.  We route
         *   the notification from the target to the issuer in keeping
         *   with conversation we're modelling: the issuer asked the
         *   target to do something, so the target is now replying with
         *   information explaining why the target can't do as asked. 
         */
        targetActor.notifyParseFailure(issuingActor, message_, args_);
    }

    displayException() { "Parse failure exception"; }

    /* the message property ID */
    message_ = nil

    /* the (varargs) parameters to the message */
    args_ = nil
;

/*
 *   Exception: Retry a command with new tokens.  In some cases, the
 *   parser processes a command by replacing the command with a new one
 *   and processing the new one instead of the original.  When this
 *   happens, the parser will throw this exception, filling in newTokens_
 *   with the replacement token list.
 *   
 *   Note that this is meant to replace the current command only - this
 *   exception effectively *edits* the current command.  Any pending
 *   tokens after the current command should be retained when this
 *   exception is thrown.  
 */
class RetryCommandTokensException: ParserException
    construct(lst)
    {
        /* remember the new token list */
        newTokens_ = lst;
    }

    /* 
     *   The replacement token list.  Note that this is in the same format
     *   as the token list returned from the tokenizer, so this is a list
     *   consisting of two sublists - one for the token strings, the other
     *   for the corresponding token types.  
     */
    newTokens_ = []
;

/*
 *   Replacement command string exception.  Abort any current command
 *   line, and start over with a brand new input string.  Note that any
 *   pending, unparsed tokens on the previous command line should be
 *   discarded.  
 */
class ReplacementCommandStringException: ParserException
    construct(str, issuer, target)
    {
        /* remember the new command string */
        newCommand_ = str;

        /* 
         *   note the issuing actor; if the caller specified this as nil,
         *   use the current player character as the default 
         */
        issuingActor_ = (issuer == nil ? libGlobal.playerChar : issuer);

        /* note the default target actor, defaulting to the player */
        targetActor_ = (target == nil ? libGlobal.playerChar : target);
    }
    
    /* the new command string */
    newCommand_ = ''

    /* the actor issuing the command */
    issuingActor_ = nil

    /* the default target actor of the command */
    targetActor_ = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Parser debugging helpers 
 */

#ifdef PARSER_DEBUG
/*
 *   Show a list of match trees 
 */
showGrammarList(matchList)
{
    /* show the list only if we're in debug mode */
    if (libGlobal.parserDebugMode)
    {
        /* show each match tree in the list */
        foreach (local cur in matchList)
        {
            "\n----------\n";
            showGrammar(cur, 0);
        }
    }
}

/*
 *   Show a winning match tree 
 */
showGrammarWithCaption(headline, match)
{
    /* show the list only if we're in debug mode */
    if (libGlobal.parserDebugMode)
    {
        "\n----- <<headline>> -----\n";
        showGrammar(match, 0);
    }
}

/*
 *   Show a grammar tree 
 */
showGrammar(prod, indent)
{
    local info;

    /* if we're not in parser debug mode, do nothing */
    if (!libGlobal.parserDebugMode)
        return;
    
    /* indent to the requested level */
    for (local i = 0 ; i < indent ; ++i)
        "\ \ ";

    /* check for non-production objects */
    if (prod == nil)
    {
        /* this tree element isn't used - skip it */
        return;
    }
    else if (dataType(prod) == TypeSString)
    {
        /* show the item literally, and we're done */
        "'<<prod>>'\n";
        return;
    }

    /* get the information for this item */
    info = prod.grammarInfo();

    /* if it's nil, there's nothing more to do */
    if (info == nil)
    {
        "<no information>\n";
        return;
    }

    /* show the name */
    "<<info[1]>> [<<prod.getOrigText()>>]\n";

    /* show the subproductions */
    for (local i = 2 ; i <= info.length ; ++i)
        showGrammar(info[i], indent + 1);
}
#endif /* PARSER_DEBUG */

