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