# relations.t | documentation

    #charset "us-ascii"
    #include "advlite.h"

    /*
     *   RELATIONS EXTENSION by Eric Eve July 2014
     *
     *   The relations.t extension allows Inform7-style relations to be
     *   defined in a TADS 3 game.
     */

    /*
     *   The Relation class is used to define any kind of binary relation you like
     *   between objects, or between items of any other kind, e.g. x love y, or a is
     *   the father of b, or c knows d.
     *
     *   REQUIRES THE RELATIONS EXTENSION.
     */
    class Relation: PreinitObject
        /*
         *   A string name that can be used to refer to this relation, e.g. 'loves'
         *   or 'is the parent of' [RELATIONS EXTENSION]
         */
        name = nil

        /*
         *   A string name that can be used to refer to this relation in reverse,
         *   e.g. 'loved by' or 'is a child of' [RELATIONS EXTENSION]
         */
        reverseName = nil

        /*
         *   The type of relation we are; this can be one of oneToOne, oneToMany,
         *   manyToOne or manyToMany. [RELATIONS EXTENSION]
         */
        relationType = oneToOne

        /*
         *   Flag: are we a reciprocal relation (i.e. does x relation b imply b
         *   relation x)? Note that only oneToOne and manyToMany relations can be
         *   reciprocal. [RELATIONS EXTENSION]
         */
        reciprocal = nil

        /*
         *   A LookupTable to hold data about the items related via this relation.
         *   This is maintained by the library code and shouldn't normally be
         *   directly accessed via game code. [RELATIONS EXTENSION]
         */
        relTab = nil

        /*   Return a list of items related to a via this relation. [RELATIONS EXTENSION] */
        relatedTo(a)
        {
            return relTab == nil ? [] : relTab[a];
        }

        /*   Test whether a is related to b via this relation. [RELATIONS EXTENSION] */
        isRelated(a, b)
        {
            return relatedTo(a).indexOf(b) != nil;
        }

        /*
         *   Return a list of items inverselty related to a via this relation (e.g.
         *   if this is loving relation, return a list of the people a is loved by.
    	 *   [RELATIONS EXTENSION]
         */
        inverselyRelatedTo(a)
        {
            /*
             *   If we're a reciprocal relationship, then asking whether we're
             *   inversely related to a is the same as asking whether we're related
             *   to a, which is a far quicker calculation.
             */
            if(reciprocal)
                return relatedTo(a);

            /*
             *   Otherwise we need to iterate over our LookUpTable to find key
             *   values that correspond to values of a.
             */

            local lst = relTab ? relTab.keysToList() : [];

            /* Set up a Vector as a temporary store for our results. */
            local vec = new Vector;

            /*
             *   Go through each key in the relTab LookupTable to see if a occurs in
             *   its corresponding value (which should be a list). If so append cur
             *   to our vector.
             */
            foreach(local cur in lst)
            {
                if(valToList(relTab[cur]).indexOf(a))
                    vec.append(cur);
            }

            /* Convert the vector to a list and return the result. */
            return vec.toList();
        }

        /* Test whether a is inversely related to b via this relation. [RELATIONS EXTENSION] */
        isInverselyRelated(a, b)
        {
            /*
             *   Simply turn the question round; asking if a is loved by b is
             *   exactly the same as asking if b loves a.
             */
            return isRelated(b, a);
        }

        /*
         *   Make two objects related via this relation. The objs should be supplied
         *   as a two-element list (e.g. [a, b]) such that a will be related to b.
    	 *   [RELATIONS EXTENSION]
         */
        addRelation(objs)
        {

            /*
             *   If we do not already have a LookupTable associated with our relTab
             *   property, perform some sanity checks and then create one if all is
             *   okay.
             */
            if(relTab == nil)
            {

                /*
                 *   Check whether the setting of the reciprocal property on this
                 *   relation conflicts with the setting of the relationType
                 *   property. Neither a oneToMany nor a manyToOne relation can be
                 *   reciprocal (since oneToMany and manyToOne both imply an
                 *   asymmetry), so issue a warning if there's a conflict.
                 */
                if(reciprocal && relationType is in (oneToMany, manyToOne))
                {
                    "<b>ERROR!</b> The <<name>> relation cannot be both <<if
                      relationType == oneToMany>>one-to-many<<else>>many to
                    one<<end>> and reciprocal. ";

                    return;
                }

                /*
                 *   If we do not yet have a LookupTable attached to our relTab,
                 *   create it now.
                 */
                relTab = new LookupTable;
                relTab.setDefaultValue([]);
            }

            /*
             *   The obs parameter should have been supplied as a two element list,
             *   [a, b]. When we add this to our relTab LookupTable the virst item
             *   in the list will be a key in the table and the second will be a
             *   value.
             */
            local key = objs[1];
            local val = objs[2];

            /*
             *   if val is nil we're saying we don't want key to be related to
             *   anything, so remove it from the table
             */
            if(val == nil)
            {
                /*
                 *   If we're a reciprocal relation then we also need to remove the
                 *   other side of the relation; if a is no longer related to b then
                 *   b is no longer related to a in a reciprocal relationship.
                 */
                if(reciprocal)
                {
                    /*
                     *   relTab[key] should evaluate a list of the items to which
                     *   key is related. For each of these item remove key from the
                     *   list of items to which they in turn are related.
                     */
                    foreach(local cur in relTab[key])
                        relTab[cur] = relTab[cur] - key;
                }

                /*
                 *   Simce key is no longer related to anything, we can remove it
                 *   from the relTab LookupTable.
                 */
                relTab.removeElement(key);

                /*   Then we're done. */
                return;
            }

            /*
             *   If key is nil we're saying the reverse relation no longer applies
             *   to val, so we need to remove val wherever it appears.
             */
            if(key == nil)
            {
                /*
                 *   If we've a reciprocal relation, val is no longer related to
                 *   anything, so we can remove the val entry from the relTab.
                 */
                if(reciprocal)
                    relTab.removeElement[val];

                /* Get a list of the keys in relTab */
                local lst = relTab.keysToList();

                /*
                 *   Iterate over that list removing val from the list of values
                 *   associated with every key.
                 */
                foreach(local cur in lst)
                {
                    local curVal = relTab[cur];
                    relTab[cur] = curVal - val;
                }

                /* Then we're done. */
                return;
            }

            /*  Note the current value associated with key in relTab. */
            local existing = relTab[key];

            /*  What happens next depends on our relationType */
            switch(relationType)
            {

            case oneToOne:
                /*
                 *   If we're a one-to-one relation, we can simply set the new value
                 *   corresponding to key to [val], either thereby creating a new
                 *   entry in the relTab LookupTable or overwriting the existing
                 *   one. We make [val] a list since this is how the relTab
                 *   LookupTable stores its values.
                 */
                relTab[key] = [val];

                /*
                 *   Ensure that val is not a value for any other key in the table,
                 *   since in a one-to-one relationship each key can be related to
                 *   at most one val, and each val to at most one key.
                 */
                makeUnique(key, val);

                /*
                 *   If we're a reciprocal relationship we need to enter the same
                 *   pair of items in relTab the other way round as well.
                 */
                if(reciprocal)
                {
                    relTab[val] = [key];
                    makeUnique(val, key);
                }
                break;
            case oneToMany:

                /* Deliberate fall-through */
            case manyToMany:
                /*
                 *   In the case of a one-to-many or many-to-many relationships,
                 *   several values can be associated with each key, so we append
                 *   the new val to the list of values already associated with key.
                 */
                relTab[key] = (nilToList(existing)).appendUnique([val]);

                /*
                 *   If we're a reciprocal relationship we must also be a
                 *   many-to-many one, or else we would not have reached this point
                 *   (the block beginning if(relTab == nil) above would have
                 *   prevented it). So, if we're reciprocal, we also add key to the
                 *   list of values associated with val.
                 */
                if(reciprocal)
                    relTab[val] = nilToList(relTab[val]).appendUnique([key]);
                break;

            case manyToOne:
                /*
                 *   For a many-to-one relationship, simply make [val] the new value
                 *   corresponding to key.
                 */
                relTab[key] = [val];

                break;
            }
        }

        /*
         *   Ensure that key is the only entry in relTab with a value of [val].
    	 *   [RELATIONS EXTENSION]
         *
         */
        makeUnique(key, val)
        {
            /* Get a list of all the keys in the relTab LookupTable. */
            local lst = relTab.keysToList();

            /*
             *   Go through all the keys in relTab, deleting all that value a value
             *   of [val] apart from key.
             */
            foreach(local cur in lst)
            {
                if(cur != key && relTab[cur] == [val])
                    relTab.removeElement(cur);

            }
        }

        /*
         *   Remove this relation between the items specified in objs, which should
         *   be supplied as a two-element list [a, b], where a is the item that is
         *   no longer related to b. [RELATIONS EXTENSION]
         */
        removeRelation(objs)
        {
            /*
             *   If relTab hasn't been set up yet we've nothing to do, since no
             *   objects are yet related via this relation.
             */
            if(relTab == nil)
                return;

            /*  Extract the key and val values from our two-element objs list. */
            local key = objs[1];
            local val = objs[2];

            /*
             *   What happens next depends on what kind of relation we are. Note
             *   that if we have a non-nil relTab at all, we'll already have checked
             *   we can only be a reciprocal relation if we're a one-to-one or
             *   many-to-many relation.
             */
            switch(relationType)
            {
            case oneToOne:
            case manyToOne:
                /*
                 *   If the key can only be related to one value, simply remove the
                 *   key from relTab.
                 */
                relTab.removeElement(key);

                /*
                 *   If this relation is reciprocal, remove val from relTab as well,
                 *   since if this relation no longer holds one way round, it can no
                 *   hold the other way round either.
                 */
                if(reciprocal)
                    relTab.removeElement(val);
                break;

            case oneToMany:
            case manyToMany:
                /*
                 *   If the key can be related to many values, remove val from the
                 *   list of values it's related to.
                 */
                relTab[key] = relTab[key] - val;

                /*
                 *   If this relation is reciprocal, remove key from the list of
                 *   values corresponding to val.
                 */
                if(reciprocal)
                    relTab[val] = relTab[val] - key;
                break;
            }

        }

        /* Make relation[b] = c work like relate(b, relation, c) [RELATIONS EXTENSION] */
        operator []=(b, c)
        {
            addRelation([b, c]);
            return self;
        }

        /* make relation[b] work like related(b, relation) [RELATIONS EXTENSION] */
        operator [] (b)
        {
            return relatedTo(b);
        }
    ;

    /*
     *   A DerivedRelation is one that doesn't maintain its own table of what it
     *   related to what, but works out what is related to what on the basis of some
     *   other relation(s) (e.g. a sibling relation might work by testing for common
     *   parents).
     *
     *   REQUIRES THE RELATIONS EXTENSION
     */
    DerivedRelation: Relation

        /*
         *   Instances need to override to provide a method that returns a list of
         *   items related to a via this relationship, on the basis of whatever
         *   criteria are appropriate.
         */
        relatedTo(a) { return []; }

        inverselyRelatedTo(a) { return []; }

        /*
         *   By default we don't permit the direct addition of relationships via
         *   this relation, since this is a relation dependent upon external
         *   conditions.
         */
        addRelation(objs)
        {
            DMsg(cannot add to derived relation, 'ERROR! You cannot explicitly
                relate items via a derived relation (%1). ', name);
        }

        /*
         *   By default we don't permit the direct removal of relationships via
         *   this relation, since this is a relation dependent upon external
         *   conditions.
         */
        removeRelation(objs)
        {
            DMsg(cannot remove from derived relation, 'ERROR! You cannot explicitly
                remove a derived relation (%) between items. ', name);
        }
    ;

    /*
     *   Used internally by the RELATIONS EXTENSION to keep track of which relations
     *   correspond to which (string) names.
     */
    relationTable: PreinitObject
        /*
         *   LookupTable to restore data relating names to relations. Each key is a
         *   string containing a relation name. Each corresponding value is a
         *   two-item list [rel, type] where rel is the name of the corresponding
         *   relation object and type is either normalRelation or reverseRelation.
         */
        nameTab = static new LookupTable

        /*
         *   Go through all the relations in the game and add their names and
         *   reverseNames to our nameTab.
         */
        execute()
        {
            for(local rel = firstObj(Relation); rel != nil; rel = nextObj(rel,
                Relation))
            {
                if(rel.name)
                    nameTab[rel.name] = [rel, normalRelation];
                if(rel.reverseName)
                    nameTab[rel.reverseName] = [rel, reverseRelation];
            }
        }

        /*
         *   Get the relation corresponding to a string version of its name. Return
         *   a two-item list [rel, type] where rel is the name of the corresponding
         *   relation object and type is either normalRelation or reverseRelation.     *

         */
        getRelation(rel)
        {
            if(dataType(rel) == TypeSString)
                return nameTab[rel];
            /*
             *   If the rel argument is not supplied as a string, it's presumably
             *   the name of a relation object.
             */
            return [rel, normalRelation];
        }

        /*
         *   Message to display when there's no relation in our nameTab
         *   corresponding to the name rel.
         */
        sayNoSuchRelation(rel)
        {
            DMsg(no such relation, 'ERROR, there is no such relation as <q>{1}</q>. ',
                 rel);
        }
    ;

    /*
     *   Make a related to b via the rel relation. The rel parameter can be
     *   specified either as an object (in which case its the relevant relation
     *   object) or as a single-quoted string (in which cast it's either the name or
     *   the reverseName of a relation object.
     *
     *   PART OF THE RELATIONS EXTENSION
     */
    relate(a, rel, b)
    {
        /* Get the relation referred to by the rel parameter. */
        local relData = relationTable.getRelation(rel);

        /* Check that we actually found a relation before trying to use it. */
        if(relData == nil)
        {
            relationTable.sayNoSuchRelation(rel);
            return;
        }

        /*
         *   If it's a normal relation, then use the addRelation method of the
         *   relation object to create a relation between a and b.
         */
        if(relData[2] == normalRelation)
            relData[1].addRelation([a, b]);

        /*
         *   If it's a reverse relation (e.g. 'child of' when the relation is
         *   fatherOf) swap the a and b arguments round when calling the relation's
         *   addRelation() method (e.g. to make a the child of b using the fatherOf
         *   relation, we make b the father of a).
         */
        if(relData[2] == reverseRelation)
            relData[1].addRelation([b, a]);

    }

    /*
     *   Remove the rel relation between a and b
     *
     *   PART OF THE RELATIONS EXTENSION
     */
    unrelate(a, rel, b)
    {
        /* Get the relation referred to by the rel parameter. */
        local relData = relationTable.getRelation(rel);

        /* Check that we actually found a relation before trying to use it. */
        if(relData == nil)
        {
            relationTable.sayNoSuchRelation(rel);
            return;
        }

        /*
         *   If it's a normal relation, then use the removeRelation method of the
         *   relation object to remove the relation between a and b.
         */
        if(relData[2] == normalRelation)
            relData[1].removeRelation([a, b]);

        /*
         *   If it's a reverse relation (e.g. 'child of' when the relation is
         *   fatherOf) swap the a and b arguments round when calling the relation's
         *   removeRelation() method (e.g. to make a no longer the child of b using
         *   the fatherOf relation, we make b no longer the father of a).
         */
        if(relData[2] == reverseRelation)
            relData[1].removeRelation([b, a]);

    }

    /*
     *   If two arguments are supplied (e.g. related(a, knows)) returns a list of
     *   items related to a via the rel relation. If three arguments are supplied
     *   (e.g. related(a, knows, b)) then return true if a is related to b via the
     *   knows relation and b otherwise.
     *
     *   PART OF THE RELATIONS EXTENSION
     */
    related(a, rel, b?)
    {
         /* Get the relation referred to by the rel parameter. */
        local relData = relationTable.getRelation(rel);

        /* Check that we actually found a relation before trying to use it. */
        if(relData == nil)
        {
            relationTable.sayNoSuchRelation(rel);
            return nil;
        }

        /*
         *   If b has not been supplied, we've been asked to supply a list of
         *   objects related to a via rel.
         */
        if(b == nil)
        {
            if(relData[2] == normalRelation)
                return relData[1].relatedTo(a);
            else
                return relData[1].inverselyRelatedTo(a);

        }

        /*
         *   Otherwise, if b has been supplied, we're being asked if a is related to
         *   b.
         */
        else
        {
            if(relData[2] == normalRelation)
                return relData[1].isRelated(a, b);
            else
                return relData[1].isInverselyRelated(a, b);
        }
    }

    /*
     *   The relationPathfinder tries to find a path from start to target via the
     *   rel relation. If it finds one it returns the shortest posssible list of
     *   items starting with start and ending with target, in which each item in the
     *   list is related to the next via the rel relation. E.g. if John is the
     *   father of Jo, and Jo is the father of Jim, and Jim is the father of Jeremy,
     *   relationPathfinder.findPath(John, fatherOf, Jeremy) should return a list
     *   like [John, Jo, Jim, Jeremy] (assuming the appropriate definition of the
     *   fatherOf relationship).
     *
     *   PART OF THE RELATIONS EXTENSION
     */
    relationPathfinder: Pathfinder

        /*
         *   Find a path from start to target via the rel relation. The rel
         *   parameter may be supplied as a relation object, a relation string name
         *   or reverseName, or a list of any of these, in which case any of the
         *   relations contained in the list may be used to step from one object to
         *   the next.
         */
        findPath(start, rel, target)
        {

            local vec = new Vector();
            local relation;

            /* Go through every relation supplied in the rel parameter. */
            foreach(local cur in valToList(rel))
            {
                /* If it's an object, add it to our Vector as a normal relation. */
                if(dataType(cur) == TypeObject)
                    vec.append([cur, normalRelation]);

                /* If it's a string, look it up in the relationTable. */
                if(dataType(cur) == TypeSString)
                {
                    relation = relationTable.getRelation(cur);

                    /*
                     *   If we can't find the string in the relationTable, give up
                     *   and return nil, since there's an error in the rel
                     *   parameter. Also issue a warning message.
                     */
                    if(relation == nil)
                    {
                        relationTable.sayNoSuchRelation(rel);
                        return nil;
                    }

                    /* If we found the relation, add it to our vector. */
                    vec.append(relationTable.getRelation(cur));
                }
            }

            /*
             *   Convert our vector to a list and store it in our relationList
             *   property.
             */
            relationList = vec.toList();

            /*
             *   Use the inherited handling to find the path and store it in a local
             *   variable.
             */
            local res = inherited(start, target);

            /*
             *   If the result was nil, simply return nil to indicate that no path
             *   was found. Otherwise, if the rel parameter was passed as a list,
             *   return the resulting path list unchanged. Otherwise (if rel was
             *   passed as a single relation), return a list consisting of the
             *   objects (or other items) on the path only, since the relation
             *   information for each step would be redundant.
             *
             *   Thus, if rel was passes as a list, the return value might resemble,
             *   [[nil, john], ['child of', mark], ['sibling', mary]], whereas if it
             *   was passed as a single relation the return value might resemble
             *   [johh, mark, alan].
             */
            return res == nil ? nil :
            (dataType(rel) == TypeList ? res : res.mapAll({e: e[2]}));
        }

        findDestinations(cur)
        {
            /* Note the object our current path leads to */
            local obj = cur[steps - 1][2];

            /* Find everything related to this object via relation. */
            local lst = [];

            foreach(local rel in relationList)
            {
                local rname;

                if(rel[2] == normalRelation)
                {
                    lst += rel[1].relatedTo(obj);
                    rname = rel[1].name;
                }
                else
                {
                    lst += rel[1].inverselyRelatedTo(obj);
                    rname = rel[1].reverseName;
                }

                /* Find everything related to everything in the list. */
                foreach(local dest in lst)
                {
                    local newPath = new Vector(cur);
                    newPath.append([rname, dest]);
                    pathsFound.append(newPath);
                }
            }

        }

        /*
         *   Property used internally to hold the list of relations we're finding a
         *   route through.
         */
        relationList = nil

    ;

    /*
     *   Find a path from start to target via the rel relation, where rel can be a
     *   single relation or a list of relations, and can be specified either via a
     *   relation object name or a string name (or reverseName). We provide this as
     *   a convenient wrapper for the more verbose method call.
     *
     *   PART OF THE RELATIONS EXTENSION.
     */
    relationPath(start, rel, target)
    {
        return relationPathfinder.findPath(start, rel, target);
    }

    #ifdef __DEBUG
    /*  Debugging commands for RELATIONS EXTENSION */

    /* List relations defined in the game [RELATIONS EXTENSION] */
    DefineSystemAction(ListRelations)
        execAction(c)
        {
            /* Get a list of relations defined in the relation table */
            local lst = relationTable.nameTab.keysToList();

            /* If the list is empty, say so and exit. */
            if(lst.length == 0)
            {
                DMsg(no relations defined, 'No relations are defined in this game.
                    ');
            }

            /* Sort the list in order of name */
            lst = lst.sort(SortAsc, {a, b: a.compareIgnoreCase(b)});

            /* Go through every item in the list */
            foreach(local cur in lst)
            {
                /* Get the value relating to the name cur */
                local val = relationTable.nameTab[cur];

                /*
                 *   Only report it if it's a normalRelation (so that we don't
                 *   duplicate relations in our list by also listing them with their
                 *   reverseName.
                 */
                if(val[2] == normalRelation)
                {
                    /* The relation is the first item in val */
                    local rel = val[1];

                    /* Show the details of this relation. */
                    showRelation(rel);
                }
            }
        }

        /* Show the details of relation rel. */
        showRelation(rel)
        {
            /* First display the programmatic object name */
            "<b><<symTab.ctab[rel]>></b> ";

            /* If it's a DerivedRelation, say so. */
            if(rel.ofKind(DerivedRelation))
                "<i>(DerivedRelation)</i> ";

            /* Then show the relationType (an enum). */
            "<<symTab.ctab[rel.relationType]>>: ";

            /* If rel is a reciprocal relation, say so. */
            if(rel.reciprocal)
                "(reciprocal): ";

            /* Show the (string) name of the relation. */
            "<i>name</i> = '<<rel.name>>' ";

            /* If the relation has a reverseName, show that too. */
            if(rel.reverseName)
                "<i>reverseName</i> = '<<rel.reverseName>>' ";

            /* Move to the next line. */
            "\n";
        }
    ;

    VerbRule(ListRelations)
        ('list'|) 'relations'
        :VerbProduction
        action = ListRelations
        verbPhrase = 'list/listing relations'
    ;

    /* Debugging action to list what a relation currently relates [RELATIONS EXTENSION]*/
    DefineSystemAction(RelationDetails)
        execAction(c)
        {
            /* Note the literal string associated with this command. */
            literal = c.dobj.name;

            /*
             *   Try to get the relation associated with this string value from the
             *   relationTable.
             */
            local relInfo = relationTable.getRelation(literal);

            /*  Set up a local variable to hold the relation object. */
            local rel;

            if(relInfo == nil)
            {
                /*
                 *   If we didn't find anything in the relationTable, it may be the
                 *   tester entered the programmatic name instead of the string name
                 *   of the relation; try looking up the programatic name of the
                 *   relation in the global symbol table.
                 */
                rel = t3GetGlobalSymbols()[literal];

                /*
                 *   If we didn't find anything, or what we found wasn't a Relation,
                 *   say there's no such relation and exit.
                 */
                if(rel == nil || dataType(rel) != TypeObject ||
                   !rel.ofKind(Relation))
                {
                    DMsg(no such relation, 'There is no such relation in the game as
                        {1}. ', literal);
                    return;
                }
            }
            /*
             *   Otherwise, if we found an entry in the relationTable, the relation
             *   we want is the first item in the list returned from that table.
             */
            else
                rel = relInfo[1];

            /*  Show the details of what that relation is. */
            ListRelations.showRelation(rel);

            /*
             *   If it's a DerivedRelation, say so and exit; we can't list what a
             *   DerivedRelation relates.
             */
            if(rel.ofKind(DerivedRelation))
            {
                DMsg(cant list derived relation, '<i>Since {1} is a DerivedRelation,
                    any items it relates cannot be listed.</i> ', valToSym(rel));

                return;
            }

            /*
             *   Get a list of the keys in our relation's reltab table (which may be
             *   nil).
             */
            local lst = rel.relTab == nil ? [] : rel.relTab.keysToList();

            /*  If the list is empty, say so and exit. */
            if(lst.length == 0)
            {
                DMsg(no relations defined, '<i>no relations defined</i> ');
                return;
            }

            /* Sort the list in ascending key order. */
            lst = lst.sort(SortAsc, {a, b:
                           symTab.ctab[a].compareIgnoreCase(symTab.ctab[b])});

            /*
             *   Go through each key in the list. This will be the object (or other
             *   value) that relates to other objects via this relation.
             */
            foreach(local cur in lst)
            {
                /*
                 *   Display the name of the current object and list the items to
                 *   which it relates.
                 */
                "<<symTab.ctab[cur]>>  ->  <<valToSym(rel.relTab[cur])>>\n";

            }

        }

        literal = nil
    ;

    /* [RELATIONS EXTENSION] List relation details */
    VerbRule(RelationDetails)
        ('relation' | 'relations' | 'rel') literalDobj
        : VerbProduction
        action = RelationDetails
        verbPhrase = 'list/listing relation details'
    ;

    #endif

Adv3Lite Library Reference Manual
Generated on 01/03/2024 from adv3Lite version 1.6.2