#<p>
#  This file defines a series of classes that can be used associate
#     Unicon language entities with comments and components.
#</p>
#<p>
#  The intent is to provide a basis for generating documentation
#    (e.g. HTML) from Unicon source files.  The classes constructed
#    by the the <b>UniAll::processFile()</b> method embody all the relevant
#    information needed to generate documentation about a Unicon
#    program and/or library file.
#<p>
#<p>
# <b>Author:</b> Steve Wampler (<i>sbw@tapestry.tucson.az.us</i>)
#</p>
#<p>
#  This file is in the <i>public domain</i>.
#</p>

# <p>The UniDoc package supports the <b>UniDoc</b> program.
package UniDoc

import util
import lang
import parser

global fixName

#<p>
#  The UniAll class holds the entire internal form of a Unicon
#    documentation set.
#</p>
class UniAll : Object (files, packages, 
                      curFile, curClass, curPackage, curComments, fileHead,
                      uniFile, resolveFlag, targetDir, saveSrcFlag,
                      importSet, linkSet, badFiles, sourcePath, uniFilez)

    # <p>
    # Set the level of entity resolution.  If <b>flag</b> is <b>&null</b>,
    # then references to entities <i>not</i> contained in files passed
    # to <b>processFile()</b> are not expanded.  If <b>flag</b> is non-null,
    # then the current sourcePath is used to locate these entities.
    # </p>
    method fullyResolve(flag)
        if map(flag[1]) == "n" then flag := &null
        resolveFlag := flag
    end

    # <p>
    # Set the source path to be used when attempting to resolve
    # unknown entities.
    # </p>
    method setSourcePath(p)
        sourcePath := p
    end

    # <p>
    # Decide whether or not to save a copy of source code to link to.
    # </p>
    method setSaveSrc(flag)
        if map(flag[1]) == "n" then flag := &null
        saveSrcFlag := flag
    end

    # <p>
    # Remember the target directory (needed if we're output source
    # code for later linking.)
    # </p>
    method setTargetDir(tDir)
        targetDir := trim(tDir, '/\\')||"/"
    end

    # <p>
    # Produce the target directory.
    # <p>
    method getTargetDir()
        return targetDir
    end

    # <p>
    # Remember <b>fName</b> as a file that has been processed.
    # </p>
    # <i>This is intended for internal use only!</i>
    #
    method addFile(fName)
        if curFile := /files[fName] := UFile(fName) then {
            if \saveSrcFlag then 
                curFile.setSrcFile(uniFile.getFilename())
            return curFile
            }
    end

    # <p>
    # Insert a tag for an entity into the saved source code, if any.
    # </p>
    method addTag(obj)
        if uniFile.hasSource() then {
            if \saveSrcFlag then 
                obj.setSrcFile(uniFile.getFilename())
            tag := obj.getFormType()||"_"||obj.getName()
            uniFile.setTag(tag)
            }
    end

    # <p>
    #  Remove any files that have already been processed from the
    #    import and link sets.
    # </p>
    # <i>This is intended for internal use only!</i>
    #
    method cleanSets()
        local fName, pName, fList

        # for now, just be satisfied with the link set, since
        #     imports are *not* file names!
        #
        every fName := !linkSet do {
            if \files[fName] then {     # Already processed, remove
                delete(linkSet, fName)
                }
            }
        every pName := !importSet do {
            every fName := genPackageFiles(pName) do {
                if /fName | (*fName == 0) then next
                if /files[fName] then {     # Need to process
                    insert(linkSet, \fName)
                    }
                }
            }
        every delete(linkSet, !badFiles)

    end

    # <p>
    #   Returns its argument if that argument ends in the specified suffix.
    #   <[param s -- string to test]>
    #   <[param suffix -- suffix to test against]>
    #   <[returns <tt>s</tt> if it ends in <tt>suffix</tt>]>
    # </p>
    method hasSuffix(s, suffix)
        return (s[-*suffix:0] == suffix, s)
    end

    # <p>
    # This is the main entry point to the <b>UniAll</b> class.  When
    # invoked, the file <b>fName</b> is parsed and added to an
    # internal representation for the Unicon source code being
    # handled by this class instance.  May be called repeatedly to
    # process multiple files.
    # </p>
    # <p>
    # If the argument is a directory, then all files suffixed with ".icn"
    # within that directory are processed.
    # </p>
    method processFile(fName, contents)
        if /fName | (*fName = 0) then fail

        if stat(fName||"/") then {   # Handle source files in directory
           every processFile(hasSuffix(!open(fName), ".icn"))
           return
           }

        fName := delSuffix(fName, ".icn") || ".icn"

        uniFile := UniFile(fName) |
                   UniFile(fName, genDirs(\sourcePath)) | {
                   insert(badFiles, fName)
                   fail
                   } 

        if \contents then {
            if \uniFile := uniFilez[fName] then {
                uniFile.updateContent(contents) | continue
                classifyLines(uniFile)
                uniFilez[fName] := uniFile
            }
        } else if f := addFile(fName) then {
            uniFile.setSaveSrc(saveSrcFlag)
            fileHead := "yes"
            curClass := &null
            curComments := Comments()

            # Start by assuming this file is not part of a package
            curPackage := packages["(main)"]
            curFile.setPackage(curPackage)
            packages["(main)"].addFile(f)
            classifyLines(uniFile)
            if uniFile.hasSource() then {
                UniDoc::writeSrcFile(targetDir, uniFile)
                }

            uniFilez[fName] := uniFile
            }
        
        # See if we need to resolve imports and links...
        
        if \resolveFlag then {     # Yep...

            cleanSets()

            # Have added package files from imports to linkSet, since
            #     imports are *not* file names!
            #
            while *linkSet > 0 do {
                every fName := !linkSet do {
                    processFile(fName)
                    }
                cleanSets()
                }

            }

            
    end

    #<p>
    #    Given the name of a file and line number, retrieve the specific
    #    line directly from the raw buffer inside UniFile.
    #</p>
    method getUniFileLine(fName, lineNum)
        if (*/fName) = 0 then fail
        fName := delSuffix(fName, ".icn") || ".icn"
        if uniFile := \uniFilez[fName] then return uniFile.Read(lineNum)
    end

    #<p>
    #  Given the name of a package, generates all the files associated with
    #    that package.  (Uses <b>parser</b> package from <i>Robert Parlett</i>
    #    and requires that the package has been compiled and exists on IPATH.)
    #</p>
    method genPackageFiles(pName)
        local pi

        pi := parser::load_package_info(pName) | fail
        suspend !pi.get_files()
    end

    #<p>
    # Given a string of comma-separated directories, generates the
    # individual directories from that string.  Guarantees that
    # each directory produced ends with "/".
    #</p>
    # <i>This is intended for internal use only!</i>
    method genDirs(s)
        suspend trim(genFields(s,','),"/\\")||"/"
    end

    #<p>
    # Output some simple statistics on the amount of processing
    #  that has been performed.  Give more detail if <b>detail</b>
    #  is non-null.
    #</p>
    method dumpStatistics(detail)
        local p

        write()
        write()
        write("UniDoc processing:")
        write()

        write("Files:         ",*files)
        if \detail then dumpNames("\t",files)
        write("Packages:      ",*packages)
        if \detail then {
            every p := (!sort(packages))[2] do {
                write("\t",p.getName(),":")
                write("\t\tfiles:",      p.files.size())
                write("\t\tpackages:",   p.imports.size())
                write("\t\tclasses:",    p.classes.size())
                write("\t\tprocedures:", p.procs.size())
                write("\t\trecords:",    p.records.size())
                write("\t\tglobals:",    p.globals.size())
                write()
                }
            }

        write("\n")
        write("Unprocessed files: ",*badFiles)
        if \detail then dumpNames("\t", badFiles)
    end

    # <p>
    # Outputs a list of entity names.
    # </p>
    # <i>This is intended for internal use only!</i>
    method dumpNames(prefix, data)
        if lang::istype(data, "table") then {
            every write(prefix, key(data))
            }
        else if lang::istype(data, "UniDoc::Sequence") then {
            every write(prefix, data.get().getName())
            }
        else if lang::istype(data, "UniDoc::Set") then {
            every write(prefix, data.get().getName())
            }
        else {
            every write(prefix, !sort(data))
            }
    end

    # <p>
    # Parse a file, building an internal representation.
    # </p>
    # <i>This is intended for internal use only!</i>
    method classifyLines(uniFile)
        while classify(uniFile)
    end

    # <p>
    #  Classify the type of an input line based on tokens produced
    #  during parsing of the file <b>uniFile</b>.
    # </p>
    # <i>This is intended for internal use only!</i>
    method classify(uniFile)
        local token

        token := uniFile.nextToken() | fail

        #write("token = " || ximage(token))
    
        case token.Type() of {
            "UniDoc::BlankLine" : processBlank(uniFile)
            "UniDoc::Comment"   : processComment(uniFile, token.value)
            "UniDoc::Keyword"   : processKeyword(uniFile, token.value)
            }
    
        uniFile.clearLine()
        return
    end
    
    # <p>
    #   Process a blank line.  Blank lines are only interesting
    #   in that they serve to distinguish comment blocks.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processBlank(f)
        if \fileHead then {     # File-level comments
            fileHead := &null
            curFile.setComments(curComments)
            }
        curComments := Comments()    # discard current comment block
    end
    
    # <p>
    #   Process a comment.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processComment(f, s)
        curComments.add(s)
    end
    
    # <p>
    # Process a keyword.  Not all keywords are interesting.  Many
    # are simply ignored.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processKeyword(f, word)
        case word of {
            "package"   : processPackage(f)
            "import"    : processImport(f)
            "link"      : processLink(f)
            "class"     : processClass(f)
            "initially" : processInitially(f)
            "method"    : processMethod(f)
            "procedure" : processProcedure(f)
            "global"    : processGlobal(f)
            "record"    : processRecord(f)
            }
    end
    
    # <p>
    # There are places in a Unicon program where an entity may be
    # represented by either a <i>name</i> or a <i>string</i>.
    # This guarentees that it's represented by a name.
    # </p>
    # <i>This is intended for internal use only!</i>
    method getName(token)
        if token.Type() == "UniDoc::String" then {
            return token.value[2:-1]
            }
        return token.value
    end

    # <p>
    # Process a package header.  Note that this implies that
    # the current file isn't part of the <i>default</i> package.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processPackage(f)
        local pName

        pName := getName(f.nextToken()) | fail
        /packages[pName] := UPackage(pName, curFile)
        curPackage := packages[pName]
        curPackage.mergeComments(curComments)
        curComments := Comments()
        curFile.setPackage(curPackage)
        curPackage.addFile(curFile)
        packages["(main)"].delFile(curFile.getName())
    end
    
    # <p>
    # Process an import statement.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processImport(f)
        local iName, iList, imp, token, sawComma

        iName := getName(f.nextToken()) | fail
        iList := iName
        insert(importSet, iName)           # Remember for later processing
        imp := UImport(iName, curFile, curComments)
        curComments := Comments()
        curFile.addImport(imp)
        curPackage.addImport(imp)
        while not f.EOS() do {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comma" then {
                token := f.nextToken()
                sawComma := "yes"
                }
            if token.Type() == "UniDoc::Comment" then {
                imp.addComment(token.value)
                if \sawComma then {
                    f.pushback(Comma())
                    }
                }
            else if token.Type() == ("UniDoc::Name" | "UniDoc::String") then {
                iName := getName(token)
                insert(importSet, iName)   # Remember for later processing
                imp := UImport(iName, curFile)
                iList ||:= " " || iName
                curFile.addImport(imp)
                curPackage.addImport(imp)
                sawComma := &null
                }
            }
    
    end
    
    # <p>
    # Process a link statement.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processLink(f)
        local lName, lList, lnk, token, sawComma

        lName := getName(f.nextToken()) | fail
        lList := lName
        lName := delSuffix(lName, ".icn")||".icn"
        insert(linkSet, lName)             # Remember for later processing
        lName := delSuffix(lName,".icn")||".icn"
        lnk := ULink(lName, curFile, curComments)
        curComments := Comments()
        curFile.addLink(lnk)
        while not f.EOS() do {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comma" then {
                token := f.nextToken()
                sawComma := "yes"
                }
            if token.Type() == "UniDoc::Comment" then {
                lnk.addComment(token.value)
                if \sawComma then {
                    f.pushback(Comma())
                    }
                }
            else if token.Type() == ("UniDoc_Name" | "UniDoc_String") then {
                lName := getName(token)
                insert(linkSet, lName)     # Remember for later processing
                lList ||:= " " || lName
                lName := delSuffix(lName,".icn")||".icn"
                lnk := ULink(lName, curFile)
                curFile.addLink(lnk)
                sawComma := &null
                }
            }
    
    end
    
    # <p>
    # Process a class header.  If there's an <b>initially</b> clause that
    # will get tacked on later.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processClass(f)
        local cName, const, scList, token, scName, pList, field

        cName := getName(f.nextToken()) | fail
        curClass := UClass(cName, curFile, curComments)
        addTag(curClass)

        const := UConstructor(curClass.getName(), curClass, Comments())
        curClass.setConstructor(const)
        curComments := Comments()

        curClass.setFile(curFile)
        curClass.setPackage(curPackage)
        curFile.addClass(curClass)
        curPackage.addClass(curClass)
        scList := ""

        while (token := f.nextToken()).className() ~== "UniDoc::LParen" do {
            if token.Type() == "UniDoc::Colon" then {
                next
                }
            if token.Type() == "UniDoc::Comment" then {
                if /scName then {   # attach to class
                    curClass.addComment(token.value)
                    }
                else {
                    sClass.addComment(token.value)
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                scName := getName(token)
                sClass := UName(scName, curClass)
                sClass.setCategory("class")
                curClass.addSuperClass(sClass)
                scList ||:= ": " || scName
                }
            }
        f.pushback(token)
        pList := ""
        while (token := f.nextToken()).className() ~== "UniDoc::RParen" do {
            if token.Type() == "UniDoc::Comma" then {
                next
                }
            if token.Type() == "UniDoc::Comment" then {
                if /field then {        # apply to class itself
                    curClass.addComment(token.value)
                    }
                else {                # apply to field name
                    field.addComment(token.value)
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                field := getParam(f, token, "field", curClass)
                pList ||:= " " || field.getName()
                curClass.addField(field)
                }
            }
    
        if not f.EOS() then {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comment" then {
                curClass.addComment(token.value)
                }
            else f.pushback(token)
            }

        curClass.getConstructor().setParams(curClass.getParams())
    end

    # <p>
    # Get a parameter.
    # </p>
    method getParam(f, token, category, parent)
        fName := getName(token)
        param := UName(fName, parent)
        param.setCategory(category)

        # Look for default value and or type. (Note: if
        #   only one or the other, can't distinquish so assume default value
        #
        if (token := f.nextToken()).Type() == "UniDoc::Colon" then {
            token := f.nextToken()      # initializer
            param.setDefValue(token.get())
            if (token := f.nextToken()).Type() == "UniDoc::Colon" then {
                # oops, had a type earlier!
                param.setTypeValue(param.getDefValue())
                token := f.nextToken()      # initializer
                param.setDefValue(token.get())
                }
            else f.pushback(token)
            }
        else f.pushback(token)

        return param
    end

    # <p>
    # Get a global.
    # </p>
    method getGlobal(f, token, parent, comments)
        gName := getName(token)
        glob := UGlobal(gName, parent, comments)
        addTag(glob)

        # Look for default value and or type. (Note: if
        #   only one or the other, can't distinquish so assume default
        #   value
        if (token := f.nextToken()).Type() == "UniDoc::Colon" then {
            token := f.nextToken()      # initializer
            glob.setDefValue(token.get())
            if (token := f.nextToken()).Type() == "UniDoc::Colon" then {
                # oops, had a type earlier!
                glob.setTypeValue(glob.getDefValue())
                token := f.nextToken()      # initializer
                glob.setDefValue(token.get())
                }
            else f.pushback(token)
            }
        else f.pushback(token)

        curFile.addGlobal(glob)
        curPackage.addGlobal(glob)

        return glob
    end
    
    # <p>
    # Process an initially clause, treating it as a class
    # constructor.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processInitially(f)
        local const, token, pList, param

        const := UConstructor(curClass.getName(), curClass, curComments)
        addTag(const)
        curComments := Comments()
        curClass.setConstructor(const)
        token := f.nextToken()
        if token.Type() == "UniDoc::LParen" then {
            const.setParams(Sequence())
            pList := ""
            while (token := f.nextToken()).className() ~== "UniDoc::RParen" do {
                if token.Type() == "UniDoc::Comma" then {
                    next
                    }
                if token.Type() == "UniDoc::Comment" then {
                    if /param then {        # apply to constructor itself
                        const.addComment(token.value)
                        }
                    else {                # apply to parameter name
                        param.addComment(token.value)
                        }
                    }
                else if token.Type() == "UniDoc::Name" then {
                    param := getParam(f, token, "param", curClass)
                    pList ||:= " " || param.getName()
                    const.addParam(param)
                    }
                }
            }
        else {
            f.pushback(token)
            }
    
        if not f.EOS() then {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comment" then {
                const.addComment(token.value)
                }
            else f.pushback(token)
            }
        
        # No param list on constructor, so assume all fields!
        if /const.getParams() then {
            const.setParams(curClass.getParams())
            }
    end
    
    # <p>
    # Process a method definition.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processMethod(f)
        local mName, pList, met, token, param
        if /curClass then fail
        mName := getName(f.nextToken()) | fail
        pList := ""
        met := UMethod(mName, curClass, curComments)
        addTag(met)
        curClass.addMethod(met)
        curComments := Comments()
        token := f.nextToken()
        if token.Type() == "UniDoc::LParen" then {
            token := f.nextToken()
            }
        if token.Type() == "UniDoc::Comment" then {
            met.addComment(token.value)
            }
        else if token.Type() ~== "UniDoc::LParen" then {
            f.pushback(token)
            }
        
        while (token := f.nextToken()).className() ~== "UniDoc::RParen" do {
            if token.Type() == "UniDoc::Comma" then {
                next
                }
            if token.Type() == "UniDoc::Comment" then {
                if /param then {        # apply to method itself
                    met.addComment(token.value)
                    }
                else {                # apply to parameter name
                    param.addComment(token.value)
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                param := getParam(f, token, "param", curClass)
                pList ||:= " " || param.getName()
                met.addParam(param)
                }
            }
    
        if not f.EOS() then {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comment" then {
                met.addComment(token.value)
                }
            else f.pushback(token)
            }
        
    end
    
    # <p>
    # Process a procedure definition.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processProcedure(f)
        local pName, pList, proced, token, param

        pName := getName(f.nextToken()) | fail
        pList := ""
        proced := UProc(pName, curFile, curComments)
        addTag(proced)
        curFile.addProcedure(proced)
        curPackage.addProcedure(proced)
        curComments := Comments()
        token := f.nextToken() | fail
        if token.Type() == "UniDoc::LParen" then {
            token := f.nextToken()
            }
        if token.Type() == "UniDoc::Comment" then {
            proced.addComment(token.value)
            }
        else if token.Type() ~== "UniDoc::LParen" then {
            f.pushback(token)
            }
        
        while (token := f.nextToken()).className() ~== "UniDoc::RParen" do {
            if token.Type() == "UniDoc::Comma" then {
                next
                }
            if token.Type() == "UniDoc::Comment" then {
                if /param then {        # apply to record itself
                    proced.addComment(token.value)
                    }
                else {                # apply to field name
                    param.addComment(token.value)
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                param := getParam(f, token, "param", proced)
                pList ||:= " " || param.getName()
                proced.addParam(param)
                }
            }
    
        if not f.EOS() then {
            token := f.nextToken() | fail
            if token.Type() == "UniDoc::Comment" then {
                proced.addComment(token.value)
                }
            else f.pushback(token)
            }
    end
    
    # <p>
    # Process a global statement.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processGlobal(f)
        local gName, gList, glob, token, sawComma
        gList := ""

        glob := getGlobal(f, f.nextToken(), curFile, curComments) | fail
        gList ||:= " " || glob.getName()
        curComments := Comments()
        while not f.EOS() do {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comma" then {
                token := f.nextToken()
                sawComma := "yes"
                }
            if token.Type() == "UniDoc::Comment" then {
                glob.addComment(token.value)
                if \sawComma then {
                    f.pushback(Comma())
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                glob := getGlobal(f, token, curFile)
                gList ||:= " " || glob.getName()
                sawComma := &null
                }

            }

            uniFile.setInside()                # re-enable Unicon keywords
    end
    
    # <p>
    # Process a record definition.
    # </p>
    # <i>This is intended for internal use only!</i>
    method processRecord(f)
        local rName, pList, rec, token, field

        rName := getName(f.nextToken()) | fail
        pList := ""
        rec := URecord(rName, curFile, curComments)
        addTag(rec)
        curFile.addRecord(rec)
        curPackage.addRecord(rec)
        curComments := Comments()
        token := f.nextToken()
        if token.Type() == "UniDoc::LParen" then {
            token := f.nextToken()
            }
        if token.Type() == "UniDoc::Comment" then {
            rec.addComment(token.value)
            }
        else if token.Type() ~== "UniDoc::LParen" then {
            f.pushback(token)
            }
        
        while (token := f.nextToken()).className() ~== "UniDoc::RParen" do {
            if token.Type() == "UniDoc::Comma" then {
                next
                }
            if token.Type() == "UniDoc::Comment" then {
                if /field then {        # apply to record itself
                    rec.addComment(token.value)
                    }
                else {                # apply to field name
                    field.addComment(token.value)
                    }
                }
            else if token.Type() == "UniDoc::Name" then {
                field := getParam(f, token, "field", rec)
                pList ||:= " " || field.getName()
                rec.addField(field)
                }
            }
    
        if not f.EOS() then {
            token := f.nextToken()
            if token.Type() == "UniDoc::Comment" then {
                rec.addComment(token.value)
                }
            else f.pushback(token)
            }
        
            uniFile.setInside()                # re-enable Unicon keywords
    end

    # <p>
    # Produce a table of all methods that are inherited by
    #   a class.  Keys are superclasses, entries are 
    #   UniDoc::Set()s of methods.
    # </p>
    method inherited(aClass)
        local iSet, mSet, sc, metd, mName

        # Start with all the methods in the current class
        every insert(mSet := ::set(), aClass.getMethods().get().getName())

        iSet := table()
        #mSet := set()
        every sc := genAllSuperClasses(aClass) do {
            every metd := sc.getMethods().get() do {
                mName := metd.getName()
                if not member(mSet, mName) then {
                    insert(mSet, mName)
                    /iSet[sc] := Set()
                    iSet[sc].add(mName, metd)
                    }
                }
            }

        return iSet
    end

    # <p>
    # Produce a sorted list (by name) of superclasses,
    #    given a table whose keys are superclasses.
    # </p>
    method makeNameTab(aTab)
        
        nTab := table()
        every sc := key(aTab) do {
            nTab[sc.getName()] := sc
            }
        every put(nList := [], (!sort(nTab))[2])
        return nList
    end
            
    # <p>
    # Produce a list of <i>all</i> packages.
    # </p>
    method getAllPackages()
        local ptab, p, plist

        ptab := table()
        every p := !packages do {
            ptab[p] := p.getName()
            }
        every put(plist := [], (!sort(ptab, 2))[1])
        return plist
    end

    # <p>
    # Produce a list of <i>all</i> classes.
    # </p>
    method getAllClasses()
        local ctab, p, g, clist

        ctab := table()
        every p := !files do {
            every g := p.getClasses().get() do {
                ctab[g] := g.getName()
                }
            }
        every put(clist := [], (!sort(ctab, 2))[1])
        return clist
    end

    # <p>
    # Produce a list of <i>all</i> procedures.
    # </p>
    method getAllProcedures()
        local ctab, p, g, clist

        ctab := table()
        every p := !files do {
            every g := p.getProcedures().get() do {
                ctab[g] := g.getName()
                }
            }
        every put(clist := [], (!sort(ctab, 2))[1])
        return clist
    end

    # <p>
    # Produce a list of <i>all</i> records.
    # </p>
    method getAllRecords()
        local ctab, p, g, clist
        ctab := table()
        every p := !files do {
            every g := p.getRecords().get() do {
                ctab[g] := g.getName()
                }
            }
        every put(clist := [], (!sort(ctab, 2))[1])
        return clist
    end

    # <p>
    # Produce a list of <i>all</i> globals.
    # </p>
    method getAllGlobals()
        local ctab, p, g, clist

        ctab := table()
        every p := !files do {
            every g := p.getGlobals().get() do {
                ctab[g] := g.getName()
                }
            }
        every put(clist := [], (!sort(ctab, 2))[1])
        return clist
    end

    # <p>
    # Produce a list of <i>all</i> files that have been processed,
    # whether by explicit reference <b>processFile()</b> or during
    # entity resolution.
    # </p>
    method getAllFiles()
        local ctab, f, clist

        ctab := table()
        every f := \!files do {
           ctab[f.getName()] := f
           }
        every put(clist := [], (!sort(ctab))[2])
        return clist
    end

    method locatePack(pName)
        every dir := !linkPath do {
            d := open(dir) | next
            every cf := !d do {
                #  Note that this is a best guess effort: it may fail, or
                #   worse find wrong package!
                fName := "pack_"||pName||".html"
                if fName == cf then {
                   return (close(d), dir||"/"||fName)
                   }
                }
            close(d)
            }
        # Give up
        return "pack_"||fixName(pName)||".html"  # Placeholder code
    end

    # <p>
    # Given the name of a class, attempts to find an existing
    # html page for that class, by searching the <b>linkPath</b>.
    # </p>
    # <p><b>This method is currently broken!</b></p>
    method classOnLinkPath(bClass, cName)
        local pName

        imps := curFile.getImportNames()
        every dir := !linkPath do {
            d := open(dir) | next
            every cf := !d do {
                # Start by hoping it's a full class name (with package)
                cName ? {
                    if pName := tabSkip("::"|"__") then {
                        if pName == (""|"(main)") then pName := "0main"
                        cName := tab(0)
                        return dir||"/class_"||pName||"_"||cName||".html"
                        }
                    }

                #  Have to check imports next.
                #  Note that this is a best guess effort: it may fail, or
                #   worse find the class in the wrong package!
                every pName := ("0main" | !\imps) do {
                    fName := "class_"||pName||"_"||cName||".html"
                    if fName == cf then return (close(d), dir||"/"||fName)
                    }
                }
            close(d)
            }
    end

    # <p>
    # Given a class and the name of a superclass of that class,
    #   return that superclass.
    # </p>
    method locateSuperClass(aClass, scName)
        local pName, p, f, sc

        # If the superclass name tells us the package, use that.
        scName ? {
            if pName := tabSkip("::"|"__") then {
                if pName == (""|"(main)") then pName := "0main"
                scName := tab(0)
                if \(p := packages[pName]) then {
                    sc := p.getClass(scName)
                    return \sc
                    }
                fail                # No hope of finding it!
                }
            }

        # Have to look for it.  Go file, package, imports, links...
        f := aClass.getFile()
        p := f.getPackage()
        if \(sc := (f|p).getClass(scName)) then return sc
        every pName := f.getImports().get().getName() do {
            if \(p := packages[pName]) then {
                if \(sc := p.getClass(scName)) then return sc
                }
            }
        every fName := f.getLinks().get().getName() do {
            if \(p := files[pName]) then {
                if \(sc := p.getClass(scName)) then return sc
                }
            }

        # Last chance - check the (main) package.
        sc := packages["(main)"].getClass(scName)
        return \sc

    end

    # <p>
    # Generate (in order) all the superclasses of a class
    # </p>
    method genAllSuperClasses(aClass, scSet)
        /scSet := ::set()
        insert(scSet, aClass)
        scSeq := aClass.getSuperClasses()
        every scName := scSeq.get().getName() do {
            if sc := locateSuperClass(aClass, scName) then {
                if not member(scSet, sc) then {   # Haven't seen it yet
                    suspend sc | genAllSuperClasses(sc, scSet)
                    }
                }
            }

    end

    # <p>
    # Given a class and a method name from that class, does this method
    #   override any method from a superclass?
    # </p><p>
    # Returns the superclass of the overridden method, if any.
    # </p>
    method overrides(aClass, mName)
        every sc := genAllSuperClasses(aClass) do {
            if sc.hasMethod(mName) then {
                return sc
                }
            }
    end

    initially ()
        every (files|packages) := table()
        packages["(main)"]     := UPackage("(main)")
        resolveFlag            := &null
        sourcePath             := &null
        importSet              := ::set()
        linkSet                := ::set()
        lName                  := delSuffix(lName, ".icn")||".icn"
        badFiles               := ::set()
        uniFilez               := table()
end

# <p>
# Given a <b>UniDoc::Set()</b> or a <b>UniDoc::Sequence()</b>,
#   produces a list of the entities contained in it, sorted by
#   name.
# </p>
procedure mkList(aSet)
    local ctab, obj, clist

    ctab := table()
    every obj := \aSet.get() do {
        ctab[obj] := obj.getName()
        }
    every put(clist := [], (!sort(ctab, 2))[1])
    return clist
end

# <p>
# Given a <b>UniDoc::UMethod()</b>, <b>UniDoc::UConstructor()</b>,
#   <b>UniDoc::UProc()</b>, <b>UniDoc::UClass()</b>, or
#   <b>UniDoc::URecord()</b>, produces a string showing the
#   syntax of call.
# </p>
procedure mkCallStr(obj)
    local s

    s := obj.getName()
    case obj.className() of {
        "UniDoc::UMethod"     |
        "UniDoc::UConstructor"|
        "UniDoc::UProc"       |
        "UniDoc::UClass"      |
        "UniDoc::URecord"     :
                              s ||:= mkParamListStr(obj.getParams())
        }
    return s
end

# <p>
# Given a UniDoc::Sequence() representing a field or parameter list,
#   produces a string showing that parameter list (<i>including the
#   enclosing parentheses</i>).
# </p>
procedure mkParamListStr(sequence)
    return "("||mkListStr(sequence)||")"
end

# <p>
# Given a UniDoc::Sequence(),
#   produces a string showing that list.
# </p>
procedure mkListStr(sequence)
    local s, item, n

    s := ""
    if \sequence then {
        every item := sequence.get() do {
            n := item.getName()
            if item.Type() == ("UniDoc::UName"|"UniDoc::UGlob") then {
            n ||:= ":"||item.getTypeValue()
                n ||:= ":"||item.getDefValue()
                }
            s ||:= n||", "
            }
        }
    return (s[1:-2] | "")
end
