/*****************************************************************************/
/*        Copyright (C) 2021  NORMAN MEGILL  nm at alum.mit.edu              */
/*            License terms:  GNU General Public License                     */
/*****************************************************************************/
/*34567890123456 (79-character line to adjust editor window) 2345678901234567*/

#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdarg.h>
#include <setjmp.h>
#include "mmvstr.h"
#include "mmdata.h"
#include "mminou.h"
#include "mmpars.h"
/* #include "mmcmds.h" */  /* For getContribs() if used */
#include "mmpfas.h" /* Needed for g_pipDummyVars, subproofLen() */
#include "mmunif.h" /* Needed for g_minSubstLen */
#include "mmcmdl.h" /* Needed for g_rootDirectory */

long potentialStatements; /* Potential statements in source file (upper
                                 limit) for memory allocation purposes */
flag illegalLabelChar[256]; /* Illegal characters for labels -- initialized
                               by parseLabels() */
/* (Next 2 are global in mmpars.c only) */
long *g_labelKeyBase; /* Start of assertion ($a, $p) labels */
long g_numLabelKeys; /* Number of assertion labels */

long *g_allLabelKeyBase; /* Start of all labels */
long g_numAllLabelKeys; /* Number of all labels */


/* Working structure for parsing proofs */
/* This structure should be deallocated by the ERASE command. */
long g_wrkProofMaxSize = 0; /* Maximum size so far - it may grow */
long wrkMathPoolMaxSize = 0; /* Max mathStringPool size so far - it may grow */
struct wrkProof_struct g_WrkProof;



/* This function returns a pointer to a buffer containing the contents of an
   input file and its 'include' calls.  'Size' returns the buffer's size.
   Partial parsing is done; when 'include' statements are found, this function
   is called recursively.
   The file g_input_fn is assumed to be opened when this is called. */
char *readRawSource(/*vstring inputFn,*/ /* 2-Feb-2018 nm Unused argument */
         vstring fileBuf, /* added 31-Dec-2017 */
         /*long bufOffsetSoFar,*/  /* deleted 31-Dec-2017 nm */
         long *size /* 31-Dec-2017 Now an input argument */)
{
  long charCount = 0;
  /*char *fileBuf;*/ /* 31-Dec-2017 */
  char *fbPtr;
  /*long lineNum;*/ /* 2-Feb-2018 Now computed in rawSourceError() */
  flag insideComment;
  /* flag insideLineComment; */ /* obsolete */
  char mode;
  char tmpch;

  /* 31-Dec-2017 nm */
  charCount = *size;

  /* Look for $[ and $] 'include' statement start and end */
  /* 31-Dec-2017 nm - these won't happen since they're now expanded earlier */
  /* But it can't hurt, since the code is already written.
     TODO - clean it up for speedup? */
  fbPtr = fileBuf;
  /*lineNum = 1;*/
  mode = 0; /* 0 = outside of 'include', 1 = inside of 'include' */
  insideComment = 0; /* 1 = inside $( $) comment */
  /* insideLineComment = 0; */ /* 1 = inside $! comment */
  while (1) {
    /* Find a keyword or newline */
    /* fbPtr = strpbrk(fbPtr, "\n$"); */ /* Takes 10 msec on VAX 4000/60 ! */
    tmpch = fbPtr[0];
    if (!tmpch) { /* End of file */
      if (insideComment) {
        rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum - 1, inputFn,*/
         "The last comment in the file is incomplete.  \"$)\" was expected.");
      } else {
        if (mode != 0) {
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum - 1, inputFn,*/
   "The last include statement in the file is incomplete.  \"$]\" was expected."
           );
        }
      }
      break;
    }
    if (tmpch != '$') {
      if (tmpch == '\n') {
        /* insideLineComment = 0; */ /* obsolete */
        /*lineNum++;*/
      } else {
        /* if (!insideComment && !insideLineComment) { */
          if (!isgraph((unsigned char)tmpch) && !isspace((unsigned char)tmpch)) {
            /* 19-Oct-2010 nm Used to bypass "lineNum++" below, which messed up
               line numbering. */
            rawSourceError(fileBuf, fbPtr, 1, /*lineNum, inputFn,*/
                cat("Illegal character (ASCII code ",
                str((double)((unsigned char)tmpch)),
                " decimal).",NULL));
        /* } */
        }
      }
      fbPtr++;
      continue;
    }


    /* 11-Sep-2009 nm Detect missing whitespace around keywords (per current
       Metamath language spec) */
    if (fbPtr > fileBuf) {  /* If this '$' is not the 1st file character */
      if (isgraph((unsigned char)(fbPtr[-1]))) {
        /* The character before the '$' is not white space */
        if (!insideComment || fbPtr[1] == ')') {
          /* Inside comments, we only care about the "$)" keyword */
          rawSourceError(fileBuf, fbPtr, 2, /*lineNum, inputFn,*/
              "A keyword must be preceded by white space.");
        }
      }
    }
    fbPtr++; /* (This line was already here before 11-Sep-2009 mod) */
    if (fbPtr[0]) {  /* If the character after '$' is not end of file (which
                        would be an error anyway, but detected elsewhere) */
      if (isgraph((unsigned char)(fbPtr[1]))) {
        /* The character after the character after the '$' is not white
           space (nor end of file) */
        if (!insideComment || fbPtr[0] == ')') {
          /* Inside comments, we only care about the "$)" keyword */
          rawSourceError(fileBuf, fbPtr + 1, 1, /*lineNum, inputFn,*/
              "A keyword must be followed by white space.");
          }
      }
    }
    /* End of 11-Sep-2009 mod */

    switch (fbPtr[0]) {
      case '(': /* Start of comment */
        if (insideComment) {
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum, inputFn,*/
          "Nested comments are not allowed.");
        }
        insideComment = 1;
        continue;
      case ')': /* End of comment */
        if (!insideComment) {
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum, inputFn,*/
              "A comment terminator was found outside of a comment.");
        }
        insideComment = 0;
        continue;
      /* Comment to end-of-line */  /* obsolete */
      /*
      case '!':
        if (!insideComment) {
          insideLineComment = 1;
          fbPtr++;
          continue;
        }
      */
    }
    if (insideComment) continue;
    switch (fbPtr[0]) {
      case '[':
        if (mode != 0) {
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum, inputFn,*/
              "Nested include statements are not allowed.");
        } else {
          /* 31-Dec-2017 nm */
          /* $[ ... $] should have been processed by readSourceAndIncludes() */
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum, inputFn,*/
              "\"$[\" is unterminated or has ill-formed \"$]\".");

        }
        continue;
      case ']':
        if (mode == 0) {
          rawSourceError(fileBuf, fbPtr - 1, 2, /*lineNum, inputFn,*/
              "A \"$[\" is required before \"$]\".");
          continue;
        }

        /* 31-Dec-2017 nm */
        /* Because $[ $] are already expanded here, this should never happen */
        bug(1759);

        continue;

      case '{':
      case '}':
      case '.':
        potentialStatements++; /* Number of potential statements for malloc */
        break;
    } /* End switch fbPtr[0] */
  } /* End while */

  if (fbPtr != fileBuf + charCount) {
    /* To help debugging: */
    printf("fbPtr=%ld fileBuf=%ld charCount=%ld diff=%ld\n",
        (long)fbPtr, (long)fileBuf, charCount, fbPtr - fileBuf - charCount);
    bug(1704);
  }

  /* 31-Dec-2017 nm */
  print2("%ld bytes were read into the source buffer.\n", charCount);

  if (*size != charCount) bug(1761);
  return (fileBuf);

} /* readRawSource */

/* This function initializes the g_Statement[] structure array and assigns
   sections of the raw input text.  statements is updated.
   g_sourcePtr is assumed to point to the raw input buffer.
   g_sourceLen is assumed to be length of the raw input buffer. */
void parseKeywords(void)
{
  long i, j, k;
  char *fbPtr;
  flag insideComment;
  char mode, type;
  char *startSection;
  char tmpch;
  long dollarPCount = 0; /* For statistics only */
  long dollarACount = 0; /* For statistics only */

  /*** 9-Jan-2018 nm Deleted
  /@ Variables needed for line number and file name @/
  long inclCallNum;
  char *nextInclPtr;
  long lineNum;
  vstring fileName;
  ***/

  /* Initialization to avoid compiler warning (should not be theoretically
     necessary) */
  type = 0;

  /* Determine the upper limit of the number of new statements added for
     allocation purposes (computed in readRawInput) */
  potentialStatements = potentialStatements + 3; /* To be cautious */
/*E*/if(db5)print2("There are up to %ld potential statements.\n",
/*E*/   potentialStatements);

  /* Reallocate the statement array for all potential statements */
  g_Statement = realloc(g_Statement, (size_t)potentialStatements
      * sizeof(struct statement_struct));
  if (!g_Statement) outOfMemory("#4 (statement)");

  /* Initialize the statement array */
  i = 0;
  g_Statement[i].lineNum = 0;   /* assigned by assignStmtFileAndLineNum() */
  g_Statement[i].fileName = ""; /* assigned by assignStmtFileAndLineNum() */
  g_Statement[i].labelName = "";
  g_Statement[i].uniqueLabel = 0;
  g_Statement[i].type = illegal_;
  g_Statement[i].scope = 0;
  g_Statement[i].beginScopeStatementNum = 0;
  g_Statement[i].endScopeStatementNum = 0; /* 24-Aug-2020 nm */
  g_Statement[i].labelSectionPtr = "";
  g_Statement[i].labelSectionLen = 0; /* 3-May-2017 nm */
  g_Statement[i].labelSectionChanged = 0;
  g_Statement[i].statementPtr = ""; /* input to assignStmtFileAndLineNum() */
  g_Statement[i].mathSectionPtr = "";
  g_Statement[i].mathSectionLen = 0;
  g_Statement[i].mathSectionChanged = 0; /* 3-May-2017 nm */
  g_Statement[i].proofSectionPtr = "";
  g_Statement[i].proofSectionLen = 0;
  g_Statement[i].proofSectionChanged = 0; /* 3-May-2017 nm */
  g_Statement[i].mathString = NULL_NMBRSTRING;
  g_Statement[i].mathStringLen = 0;
  g_Statement[i].proofString = NULL_NMBRSTRING;
  g_Statement[i].reqHypList = NULL_NMBRSTRING;
  g_Statement[i].optHypList = NULL_NMBRSTRING;
  g_Statement[i].numReqHyp = 0;
  g_Statement[i].reqVarList = NULL_NMBRSTRING;
  g_Statement[i].optVarList = NULL_NMBRSTRING;
  g_Statement[i].reqDisjVarsA = NULL_NMBRSTRING;
  g_Statement[i].reqDisjVarsB = NULL_NMBRSTRING;
  g_Statement[i].reqDisjVarsStmt = NULL_NMBRSTRING;
  g_Statement[i].optDisjVarsA = NULL_NMBRSTRING;
  g_Statement[i].optDisjVarsB = NULL_NMBRSTRING;
  g_Statement[i].optDisjVarsStmt = NULL_NMBRSTRING;
  g_Statement[i].pinkNumber = 0;
  g_Statement[i].headerStartStmt = 0; /* 18-Dec-2016 nm */
  for (i = 1; i < potentialStatements; i++) {
    g_Statement[i] = g_Statement[0];
  }

  /* 10-Dec-2018 nm */
  /* In case there is no relevant statement (originally added for MARKUP
     command) */
  g_Statement[0].labelName = "(N/A)";

/*E*/if(db5)print2("Finished initializing statement array.\n");

  /* Fill in the statement array with raw source text */
  fbPtr = g_sourcePtr;
  mode = 0; /* 0 = label section, 1 = math section, 2 = proof section */
  insideComment = 0; /* 1 = inside comment */
  startSection = fbPtr;

  while (1) {
    /* Find a keyword or newline */
    /* fbPtr = strpbrk(fbPtr, "\n$"); */ /* Takes 10 msec on VAX 4000/60 ! */
    tmpch = fbPtr[0];
    if (!tmpch) { /* End of file */
      if (mode != 0) {
        sourceError(fbPtr - 1, 2, g_statements,
            "Expected \"$.\" here (last line of file).");
        if (g_statements) { /* Adjustment for error messages */
          startSection = g_Statement[g_statements].labelSectionPtr;
          g_statements--;
        }
      }
      break;
    }

    if (tmpch != '$') {
      /* 9-Jan-2018 nm Deleted - now computed by assignStmtFileAndLineNum() */
      /* if (tmpch == '\n') lineNum++; */
      fbPtr++;
      continue;
    }
    fbPtr++;
    switch (fbPtr[0]) {
      case '$': /* "$$" means literal "$" */
        fbPtr++;
        continue;
      case '(': /* Start of comment */
        /* if (insideComment) { */
          /* "Nested comments are not allowed." - detected by readRawSource */
        /* } */
        insideComment = 1;
        continue;
      case ')': /* End of comment */
        /* if (!insideComment) { */
          /* "Comment terminator found outside of comment."- detected by
             readRawSource */
        /* } */
        insideComment = 0;
        continue;
      case '!': /* Comment to end-of-line */
        if (insideComment) continue; /* Not really a comment */
        /* 8/23/99 - Made this syntax obsolete.  It was really obsolete
           before but tolerating it created problems with LaTeX output. */
        /******* Commented out these lines to force an error upon "$!"
        fbPtr = strchr(fbPtr, (int)'\n');
        if (!fbPtr) bug(1705);
        lineNum++;
        fbPtr++;
        continue;
        *******/
    }
    if (insideComment) continue;
    switch (fbPtr[0]) {
      case 'c':  type = c_; break;
      case 'v':  type = v_; break;
      case 'e':  type = e_; break;
      case 'f':  type = f_; break;
      case 'd':  type = d_; break;
      case 'a':  type = a_; dollarACount++; break;
      case 'p':  type = p_; dollarPCount++; break;
      case '{':  type = lb_; break;
      case '}':  type = rb_; break;
    }
    switch (fbPtr[0]) {
      case 'c':
      case 'v':
      case 'e':
      case 'f':
      case 'd':
      case 'a':
      case 'p':
      case '{':
      case '}':
        if (mode != 0) {
          if (mode == 2 || type != p_) {
            sourceError(fbPtr - 1, 2, g_statements,
                "Expected \"$.\" here.");
          } else {
            sourceError(fbPtr - 1, 2, g_statements,
                "Expected \"$=\" here.");
          }
          continue;
        }
        /* Initialize a new statement */
        g_statements++;
        /*** 9-Jan-2018 nm Now assigned by assignStmtFileAndLineNum() as needed
        g_Statement[g_statements].lineNum = lineNum;
        g_Statement[g_statements].fileName = fileName;
        ****/
        g_Statement[g_statements].type = type;
        g_Statement[g_statements].labelSectionPtr = startSection;
        g_Statement[g_statements].labelSectionLen = fbPtr - startSection - 1;
        /* The character after label section is used by
           assignStmtFileAndLineNum() to determine the "line number" for the
           statement as a whole */
        g_Statement[g_statements].statementPtr = startSection
            + g_Statement[g_statements].labelSectionLen; /* 9-Jan-2018 nm */
        startSection = fbPtr + 1;
        if (type != lb_ && type != rb_) mode = 1;
        continue;
      default:
        if (mode == 0) {
          sourceError(fbPtr - 1, 2, g_statements, cat(
              "Expected \"$c\", \"$v\", \"$e\", \"$f\", \"$d\",",
              " \"$a\", \"$p\", \"${\", or \"$}\" here.",NULL));
          continue;
        }
        if (mode == 1) {
          if (type == p_ && fbPtr[0] != '=') {
            sourceError(fbPtr - 1, 2, g_statements,
                "Expected \"$=\" here.");
            if (fbPtr[0] == '.') {
              mode = 2; /* If $. switch mode to help reduce error msgs */
            }
          }
          if (type != p_ && fbPtr[0] != '.') {
            sourceError(fbPtr - 1, 2, g_statements,
                "Expected \"$.\" here.");
            continue;
          }
          /* Add math section to statement */
          g_Statement[g_statements].mathSectionPtr = startSection;
          g_Statement[g_statements].mathSectionLen = fbPtr - startSection - 1;
          startSection = fbPtr + 1;
          if (type == p_ && mode != 2 /* !error msg case */) {
            mode = 2; /* Switch mode to proof section */
          } else {
            mode = 0;
          }
          continue;
        } /* End if mode == 1 */
        if (mode == 2) {
          if (fbPtr[0] != '.') {
            sourceError(fbPtr - 1, 2, g_statements,
                "Expected \"$.\" here.");
            continue;
          }
          /* Add proof section to statement */
          g_Statement[g_statements].proofSectionPtr = startSection;
          g_Statement[g_statements].proofSectionLen = fbPtr - startSection - 1;
          startSection = fbPtr + 1;
          mode = 0;
          continue;
        } /* End if mode == 2 */
    } /* End switch fbPtr[0] */
  } /* End while */

  if (fbPtr != g_sourcePtr + g_sourceLen) bug(1706);

  print2("The source has %ld statements; %ld are $a and %ld are $p.\n",
       g_statements, dollarACount, dollarPCount);

  /* Put chars after the last $. into the label section of a dummy statement */
  /* g_Statement[g_statements + 1].lineNum = lineNum - 1; */
  /* 10/14/02 Changed this to lineNum so mmwtex $t error msgs will be correct */
  /* Here, lineNum will be one plus the number of lines in the file */
  /*** 9-Jan-2018 nm Now assigned by assignStmtFileAndLineNum() as needed
  g_Statement[g_statements].lineNum = lineNum;
  g_Statement[g_statements].fileName = fileName;
  ****/
  g_Statement[g_statements + 1].type = illegal_;
  g_Statement[g_statements + 1].labelSectionPtr = startSection;
  g_Statement[g_statements + 1].labelSectionLen = fbPtr - startSection;
  /* Point to last character of file in case we ever need lineNum/fileName */
  g_Statement[g_statements + 1].statementPtr = fbPtr - 1; /* 9-Jan-2018 */

  /* 10/25/02 Initialize the pink number to print after the statement labels
   in HTML output. */
  /* The pink number only counts $a and $p statements, unlike the statement
     number which also counts $f, $e, $c, $v, ${, $} */
  j = 0;
  k = 0; /* 18-Dec-2016 nm */
  for (i = 1; i <= g_statements; i++) {
    if (g_Statement[i].type == a_ || g_Statement[i].type == p_) {

      /* 18-Dec-2016 nm */
      /* Use the statement _after_ the previous $a or $p; that is the start
         of the "header area" for use by getSectionHeadings() in mmwtex.c.
         headerStartStmt will be equal to the current statement if the
         previous statement is also a $a or $p) */
      g_Statement[i].headerStartStmt = k + 1;
      k = i;

      j++;
      g_Statement[i].pinkNumber = j;
    }
  }
  /* 10-Jan-04  Also, put the largest pink number in the last statement, no
     matter what it kind it is, so we can look up the largest number in
     pinkHTML() in mmwtex.c */
  g_Statement[g_statements].pinkNumber = j;


/*E*/if(db5){for (i=1; i<=g_statements; i++){
/*E*/  if (i == 5) { print2("(etc.)\n");} else { if (i<5) {
/*E*/  assignStmtFileAndLineNum(i);
/*E*/  print2("Statement %ld: line %ld file %s.\n",i,g_Statement[i].lineNum,
/*E*/      g_Statement[i].fileName);
/*E*/}}}}

}

/* This function parses the label sections of the g_Statement[] structure array.
   g_sourcePtr is assumed to point to the beginning of the raw input buffer.
   g_sourceLen is assumed to be length of the raw input buffer. */
void parseLabels(void)
{
  long i, j, k;
  char *fbPtr;
  char type;
  long stmt;
  flag dupFlag;

  /* Define the legal label characters */
  for (i = 0; i < 256; i++) {
    illegalLabelChar[i] = !isalnum(i);
  }
  illegalLabelChar['-'] = 0;
  illegalLabelChar['_'] = 0;
  illegalLabelChar['.'] = 0;


  /* Scan all statements and extract their labels */
  for (stmt = 1; stmt <= g_statements; stmt++) {
    type = g_Statement[stmt].type;
    fbPtr = g_Statement[stmt].labelSectionPtr;
    fbPtr = fbPtr + whiteSpaceLen(fbPtr);
    j = tokenLen(fbPtr);
    if (j) {
      for (k = 0; k < j; k++) {
        if (illegalLabelChar[(unsigned char)fbPtr[k]]) {
          sourceError(fbPtr + k, 1, stmt,
        "Only letters, digits, \"_\", \"-\", and \".\" are allowed in labels.");
          break;
        }
      }
      switch (type) {
        case d_:
        case rb_:
        case lb_:
        case v_:
        case c_:
          sourceError(fbPtr, j, stmt,
                "A label isn't allowed for this statement type.");
      }
      g_Statement[stmt].labelName = malloc((size_t)j + 1);
      if (!g_Statement[stmt].labelName) outOfMemory("#5 (label)");
      g_Statement[stmt].labelName[j] = 0;
      memcpy(g_Statement[stmt].labelName, fbPtr, (size_t)j);
      fbPtr = fbPtr + j;
      fbPtr = fbPtr + whiteSpaceLen(fbPtr);
      j = tokenLen(fbPtr);
      if (j) {
        sourceError(fbPtr, j, stmt,
            "A statement may have only one label.");
      }
    } else {
      switch (type) {
        case e_:
        case f_:
        case a_:
        case p_:
          sourceError(fbPtr, 2, stmt,
                "A label is required for this statement type.");
      }
    }
  } /* Next stmt */

  /* Make sure there is no token after the last statement */
  fbPtr = g_Statement[g_statements + 1].labelSectionPtr; /* Last (dummy) statement*/
  i = whiteSpaceLen(fbPtr);
  j = tokenLen(fbPtr + i);
  if (j) {
    sourceError(fbPtr + i, j, 0,
        "There should be no tokens after the last statement.");
  }

  /* Sort the labels for later lookup */
  g_labelKey = malloc(((size_t)g_statements + 1) * sizeof(long));
  if (!g_labelKey) outOfMemory("#6 (g_labelKey)");
  for (i = 1; i <= g_statements; i++) {
    g_labelKey[i] = i;
  }
  g_labelKeyBase = &g_labelKey[1];
  g_numLabelKeys = g_statements;
  qsort(g_labelKeyBase, (size_t)g_numLabelKeys, sizeof(long), labelSortCmp);

  /* Skip null labels. */
  for (i = 1; i <= g_statements; i++) {
    if (g_Statement[g_labelKey[i]].labelName[0]) break;
  }
  g_labelKeyBase = &g_labelKey[i];
  g_numLabelKeys = g_statements - i + 1;
/*E*/if(db5)print2("There are %ld non-empty labels.\n", g_numLabelKeys);
/*E*/if(db5){print2("The first (up to 5) sorted labels are:\n");
/*E*/  for (i=0; i<5; i++) {
/*E*/    if (i >= g_numLabelKeys) break;
/*E*/    print2("%s ",g_Statement[g_labelKeyBase[i]].labelName);
/*E*/  } print2("\n");}



  /* Copy the keys for all possible labels for lookup by the
     squishProof command when local labels are generated in packed proofs. */
  g_allLabelKeyBase = malloc((size_t)g_numLabelKeys * sizeof(long));
  if (!g_allLabelKeyBase) outOfMemory("#60 (g_allLabelKeyBase)");
  memcpy(g_allLabelKeyBase, g_labelKeyBase, (size_t)g_numLabelKeys * sizeof(long));
  g_numAllLabelKeys = g_numLabelKeys;

  /* Now back to the regular label stuff. */
  /* Check for duplicate labels */
  /* (This will go away if local labels on hypotheses are allowed.) */
  /* 17-Sep-2005 nm - This code was reinstated to conform to strict spec.
     The old check for duplicate active labels (see other comment for this
     date below) was removed since it becomes redundant . */
  dupFlag = 0;
  for (i = 0; i < g_numLabelKeys; i++) {
    if (dupFlag) {
      /* This "if" condition causes only the 2nd in a pair of duplicate labels
         to have an error message. */
      dupFlag = 0;
      if (!strcmp(g_Statement[g_labelKeyBase[i]].labelName,
          g_Statement[g_labelKeyBase[i - 1]].labelName)) dupFlag = 1;
    }
    if (i < g_numLabelKeys - 1) {
      if (!strcmp(g_Statement[g_labelKeyBase[i]].labelName,
          g_Statement[g_labelKeyBase[i + 1]].labelName)) dupFlag = 1;
    }
    if (dupFlag) {
      fbPtr = g_Statement[g_labelKeyBase[i]].labelSectionPtr;
      k = whiteSpaceLen(fbPtr);
      j = tokenLen(fbPtr + k);
      sourceError(fbPtr + k, j, g_labelKeyBase[i],
         "This label is declared more than once.  All labels must be unique.");
    }
  }

}

/* This functions retrieves all possible math symbols from $c and $v
   statements. */
void parseMathDecl(void)
{
  long potentialSymbols;
  long stmt;
  char *fbPtr;
  long i, j, k;
  char *tmpPtr;
  nmbrString *nmbrTmpPtr;
  long oldG_mathTokens;
  void *voidPtr; /* bsearch returned value */  /* 4-Jun-06 nm */

  /* Find the upper limit of the number of symbols declared for
     pre-allocation:  at most, the number of symbols is half the number of
     characters, since $c and $v statements require white space. */
  potentialSymbols = 0;
  for (stmt = 1; stmt <= g_statements; stmt++) {
    switch (g_Statement[stmt].type) {
      case c_:
      case v_:
        potentialSymbols = potentialSymbols + g_Statement[stmt].mathSectionLen;
    }
  }
  potentialSymbols = (potentialSymbols / 2) + 2;
/*E*/if(db5)print2("%ld potential symbols were computed.\n",potentialSymbols);
  g_MathToken = realloc(g_MathToken, (size_t)potentialSymbols *
      sizeof(struct mathToken_struct));
  if (!g_MathToken) outOfMemory("#7 (g_MathToken)");

  /* Scan $c and $v statements to accumulate all possible math symbols */
  g_mathTokens = 0;
  for (stmt = 1; stmt <= g_statements; stmt++) {
    switch (g_Statement[stmt].type) {
      case c_:
      case v_:
        oldG_mathTokens = g_mathTokens;
        fbPtr = g_Statement[stmt].mathSectionPtr;
        while (1) {
          i = whiteSpaceLen(fbPtr);
          j = tokenLen(fbPtr + i);
          if (!j) break;
          tmpPtr = malloc((size_t)j + 1); /* Math symbol name */
          if (!tmpPtr) outOfMemory("#8 (symbol name)");
          tmpPtr[j] = 0; /* End of string */
          memcpy(tmpPtr, fbPtr + i, (size_t)j);
          fbPtr = fbPtr + i + j;
          /* Create a new math symbol */
          g_MathToken[g_mathTokens].tokenName = tmpPtr;
          g_MathToken[g_mathTokens].length = j;
          if (g_Statement[stmt].type == c_) {
            g_MathToken[g_mathTokens].tokenType = (char)con_;
          } else {
            g_MathToken[g_mathTokens].tokenType = (char)var_;
          }
          g_MathToken[g_mathTokens].active = 0;
          g_MathToken[g_mathTokens].scope = 0; /* Unknown for now */
          g_MathToken[g_mathTokens].tmp = 0; /* Not used for now */
          g_MathToken[g_mathTokens].statement = stmt;
          g_MathToken[g_mathTokens].endStatement = g_statements; /* Unknown for now */
                /* (Assign to 'g_statements' in case it's active until the end) */
          g_mathTokens++;

        }

        /* Create the symbol list for this statement */
        j = g_mathTokens - oldG_mathTokens; /* Number of tokens in this statement */
        nmbrTmpPtr = poolFixedMalloc((j + 1) * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#9 (symbol table)"); */ /*??? Not nec. with poolMalloc */
        nmbrTmpPtr[j] = -1;
        for (i = 0; i < j; i++) {
          nmbrTmpPtr[i] = oldG_mathTokens + i;
        }
        g_Statement[stmt].mathString = nmbrTmpPtr;
        g_Statement[stmt].mathStringLen = j;
        if (!j) {
          sourceError(fbPtr, 2, stmt,
           "At least one math symbol should be declared.");
        }
    } /* end switch (g_Statement[stmt].type) */
  } /* next stmt */

/*E*/if(db5)print2("%ld math symbols were declared.\n",g_mathTokens);
  /* Reallocate from potential to actual to reduce memory space */
  /* Add 100 to allow for initial Proof Assistant use, and up to 100
     errors in undeclared token references */
  g_MAX_MATHTOKENS = g_mathTokens + 100;
  g_MathToken = realloc(g_MathToken, (size_t)g_MAX_MATHTOKENS *
      sizeof(struct mathToken_struct));
  if (!g_MathToken) outOfMemory("#10 (g_MathToken)");

  /* Create a special "$|$" boundary token to separate real and dummy ones */
  g_MathToken[g_mathTokens].tokenName = "";
  let(&g_MathToken[g_mathTokens].tokenName, "$|$");
  g_MathToken[g_mathTokens].length = 2; /* Never used */
  g_MathToken[g_mathTokens].tokenType = (char)con_;
  g_MathToken[g_mathTokens].active = 0; /* Never used */
  g_MathToken[g_mathTokens].scope = 0; /* Never used */
  g_MathToken[g_mathTokens].tmp = 0; /* Never used */
  g_MathToken[g_mathTokens].statement = 0; /* Never used */
  g_MathToken[g_mathTokens].endStatement = g_statements; /* Never used */


  /* Sort the math symbols for later lookup */
  g_mathKey = malloc((size_t)g_mathTokens * sizeof(long));
  if (!g_mathKey) outOfMemory("#11 (g_mathKey)");
  for (i = 0; i < g_mathTokens; i++) {
    g_mathKey[i] = i;
  }
  qsort(g_mathKey, (size_t)g_mathTokens, sizeof(long), mathSortCmp);
/*E*/if(db5){print2("The first (up to 5) sorted math tokens are:\n");
/*E*/  for (i=0; i<5; i++) {
/*E*/    if (i >= g_mathTokens) break;
/*E*/    print2("%s ",g_MathToken[g_mathKey[i]].tokenName);
/*E*/  } print2("\n");}


  /* 4-Jun-06 nm Check for labels with the same name as math tokens */
  /* (This section implements the Metamath spec change proposed by O'Cat that
     lets labels and math tokens occupy the same namespace and thus forbids
     them from having common names.) */
  /* For maximum speed, we scan M math tokens and look each up in the list
     of L labels.  The we have M * log L comparisons, which is optimal when
     (as in most cases) M << L. */
  for (i = 0; i < g_mathTokens; i++) {
    /* See if the math token is in the list of labels */
    voidPtr = (void *)bsearch(g_MathToken[i].tokenName, g_labelKeyBase,
        (size_t)g_numLabelKeys, sizeof(long), labelSrchCmp);
    if (voidPtr) { /* A label matching the token was found */
      stmt = (*(long *)voidPtr); /* Statement number */
      fbPtr = g_Statement[stmt].labelSectionPtr;
      k = whiteSpaceLen(fbPtr);
      j = tokenLen(fbPtr + k);
      /* Note that the line and file are only assigned when requested,
         for speedup. */
      assignStmtFileAndLineNum(stmt); /* 9-Jan-2018 nm */
      assignStmtFileAndLineNum(g_MathToken[i].statement); /* 10-Dec-2019 nm */
      sourceError(fbPtr + k, j, stmt, cat(
         "This label has the same name as the math token declared on line ",
         str((double)(g_Statement[g_MathToken[i].statement].lineNum)),
         " of file \"",
         g_Statement[g_MathToken[i].statement].fileName,
         "\".", NULL));
    }
  }
  /* End of 4-Jun-06 */


}


/* This functions parses statement contents, except for proofs */
void parseStatements(void)
{
  long stmt;
  char type;
  long i, j, k, m, n, p;
  char *fbPtr;
  long mathStringLen;
  long tokenNum;
  long lowerKey, upperKey;
  long symbolLen, origSymbolLen, mathSectionLen, g_mathKeyNum;
  void *g_mathKeyPtr; /* bsearch returned value */
  int maxScope;
  long reqHyps, optHyps, reqVars, optVars;
  flag reqFlag;
  int undeclErrorCount = 0;
  vstring tmpStr = "";

  nmbrString *nmbrTmpPtr;

  long *mathTokenSameAs; /* Flag that symbol is unique (for speed up) */
  long *reverseMathKey; /* Map from g_mathTokens to g_mathKey */

  long *labelTokenSameAs; /* Flag that label is unique (for speed up) */
  long *reverseLabelKey; /* Map from statement # to label key */
  flag *labelActiveFlag; /* Flag that label is active */

  struct activeConstStack_struct {
    long tokenNum;
    int scope;
  };
  struct activeConstStack_struct *activeConstStack; /* Stack of active consts */
  long activeConstStackPtr = 0;

  struct activeVarStack_struct {
    long tokenNum;
    int scope;
    char tmpFlag; /* Used by hypothesis variable scan; must be 0 otherwise */
  };
  struct activeVarStack_struct *activeVarStack; /* Stack of active variables */
  nmbrString *wrkVarPtr1;
  nmbrString *wrkVarPtr2;
  long activeVarStackPtr = 0;

  struct activeEHypStack_struct { /* Stack of $e hypotheses */
    long statemNum;
    nmbrString *varList; /* List of variables in the hypothesis */
    int scope;
  };
  struct activeEHypStack_struct *activeEHypStack;
  long activeEHypStackPtr = 0;
  struct activeFHypStack_struct { /* Stack of $f hypotheses */
    long statemNum;
    nmbrString *varList; /* List of variables in the hypothesis */
    int scope;
  };
  struct activeFHypStack_struct *activeFHypStack;
  long activeFHypStackPtr = 0;
  nmbrString *wrkHypPtr1;
  nmbrString *wrkHypPtr2;
  nmbrString *wrkHypPtr3;
  long activeHypStackSize = 30; /* Starting value; could be as large as
                                   g_statements. */


  struct activeDisjHypStack_struct { /* Stack of disjoint variables in $d's */
    long tokenNumA; /* First variable in disjoint pair */
    long tokenNumB; /* Second variable in disjoint pair */
    long statemNum; /* Statement it occurred in */
    int scope;
  };
  struct activeDisjHypStack_struct *activeDisjHypStack;
  nmbrString *wrkDisjHPtr1A;
  nmbrString *wrkDisjHPtr1B;
  nmbrString *wrkDisjHPtr1Stmt;
  nmbrString *wrkDisjHPtr2A;
  nmbrString *wrkDisjHPtr2B;
  nmbrString *wrkDisjHPtr2Stmt;
  long activeDisjHypStackPtr = 0;
  long activeDisjHypStackSize = 30; /* Starting value; could be as large as
                                        about g_mathTokens^2/2 */

  /* Temporary working space */
  long wrkLen;
  nmbrString *wrkNmbrPtr;
  char *wrkStrPtr;

  long maxSymbolLen; /* Longest math symbol (for speedup) */
  flag *symbolLenExists; /* A symbol with this length exists (for speedup) */

  long beginScopeStmtNum = 0;

  /* Initialization to avoid compiler warning (should not be theoretically
     necessary) */
  mathStringLen = 0;
  tokenNum = 0;

  /* Initialize flags for g_mathKey array that identify math symbols as
     unique (when 0) or, if not unique, the flag is a number identifying a group
     of identical names */
  mathTokenSameAs = malloc((size_t)g_mathTokens * sizeof(long));
  if (!mathTokenSameAs) outOfMemory("#12 (mathTokenSameAs)");
  reverseMathKey = malloc((size_t)g_mathTokens * sizeof(long));
  if (!reverseMathKey) outOfMemory("#13 (reverseMathKey)");
  for (i = 0; i < g_mathTokens; i++) {
    mathTokenSameAs[i] = 0; /* 0 means unique */
    reverseMathKey[g_mathKey[i]] = i; /* Initialize reverse map to g_mathKey */
  }
  for (i = 1; i < g_mathTokens; i++) {
    if (!strcmp(g_MathToken[g_mathKey[i]].tokenName,
        g_MathToken[g_mathKey[i - 1]].tokenName)) {
      if (!mathTokenSameAs[i - 1]) mathTokenSameAs[i - 1] = i;
      mathTokenSameAs[i] = mathTokenSameAs[i - 1];
    }
  }

  /* Initialize flags for g_labelKey array that identify labels as
     unique (when 0) or, if not unique, the flag is a number identifying a group
     of identical names */
  labelTokenSameAs = malloc(((size_t)g_statements + 1) * sizeof(long));
  if (!labelTokenSameAs) outOfMemory("#112 (labelTokenSameAs)");
  reverseLabelKey = malloc(((size_t)g_statements + 1) * sizeof(long));
  if (!reverseLabelKey) outOfMemory("#113 (reverseLabelKey)");
  labelActiveFlag = malloc(((size_t)g_statements + 1) * sizeof(flag));
  if (!labelActiveFlag) outOfMemory("#114 (labelActiveFlag)");
  for (i = 1; i <= g_statements; i++) {
    labelTokenSameAs[i] = 0; /* Initialize:  0 = unique */
    reverseLabelKey[g_labelKey[i]] = i; /* Initialize reverse map to g_labelKey */
    labelActiveFlag[i] = 0; /* Initialize */
  }
  for (i = 2; i <= g_statements; i++) {
    if (!strcmp(g_Statement[g_labelKey[i]].labelName,
        g_Statement[g_labelKey[i - 1]].labelName)) {
      if (!labelTokenSameAs[i - 1]) labelTokenSameAs[i - 1] = i;
      labelTokenSameAs[i] = labelTokenSameAs[i - 1];
    }
  }

  /* Initialize variable and hypothesis stacks */

  /* Allocate g_MAX_MATHTOKENS and not just g_mathTokens of them so that
     they can accomodate any extra non-declared tokens (which get
     declared as part of error handling, where the g_MAX_MATHTOKENS
     limit is checked) */
  activeConstStack = malloc((size_t)g_MAX_MATHTOKENS
      * sizeof(struct activeConstStack_struct));
  activeVarStack = malloc((size_t)g_MAX_MATHTOKENS
      * sizeof(struct activeVarStack_struct));
  wrkVarPtr1 = malloc((size_t)g_MAX_MATHTOKENS * sizeof(nmbrString));
  wrkVarPtr2 = malloc((size_t)g_MAX_MATHTOKENS * sizeof(nmbrString));
  if (!activeConstStack || !activeVarStack || !wrkVarPtr1 || !wrkVarPtr2)
      outOfMemory("#14 (activeVarStack)");

  activeEHypStack = malloc((size_t)activeHypStackSize
      * sizeof(struct activeEHypStack_struct));
  activeFHypStack = malloc((size_t)activeHypStackSize
      * sizeof(struct activeFHypStack_struct));
  wrkHypPtr1 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
  wrkHypPtr2 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
  wrkHypPtr3 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
  if (!activeEHypStack || !activeFHypStack || !wrkHypPtr1 || !wrkHypPtr2 ||
      !wrkHypPtr3)
      outOfMemory("#15 (activeHypStack)");

  activeDisjHypStack = malloc((size_t)activeDisjHypStackSize *
      sizeof(struct activeDisjHypStack_struct));
  wrkDisjHPtr1A = malloc((size_t)activeDisjHypStackSize * sizeof(nmbrString));
  wrkDisjHPtr1B = malloc((size_t)activeDisjHypStackSize * sizeof(nmbrString));
  wrkDisjHPtr1Stmt = malloc((size_t)activeDisjHypStackSize
      * sizeof(nmbrString));
  wrkDisjHPtr2A = malloc((size_t)activeDisjHypStackSize * sizeof(nmbrString));
  wrkDisjHPtr2B = malloc((size_t)activeDisjHypStackSize * sizeof(nmbrString));
  wrkDisjHPtr2Stmt = malloc((size_t)activeDisjHypStackSize
      * sizeof(nmbrString));
  if (!activeDisjHypStack
      || !wrkDisjHPtr1A || !wrkDisjHPtr1B || !wrkDisjHPtr1Stmt
      || !wrkDisjHPtr2A || !wrkDisjHPtr2B || !wrkDisjHPtr2Stmt)
      outOfMemory("#27 (activeDisjHypStack)");

  /* Initialize temporary working space for parsing tokens */
  wrkLen = 1;
  wrkNmbrPtr = malloc((size_t)wrkLen * sizeof(nmbrString));
  if (!wrkNmbrPtr) outOfMemory("#22 (wrkNmbrPtr)");
  wrkStrPtr = malloc((size_t)wrkLen + 1);
  if (!wrkStrPtr) outOfMemory("#23 (wrkStrPtr)");

  /* Find declared math symbol lengths (used to speed up parsing) */
  maxSymbolLen = 0;
  for (i = 0; i < g_mathTokens; i++) {
    if (g_MathToken[i].length > maxSymbolLen) {
      maxSymbolLen = g_MathToken[i].length;
    }
  }
  symbolLenExists = malloc(((size_t)maxSymbolLen + 1) * sizeof(flag));
  if (!symbolLenExists) outOfMemory("#25 (symbolLenExists)");
  for (i = 0; i <= maxSymbolLen; i++) {
    symbolLenExists[i] = 0;
  }
  for (i = 0; i < g_mathTokens; i++) {
    symbolLenExists[g_MathToken[i].length] = 1;
  }


  g_currentScope = 0;
  beginScopeStmtNum = 0;

  /* Scan all statements.  Fill in statement structure and look for errors. */
  for (stmt = 1; stmt <= g_statements; stmt++) {

#ifdef VAXC
    /* This line fixes an obscure bug with the VAXC compiler.  If it is taken
       out, the variable 'stmt' does not get referenced properly when used as
       an array index.  May be due to some boundary condition in optimization?
       The assembly code is significantly different with this statement
       removed. */
    stmt = stmt;  /* Work around VAXC bug */
#endif

    g_Statement[stmt].beginScopeStatementNum = beginScopeStmtNum;
    /* endScopeStatementNum is always 0 except in ${ statements */
    g_Statement[stmt].endScopeStatementNum = 0; /* 24-Aug-2020 nm */
    g_Statement[stmt].scope = g_currentScope;
    type = g_Statement[stmt].type;
    /******* Determine scope, stack active variables, process math strings ****/

    switch (type) {
      case lb_:
        g_currentScope++;
        if (g_currentScope > 32000) outOfMemory("#16 (more than 32000 \"${\"s)");
            /* Not really an out-of-memory situation, but use the error msg. */
        /* Note that g_Statement[stmt].beginScopeStatementNum for this ${
           points to the previous ${ (or 0 if in outermost scope) */
        beginScopeStmtNum = stmt; /* 24-Aug-2020 nm */
        /* Note that g_Statement[stmt].endScopeStatementNum for this ${
           will be assigned in the rb_ case below. */
        break;
      case rb_:
        /* Remove all variables and hypotheses in current scope from stack */

        while (activeConstStackPtr) {
          if (activeConstStack[activeConstStackPtr - 1].scope < g_currentScope)
              break;
          activeConstStackPtr--;
          g_MathToken[activeConstStack[activeConstStackPtr].tokenNum].active = 0;
          g_MathToken[activeConstStack[activeConstStackPtr].tokenNum
              ].endStatement = stmt;
        }

        while (activeVarStackPtr) {
          if (activeVarStack[activeVarStackPtr - 1].scope < g_currentScope) break;
          activeVarStackPtr--;
          g_MathToken[activeVarStack[activeVarStackPtr].tokenNum].active = 0;
          g_MathToken[activeVarStack[activeVarStackPtr].tokenNum].endStatement
              = stmt;
        }

        while (activeEHypStackPtr) {
          if (activeEHypStack[activeEHypStackPtr - 1].scope < g_currentScope)
              break;
          activeEHypStackPtr--;
          labelActiveFlag[activeEHypStack[activeEHypStackPtr].statemNum] = 0;
                                                   /* Make the label inactive */
          free(activeEHypStack[activeEHypStackPtr].varList);
        }
        while (activeFHypStackPtr) {
          if (activeFHypStack[activeFHypStackPtr - 1].scope < g_currentScope)
              break;
          activeFHypStackPtr--;
          labelActiveFlag[activeFHypStack[activeFHypStackPtr].statemNum] = 0;
                                                   /* Make the label inactive */
          free(activeFHypStack[activeFHypStackPtr].varList);
        }
        while (activeDisjHypStackPtr) {
          if (activeDisjHypStack[activeDisjHypStackPtr - 1].scope
              < g_currentScope) break;
          activeDisjHypStackPtr--;
        }
        g_currentScope--;
        if (g_currentScope < 0) {
          sourceError(g_Statement[stmt].labelSectionPtr +
              g_Statement[stmt].labelSectionLen, 2, stmt,
              "Too many \"$}\"s at this point.");
        }

        /* 24-Aug-2020 nm */
        if (beginScopeStmtNum > 0) { /* We're not in outermost scope
                  (precaution if there were too many $}'s) */
          if (g_Statement[beginScopeStmtNum].type != lb_) bug(1773);
          /* Populate the previous ${ with a pointer to this $} */
          g_Statement[beginScopeStmtNum].endScopeStatementNum = stmt;
          /* Update beginScopeStmtNum with start of outer scope */
          beginScopeStmtNum
              = g_Statement[beginScopeStmtNum].beginScopeStatementNum;
        }

        break;
      case c_:
      case v_:
        /* Scan all symbols declared (they have already been parsed) and
           flag them as active, add to stack, and check for errors */

        /* 3-Jun-2018 nm Added this check back in (see 1-May-2018 Google
           Group post) */
        if (type == c_) {
          if (g_currentScope > 0) {
            sourceError(g_Statement[stmt].labelSectionPtr +
                g_Statement[stmt].labelSectionLen, 2, stmt,
        "A \"$c\" constant declaration may occur in the outermost scope only.");
          }
        }


        i = 0; /* Symbol position in mathString */
        nmbrTmpPtr = g_Statement[stmt].mathString;
        while (1) {
          tokenNum = nmbrTmpPtr[i];
          if (tokenNum == -1) break; /* Done scanning symbols in $v or $c */
          if (mathTokenSameAs[reverseMathKey[tokenNum]]) {
            /* The variable name is not unique.  Find out if there's a
               conflict with the others. */
            lowerKey = reverseMathKey[tokenNum];
            upperKey = lowerKey;
            j = mathTokenSameAs[lowerKey];
            while (lowerKey) {
              if (j != mathTokenSameAs[lowerKey - 1]) break;
              lowerKey--;
            }
            while (upperKey < g_mathTokens - 1) {
              if (j != mathTokenSameAs[upperKey + 1]) break;
              upperKey++;
            }
            for (j = lowerKey; j <= upperKey; j++) {
              if (g_MathToken[g_mathKey[j]].active) {
                /* 18-Jun-2011 nm Detect conflicting active vars declared
                   in multiple scopes */
                if (g_MathToken[g_mathKey[j]].scope <= g_currentScope) {
                /* if (g_MathToken[g_mathKey[j]].scope == g_currentScope) { bad */
                  mathTokenError(i, nmbrTmpPtr, stmt,
                      "This symbol has already been declared in this scope.");
                }
              }
            }


            /************** Start of 9-Dec-2010 ****************/
            /* 9-Dec-2010 nm Make sure that no constant has the same name
               as a variable or vice-versa */
            k = 0; /* Flag for $c */
            m = 0; /* Flag for $v */
            for (j = lowerKey; j <= upperKey; j++) {
              if (g_MathToken[g_mathKey[j]].tokenType == (char)con_) k = 1;
              if (g_MathToken[g_mathKey[j]].tokenType == (char)var_) m = 1;
            }
            if ((k == 1 && g_MathToken[tokenNum].tokenType == (char)var_) ||
                (m == 1 && g_MathToken[tokenNum].tokenType == (char)con_)) {
               mathTokenError(i, nmbrTmpPtr, stmt,
                   "A symbol may not be both a constant and a variable.");
            }
            /************** End of 9-Dec-2010 ****************/

          }

          /* Flag the token as active */
          g_MathToken[tokenNum].active = 1;
          g_MathToken[tokenNum].scope = g_currentScope;

          if (type == v_) {

            /* Identify this stack position in the g_MathToken array, for use
               by the hypothesis variable scan below */
            g_MathToken[tokenNum].tmp = activeVarStackPtr;

            /* Add the symbol to the stack */
            activeVarStack[activeVarStackPtr].tokenNum = tokenNum;
            activeVarStack[activeVarStackPtr].scope = g_currentScope;
            activeVarStack[activeVarStackPtr].tmpFlag = 0;
            activeVarStackPtr++;
          } else {

            /* Add the symbol to the stack */
            activeConstStack[activeConstStackPtr].tokenNum = tokenNum;
            activeConstStack[activeConstStackPtr].scope = g_currentScope;
            activeConstStackPtr++;

          }

          i++;
        }
        break;
      case d_:
      case f_:
      case e_:
      case a_:
      case p_:
        /* Make sure we have enough working space */
        mathSectionLen = g_Statement[stmt].mathSectionLen;
        if (wrkLen < mathSectionLen) {
          free(wrkNmbrPtr);
          free(wrkStrPtr);
          wrkLen = mathSectionLen + 100;
          wrkNmbrPtr = malloc((size_t)wrkLen * sizeof(nmbrString));
          if (!wrkNmbrPtr) outOfMemory("#20 (wrkNmbrPtr)");
          wrkStrPtr = malloc((size_t)wrkLen + 1);
          if (!wrkStrPtr) outOfMemory("#21 (wrkStrPtr)");
        }

        /* Scan the math section for tokens */
        mathStringLen = 0;
        fbPtr = g_Statement[stmt].mathSectionPtr;
        while (1) {
          fbPtr = fbPtr + whiteSpaceLen(fbPtr);
          origSymbolLen = tokenLen(fbPtr);
          if (!origSymbolLen) break; /* Done scanning source line */

          /* Scan for largest matching token from the left */
         nextAdjToken:
          /* maxSymbolLen is the longest declared symbol */
          /* 18-Sep-2013 Disable unused old code
          if (origSymbolLen > maxSymbolLen) {
            symbolLen = maxSymbolLen;
          } else {
            symbolLen = origSymbolLen;
          }
          */

          /* New code: don't allow missing white space */
          symbolLen = origSymbolLen;

          memcpy(wrkStrPtr, fbPtr, (size_t)symbolLen);

          /* Old code: tolerate unambiguous missing white space
          for (; symbolLen > 0; symbolLen--) {
          */
          /* New code: don't allow missing white space */
          /* ???Speed-up is possible by rewriting this now unnec. code */
          for (; symbolLen > 0; symbolLen = 0) {

            /* symbolLenExists means a symbol of this length was declared */
            if (!symbolLenExists[symbolLen]) continue;
            wrkStrPtr[symbolLen] = 0; /* Define end of trial token to look up */
            g_mathKeyPtr = (void *)bsearch(wrkStrPtr, g_mathKey, (size_t)g_mathTokens,
                sizeof(long), mathSrchCmp);
            if (!g_mathKeyPtr) continue; /* Trial token was not declared */
            g_mathKeyNum = (long *)g_mathKeyPtr - g_mathKey; /* Pointer arithmetic! */
            if (mathTokenSameAs[g_mathKeyNum]) { /* Multiply-declared symbol */
              lowerKey = g_mathKeyNum;
              upperKey = lowerKey;
              j = mathTokenSameAs[lowerKey];
              while (lowerKey) {
                if (j != mathTokenSameAs[lowerKey - 1]) break;
                lowerKey--;
              }
              while (upperKey < g_mathTokens - 1) {
                if (j != mathTokenSameAs[upperKey + 1]) break;
                upperKey++;
              }
              /* Find the active symbol with the most recent declaration */
              /* (Note:  Here, 'active' means it's on the stack, not the
                 official def.) */
              maxScope = -1;
              for (i = lowerKey; i <= upperKey; i++) {
                j = g_mathKey[i];
                if (g_MathToken[j].active) {
                  if (g_MathToken[j].scope > maxScope) {
                    tokenNum = j;
                    maxScope = g_MathToken[j].scope;
                    if (maxScope == g_currentScope) break; /* Speedup */
                  }
                }
              }
              if (maxScope == -1) {
                tokenNum = g_mathKey[g_mathKeyNum]; /* Pick an arbitrary one */
                sourceError(fbPtr, symbolLen, stmt,
       "This math symbol is not active (i.e. was not declared in this scope).");
                /*??? (This is done in 3 places. Make it a fn call & clean up?*/
                /* Prevent stray pointers later */
                g_MathToken[tokenNum].tmp = 0; /* Loc in active variable stack */
                if (!activeVarStackPtr) { /* Make a ficticious entry */
                  activeVarStack[activeVarStackPtr].tokenNum = tokenNum;
                  activeVarStack[activeVarStackPtr].scope = g_currentScope;
                  activeVarStack[activeVarStackPtr].tmpFlag = 0;
                  activeVarStackPtr++;
                }
              }
            } else { /* The symbol was declared only once. */
              tokenNum = *((long *)g_mathKeyPtr);
                  /* Same as: tokenNum = g_mathKey[g_mathKeyNum]; but faster */
              if (!g_MathToken[tokenNum].active) {
                sourceError(fbPtr, symbolLen, stmt,
       "This math symbol is not active (i.e. was not declared in this scope).");
                /* Prevent stray pointers later */
                g_MathToken[tokenNum].tmp = 0; /* Loc in active variable stack */
                if (!activeVarStackPtr) { /* Make a ficticious entry */
                  activeVarStack[activeVarStackPtr].tokenNum = tokenNum;
                  activeVarStack[activeVarStackPtr].scope = g_currentScope;
                  activeVarStack[activeVarStackPtr].tmpFlag = 0;
                  activeVarStackPtr++;
                }
              }
            } /* End if multiply-defined symbol */
            break; /* The symbol was found, so we are done */
          } /* Next symbolLen */
          if (symbolLen == 0) { /* Symbol was not found */
            symbolLen = tokenLen(fbPtr);
            sourceError(fbPtr, symbolLen, stmt,
      "This math symbol was not declared (with a \"$c\" or \"$v\" statement).");
            /* Call the symbol a dummy token of type variable so that spurious
               errors (constants in $d's) won't be flagged also.  Prevent
               stray pointer to active variable stack. */
            undeclErrorCount++;
            tokenNum = g_mathTokens + undeclErrorCount;
            if (tokenNum >= g_MAX_MATHTOKENS) {
              /* 21-Aug-04 nm */
              /* There are current 100 places for bad tokens */
              print2(
"?Error: The temporary space for holding bad tokens has run out, because\n");
              print2(
"there are too many errors.  Therefore we will force an \"out of memory\"\n");
              print2("program abort:\n");
              outOfMemory("#33 (too many errors)");
            }
            g_MathToken[tokenNum].tokenName = "";
            let(&g_MathToken[tokenNum].tokenName, left(fbPtr,symbolLen));
            g_MathToken[tokenNum].length = symbolLen;
            g_MathToken[tokenNum].tokenType = (char)var_;
            /* Prevent stray pointers later */
            g_MathToken[tokenNum].tmp = 0; /* Location in active variable stack */
            if (!activeVarStackPtr) { /* Make a ficticious entry */
              activeVarStack[activeVarStackPtr].tokenNum = tokenNum;
              activeVarStack[activeVarStackPtr].scope = g_currentScope;
              activeVarStack[activeVarStackPtr].tmpFlag = 0;
              activeVarStackPtr++;
            }
          }

          if (type == d_) {
            if (g_MathToken[tokenNum].tokenType == (char)con_) {
              sourceError(fbPtr, symbolLen, stmt,
                  "Constant symbols are not allowed in a \"$d\" statement.");
            }
          } else {
            if (mathStringLen == 0) {
              if (g_MathToken[tokenNum].tokenType != (char)con_) {
                sourceError(fbPtr, symbolLen, stmt, cat(
                    "The first symbol must be a constant in a \"$",
                    chr(type), "\" statement.", NULL));
              }
            } else {
              if (type == f_) {
                if (mathStringLen == 1) {
                  if (g_MathToken[tokenNum].tokenType == (char)con_) {
                    sourceError(fbPtr, symbolLen, stmt,
                "The second symbol must be a variable in a \"$f\" statement.");
                  }
                } else {
                  if (mathStringLen == 2) {
                    sourceError(fbPtr, symbolLen, stmt,
               "There cannot be more than two symbols in a \"$f\" statement.");
                  }
                }
              }
            }
          }

          /* Add symbol to mathString */
          wrkNmbrPtr[mathStringLen] = tokenNum;
          mathStringLen++;
          fbPtr = fbPtr + symbolLen; /* Move on to next symbol */

          if (symbolLen < origSymbolLen) {
            /* This symbol is not separated from next by white space */
            /* Speed-up: don't call tokenLen again; just jump past it */
            origSymbolLen = origSymbolLen - symbolLen;
            goto nextAdjToken; /* (Instead of continue) */
          }
        } /* End while */

        if (type == d_) {
          if (mathStringLen < 2) {
            sourceError(fbPtr, 2, stmt,
                "A \"$d\" statement requires at least two variable symbols.");
          }
        } else {
          if (!mathStringLen) {
            sourceError(fbPtr, 2, stmt,
                "This statement type requires at least one math symbol.");
          } else {
            if (type == f_ && mathStringLen < 2) {
              sourceError(fbPtr, 2, stmt,
                  "A \"$f\" statement requires two math symbols.");
            }
          }
        }


        /* Assign mathString to statement array */
        nmbrTmpPtr = poolFixedMalloc(
            (mathStringLen + 1) * (long)(sizeof(nmbrString)));
        /*if (!nmbrTmpPtr) outOfMemory("#24 (mathString)");*/ /*???Not nec. w/ poolMalloc */
        for (i = 0; i < mathStringLen; i++) {
          nmbrTmpPtr[i] = wrkNmbrPtr[i];
        }
        nmbrTmpPtr[mathStringLen] = -1;
        g_Statement[stmt].mathString = nmbrTmpPtr;
        g_Statement[stmt].mathStringLen = mathStringLen;
/*E*/if(db5){if(stmt<5)print2("Statement %ld mathString: %s.\n",stmt,
/*E*/  nmbrCvtMToVString(nmbrTmpPtr)); if(stmt==5)print2("(etc.)\n");}

        break;  /* Switch case break */
      default:
        bug(1707);

    } /* End switch */

    /****** Process hypothesis and variable stacks *******/
    /* (The switch section above does not depend on what is done in this
       section, although this section assumes the above section has been done.
       Variables valid only in this pass of the above section are so
       indicated.) */

    switch (type) {
      case f_:
      case e_:
      case a_:
      case p_:
        /* These types have labels.  Make the label active, and make sure that
           there is no other identical label that is also active. */
        /* (If the label name is unique, we don't have to worry about this.) */
        /* 17-Sep-05 nm - This check is no longer needed since all labels
           must now be unique according to strict spec (see the other comment
           for this date above).  So the code below was commented out. */
        /*
        if (labelTokenSameAs[reverseLabelKey[stmt]]) {
          /@ The label is not unique.  Find out if there's a
             conflict with the others. @/
          lowerKey = reverseLabelKey[stmt];
          upperKey = lowerKey;
          j = labelTokenSameAs[lowerKey];
          while (lowerKey > 1) {
            if (j != labelTokenSameAs[lowerKey - 1]) break;
            lowerKey--;
          }
          while (upperKey < g_statements) {
            if (j != labelTokenSameAs[upperKey + 1]) break;
            upperKey++;
          }
          for (j = lowerKey; j <= upperKey; j++) {
            if (labelActiveFlag[g_labelKey[j]]) {
              fbPtr = g_Statement[stmt].labelSectionPtr;
              fbPtr = fbPtr + whiteSpaceLen(fbPtr);
              sourceError(fbPtr, tokenLen(fbPtr), g_labelKey[j], cat(
                  "This label name is currently active (i.e. in use).",
                  "  It became active at statement ",
                  str(g_labelKey[j]),
                  ", line ", str(g_Statement[g_labelKey[j]].lineNum),
                  ", file \"", g_Statement[g_labelKey[j]].fileName,
                  "\".  Use another name for this label.", NULL));
              break;
            }
          }
        }
        */

        /* Flag the label as active */
        labelActiveFlag[stmt] = 1;

    } /* End switch */


    switch (type) {
      case d_:

        nmbrTmpPtr = g_Statement[stmt].mathString;
        /* Stack all possible pairs of disjoint variables */
        for (i = 0; i < mathStringLen - 1; i++) { /* mathStringLen is from the
             above switch section; it is valid only in this pass of the above
             section. */
          p = nmbrTmpPtr[i];
          for (j = i + 1; j < mathStringLen; j++) {
            n = nmbrTmpPtr[j];
            /* Get the disjoint variable pair m and n, sorted by tokenNum */
            if (p < n) {
              m = p;
            } else {
              if (p > n) {
                /* Swap them */
                m = n;
                n = p;
              } else {
                mathTokenError(j, nmbrTmpPtr, stmt,
                    "All variables in a \"$d\" statement must be unique.");
                break;
              }
            }
            /* See if this pair of disjoint variables is already on the stack;
               if so, don't add it again */
            for (k = 0; k < activeDisjHypStackPtr; k++) {
              if (m == activeDisjHypStack[k].tokenNumA)
                if (n == activeDisjHypStack[k].tokenNumB)
                  break; /* It matches */
            }
            if (k == activeDisjHypStackPtr) {
              /* It wasn't already on the stack, so add it. */
              /* Increase stack size if necessary */
              if (activeDisjHypStackPtr >= activeDisjHypStackSize) {
                free(wrkDisjHPtr1A);
                free(wrkDisjHPtr1B);
                free(wrkDisjHPtr1Stmt);
                free(wrkDisjHPtr2A);
                free(wrkDisjHPtr2B);
                free(wrkDisjHPtr2Stmt);
                activeDisjHypStackSize = activeDisjHypStackSize + 100;
                activeDisjHypStack = realloc(activeDisjHypStack,
                    (size_t)activeDisjHypStackSize
                    * sizeof(struct activeDisjHypStack_struct));
                wrkDisjHPtr1A = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                wrkDisjHPtr1B = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                wrkDisjHPtr1Stmt = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                wrkDisjHPtr2A = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                wrkDisjHPtr2B = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                wrkDisjHPtr2Stmt = malloc((size_t)activeDisjHypStackSize
                    * sizeof(nmbrString));
                if (!activeDisjHypStack
                    || !wrkDisjHPtr1A || !wrkDisjHPtr1B || !wrkDisjHPtr1Stmt
                    || !wrkDisjHPtr2A || !wrkDisjHPtr2B || !wrkDisjHPtr2Stmt)
                    outOfMemory("#28 (activeDisjHypStack)");
              }
              activeDisjHypStack[activeDisjHypStackPtr].tokenNumA = m;
              activeDisjHypStack[activeDisjHypStackPtr].tokenNumB = n;
              activeDisjHypStack[activeDisjHypStackPtr].scope = g_currentScope;
              activeDisjHypStack[activeDisjHypStackPtr].statemNum = stmt;

              activeDisjHypStackPtr++;
            }

          } /* Next j */
        } /* Next i */

        break; /* Switch case break */

      case f_:
      case e_:

        /* Increase stack size if necessary */
        /* For convenience, we will keep the size greater than the sum of
           active $e and $f hypotheses, as this is the size needed for the
           wrkHypPtr's, even though it wastes (temporary) memory for the
           activeE and activeF structure arrays. */
        if (activeEHypStackPtr + activeFHypStackPtr >= activeHypStackSize) {
          free(wrkHypPtr1);
          free(wrkHypPtr2);
          free(wrkHypPtr3);
          activeHypStackSize = activeHypStackSize + 100;
          activeEHypStack = realloc(activeEHypStack, (size_t)activeHypStackSize
              * sizeof(struct activeEHypStack_struct));
          activeFHypStack = realloc(activeFHypStack, (size_t)activeHypStackSize
              * sizeof(struct activeFHypStack_struct));
          wrkHypPtr1 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
          wrkHypPtr2 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
          wrkHypPtr3 = malloc((size_t)activeHypStackSize * sizeof(nmbrString));
          if (!activeEHypStack || !activeFHypStack || !wrkHypPtr1 ||
              !wrkHypPtr2 || !wrkHypPtr3) outOfMemory("#32 (activeHypStack)");
        }

        /* Add the hypothesis to the stack */
        if (type == e_) {
          activeEHypStack[activeEHypStackPtr].statemNum = stmt;
          activeEHypStack[activeEHypStackPtr].scope = g_currentScope;
        } else {
          activeFHypStack[activeFHypStackPtr].statemNum = stmt;
          activeFHypStack[activeFHypStackPtr].scope = g_currentScope;
        }

        /* Create the list of variables used by this hypothesis */
        reqVars = 0;
        j = 0;
        nmbrTmpPtr = g_Statement[stmt].mathString;
        k = nmbrTmpPtr[j]; /* Math symbol number */
        while (k != -1) {
          if (g_MathToken[k].tokenType == (char)var_) {
            if (!activeVarStack[g_MathToken[k].tmp].tmpFlag) {
              /* Variable has not been already added to list */
              wrkVarPtr1[reqVars] = k;
              reqVars++;
              activeVarStack[g_MathToken[k].tmp].tmpFlag = 1;
            }
          }
          j++;
          k = nmbrTmpPtr[j];
        }
        nmbrTmpPtr = malloc(((size_t)reqVars + 1) * sizeof(nmbrString));
        if (!nmbrTmpPtr) outOfMemory("#32 (hypothesis variables)");
        memcpy(nmbrTmpPtr, wrkVarPtr1, (size_t)reqVars * sizeof(nmbrString));
        nmbrTmpPtr[reqVars] = -1;
        /* Clear the variable flags for future re-use */
        for (i = 0; i < reqVars; i++) {
          activeVarStack[g_MathToken[nmbrTmpPtr[i]].tmp].tmpFlag = 0;
        }

        if (type == e_) {
          activeEHypStack[activeEHypStackPtr].varList = nmbrTmpPtr;
          activeEHypStackPtr++;
        } else {
          /* Taken care of earlier.
          if (nmbrTmpPtr[0] == -1) {
            sourceError(g_Statement[stmt].mathSectionPtr +
                g_Statement[stmt].mathSectionLen, 2, stmt,
                "A \"$f\" statement requires at least one variable.");
          }
          */
          activeFHypStack[activeFHypStackPtr].varList = nmbrTmpPtr;
          activeFHypStackPtr++;
        }

        break;  /* Switch case break */

      case a_:
      case p_:

        /* Scan this statement for required variables */
        reqVars = 0;
        j = 0;
        nmbrTmpPtr = g_Statement[stmt].mathString;
        k = nmbrTmpPtr[j]; /* Math symbol number */
        while (k != -1) {
          if (g_MathToken[k].tokenType == (char)var_) {
            if (!activeVarStack[g_MathToken[k].tmp].tmpFlag) {
              /* Variable has not been already added to list */
              wrkVarPtr1[reqVars] = k;
              reqVars++;
              activeVarStack[g_MathToken[k].tmp].tmpFlag = 2;
                       /* 2 means it's an original variable in the assertion */
                       /* (For error-checking) */
            }
          }
          j++;
          k = nmbrTmpPtr[j];
        }

        /* Scan $e stack for required variables and required hypotheses */
        for (i = 0; i < activeEHypStackPtr; i++) {

          /* Add $e hypotheses to required list */
          wrkHypPtr1[i] = activeEHypStack[i].statemNum;

          /* Add the $e's variables to required variable list */
          nmbrTmpPtr = activeEHypStack[i].varList;
          j = 0; /* Location in variable list */
          k = nmbrTmpPtr[j]; /* Symbol number of variable */
          while (k != -1) {
            if (!activeVarStack[g_MathToken[k].tmp].tmpFlag) {
              /* Variable has not been already added to list */
              wrkVarPtr1[reqVars] = k;
              reqVars++;
            }
            activeVarStack[g_MathToken[k].tmp].tmpFlag = 1;
                            /* Could have been 0 or 2; 1 = in some hypothesis */
            j++;
            k = nmbrTmpPtr[j];
          }
        }

        reqHyps = activeEHypStackPtr; /* The number of required hyp's so far */

        /* We have finished determining required variables, so allocate the
           permanent list for the statement array */
        nmbrTmpPtr = poolFixedMalloc((reqVars + 1)
            * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#30 (reqVars)"); */
                                                    /* Not nec. w/ poolMalloc */
        memcpy(nmbrTmpPtr, wrkVarPtr1, (size_t)reqVars * sizeof(nmbrString));
        nmbrTmpPtr[reqVars] = -1;
        g_Statement[stmt].reqVarList = nmbrTmpPtr;

        /* Scan the list of $f hypotheses to find those that are required */
        optHyps = 0;
        for (i = 0; i < activeFHypStackPtr; i++) {
          nmbrTmpPtr = activeFHypStack[i].varList; /* Variable list */
          tokenNum = nmbrTmpPtr[0];
          if (tokenNum == -1) {
            /* Default if no variables (an error in current version): */
            /* Add it to list of required hypotheses */
            wrkHypPtr1[reqHyps] = activeFHypStack[i].statemNum;
            reqHyps++;
            continue;
          } else {
            reqFlag = activeVarStack[g_MathToken[tokenNum].tmp].tmpFlag;
          }
          if (reqFlag) {
            /* Add it to list of required hypotheses */
            wrkHypPtr1[reqHyps] = activeFHypStack[i].statemNum;
            reqHyps++;
            reqFlag = 1;
            activeVarStack[g_MathToken[tokenNum].tmp].tmpFlag = 1;
                                /* Could have been 2; 1 = in some hypothesis */
          } else {
            /* Add it to list of optional hypotheses */
            wrkHypPtr2[optHyps] = activeFHypStack[i].statemNum;
            optHyps++;
          }

          /* Scan the other variables in the $f hyp to check for conflicts. */
          j = 1;
          tokenNum = nmbrTmpPtr[1];
          while (tokenNum != -1) {
            if (activeVarStack[g_MathToken[tokenNum].tmp].tmpFlag == 2) {
              activeVarStack[g_MathToken[tokenNum].tmp].tmpFlag = 1;
                                /* 2 = in $p; 1 = in some hypothesis */
            }
            if (reqFlag != activeVarStack[g_MathToken[tokenNum].tmp].tmpFlag) {
              k = activeFHypStack[i].statemNum;
              m = nmbrElementIn(1, g_Statement[k].mathString, tokenNum);
              n = nmbrTmpPtr[0];
              if (reqFlag) {
                mathTokenError(m - 1, g_Statement[k].mathString, k,
                    cat("This variable does not occur in statement ",
                    str((double)stmt)," (label \"",g_Statement[stmt].labelName,
                    "\") or statement ", str((double)stmt),
                    "'s \"$e\" hypotheses, whereas variable \"",
                    g_MathToken[n].tokenName,
                   "\" DOES occur.  A \"$f\" hypothesis may not contain such a",
                    " mixture of variables.",NULL));
              } else {
                mathTokenError(m - 1, g_Statement[k].mathString, k,
                    cat("This variable occurs in statement ",
                    str((double)stmt)," (label \"",g_Statement[stmt].labelName,
                    "\") or statement ", str((double)stmt),
                    "'s \"$e\" hypotheses, whereas variable \"",
                    g_MathToken[n].tokenName,
               "\" does NOT occur.  A \"$f\" hypothesis may not contain such a",
                    " mixture of variables.",NULL));
              }
              break;
            } /* End if */
            j++;
            tokenNum = nmbrTmpPtr[j];
          } /* End while */

        } /* Next i */


        /* Error check:  make sure that all variables in the original statement
           appeared in some hypothesis */
        j = 0;
        nmbrTmpPtr = g_Statement[stmt].mathString;
        k = nmbrTmpPtr[j]; /* Math symbol number */
        while (k != -1) {
          if (g_MathToken[k].tokenType == (char)var_) {
            if (activeVarStack[g_MathToken[k].tmp].tmpFlag == 2) {
              /* The variable did not appear in any hypothesis */
              mathTokenError(j, g_Statement[stmt].mathString, stmt,
                    cat("This variable does not occur in any active ",
                    "\"$e\" or \"$f\" hypothesis.  All variables in \"$a\" and",
                    " \"$p\" statements must appear in at least one such",
                    " hypothesis.",NULL));
              activeVarStack[g_MathToken[k].tmp].tmpFlag = 1; /* One msg per var*/
            }
          }
          j++;
          k = nmbrTmpPtr[j];
        }


        /* We have finished determining required $e & $f hyps, so allocate the
           permanent list for the statement array */
        /* First, sort the required hypotheses by statement number order
           into wrkHypPtr3 */
        i = 0; /* Start of $e's in wrkHypPtr1 */
        j = activeEHypStackPtr; /* Start of $f's in wrkHypPtr1 */
        for (k = 0; k < reqHyps; k++) {
          if (i >= activeEHypStackPtr) {
            wrkHypPtr3[k] = wrkHypPtr1[j];
            j++;
            continue;
          }
          if (j >= reqHyps) {
            wrkHypPtr3[k] = wrkHypPtr1[i];
            i++;
            continue;
          }
          if (wrkHypPtr1[i] > wrkHypPtr1[j]) {
            wrkHypPtr3[k] = wrkHypPtr1[j];
            j++;
          } else {
            wrkHypPtr3[k] = wrkHypPtr1[i];
            i++;
          }
        }

        /* Now do the allocation */
        nmbrTmpPtr = poolFixedMalloc((reqHyps + 1)
            * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#33 (reqHyps)"); */
                                       /* Not nec. w/ poolMalloc */
        memcpy(nmbrTmpPtr, wrkHypPtr3, (size_t)reqHyps * sizeof(nmbrString));
        nmbrTmpPtr[reqHyps] = -1;
        g_Statement[stmt].reqHypList = nmbrTmpPtr;
        g_Statement[stmt].numReqHyp = reqHyps;

        /* We have finished determining optional $f hyps, so allocate the
           permanent list for the statement array */
        if (type == p_) { /* Optional ones are not used by $a statements */
          nmbrTmpPtr = poolFixedMalloc((optHyps + 1)
              * (long)(sizeof(nmbrString)));
          /* if (!nmbrTmpPtr) outOfMemory("#34 (optHyps)"); */ /* Not nec. w/ poolMalloc */
          memcpy(nmbrTmpPtr, wrkHypPtr2, (size_t)optHyps * sizeof(nmbrString));
          nmbrTmpPtr[optHyps] = -1;
          g_Statement[stmt].optHypList = nmbrTmpPtr;
        }


        /* Scan the list of disjoint variable ($d) hypotheses to find those
           that are required */
        optHyps = 0;
        reqHyps = 0;
        for (i = 0; i < activeDisjHypStackPtr; i++) {
          m = activeDisjHypStack[i].tokenNumA; /* First var in disjoint pair */
          n = activeDisjHypStack[i].tokenNumB; /* 2nd var in disjoint pair */
          if (activeVarStack[g_MathToken[m].tmp].tmpFlag &&
              activeVarStack[g_MathToken[n].tmp].tmpFlag) {
            /* Both variables in the disjoint pair are required, so put the
               disjoint hypothesis in the required list. */
            wrkDisjHPtr1A[reqHyps] = m;
            wrkDisjHPtr1B[reqHyps] = n;
            wrkDisjHPtr1Stmt[reqHyps] =
                activeDisjHypStack[i].statemNum;
            reqHyps++;
          } else {
            /* At least one variable is not required, so the disjoint hypothesis\
               is not required. */
            wrkDisjHPtr2A[optHyps] = m;
            wrkDisjHPtr2B[optHyps] = n;
            wrkDisjHPtr2Stmt[optHyps] =
                activeDisjHypStack[i].statemNum;
            optHyps++;
          }
        }

        /* We have finished determining required $d hyps, so allocate the
           permanent list for the statement array */

        nmbrTmpPtr = poolFixedMalloc((reqHyps + 1)
            * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#40 (reqDisjHyps)"); */ /* Not nec. w/ poolMalloc */
        memcpy(nmbrTmpPtr, wrkDisjHPtr1A, (size_t)reqHyps
            * sizeof(nmbrString));
        nmbrTmpPtr[reqHyps] = -1;
        g_Statement[stmt].reqDisjVarsA = nmbrTmpPtr;

        nmbrTmpPtr = poolFixedMalloc((reqHyps + 1)
            * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#41 (reqDisjHyps)"); */ /* Not nec. w/ poolMalloc */
        memcpy(nmbrTmpPtr, wrkDisjHPtr1B, (size_t)reqHyps
            * sizeof(nmbrString));
        nmbrTmpPtr[reqHyps] = -1;
        g_Statement[stmt].reqDisjVarsB = nmbrTmpPtr;

        nmbrTmpPtr = poolFixedMalloc((reqHyps + 1)
            * (long)(sizeof(nmbrString)));
        /* if (!nmbrTmpPtr) outOfMemory("#42 (reqDisjHyps)"); */ /* Not nec. w/ poolMalloc */
        memcpy(nmbrTmpPtr, wrkDisjHPtr1Stmt, (size_t)reqHyps
            * sizeof(nmbrString));
        nmbrTmpPtr[reqHyps] = -1;
        g_Statement[stmt].reqDisjVarsStmt = nmbrTmpPtr;

        /* We have finished determining optional $d hyps, so allocate the
           permanent list for the statement array */

        if (type == p_) { /* Optional ones are not used by $a statements */

          nmbrTmpPtr = poolFixedMalloc((optHyps + 1)
              * (long)(sizeof(nmbrString)));
          /* if (!nmbrTmpPtr) outOfMemory("#43 (optDisjHyps)"); */ /* Not nec. w/ poolMalloc */
          memcpy(nmbrTmpPtr, wrkDisjHPtr2A, (size_t)optHyps
              * sizeof(nmbrString));
          nmbrTmpPtr[optHyps] = -1;
          g_Statement[stmt].optDisjVarsA = nmbrTmpPtr;

          nmbrTmpPtr = poolFixedMalloc((optHyps + 1)
              * (long)(sizeof(nmbrString)));
          /* if (!nmbrTmpPtr) outOfMemory("#44 (optDisjHyps)"); */ /* Not nec. w/ poolMalloc */
          memcpy(nmbrTmpPtr, wrkDisjHPtr2B, (size_t)optHyps
              * sizeof(nmbrString));
          nmbrTmpPtr[optHyps] = -1;
          g_Statement[stmt].optDisjVarsB = nmbrTmpPtr;

          nmbrTmpPtr = poolFixedMalloc((optHyps + 1)
              * (long)(sizeof(nmbrString)));
          /* if (!nmbrTmpPtr) outOfMemory("#45 (optDisjHyps)"); */ /* Not nec. w/ poolMalloc */
          memcpy(nmbrTmpPtr, wrkDisjHPtr2Stmt, (size_t)optHyps
              * sizeof(nmbrString));
          nmbrTmpPtr[optHyps] = -1;
          g_Statement[stmt].optDisjVarsStmt = nmbrTmpPtr;

        }


        /* Create list of optional variables (i.e. active but not required) */
        optVars = 0;
        for (i = 0; i < activeVarStackPtr; i++) {
          if (activeVarStack[i].tmpFlag) {
            activeVarStack[i].tmpFlag = 0; /* Clear it for future use */
          } else {
            wrkVarPtr2[optVars] = activeVarStack[i].tokenNum;
            optVars++;
          }
        }
        /* We have finished determining optional variables, so allocate the
           permanent list for the statement array */
        if (type == p_) { /* Optional ones are not used by $a statements */
          nmbrTmpPtr = poolFixedMalloc((optVars + 1)
              * (long)(sizeof(nmbrString)));
          /* if (!nmbrTmpPtr) outOfMemory("#31 (optVars)"); */ /* Not nec. w/ poolMalloc */
          memcpy(nmbrTmpPtr, wrkVarPtr2, (size_t)optVars * sizeof(nmbrString));
          nmbrTmpPtr[optVars] = -1;
          g_Statement[stmt].optVarList = nmbrTmpPtr;
        }

        if (optVars + reqVars != activeVarStackPtr) bug(1708);


        break;  /* Switch case break */
    }

    /************** Start of 27-Sep-2010 ****************/
    /* 27-Sep-2010 nm If a $a statement consists of a single constant,
       e.g. "$a wff $.", it means an empty expression (wff) is allowed.
       Before the user had to allow this manually with
       SET EMPTY_SUBSTITUTION ON; now it is done automatically. */
    type = g_Statement[stmt].type;
    if (type == a_) {
      if (g_minSubstLen) {
        if (g_Statement[stmt].mathStringLen == 1) {
          g_minSubstLen = 0;
          printLongLine(cat("SET EMPTY_SUBSTITUTION was",
             " turned ON (allowed) for this database.", NULL),
             "    ", " ");
          /* More detailed but more distracting message:
          printLongLine(cat("Statement \"", g_Statement[stmt].labelName,
             "\"  line ", str(g_Statement[stmt].lineNum),
             " allows empty expressions, so SET EMPTY_SUBSTITUTION was",
             " turned ON (allowed) for this database.", NULL),
             "    ", " ");
          */
        }
      }
    }
    /************** End of 27-Sep-2010 ****************/

    /************** Start of 25-Sep-2010 ****************/
    /* 25-Sep-2010 nm Ensure the current Metamath spec is met:  "There may
       not be be two active $f statements containing the same variable.  Each
       variable in a $e, $a, or $p statement must exist in an active $f
       statement."  (Metamath book, p. 94) */
    /* This section of code is stand-alone and may be removed without side
       effects (other than less stringent error checking). */
    /* ??? To do (maybe):  This might be better placed in-line with the scan
       above, for faster speed and to get the pointer to the token for the
       error message, but it would require a careful code analysis above. */
    if (type == a_ || type == p_) {
      /* Scan each hypothesis (and the statement itself in last pass) */
      reqHyps = nmbrLen(g_Statement[stmt].reqHypList);
      for (i = 0; i <= reqHyps; i++) {
        if (i < reqHyps) {
          m = (g_Statement[stmt].reqHypList)[i];
        } else {
          m = stmt;
        }
        if (g_Statement[m].type != f_) { /* Check $e,$a,$p */
          /* This block implements: "Each variable in a $e, $a, or $p
             statement must exist in an active $f statement" (Metamath
             book p. 94). */
          nmbrTmpPtr = g_Statement[m].mathString;
          /* Scan all the vars in the $e (i<reqHyps) or $a/$p (i=reqHyps) */
          for (j = 0; j < g_Statement[m].mathStringLen; j++) {
            tokenNum = nmbrTmpPtr[j];
            if (g_MathToken[tokenNum].tokenType == (char)con_) continue;
                                            /* Ignore constants */
            p = 0;  /* Initialize flag that we found a $f with the variable */
            /* Scan all the mandatory $f's before this $e,$a,$p */
            for (k = 0; k < i; k++) {
              n = (g_Statement[stmt].reqHypList)[k];
              if (g_Statement[n].type != f_) continue; /* Only check $f */
              if (g_Statement[n].mathStringLen != 2) continue; /* This was
                  already verified earlier; but if there was an error, don't
                  cause memory violation by going out of bounds */
              if ((g_Statement[n].mathString)[1] == tokenNum) {
                p = 1;  /* Set flag that we found a $f with the variable */
                break;
              }
            } /* next k ($f hyp scan) */
            if (!p) {
              sourceError(g_Statement[m].mathSectionPtr/*fbPtr*/,
                  0/*tokenLen*/,
                  m/*stmt*/, cat(
                  "The variable \"", g_MathToken[tokenNum].tokenName,
                  "\" does not appear in an active \"$f\" statement.", NULL));
            }
          } /* next j (variable scan) */
        } else { /* g_Statement[m].type == f_ */
          /* This block implements: "There may not be be two active $f
             statements containing the same variable" (Metamath book p. 94). */
          /* Check for duplicate vars in active $f's */
          if (g_Statement[m].mathStringLen != 2) continue;  /* This was
                  already verified earlier; but if there was an error, don't
                  cause memory violation by going out of bounds */
          tokenNum = (g_Statement[m].mathString)[1];
          /* Scan all the mandatory $f's before this $f */
          for (k = 0; k < i; k++) {
            n = (g_Statement[stmt].reqHypList)[k];
            if (g_Statement[n].type != f_) continue; /* Only check $f */
            if (g_Statement[n].mathStringLen != 2) continue;  /* This was
                  already verified earlier; but if there was an error, don't
                  cause memory violation by going out of bounds */
            if ((g_Statement[n].mathString)[1] == tokenNum) {
              /* We found 2 $f's with the same variable */
              assignStmtFileAndLineNum(n); /* 9-Jan-2018 nm */
              sourceError(g_Statement[m].mathSectionPtr/*fbPtr*/,
                  0/*tokenLen*/,
                  m/*stmt*/, cat(
                  "The variable \"", g_MathToken[tokenNum].tokenName,
                "\" already appears in the earlier active \"$f\" statement \"",
                  g_Statement[n].labelName, "\" on line ",
                  str((double)(g_Statement[n].lineNum)),
                  " in file \"",           /* 9-Jan-2018 nm */
                  g_Statement[n].fileName,   /* 9-Jan-2018 nm */
                  "\".", NULL));
              break; /* Optional: suppresses add'l error msgs for this stmt */
            }
          } /* next k ($f hyp scan) */
        } /* if not $f else is $f */
      } /* next i ($e hyp scan of this statement, or its $a/$p) */
    } /* if stmt is $a or $p */
    /************** End of 25-Sep-2010 ****************/

  } /* Next stmt */

  if (g_currentScope > 0) {
    if (g_currentScope == 1) {
      let(&tmpStr,"A \"$}\" is");
    } else {
      let(&tmpStr,cat(str((double)g_currentScope)," \"$}\"s are",NULL));
    }
    sourceError(g_Statement[g_statements].labelSectionPtr +
        g_Statement[g_statements].labelSectionLen, 2, 0,
        cat(tmpStr," missing at the end of the file.",NULL));
  }


  /* Filter out all hypothesis labels from the label key array.  We do not
     need them anymore, since they are stored locally in each statement
     structure.  Removing them will speed up lookups during proofs, and
     will prevent a lookup from finding an inactive hypothesis label (thus
     forcing an error message). */
  j = 0;
/*E*/if(db5)print2("Number of label keys before filter: %ld",g_numLabelKeys);
  for (i = 0; i < g_numLabelKeys; i++) {
    type = g_Statement[g_labelKeyBase[i]].type;
    if (type == e_ || type == f_) {
      j++;
    } else {
      g_labelKeyBase[i - j] = g_labelKeyBase[i];
    }
  }
  g_numLabelKeys = g_numLabelKeys - j;
/*E*/if(db5)print2(".  After: %ld\n",g_numLabelKeys);


  /* Deallocate temporary space */
  free(mathTokenSameAs);
  free(reverseMathKey);
  free(labelTokenSameAs);
  free(reverseLabelKey);
  free(labelActiveFlag);
  free(activeConstStack);
  free(activeVarStack);
  free(wrkVarPtr1);
  free(wrkVarPtr2);
  for (i = 0; i < activeEHypStackPtr; i++) {
    free(activeEHypStack[i].varList);
  }
  free(activeEHypStack);
  for (i = 0; i < activeFHypStackPtr; i++) {
    free(activeFHypStack[i].varList);
  }
  free(activeFHypStack);
  free(wrkHypPtr1);
  free(wrkHypPtr2);
  free(wrkHypPtr3);  /* 28-Aug-2013 am - added missing free */
  free(activeDisjHypStack);
  free(wrkDisjHPtr1A);
  free(wrkDisjHPtr1B);
  free(wrkDisjHPtr1Stmt);
  free(wrkDisjHPtr2A);
  free(wrkDisjHPtr2B);
  free(wrkDisjHPtr2Stmt);
  free(wrkNmbrPtr);
  free(wrkStrPtr);
  free(symbolLenExists);
  let(&tmpStr, "");



}


/* Parse proof of one statement in source file.  Uses g_WrkProof structure. */
/* Returns 0 if OK; returns 1 if proof is incomplete (is empty or has '?'
   tokens);  returns 2 if error found; returns 3 if severe error found
   (e.g. RPN stack violation); returns 4 if not a $p statement */
char parseProof(long statemNum)
{

  long i, j, k, m, tok, step;
  char *fbPtr;
  long tokLength;
  long numReqHyp;
  long numOptHyp;
  long numActiveHyp;
  char zapSave;
  flag labelFlag;
  char returnFlag = 0;
  nmbrString *nmbrTmpPtr;
  void *voidPtr; /* bsearch returned value */
  vstring tmpStrPtr;

  /* 25-Jan-2016 nm */
  flag explicitTargets = 0; /* Proof is of form <target>=<source> */
  /* Source file pointers and token sizes for targets in a /EXPLICIT proof */
  pntrString *targetPntr = NULL_PNTRSTRING; /* Pointers to target tokens */
  nmbrString *targetNmbr = NULL_NMBRSTRING; /* Size of target tokens */
  /* Variables for rearranging /EXPLICIT proof */
  nmbrString *wrkProofString = NULL_NMBRSTRING; /* Holds g_WrkProof.proofString */
  long hypStepNum, hypSubProofLen, conclSubProofLen;
  long matchingHyp;
  nmbrString *oldStepNums = NULL_NMBRSTRING; /* Just numbers 0 to numSteps-1 */
  pntrString *reqHypSubProof = NULL_PNTRSTRING; /* Subproofs of hypotheses */
  pntrString *reqHypOldStepNums = NULL_PNTRSTRING; /* Local label flag for
                                                     subproofs of hypotheses */
  nmbrString *rearrangedSubProofs = NULL_NMBRSTRING;
  nmbrString *rearrangedOldStepNums = NULL_NMBRSTRING;
  flag subProofMoved; /* Flag to restart scan after moving subproof */
                 /* 10-Mar-2016 nm */

  if (g_Statement[statemNum].type != p_) {
    bug(1723); /* 13-Oct-05 nm - should never get here */
    g_WrkProof.errorSeverity = 4;
    return (4); /* Do nothing if not $p */
  }
  fbPtr = g_Statement[statemNum].proofSectionPtr; /* Start of proof section */
  if (fbPtr[0] == 0) { /* The proof was never assigned (could be a $p statement
                          with no $=; this would have been detected earlier) */
    g_WrkProof.errorSeverity = 4;
    return (4); /* Pretend it's an empty proof */
  }
  fbPtr = fbPtr + whiteSpaceLen(fbPtr);
  if (fbPtr[0] == '(') { /* "(" is flag for compressed proof */
    g_WrkProof.errorSeverity = parseCompressedProof(statemNum);
    return (g_WrkProof.errorSeverity);
  }

  /* Make sure we have enough working space to hold the proof */
  /* The worst case is less than the number of chars in the source,
     plus the number of active hypotheses */

  numOptHyp = nmbrLen(g_Statement[statemNum].optHypList);
  if (g_Statement[statemNum].proofSectionLen + g_Statement[statemNum].numReqHyp
      + numOptHyp > g_wrkProofMaxSize) {
    if (g_wrkProofMaxSize) { /* Not the first allocation */
      free(g_WrkProof.tokenSrcPtrNmbr);
      free(g_WrkProof.tokenSrcPtrPntr);
      free(g_WrkProof.stepSrcPtrNmbr);
      free(g_WrkProof.stepSrcPtrPntr);
      free(g_WrkProof.localLabelFlag);
      free(g_WrkProof.hypAndLocLabel);
      free(g_WrkProof.localLabelPool);
      poolFree(g_WrkProof.proofString);
      free(g_WrkProof.mathStringPtrs);
      free(g_WrkProof.RPNStack);
      free(g_WrkProof.compressedPfLabelMap);
    }
    g_wrkProofMaxSize = g_Statement[statemNum].proofSectionLen
        + g_Statement[statemNum].numReqHyp + numOptHyp
        + 2; /* 2 is minimum for 1-step proof; the other terms could
                all be 0 */
    g_WrkProof.tokenSrcPtrNmbr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(nmbrString));
    g_WrkProof.tokenSrcPtrPntr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(pntrString));
    g_WrkProof.stepSrcPtrNmbr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(nmbrString));
    g_WrkProof.stepSrcPtrPntr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(pntrString));
    g_WrkProof.localLabelFlag = malloc((size_t)g_wrkProofMaxSize
        * sizeof(flag));
    g_WrkProof.hypAndLocLabel =
        malloc((size_t)g_wrkProofMaxSize * sizeof(struct sortHypAndLoc));
    g_WrkProof.localLabelPool = malloc((size_t)g_wrkProofMaxSize);
    g_WrkProof.proofString =
        poolFixedMalloc(g_wrkProofMaxSize * (long)(sizeof(nmbrString)));
         /* Use poolFixedMalloc instead of poolMalloc so that it won't get
            trimmed by memUsedPoolPurge. */
    g_WrkProof.mathStringPtrs =
        malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    g_WrkProof.RPNStack = malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    g_WrkProof.compressedPfLabelMap =
         malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    if (!g_WrkProof.tokenSrcPtrNmbr ||
        !g_WrkProof.tokenSrcPtrPntr ||
        !g_WrkProof.stepSrcPtrNmbr ||
        !g_WrkProof.stepSrcPtrPntr ||
        !g_WrkProof.localLabelFlag ||
        !g_WrkProof.hypAndLocLabel ||
        !g_WrkProof.localLabelPool ||
        /* !g_WrkProof.proofString || */ /* Redundant because of poolMalloc */
        !g_WrkProof.mathStringPtrs ||
        !g_WrkProof.RPNStack
        ) outOfMemory("#99 (g_WrkProof)");
  }

  /* Initialization for this proof */
  g_WrkProof.errorCount = 0; /* Used as threshold for how many error msgs/proof */
  g_WrkProof.numSteps = 0;
  g_WrkProof.numTokens = 0;
  g_WrkProof.numHypAndLoc = 0;
  g_WrkProof.numLocalLabels = 0;
  g_WrkProof.RPNStackPtr = 0;
  g_WrkProof.localLabelPoolPtr = g_WrkProof.localLabelPool;

  /* fbPtr points to the first token now. */

  /* First break up proof section of source into tokens */
  while (1) {
    tokLength = proofTokenLen(fbPtr);
    if (!tokLength) break;
    g_WrkProof.tokenSrcPtrPntr[g_WrkProof.numTokens] = fbPtr;
    g_WrkProof.tokenSrcPtrNmbr[g_WrkProof.numTokens] = tokLength;
    g_WrkProof.numTokens++;
    fbPtr = fbPtr + tokLength;
    fbPtr = fbPtr + whiteSpaceLen(fbPtr);
  }

  /* If there are no tokens, the proof is unknown; make the token a '?' */
  /* (g_WrkProof.tokenSrcPtrPntr won't point to the source, but this is OK since
     there will never be an error message for it.) */
  if (!g_WrkProof.numTokens) {

    /* For now, this is an error. */
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, 2, statemNum,
          "The proof is empty.  If you don't know the proof, make it a \"?\".");
    }
    g_WrkProof.errorCount++;
    if (returnFlag < 1) returnFlag = 1;

    /* Allow empty proofs anyway */
    g_WrkProof.numTokens = 1;
    g_WrkProof.tokenSrcPtrPntr[0] = "?";
    g_WrkProof.tokenSrcPtrNmbr[0] = 1; /* Length */
  }

  /* Copy active (opt + req) hypotheses to hypAndLocLabel look-up table */
  nmbrTmpPtr = g_Statement[statemNum].optHypList;
  /* Transfer optional hypotheses */
  while (1) {
    i = nmbrTmpPtr[g_WrkProof.numHypAndLoc];
    if (i == -1) break;
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelTokenNum = i;
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelName =
        g_Statement[i].labelName;
    g_WrkProof.numHypAndLoc++;
  }
  /* Transfer required hypotheses */
  j = g_Statement[statemNum].numReqHyp;
  nmbrTmpPtr = g_Statement[statemNum].reqHypList;
  for (i = 0; i < j; i++) {
    k = nmbrTmpPtr[i];
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelTokenNum = k;
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelName =
        g_Statement[k].labelName;
    g_WrkProof.numHypAndLoc++;
  }

  /* Sort the hypotheses by label name for lookup */
  numActiveHyp = g_WrkProof.numHypAndLoc; /* Save for bsearch later */
  qsort(g_WrkProof.hypAndLocLabel, (size_t)(g_WrkProof.numHypAndLoc),
      sizeof(struct sortHypAndLoc), hypAndLocSortCmp);


  /* Scan the parsed tokens for local label assignments */
  fbPtr = g_WrkProof.tokenSrcPtrPntr[0];
  if (fbPtr[0] == ':') {
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, 1, statemNum,
          "The colon at proof step 1 must be preceded by a local label.");
    }
    if (returnFlag < 2) returnFlag = 2;
    g_WrkProof.tokenSrcPtrPntr[0] = "?";
    g_WrkProof.tokenSrcPtrNmbr[0] = 1; /* Length */
    g_WrkProof.errorCount++;
  }
  fbPtr = g_WrkProof.tokenSrcPtrPntr[g_WrkProof.numTokens - 1];
  if (fbPtr[0] == ':') {
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, 1, statemNum,
          "The colon in the last proof step must be followed by a label.");
    }
    if (returnFlag < 2) returnFlag = 2;
    g_WrkProof.errorCount++;
    g_WrkProof.numTokens--;
  }
  labelFlag = 0;
  for (tok = 0; tok < g_WrkProof.numTokens; tok++) {
    fbPtr = g_WrkProof.tokenSrcPtrPntr[tok];

    /* 25-Jan-2016 nm */
    /* If next token is = then this token is a target for /EXPLICIT format,
       so don't increment the proof step number */
    if (tok < g_WrkProof.numTokens - 2) {
      if (((char *)((g_WrkProof.tokenSrcPtrPntr)[tok + 1]))[0] == '=') {
        explicitTargets = 1; /* Flag that proof has explicit targets */
        continue;
      }
    }
    if (fbPtr[0] == '=') continue; /* Skip the = token */

    /* Save pointer to source file vs. step for error messages */
    g_WrkProof.stepSrcPtrNmbr[g_WrkProof.numSteps] =
        g_WrkProof.tokenSrcPtrNmbr[tok]; /* Token length */
    g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps] = fbPtr; /* Token ptr */

    /* Save fact that this step has a local label declaration */
    g_WrkProof.localLabelFlag[g_WrkProof.numSteps] = labelFlag;
    labelFlag = 0;

    g_WrkProof.numSteps++;
    if (fbPtr[0] != ':') continue;

    /* Colon found -- previous token is a label */
    labelFlag = 1;

    g_WrkProof.numSteps = g_WrkProof.numSteps - 2;
    fbPtr = g_WrkProof.tokenSrcPtrPntr[tok - 1]; /* The local label */
    tokLength = g_WrkProof.tokenSrcPtrNmbr[tok - 1]; /* Its length */

    /* Check for illegal characters */
    for (j = 0; j < tokLength; j++) {
      if (illegalLabelChar[(unsigned char)fbPtr[j]]) {
        if (!g_WrkProof.errorCount) {
          sourceError(fbPtr + j, 1, statemNum,cat(
              "The local label at proof step ",
              str((double)(g_WrkProof.numSteps + 1)),
              " is incorrect.  Only letters,",
              " digits, \"_\", \"-\", and \".\" are allowed in local labels.",
              NULL));
        }
        if (returnFlag < 2) returnFlag = 2;
        g_WrkProof.errorCount++;
      }
    }

    /* Add the label to the local label pool and hypAndLocLabel table */
    memcpy(g_WrkProof.localLabelPoolPtr, fbPtr, (size_t)tokLength);
    g_WrkProof.localLabelPoolPtr[tokLength] = 0; /* String terminator */
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelTokenNum =
       -g_WrkProof.numSteps - 1000; /* offset of -1000 is flag for local label*/
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelName
        = g_WrkProof.localLabelPoolPtr;

    /* Make sure local label is different from all earlier $a and $p labels */
    voidPtr = (void *)bsearch(g_WrkProof.localLabelPoolPtr, g_labelKeyBase,
        (size_t)g_numLabelKeys, sizeof(long), labelSrchCmp);
    if (voidPtr) { /* It was found */
      j = *(long *)voidPtr; /* Statement number */
      if (j <= statemNum) {
        if (!g_WrkProof.errorCount) {
          assignStmtFileAndLineNum(j); /* 9-Jan-2018 nm */
          sourceError(fbPtr, tokLength, statemNum,cat(
              "The local label at proof step ",
              str((double)(g_WrkProof.numSteps + 1)),
              " is the same as the label of statement ",
              str((double)j),
              " at line ",
              str((double)(g_Statement[j].lineNum)),
              " in file \"",
              g_Statement[j].fileName,
              "\".  Local labels must be different from active statement labels.",
              NULL));
        }
        g_WrkProof.errorCount++;
        if (returnFlag < 2) returnFlag = 2;
      }
    }

    /* Make sure local label is different from all active $e and $f labels */
    voidPtr = (void *)bsearch(g_WrkProof.localLabelPoolPtr,
        g_WrkProof.hypAndLocLabel,
        (size_t)numActiveHyp, sizeof(struct sortHypAndLoc), hypAndLocSrchCmp);
    if (voidPtr) { /* It was found */
      j = ( (struct sortHypAndLoc *)voidPtr)->labelTokenNum; /* Statement number */
      if (!g_WrkProof.errorCount) {
        assignStmtFileAndLineNum(j); /* 9-Jan-2018 nm */
        sourceError(fbPtr, tokLength, statemNum,cat(
            "The local label at proof step ",
            str((double)(g_WrkProof.numSteps + 1)),
            " is the same as the label of statement ",
            str((double)j),
            " at line ",
            str((double)(g_Statement[j].lineNum)),
            " in file \"",
            g_Statement[j].fileName,
            "\".  Local labels must be different from active statement labels.",
            NULL));
      }
      g_WrkProof.errorCount++;
      if (returnFlag < 2) returnFlag = 2;
      g_WrkProof.numHypAndLoc--; /* Ignore the label */
    }

    g_WrkProof.numHypAndLoc++;
    g_WrkProof.localLabelPoolPtr = &g_WrkProof.localLabelPoolPtr[tokLength + 1];

  } /* Next i */

  /* 25-Jan-2016 nm */
  /* Collect all target labels in /EXPLICIT format */
  /* I decided not to make targetPntr, targetNmbr part of the g_WrkProof
     structure since other proof formats don't assign it, so we can't
     reference it reliably outside of this function.  And it would waste
     some memory if we don't use /EXPLICIT, which is intended primarily
     for database maintenance. */
  if (explicitTargets == 1) {
    pntrLet(&targetPntr, pntrSpace(g_WrkProof.numSteps));
    nmbrLet(&targetNmbr, nmbrSpace(g_WrkProof.numSteps));
    step = 0;
    for (tok = 0; tok < g_WrkProof.numTokens - 2; tok++) {
      /* If next token is = then this token is a target for /EXPLICIT format,
         so don't increment the proof step number */
      if (((char *)((g_WrkProof.tokenSrcPtrPntr)[tok + 1]))[0] == '=') {
        fbPtr = g_WrkProof.tokenSrcPtrPntr[tok];
        if (step >= g_WrkProof.numSteps) {
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, g_WrkProof.tokenSrcPtrNmbr[tok], statemNum, cat(
                "There are more target labels than proof steps.", NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          break;
        }
        targetPntr[step] = fbPtr;
        targetNmbr[step] = g_WrkProof.tokenSrcPtrNmbr[tok];
        if (g_WrkProof.tokenSrcPtrPntr[tok + 2]
            != g_WrkProof.stepSrcPtrPntr[step]) {
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, g_WrkProof.tokenSrcPtrNmbr[tok], statemNum, cat(
                "The target label for step ", str((double)step + 1),
                " is not assigned to that step.  ",
                "(Check for missing or extra \"=\".)", NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        }
        step++;
      }
    } /* next tok */
    if (step != g_WrkProof.numSteps) {
      if (!g_WrkProof.errorCount) {
        sourceError(
            (char *)((g_WrkProof.tokenSrcPtrPntr)[g_WrkProof.numTokens - 1]),
            g_WrkProof.tokenSrcPtrNmbr[g_WrkProof.numTokens - 1],
            statemNum, cat(
                "There are ", str((double)(g_WrkProof.numSteps)), " proof steps but only ",
                str((double)step), " target labels.", NULL));
      }
      g_WrkProof.errorCount++;
      if (returnFlag < 2) returnFlag = 2;
    }
  } /* if explicitTargets == 1 */

  if (g_WrkProof.numHypAndLoc > numActiveHyp) { /* There were local labels */

    /* Sort the local labels into the hypAndLocLabel look-up table */
    qsort(g_WrkProof.hypAndLocLabel, (size_t)(g_WrkProof.numHypAndLoc),
        sizeof(struct sortHypAndLoc), hypAndLocSortCmp);

    /* Check for duplicate local labels */
    for (i = 1; i < g_WrkProof.numHypAndLoc; i++) {
      if (!strcmp(g_WrkProof.hypAndLocLabel[i - 1].labelName,
          g_WrkProof.hypAndLocLabel[i].labelName)) { /* Duplicate label */
        /* Get the step numbers */
        j = -(g_WrkProof.hypAndLocLabel[i - 1].labelTokenNum + 1000);
        k = -(g_WrkProof.hypAndLocLabel[i].labelTokenNum + 1000);
        if (j > k) {
          m = j;
          j = k; /* Smaller step number */
          k = m; /* Larger step number */
        }
        /* Find the token - back up a step then move forward to loc label */
        fbPtr = g_WrkProof.stepSrcPtrPntr[k - 1]; /* Previous step */
        fbPtr = fbPtr + g_WrkProof.stepSrcPtrNmbr[k - 1];
        fbPtr = fbPtr + whiteSpaceLen(fbPtr);
        if (!g_WrkProof.errorCount) {
          sourceError(fbPtr,
              proofTokenLen(fbPtr), statemNum,
              cat("The local label at proof step ", str((double)k + 1),
              " is the same as the one declared at step ",
              str((double)j + 1), ".", NULL));
        }
        g_WrkProof.errorCount++;
        if (returnFlag < 2) returnFlag = 2;
      } /* End if duplicate label */
    } /* Next i */

  } /* End if there are local labels */

  /* Build the proof string and check the RPN stack */
  g_WrkProof.proofString[g_WrkProof.numSteps] = -1; /* End of proof */
  nmbrZapLen(g_WrkProof.proofString, g_WrkProof.numSteps);
     /* Zap mem pool actual length (because nmbrLen will be used later on this)*/

  /* 25-Jan-2016 nm */
  if (explicitTargets == 1) {
    /* List of original step numbers to keep track of local label movement */
    nmbrLet(&oldStepNums, nmbrSpace(g_WrkProof.numSteps));
    for (i = 0; i < g_WrkProof.numSteps; i++) {
      oldStepNums[i] = i;
    }
  }

  for (step = 0; step < g_WrkProof.numSteps; step++) {
    tokLength = g_WrkProof.stepSrcPtrNmbr[step];
    fbPtr = g_WrkProof.stepSrcPtrPntr[step];

    /* Handle unknown proof steps */
    if (fbPtr[0] == '?') {
      if (returnFlag < 1) returnFlag = 1;
                                      /* Flag that proof is partially unknown */
      g_WrkProof.proofString[step] = -(long)'?';
      /* Treat "?" like a hypothesis - push stack and continue */
      g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = step;
      g_WrkProof.RPNStackPtr++;
      continue;
    }

    /* Temporarily zap the token's end with a null for string comparisons */
    zapSave = fbPtr[tokLength];
    fbPtr[tokLength] = 0; /* Zap source */

    /* See if the proof token is a hypothesis or local label ref. */
    voidPtr = (void *)bsearch(fbPtr, g_WrkProof.hypAndLocLabel,
        (size_t)(g_WrkProof.numHypAndLoc), sizeof(struct sortHypAndLoc),
        hypAndLocSrchCmp);
    if (voidPtr) {
      fbPtr[tokLength] = zapSave; /* Unzap source */
      j = ((struct sortHypAndLoc *)voidPtr)->labelTokenNum; /* Label lookup number */
      g_WrkProof.proofString[step] = j; /* Proof string */

      /* Push the stack */
      g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = step;
      g_WrkProof.RPNStackPtr++;

      if (j < 0) { /* It's a local label reference */
        i = -1000 - j; /* Step number referenced */
        if (i < 0) bug(1734);

        /* Make sure we don't reference a later step */
        if (i > step) {
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, tokLength, statemNum,cat("Proof step ",
                str((double)step + 1),
                " references a local label before it is declared.",
                NULL));
          }
          g_WrkProof.proofString[step] = -(long)'?';
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        }

        if (g_WrkProof.localLabelFlag[step]) {
          if (!g_WrkProof.errorCount) {
            /* Chained labels not allowed because it complicates the language
               but doesn't buy anything */
            sourceError(fbPtr, tokLength, statemNum, cat(
                "The local label reference at proof step ",
                str((double)step + 1),
                " declares a local label.  Only \"$a\" and \"$p\" statement",
                " labels may have local label declarations.",NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        }
      } else { /* It's a hypothesis reference */
        if (g_WrkProof.localLabelFlag[step]) {
          /* Not allowed because it complicates the language but doesn't
             buy anything; would make $e to $f assignments harder to detect */
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, tokLength, statemNum, cat(
                "The hypothesis reference at proof step ",
                str((double)step + 1),
                " declares a local label.  Only \"$a\" and \"$p\" statement",
                " labels may have local label declarations.",NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        }
        if (j <= 0) bug(1709);
      }
      continue;
    } /* End if local label or hypothesis */

    /* See if token is an assertion label */
    voidPtr = (void *)bsearch(fbPtr, g_labelKeyBase, (size_t)g_numLabelKeys,
        sizeof(long), labelSrchCmp);
    fbPtr[tokLength] = zapSave; /* Unzap source */
    if (!voidPtr) {
      if (!g_WrkProof.errorCount) {
        sourceError(fbPtr, tokLength, statemNum, cat(
            "The token at proof step ",
            str((double)step + 1),
            " is not an active statement label or a local label.",NULL));
      }
      g_WrkProof.errorCount++;
      g_WrkProof.proofString[step] = -(long)'?';
      /* Push the stack */
      g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = step;
      g_WrkProof.RPNStackPtr++;
      if (returnFlag < 2) returnFlag = 2;
      continue;
    }

    /* It's an assertion ($a or $p) */
    j = *(long *)voidPtr; /* Statement number */
    if (g_Statement[j].type != a_ && g_Statement[j].type != p_) bug(1710);
    g_WrkProof.proofString[step] = j; /* Assign $a/$p label to proof string */

    if (j >= statemNum) { /* Error */
      if (!g_WrkProof.errorCount) {
        if (j == statemNum) {
          sourceError(fbPtr, tokLength, statemNum, cat(
              "The label at proof step ",
              str((double)step + 1),
              " is the label of this statement.  A statement may not be used to",
              " prove itself.",NULL));
        } else {
          assignStmtFileAndLineNum(j); /* 9-Jan-2018 nm */
          sourceError(fbPtr, tokLength, statemNum, cat(
              "The label \"", g_Statement[j].labelName, "\" at proof step ",
              str((double)step + 1),
              " is the label of a future statement (at line ",
              str((double)(g_Statement[j].lineNum)),
              " in file ",g_Statement[j].fileName,
      ").  Only local labels or previous, active statements may be referenced.",
              NULL));
        }
      }
      g_WrkProof.errorCount++;
      if (returnFlag < 2) returnFlag = 2;
    }

    /* It's a valid assertion, so pop the stack */
    numReqHyp = g_Statement[j].numReqHyp;

    /* Error check for exhausted stack */
    if (g_WrkProof.RPNStackPtr < numReqHyp) { /* Stack exhausted -- error */
      if (!g_WrkProof.errorCount) {
        tmpStrPtr = shortDumpRPNStack();
        if (strcmp(left(tmpStrPtr,18),"RPN stack is empty")){
          i = instr(1,tmpStrPtr,"contains ");
          let(&tmpStrPtr,cat(left(tmpStrPtr,i + 7)," only",
            right(tmpStrPtr,i + 8),
            NULL));
        }
        if (numReqHyp == 1) {
          let(&tmpStrPtr,cat("a hypothesis but the ",tmpStrPtr,NULL));
        } else {
          let(&tmpStrPtr,cat(str((double)numReqHyp)," hypotheses but the ",tmpStrPtr,
              NULL));
        }
        sourceError(fbPtr, tokLength, statemNum, cat(
            "At proof step ",
            str((double)step + 1),", statement \"",
            g_Statement[j].labelName,"\" requires ",
            tmpStrPtr,".",NULL));
        let(&tmpStrPtr, "");
      }
      /* Treat it like an unknown step so stack won't get exhausted */
      g_WrkProof.errorCount++;
      g_WrkProof.proofString[step] = -(long)'?';
      /* Push the stack */
      g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = step;
      g_WrkProof.RPNStackPtr++;
      if (returnFlag < 3) returnFlag = 3;
      continue;
    } /* End if stack exhausted */

    /**** Start of 25-Jan-2016 nm ***/
    /* For proofs saved with /EXPLICIT, the user may have changed the order
       of hypotheses.  First, get the subproofs for the hypotheses.  Then
       reassemble them in the right order. */
    if (explicitTargets == 1) {
      nmbrLet(&wrkProofString, g_WrkProof.proofString);
            /* nmbrString to rearrange proof then when done reassign to
               g_WrkProof.proofString structure component */
      nmbrTmpPtr = g_Statement[j].reqHypList;
      numReqHyp = g_Statement[j].numReqHyp;
      conclSubProofLen = subproofLen(wrkProofString, step);
      pntrLet(&reqHypSubProof, pntrNSpace(numReqHyp));
                                         /* Initialize to NULL_NMBRSTRINGs */
      pntrLet(&reqHypOldStepNums, pntrNSpace(numReqHyp));
                                         /* Initialize to NULL_NMBRSTRINGs */
      k = 0; /* Total hypothesis subproof lengths for error checking */
      for (i = 0; i < numReqHyp; i++) {
        m = g_WrkProof.RPNStackPtr - numReqHyp + i; /* Stack position of hyp */
        hypStepNum = g_WrkProof.RPNStack[m]; /* Step number of hypothesis i */
        hypSubProofLen = subproofLen(wrkProofString, hypStepNum);
        k += hypSubProofLen;
        nmbrLet((nmbrString **)(&(reqHypSubProof[i])),
            /* For nmbrSeg, 1 = first step */
            nmbrSeg(wrkProofString,
                hypStepNum - hypSubProofLen + 2, hypStepNum + 1));
        nmbrLet((nmbrString **)(&(reqHypOldStepNums[i])),
            /* For nmbrSeg, 1 = first step */
            nmbrSeg(oldStepNums,
                hypStepNum - hypSubProofLen + 2, hypStepNum + 1));
      } /* Next i */
      if (k != conclSubProofLen - 1 /* && returnFlag < 2 */) {
                        /* Uncomment above if bad proof triggers this bug */
        bug(1731);
      }
      nmbrLet(&rearrangedSubProofs, NULL_NMBRSTRING);
      matchingHyp = -1; /* In case there are no hypotheses */
      for (i = 0; i < numReqHyp; i++) {
        matchingHyp = -1;
        for (k = 0; k < numReqHyp; k++) {
          m = g_WrkProof.RPNStackPtr - numReqHyp + k; /* Stack position of hyp */
          hypStepNum = g_WrkProof.RPNStack[m]; /* Step number of hypothesis k */


          /* Temporarily zap the token's end with a null for string comparisons */
          fbPtr = targetPntr[hypStepNum];
          zapSave = fbPtr[targetNmbr[hypStepNum]];
          fbPtr[targetNmbr[hypStepNum]] = 0; /* Zap source */
          /* See if hypothesis i matches the target label k i.e. hypStepNum */
          if (!strcmp(g_Statement[nmbrTmpPtr[i]].labelName, fbPtr)) {
            matchingHyp = k;
          }
          fbPtr[targetNmbr[hypStepNum]] = zapSave; /* Unzap source */
          if (matchingHyp != -1) break;
        } /* next k (0 to numReqHyp-1) */
        if (matchingHyp == -1) {
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, 1/*token length*/, statemNum, cat(
                "The target labels for the hypotheses for step ", str((double)step + 1),
                " do not match hypothesis \"",
                g_Statement[nmbrTmpPtr[i]].labelName,
                "\" of the assertion \"",
                g_Statement[j].labelName,
                "\" in step ",  str((double)step + 1), ".",
                NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          break; /* Give up; don't try to rearrange hypotheses */
        }
        /* Accumulate the subproof for hypothesis i */
        nmbrLet(&rearrangedSubProofs, nmbrCat(rearrangedSubProofs,
            reqHypSubProof[matchingHyp], NULL));
        nmbrLet(&rearrangedOldStepNums, nmbrCat(rearrangedOldStepNums,
            reqHypOldStepNums[matchingHyp], NULL));
      } /* next i (0 to numReqHyp-1) */

      if (matchingHyp != -1) { /* All hypotheses found */
        if (nmbrLen(rearrangedSubProofs) != conclSubProofLen - 1
             /* && returnFlag < 2 */) {
                          /* Uncomment above if bad proof triggers this bug */
          bug(1732);
        }
        nmbrLet(&(wrkProofString), nmbrCat(
            nmbrLeft(wrkProofString, step - conclSubProofLen + 1),
            rearrangedSubProofs,
            nmbrRight(wrkProofString, step + 1), NULL));
        nmbrLet(&oldStepNums, nmbrCat(
            nmbrLeft(oldStepNums, step - conclSubProofLen + 1),
            rearrangedOldStepNums,
            nmbrRight(oldStepNums, step + 1), NULL));
      }

      /* Reassign g_WrkProof.proofString from rearranged wrkProofString */
      for (i = 0; i < step; i++) {
        /* Nothing from step to end has been changed, so stop at step */
        (g_WrkProof.proofString)[i] = wrkProofString[i];
      }
      if ((g_WrkProof.proofString)[step] != wrkProofString[step]) bug(1735);

      /* Deallocate */
      for (i = 0; i < numReqHyp; i++) {
        nmbrLet((nmbrString **)(&(reqHypSubProof[i])), NULL_NMBRSTRING);
        nmbrLet((nmbrString **)(&(reqHypOldStepNums[i])), NULL_NMBRSTRING);
      }
      pntrLet(&reqHypSubProof, NULL_PNTRSTRING);
      pntrLet(&reqHypOldStepNums, NULL_PNTRSTRING);
      nmbrLet(&rearrangedSubProofs, NULL_NMBRSTRING);
      nmbrLet(&rearrangedOldStepNums, NULL_NMBRSTRING);
      nmbrLet(&wrkProofString, NULL_NMBRSTRING);
    } /* if explicitTargets */
    /**** End of 25-Jan-2016 ***/

    numReqHyp = g_Statement[j].numReqHyp;

    /* Error check for $e <- $f assignments (illegal) */
    /* 18-Jun-2020 nm: Although it would be unusual to to this,
       it is not illegal per the spec.  See
       https://groups.google.com/d/msg/metamath/Cx_d84uorf8/0FrNYTM9BAAJ */
    /**** Deleted 18-Jun-2020 nm ****
    nmbrTmpPtr = g_Statement[j].reqHypList;
    for (i = 0; i < numReqHyp; i++) {

      /@ 25-Jan-2016 nm @/
      /@ Skip this check if /EXPLICIT since hyps may be in random order @/
      if (explicitTargets == 1) break;

      if (g_Statement[nmbrTmpPtr[i]].type == e_) {
        m = g_WrkProof.RPNStackPtr - numReqHyp + i;
        k = g_WrkProof.proofString[g_WrkProof.RPNStack[m]];
        if (k > 0) {
          if (g_Statement[k].type == f_) {
            if (!g_WrkProof.errorCount) {
              sourceError(fbPtr, tokLength, statemNum, cat(
                  "Statement \"",g_Statement[j].labelName,"\" (proof step ",
                  str((double)step + 1),
                  ") has its \"$e\" hypothesis \"",
                  g_Statement[nmbrTmpPtr[i]].labelName,
                  "\" assigned the \"$f\" hypothesis \"",
                  g_Statement[k].labelName,
                  "\" at step ", str((double)(g_WrkProof.RPNStack[m] + 1)),
                  ".  The assignment of \"$e\" with \"$f\" is not allowed.",
                  NULL));
            }
            g_WrkProof.errorCount++;
            if (returnFlag < 2) returnFlag = 2;
          }
        }
      }
    }
    **** End of 18-Jun-2020 deletion ****/

    /* Pop the stack */
    g_WrkProof.RPNStackPtr = g_WrkProof.RPNStackPtr - numReqHyp;
    g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = step;
    g_WrkProof.RPNStackPtr++;

  } /* Next step */

  /* The stack should have one entry */
  if (g_WrkProof.RPNStackPtr != 1) {
    tmpStrPtr = shortDumpRPNStack();
    fbPtr = g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps - 1];
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, proofTokenLen(fbPtr), statemNum, cat("After proof step ",
          str((double)(g_WrkProof.numSteps))," (the last step), the ",
          tmpStrPtr,".  It should contain exactly one entry.",NULL));
    }
    g_WrkProof.errorCount++;
    if (returnFlag < 3) returnFlag = 3;
  }

  /**** Start of 25-Jan-2016 nm ***/
  if (explicitTargets) {
    /* Correct the local label refs in the rearranged proof */
    for (step = 0; step < g_WrkProof.numSteps; step++) {
      /* This is slow lookup with n^2 behavior, but should be ok since
         /EXPLICIT isn't used that often */
      k = (g_WrkProof.proofString)[step]; /* Will be <= -1000 if local label */
      if (k <= -1000) {
        k = -1000 - k; /* Restore step number */
        if (k < 0 || k >= g_WrkProof.numSteps) bug(1733);
        /* Find the original step */
        if (oldStepNums[k] == k) {
            /* Wasn't changed, so skip renumbering for speedup */
          continue;
        }
        i = 0; /* For bug check */
        /* Find the original step number and change it to the new one */
        for (m = 0; m < g_WrkProof.numSteps; m++) {
          if (oldStepNums[m] == k) {
            (g_WrkProof.proofString)[step] = -1000 - m;
            i = 1; /* Found */
            break;
          }
        }
        if (i == 0) bug(1740);
      }
    } /* next step */

    /************** Start of section deleted 10-Mar-2016 nm
    /@ Check if any local labels point to future steps: if so, we should
       expand the proof so it will verify (since many functions require
       that a local label be declared before it is used) @/
    for (step = 0; step < g_WrkProof.numSteps; step++) {
      k = (g_WrkProof.proofString)[step]; /@ Will be <= -1000 if local label @/
      if (k <= -1000) { /@ References local label i.e. subproof @/
        k = -1000 - k; /@ Restore step number subproof ends at @/
        if (k > step) { /@ Refers to label declared after this step @/
          /@ Expand the proof @/
          nmbrLet(&wrkProofString, nmbrUnsquishProof(g_WrkProof.proofString));
          /@ Recompress the proof @/
          nmbrLet(&wrkProofString, nmbrSquishProof(wrkProofString));
          /@ The number of steps shouldn't have changed @/
          /@ (If this bug is valid behavior, it means we may have to
             reallocate (grow) the wrkProof structure, which might be
             unpleasant at this point.) @/
          if (nmbrLen(wrkProofString) != g_WrkProof.numSteps) {
            bug(1736);
          }
          /@ Reassign g_WrkProof.proofString from new wrkProofString @/
          for (i = 0; i < g_WrkProof.numSteps; i++) {
            (g_WrkProof.proofString)[i] = wrkProofString[i];
          }
          break;
        } /@ if k>step @/
      } /@ if k<= -1000 @/
    } /@ next step @/
    ************************* end of 10-Mar-2016 deletion */


    /* 10-Mar-2016 nm (Replace above deleted secton) */
    /* Check if any local labels point to future steps: if so, we should
       moved the subproof they point to down (since many functions require
       that a local label be declared before it is used) */
    do {
      subProofMoved = 0;  /* Flag to rescan after moving a subproof */
      /* TODO: restart loop after step just finished for speedup?
         (maybe not worth it).
         We could just change subProofMoved to restartStep (long) and use
         restartStep > 0 as the flag, since we would restart at the
         last step processed plus 1. */
      for (step = 0; step < g_WrkProof.numSteps; step++) {
        k = (g_WrkProof.proofString)[step]; /* Will be <= -1000 if local label */
        if (k <= -1000) { /* References local label i.e. subproof */
          k = -1000 - k; /* Restore step number subproof ends at */
          if (k > step) { /* Refers to label declared after this step */
            m = subproofLen(g_WrkProof.proofString, k);
            /*m = nmbrGetSubProofLen(g_WrkProof.proofString, k);*/

            /* At this point:
                   step = the step referencing a future subproof
                   k = end of future subproof
                   m = length of future subproof */

            /* TODO - make this a direct assignment for speedup?
               (with most $f's reversed, this has about 13K hits during
               'verify proof *' - maybe not enough to justify rewriting this
               to make direct assignment instead of nmbrLet().) */
            /* Replace the step with the subproof it references */
            /* Note that nmbrXxx() positions start at 1, not 0; add 1 to step */
            nmbrLet(&wrkProofString, nmbrCat(
                /* Proof before future label ref: */
                nmbrLeft(g_WrkProof.proofString, step),
                /* The future subproof moved to the current step: */
                nmbrMid(g_WrkProof.proofString, k - m + 2, m),
                /* The steps between this step and the future subproof: */
                nmbrSeg(g_WrkProof.proofString, step + 2, k - m + 1),
                /* The future subproof replaced with the current step (a local
                   label to be renumbered below): */
                nmbrMid(g_WrkProof.proofString, step + 1, 1),
                /* The rest of the steps to the end of proof: */
                nmbrRight(g_WrkProof.proofString, k + 2),
                NULL));
            if (nmbrLen(wrkProofString) != g_WrkProof.numSteps) {
              bug(1736); /* Make sure proof length didn't change */
            }
            if (wrkProofString[k] != (g_WrkProof.proofString)[step]) {
              bug(1737); /* Make sure future subproof is now the future local
                            label (to be renumbered below) */
            }

            /* We now have this wrkProofString[...] content:
                [0]...[step-1]                   same as original proof
                [step+1]...[step+m-1]            moved subproof
                [step+m]...[k-1]                 shifted orig proof
                [k]...[k]                        subproof replaced by loc label
                [k+1]...[g_WrkProof.numSteps-1]    same as orig proof */

            /* Correct all local labels */
            for (i = 0; i < g_WrkProof.numSteps; i++) {
              j = (wrkProofString)[i]; /* Will be <= -1000 if local label */
              if (j > -1000) continue; /* Not loc label ref */
              j = -1000 - j; /* Restore step number subproof ends at */
              /* Note: the conditions before the "&&" below are redundant
                 but provide better sanity checking */
              if (j >= 0 && j < step) { /* Before moved subproof */
                /*j = j;*/ /* Same as orig proof */
                /* 24-Mar-2016 workaround to clang complaint about j = j */
                j = j + 0; /* Same as orig proof */
              } else if (j == step) { /* The original local label ref */
                bug(1738); /* A local label shouldn't ref a local label */
              } else if (j > step && j <= k - m) {
                                   /* Steps shifted up by subproof insertion */
                j = j + m - 1; /* Offset by size of subproof moved down -1 */
              } else if (j > k - m && j <= k) {
                                   /* Reference to inside the moved subproof */
                j = j + step + m - 1 - k;  /* Shift down */
              } else if (j > k && j <= g_WrkProof.numSteps - 1) {
                                              /* Ref to after moved subproof */
                /*j = j;*/ /* Same as orig proof */
                /* 24-Mar-2016 workaround to clang complaint about j = j */
                j = j + 0; /* Same as orig proof */
              } else {
                bug(1739);  /* Cases not exhausted or j is out of range */
              }
              (wrkProofString)[i] = -j - 1000; /* Update new proof */
            } /* next i */

            /* Transfer proof back to original */
            for (i = 0; i < g_WrkProof.numSteps; i++) {
              (g_WrkProof.proofString)[i] = wrkProofString[i];
            }

            /* Set flag that a subproof was moved and restart the scan */
            subProofMoved = 1;
            /* Reassign g_WrkProof.proofString from new wrkProofString */
            for (i = 0; i < g_WrkProof.numSteps; i++) {
              (g_WrkProof.proofString)[i] = wrkProofString[i];
            }
            break; /* Break out of the 'for (step...' loop */

          } /* if k>step */
        } /* if k<= -1000 */
      } /* next step */
    } while (subProofMoved);

    /* Deallocate */
    pntrLet(&targetPntr, NULL_PNTRSTRING);
    nmbrLet(&targetNmbr, NULL_NMBRSTRING);
    nmbrLet(&oldStepNums, NULL_NMBRSTRING);
    nmbrLet(&wrkProofString, NULL_NMBRSTRING);
  } /* if (explicitTargets) */
  /**** End of 25-Jan-2016 ***/

  g_WrkProof.errorSeverity = returnFlag;
  return (returnFlag);

} /* parseProof() */



/* Parse proof in compressed format */
/* Parse proof of one statement in source file.  Uses wrkProof structure. */
/* Returns 0 if OK; returns 1 if proof is incomplete (is empty or has '?'
   tokens);  returns 2 if error found; returns 3 if severe error found
   (e.g. RPN stack violation); returns 4 if not a $p statement */
char parseCompressedProof(long statemNum)
{

  long i, j, k, step, stmt;
  /* long m; */ /* 18-Jun-2020 nm No longer needed */
  char *fbPtr;
  char *fbStartProof;
  char *labelStart;
  long tokLength;
  long numReqHyp;
  long numOptHyp;
  char zapSave;
  flag breakFlag;
  char returnFlag = 0;
  nmbrString *nmbrTmpPtr;
  void *voidPtr; /* bsearch returned value */
  vstring tmpStrPtr;
  flag hypLocUnkFlag;  /* Hypothesis, local label ref, or unknown step */
  long labelMapIndex;

  static unsigned char chrWeight[256]; /* Proof label character weights */
  static unsigned char chrType[256]; /* Proof character types */
  static flag chrTablesInited = 0;
  static char *digits = "0123456789";
  static char *letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  static char labelChar = ':';
  static long lettersLen;
  static long digitsLen;

  /* 15-Oct-05 nm - Used to detect old buggy compression */
  long bggyProofLen;
  char bggyZapSave;
  flag bggyAlgo;

  /* Initialization to avoid compiler warning (should not be theoretically
     necessary) */
  labelStart = "";

  /* Do one-time initialization */
  if (!chrTablesInited) {
    chrTablesInited = 1;

    /* Compression standard with all cap letters */
    /* (For 500-700 step proofs, we only lose about 18% of file size --
        but the compressed proof is more pleasing to the eye) */
    letters = "ABCDEFGHIJKLMNOPQRST"; /* LSB is base 20 */
    digits = "UVWXY"; /* MSB's are base 5 */
    labelChar = 'Z'; /* Was colon */

    lettersLen = (long)strlen(letters);
    digitsLen = (long)strlen(digits);

    /* Initialize compressed proof label character weights */
    /* Initialize compressed proof character types */
    for (i = 0; i < 256; i++) {
      chrWeight[i] = 0;
      chrType[i] = 6; /* Illegal */
    }
    j = lettersLen;
    for (i = 0; i < j; i++) {
      chrWeight[(long)(letters[i])] = (unsigned char)i;
      chrType[(long)(letters[i])] = 0; /* Letter */
    }
    j = digitsLen;
    for (i = 0; i < j; i++) {
      chrWeight[(long)(digits[i])] = (unsigned char)i;
      chrType[(long)(digits[i])] = 1; /* Digit */
    }
    for (i = 0; i < 256; i++) {
      if (isspace(i)) chrType[i] = 3; /* White space */
    } /* Next i */
    chrType[(long)(labelChar)] = 2; /* Colon */
    chrType['$'] = 4; /* Dollar */
    chrType['?'] = 5; /* Question mark */
  }


  if (g_Statement[statemNum].type != p_) {
    bug(1724); /* 13-Oct-05 nm - should never get here */
    return (4); /* Do nothing if not $p */
  }
  fbPtr = g_Statement[statemNum].proofSectionPtr; /* Start of proof section */
  if (fbPtr[0] == 0) { /* The proof was never assigned (could be a $p statement
                          with no $=; this would have been detected earlier) */
    bug(1711);
  }
  fbPtr = fbPtr + whiteSpaceLen(fbPtr);
  if (fbPtr[0] != '(') { /* ( is flag for start of compressed proof */
    bug(1712);
  }

  /* Make sure we have enough working space to hold the proof */
  /* The worst case is less than the number of chars in the source,
     plus the number of active hypotheses */

  numOptHyp = nmbrLen(g_Statement[statemNum].optHypList);
  if (g_Statement[statemNum].proofSectionLen + g_Statement[statemNum].numReqHyp
      + numOptHyp > g_wrkProofMaxSize) {
    if (g_wrkProofMaxSize) { /* Not the first allocation */
      free(g_WrkProof.tokenSrcPtrNmbr);
      free(g_WrkProof.tokenSrcPtrPntr);
      free(g_WrkProof.stepSrcPtrNmbr);
      free(g_WrkProof.stepSrcPtrPntr);
      free(g_WrkProof.localLabelFlag);
      free(g_WrkProof.hypAndLocLabel);
      free(g_WrkProof.localLabelPool);
      poolFree(g_WrkProof.proofString);
      free(g_WrkProof.mathStringPtrs);
      free(g_WrkProof.RPNStack);
      free(g_WrkProof.compressedPfLabelMap);
    }
    g_wrkProofMaxSize = g_Statement[statemNum].proofSectionLen
        + g_Statement[statemNum].numReqHyp + numOptHyp;
    g_WrkProof.tokenSrcPtrNmbr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(nmbrString));
    g_WrkProof.tokenSrcPtrPntr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(pntrString));
    g_WrkProof.stepSrcPtrNmbr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(nmbrString));
    g_WrkProof.stepSrcPtrPntr = malloc((size_t)g_wrkProofMaxSize
        * sizeof(pntrString));
    g_WrkProof.localLabelFlag = malloc((size_t)g_wrkProofMaxSize
        * sizeof(flag));
    g_WrkProof.hypAndLocLabel =
        malloc((size_t)g_wrkProofMaxSize * sizeof(struct sortHypAndLoc));
    g_WrkProof.localLabelPool = malloc((size_t)g_wrkProofMaxSize);
    g_WrkProof.proofString =
        poolFixedMalloc(g_wrkProofMaxSize * (long)(sizeof(nmbrString)));
         /* Use poolFixedMalloc instead of poolMalloc so that it won't get
            trimmed by memUsedPoolPurge. */
    g_WrkProof.mathStringPtrs =
        malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    g_WrkProof.RPNStack = malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    g_WrkProof.compressedPfLabelMap =
         malloc((size_t)g_wrkProofMaxSize * sizeof(nmbrString));
    if (!g_WrkProof.tokenSrcPtrNmbr ||
        !g_WrkProof.tokenSrcPtrPntr ||
        !g_WrkProof.stepSrcPtrNmbr ||
        !g_WrkProof.stepSrcPtrPntr ||
        !g_WrkProof.localLabelFlag ||
        !g_WrkProof.hypAndLocLabel ||
        !g_WrkProof.localLabelPool ||
        /* !g_WrkProof.proofString || */ /* Redundant because of poolMalloc */
        !g_WrkProof.mathStringPtrs ||
        !g_WrkProof.RPNStack
        ) outOfMemory("#99 (g_WrkProof)");
  }

  /* Initialization for this proof */
  g_WrkProof.errorCount = 0; /* Used as threshold for how many error msgs/proof */
  g_WrkProof.numSteps = 0;
  g_WrkProof.numTokens = 0;
  g_WrkProof.numHypAndLoc = 0;
  g_WrkProof.numLocalLabels = 0;
  g_WrkProof.RPNStackPtr = 0;
  g_WrkProof.localLabelPoolPtr = g_WrkProof.localLabelPool;

  fbPtr++;
  /* fbPtr points to the first token now. */


  /****** This part of the code is heavily borrowed from the regular
   ****** proof parsing, with local label and RPN handling removed,
   ****** in order to easily parse the label section. */

  /* First break up the label section of proof into tokens */
  while (1) {
    fbPtr = fbPtr + whiteSpaceLen(fbPtr);
    tokLength = proofTokenLen(fbPtr);
    if (!tokLength) {
      if (!g_WrkProof.errorCount) {
        sourceError(fbPtr, 2, statemNum,
            "A \")\" which ends the label list is not present.");
      }
      g_WrkProof.errorCount++;
      if (returnFlag < 3) returnFlag = 3;
      break;
    }
    if (fbPtr[0] == ')') {  /* End of label list */
      fbPtr++;
      break;
    }
    g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps] = fbPtr;
    g_WrkProof.stepSrcPtrNmbr[g_WrkProof.numSteps] = tokLength;
    g_WrkProof.numSteps++;
    fbPtr = fbPtr + tokLength;
  }

  fbStartProof = fbPtr; /* Save pointer to start of compressed proof */

  /* Copy active (opt + req) hypotheses to hypAndLocLabel look-up table */
  nmbrTmpPtr = g_Statement[statemNum].optHypList;
  /* Transfer optional hypotheses */
  while (1) {
    i = nmbrTmpPtr[g_WrkProof.numHypAndLoc];
    if (i == -1) break;
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelTokenNum = i;
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelName =
        g_Statement[i].labelName;
    g_WrkProof.numHypAndLoc++;
  }
  /* Transfer required hypotheses */
  j = g_Statement[statemNum].numReqHyp;
  nmbrTmpPtr = g_Statement[statemNum].reqHypList;
  for (i = 0; i < j; i++) {
    k = nmbrTmpPtr[i];
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelTokenNum = -1000 - k;
       /* Required hypothesis labels are not allowed; the -1000 - k is a
          flag that tells that they are required for error detection */
    g_WrkProof.hypAndLocLabel[g_WrkProof.numHypAndLoc].labelName =
        g_Statement[k].labelName;
    g_WrkProof.numHypAndLoc++;
  }

  /* Sort the hypotheses by label name for lookup */
  qsort(g_WrkProof.hypAndLocLabel, (size_t)(g_WrkProof.numHypAndLoc),
      sizeof(struct sortHypAndLoc), hypAndLocSortCmp);

  /* Build the proof string (actually just a list of labels) */
  g_WrkProof.proofString[g_WrkProof.numSteps] = -1; /* End of proof */
  nmbrZapLen(g_WrkProof.proofString, g_WrkProof.numSteps);
     /* Zap mem pool actual length (because nmbrLen will be used later on this)*/

  /* Scan proof string with the label list (not really proof steps; we're just
     using the structure for convenience) */
  for (step = 0; step < g_WrkProof.numSteps; step++) {
    tokLength = g_WrkProof.stepSrcPtrNmbr[step];
    fbPtr = g_WrkProof.stepSrcPtrPntr[step];

    /* Temporarily zap the token's end with a null for string comparisons */
    zapSave = fbPtr[tokLength];
    fbPtr[tokLength] = 0; /* Zap source */

    /* See if the proof token is a hypothesis */
    voidPtr = (void *)bsearch(fbPtr, g_WrkProof.hypAndLocLabel,
        (size_t)(g_WrkProof.numHypAndLoc), sizeof(struct sortHypAndLoc),
        hypAndLocSrchCmp);
    if (voidPtr) {
      /* It's a hypothesis reference */
      fbPtr[tokLength] = zapSave; /* Unzap source */
      j = ((struct sortHypAndLoc *)voidPtr)->labelTokenNum;
                                                       /* Label lookup number */

      /* Make sure it's not a required hypothesis, which is implicitly
         declared */
      if (j < 0) { /* Minus is used as flag for required hypothesis */
        j = -1000 - j; /* Restore it to prevent side effects of the error */
        if (!g_WrkProof.errorCount) {
          sourceError(fbPtr, tokLength, statemNum,
              "Required hypotheses may not be explicitly declared.");
        }
        g_WrkProof.errorCount++;
        /* if (returnFlag < 2) returnFlag = 2; */
        /* 19-Aug-2006 nm Tolerate this error so we can continue to work
           on proof in Proof Assistant */
        if (returnFlag < 1) returnFlag = 1;
      }

      g_WrkProof.proofString[step] = j; /* Proof string */
      if (j <= 0) bug(1713);
      continue;
    } /* End if hypothesis */

    /* See if token is an assertion label */
    voidPtr = (void *)bsearch(fbPtr, g_labelKeyBase, (size_t)g_numLabelKeys,
        sizeof(long), labelSrchCmp);
    fbPtr[tokLength] = zapSave; /* Unzap source */
    if (!voidPtr) {
      if (!g_WrkProof.errorCount) {
        sourceError(fbPtr, tokLength, statemNum,
         "This token is not the label of an assertion or optional hypothesis.");
      }
      g_WrkProof.errorCount++;
      g_WrkProof.proofString[step] = -(long)'?';
      if (returnFlag < 2) returnFlag = 2;
      continue;
    }

    /* It's an assertion ($a or $p) */
    j = *(long *)voidPtr; /* Statement number */
    if (g_Statement[j].type != a_ && g_Statement[j].type != p_) bug(1714);
    g_WrkProof.proofString[step] = j; /* Proof string */

    if (j >= statemNum) { /* Error */
      if (!g_WrkProof.errorCount) {
        if (j == statemNum) {
          sourceError(fbPtr, tokLength, statemNum, cat(
              "The label at proof step ",
              str((double)step + 1),
             " is the label of this statement.  A statement may not be used to",
              " prove itself.",NULL));
        } else {
          assignStmtFileAndLineNum(j); /* 9-Jan-2018 nm */
          sourceError(fbPtr, tokLength, statemNum, cat(
              "The label \"", g_Statement[j].labelName, "\" at proof step ",
              str((double)step + 1),
              " is the label of a future statement (at line ",
              str((double)(g_Statement[j].lineNum)),
              " in file ",g_Statement[j].fileName,
              ").  Only previous statements may be referenced.",
              NULL));
        }
      }
      g_WrkProof.errorCount++;
      if (returnFlag < 2) returnFlag = 2;
    }

  } /* Next step */

  /******* Create the starting label map (local labels will be
           added as they are found) *****/
  g_WrkProof.compressedPfNumLabels = g_Statement[statemNum].numReqHyp;
  nmbrTmpPtr = g_Statement[statemNum].reqHypList;
  for (i = 0; i < g_WrkProof.compressedPfNumLabels; i++) {
    g_WrkProof.compressedPfLabelMap[i] = nmbrTmpPtr[i];
  }
  for (i = 0; i < g_WrkProof.numSteps; i++) {
    g_WrkProof.compressedPfLabelMap[i + g_WrkProof.compressedPfNumLabels] =
        g_WrkProof.proofString[i];
  }
  g_WrkProof.compressedPfNumLabels = g_WrkProof.compressedPfNumLabels +
      g_WrkProof.numSteps;

  /* Re-initialization for the actual proof */
  g_WrkProof.numSteps = 0;
  g_WrkProof.RPNStackPtr = 0;

  /******* Parse the compressed part of the proof *****/

  /* 15-Oct-05 nm - Check to see if the old buggy compression is used.  If so,
     warn the user to reformat, and switch to the buggy algorithm so that
     parsing can proceed. */
  bggyProofLen = g_Statement[statemNum].proofSectionLen -
             (fbPtr - g_Statement[statemNum].proofSectionPtr);
  /* Zap a zero at the end of the proof so we can use C string operations */
  bggyZapSave = fbPtr[bggyProofLen];
  fbPtr[bggyProofLen] = 0;
  /* If the proof has "UVA" but doesn't have "UUA", it means the buggy
     algorithm was used. */
  bggyAlgo = 0;
  if (strstr(fbPtr, "UV") != NULL) {
    if (strstr(fbPtr, "UU") == NULL) {
      bggyAlgo = 1;
      print2("?Warning: the proof of \"%s\" uses obsolete compression.\n",
          g_Statement[statemNum].labelName);
      print2(" Please SAVE PROOF * / COMPRESSED to reformat your proofs.\n");
    }
  }
  fbPtr[bggyProofLen] = bggyZapSave;

  /* (Build the proof string and check the RPN stack) */
  fbPtr = fbStartProof;
  breakFlag = 0;
  labelMapIndex = 0;
  while (1) {
    switch (chrType[(long)(fbPtr[0])]) {
      case 0: /* "Letter" (i.e. A...T) */
        if (!labelMapIndex) labelStart = fbPtr; /* Save for error msg */

        /* Save pointer to source file vs. step for error messages */
        tokLength = fbPtr - labelStart + 1; /* Token length */
        g_WrkProof.stepSrcPtrNmbr[g_WrkProof.numSteps] = tokLength;
        g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps] = labelStart; /* Token ptr */

        /* 15-Oct-05 nm - Obsolete (skips from YT to UVA, missing UUA) */
        /* (actually, this part is coincidentally the same:)
        labelMapIndex = labelMapIndex * lettersLen +
            chrWeight[(long)(fbPtr[0])];
        */
        /* 15-Oct-05 nm - Corrected algorithm provided by Marnix Klooster. */
        /* Decoding can be done as follows:
             * n := 0
             * for each character c:
                * if c in ['U'..'Y']: n := n * 5 + (c - 'U' + 1)
                * if c in ['A'..'T']: n := n * 20 + (c - 'A' + 1) */
        labelMapIndex = labelMapIndex * lettersLen +
            chrWeight[(long)(fbPtr[0])];
        if (labelMapIndex >= g_WrkProof.compressedPfNumLabels) {
          if (!g_WrkProof.errorCount) {
            sourceError(labelStart, tokLength, statemNum, cat(
     "This compressed label reference is outside the range of the label list.",
                "  The compressed label value is ", str((double)labelMapIndex),
                " but the largest label defined is ",
                str((double)(g_WrkProof.compressedPfNumLabels - 1)), ".", NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          labelMapIndex = 0; /* Make it something legal to avoid side effects */
        }

        stmt = g_WrkProof.compressedPfLabelMap[labelMapIndex];
        g_WrkProof.proofString[g_WrkProof.numSteps] = stmt;

        /* Update stack */
        hypLocUnkFlag = 0;
        if (stmt < 0) { /* Local label or '?' */
          hypLocUnkFlag = 1;
        } else {
          if (g_Statement[stmt].type != (char)a_ &&
              g_Statement[stmt].type != (char)p_) hypLocUnkFlag = 1;
                                                               /* Hypothesis */
        }
        if (hypLocUnkFlag) { /* Hypothesis, local label ref, or unknown step */
          g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = g_WrkProof.numSteps;
          g_WrkProof.RPNStackPtr++;
        } else { /* An assertion */

          /* It's a valid assertion, so pop the stack */
          numReqHyp = g_Statement[stmt].numReqHyp;

          /* Error check for exhausted stack */
          if (g_WrkProof.RPNStackPtr < numReqHyp) { /* Stack exhausted -- error */
            if (!g_WrkProof.errorCount) {
              tmpStrPtr = shortDumpRPNStack();
              if (strcmp(left(tmpStrPtr,18),"RPN stack is empty")){
                i = instr(1,tmpStrPtr,"contains ");
                let(&tmpStrPtr,cat(left(tmpStrPtr,i + 7)," only",
                  right(tmpStrPtr,i + 8),
                  NULL));
              }
              if (numReqHyp == 1) {
                let(&tmpStrPtr,cat("a hypothesis but the ",tmpStrPtr,NULL));
              } else {
                let(&tmpStrPtr,cat(str((double)numReqHyp)," hypotheses but the ",tmpStrPtr,
                    NULL));
              }
              sourceError(fbPtr, tokLength, statemNum, cat(
                  "At proof step ",
                  str((double)(g_WrkProof.numSteps + 1)),", statement \"",
                  g_Statement[stmt].labelName,"\" requires ",
                  tmpStrPtr,".",NULL));
              let(&tmpStrPtr, "");
            }
            /* Treat it like an unknown step so stack won't get exhausted */
            g_WrkProof.errorCount++;
            g_WrkProof.proofString[g_WrkProof.numSteps] = -(long)'?';
            /* Push the stack */
            g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = g_WrkProof.numSteps;
            g_WrkProof.RPNStackPtr++;
            if (returnFlag < 3) returnFlag = 3;
            continue;
          } /* End if stack exhausted */

          numReqHyp = g_Statement[stmt].numReqHyp;
          /* Error check for $e <- $f assignments (illegal) */
          /* 18-Jun-2020 nm: Although it would be unusual to to this,
             it is not illegal per the spec.  See
             https://groups.google.com/d/msg/metamath/Cx_d84uorf8/0FrNYTM9BAAJ */
          /**** Deleted 18-Jun-2020 nm ****
          nmbrTmpPtr = g_Statement[stmt].reqHypList;
          for (i = 0; i < numReqHyp; i++) {
            if (g_Statement[nmbrTmpPtr[i]].type == e_) {
              m = g_WrkProof.RPNStackPtr - numReqHyp + i;
              k = g_WrkProof.proofString[g_WrkProof.RPNStack[m]];
              if (k > 0) {
                if (g_Statement[k].type == f_) {
                  if (!g_WrkProof.errorCount) {
                    sourceError(fbPtr, tokLength, statemNum, cat(
                        "Statement \"", g_Statement[stmt].labelName,
                        "\" (proof step ",
                        str((double)(g_WrkProof.numSteps + 1)),
                        ") has its \"$e\" hypothesis \"",
                        g_Statement[nmbrTmpPtr[i]].labelName,
                        "\" assigned the \"$f\" hypothesis \"",
                        g_Statement[k].labelName,
                        "\" at step ", str((double)(g_WrkProof.RPNStack[m] + 1)),
                      ".  The assignment of \"$e\" with \"$f\" is not allowed.",
                        NULL));
                  }
                  g_WrkProof.errorCount++;
                  if (returnFlag < 2) returnFlag = 2;
                }
              }
            }
          }
          **** End of 18-Jun-2020 deletion ****/

          /* Pop the stack */
          g_WrkProof.RPNStackPtr = g_WrkProof.RPNStackPtr - numReqHyp;
          g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = g_WrkProof.numSteps;
          g_WrkProof.RPNStackPtr++;

        }

        g_WrkProof.numSteps++;
        labelMapIndex = 0; /* Reset it for next label */
        break;

      case 1: /* "Digit" (i.e. U...Y) */
        /* 15-Oct-05 nm - Obsolete (skips from YT to UVA, missing UUA) */
        /*
        if (!labelMapIndex) {
          / * First digit; mod digitsLen+1 * /
          labelMapIndex = chrWeight[(long)(fbPtr[0])] + 1;
          labelStart = fbPtr; / * Save label start for error msg * /
        } else {
          labelMapIndex = labelMapIndex * digitsLen +
              chrWeight[(long)(fbPtr[0])];
        }
        */
        /* 15-Oct-05 nm - Corrected algorithm provided by Marnix Klooster. */
        /* Decoding can be done as follows:
             * n := 0
             * for each character c:
                * if c in ['U'..'Y']: n := n * 5 + (c - 'U' + 1)
                * if c in ['A'..'T']: n := n * 20 + (c - 'A' + 1) */
        if (!labelMapIndex) {
          labelMapIndex = chrWeight[(long)(fbPtr[0])] + 1;
          labelStart = fbPtr; /* Save label start for error msg */
        } else {
          labelMapIndex = labelMapIndex * digitsLen +
              chrWeight[(long)(fbPtr[0])] + 1;
          if (bggyAlgo) labelMapIndex--; /* Adjust for buggy algorithm */
        }
        break;

      case 2: /* "Colon" (i.e. Z) */
        if (labelMapIndex) { /* In the middle of some digits */
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, 1, statemNum,
             "A compressed label character was expected here.");
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          labelMapIndex = 0;
        }

        /* Put local label in label map */
        g_WrkProof.compressedPfLabelMap[g_WrkProof.compressedPfNumLabels] =
          -1000 - (g_WrkProof.numSteps - 1);
        g_WrkProof.compressedPfNumLabels++;

        hypLocUnkFlag = 0;

        /* 21-Mar-06 nm Fix bug reported by o'cat */
        if (g_WrkProof.numSteps == 0) {
          /* This will happen if labelChar (Z) is in 1st char pos of
             compressed proof */
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, 1, statemNum, cat(
              "A local label character must occur after a proof step.",NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          /* We want to break here to prevent out of bound g_WrkProof.proofString
             index below. */
          break;
        }

        stmt = g_WrkProof.proofString[g_WrkProof.numSteps - 1];
        if (stmt < 0) { /* Local label or '?' */
          hypLocUnkFlag = 1;
        } else {
          if (g_Statement[stmt].type != (char)a_ &&
              g_Statement[stmt].type != (char)p_) hypLocUnkFlag = 1;
                                                                /* Hypothesis */
        }
        if (hypLocUnkFlag) { /* Hypothesis, local label ref, or unknown step */
          /* If local label references a hypothesis or other local label,
             it is not allowed because it complicates the language but doesn't
             buy anything; would make $e to $f assignments harder to detect */
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, 1, statemNum, cat(
                "The hypothesis or local label reference at proof step ",
                str((double)(g_WrkProof.numSteps)),
                " declares a local label.  Only \"$a\" and \"$p\" statement",
                " labels may have local label declarations.",NULL));
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        }

        break;

      case 3: /* White space */
        break;

      case 4: /* Dollar */
        /* See if we're at the end of the statement */
        if (fbPtr[1] == '.') {
          breakFlag = 1;
          break;
        }
        /* Otherwise, it must be a comment */
        if (fbPtr[1] != '(' && fbPtr[1] != '!') {
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr + 1, 1, statemNum,
             "Expected \".\", \"(\", or \"!\" here.");
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
        } else {
          fbPtr = fbPtr + whiteSpaceLen(fbPtr) - 1; /* -1 because
                  fbPtr will get incremented at end of loop */
        }
        break;

      case 5: /* Question mark */
        if (labelMapIndex) { /* In the middle of some digits */
          if (!g_WrkProof.errorCount) {
            sourceError(fbPtr, 1, statemNum,
             "A compressed label character was expected here.");
          }
          g_WrkProof.errorCount++;
          if (returnFlag < 2) returnFlag = 2;
          labelMapIndex = 0;
        }

        /* Save pointer to source file vs. step for error messages */
        g_WrkProof.stepSrcPtrNmbr[g_WrkProof.numSteps] = 1;
        g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps] = fbPtr; /* Token ptr */

        g_WrkProof.proofString[g_WrkProof.numSteps] = -(long)'?';
        /* returnFlag = 1 means that proof has unknown steps */
        /* 6-Oct-05 nm Ensure that a proof with unknown steps doesn't
           reset the severe error flag if returnFlag > 1 */
        /* returnFlag = 1; */ /*bad - resets severe error flag*/
        if (returnFlag < 1) returnFlag = 1; /* 6-Oct-05 */

        /* Update stack */
        g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = g_WrkProof.numSteps;
        g_WrkProof.RPNStackPtr++;

        g_WrkProof.numSteps++;
        break;

      case 6: /* Illegal */
        if (!g_WrkProof.errorCount) {
          sourceError(fbPtr, 1, statemNum,
           "This character is not legal in a compressed proof.");
        }
        g_WrkProof.errorCount++;
        if (returnFlag < 2) returnFlag = 2;
        break;
      default:
        bug(1715);
        break;
    } /* End switch chrType[fbPtr[0]] */

    if (breakFlag) break;
    fbPtr++;

  } /* End while (1) */

  if (labelMapIndex) { /* In the middle of some digits */
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, 1, statemNum,
       "A compressed label character was expected here.");
    }
    g_WrkProof.errorCount++;
    if (returnFlag < 2) returnFlag = 2;
    /* labelMapIndex = 0; */ /* 18-Sep-2013 never used */
  }

  /* If proof is empty, make it have one unknown step */
  if (g_WrkProof.numSteps == 0) {

    /* For now, this is an error. */
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, 2, statemNum,
          "The proof is empty.  If you don't know the proof, make it a \"?\".");
    }
    g_WrkProof.errorCount++;

    /* Save pointer to source file vs. step for error messages */
    g_WrkProof.stepSrcPtrNmbr[g_WrkProof.numSteps] = 1;
    g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps] = fbPtr; /* Token ptr */

    g_WrkProof.proofString[g_WrkProof.numSteps] = -(long)'?';
    /* 21-Mar-06 nm Deleted 2 lines below; added 3rd - there could be a
       previous error; see 21-Mar-06 entry above */
    /* if (returnFlag > 0) bug(1722); */ /* 13-Oct-05 nm */
    /* returnFlag = 1; */ /* Flag that proof has unknown steps */
    if (returnFlag < 1) returnFlag = 1; /* Flag that proof has unknown steps */

    /* Update stack */
    g_WrkProof.RPNStack[g_WrkProof.RPNStackPtr] = g_WrkProof.numSteps;
    g_WrkProof.RPNStackPtr++;

    g_WrkProof.numSteps++;
    /* 13-Oct-05 nm The line below is redundant */
    /*if (returnFlag < 1) returnFlag = 1;*/ /* Flag for proof with unknown steps */
  }

  g_WrkProof.proofString[g_WrkProof.numSteps] = -1; /* End of proof */
  nmbrZapLen(g_WrkProof.proofString, g_WrkProof.numSteps);
    /* Zap mem pool actual length (because nmbrLen will be used later on this)*/

  /* The stack should have one entry */
  if (g_WrkProof.RPNStackPtr != 1) {
    tmpStrPtr = shortDumpRPNStack();
    fbPtr = g_WrkProof.stepSrcPtrPntr[g_WrkProof.numSteps - 1];
    if (!g_WrkProof.errorCount) {
      sourceError(fbPtr, proofTokenLen(fbPtr), statemNum,
          cat("After proof step ",
          str((double)(g_WrkProof.numSteps))," (the last step), the ",
          tmpStrPtr,".  It should contain exactly one entry.",NULL));
    }
    g_WrkProof.errorCount++;
   if (returnFlag < 3) returnFlag = 3;
  }

  g_WrkProof.errorSeverity = returnFlag;
  return (returnFlag);

} /* parseCompressedProof */


/* 11-Sep-2016 nm */
/* The caller must deallocate the returned nmbrString! */
/* This function just gets the proof so the caller doesn't have to worry
   about cleaning up the g_WrkProof structure.  The returned proof is normal
   or compressed depending on the .mm source; called nmbrUnsquishProof() to
   make sure it is uncompressed if required. */
/* If there is a severe error in the proof, a 1-step proof with "?" will
   be returned. */
/* If printFlag = 1, then error messages are printed, otherwise they aren't.
   This is only partially implemented; some errors may still result in a
   printout.  TODO: pass printFlag to parseProof(), verifyProof() */
/* TODO: use this function to simplify some code that calls parseProof
   directly. */
nmbrString *getProof(long statemNum, flag printFlag) {
  nmbrString *proof = NULL_NMBRSTRING;
  parseProof(statemNum);
  /* We do not need verifyProof() since we don't care about the math
     strings for the proof steps in this function. */
  /* verifyProof(statemNum); */ /* Necessary to set RPN stack ptrs
                             before calling cleanWrkProof() */
  if (g_WrkProof.errorSeverity > 1) {
    if (printFlag) print2(
         "The starting proof has a severe error.  It will not be used.\n");
    nmbrLet(&proof, nmbrAddElement(NULL_NMBRSTRING, -(long)'?'));
  } else {
    nmbrLet(&proof, g_WrkProof.proofString);
  }
  /* Note: the g_WrkProof structure is never deallocated but grows to
     accomodate the largest proof found so far.  The ERASE command will
     deallocate it, though.   cleanWrkProof() just deallocates math strings
     assigned by verifyProof() that aren't needed by this function. */
  /* cleanWrkProof(); */ /* Deallocate verifyProof() storage */
  return proof;
} /* getProof */



/* 2-Feb-2018 nm - lineNum and fileName are now computed by
   getFileAndLineNum() */
void rawSourceError(char *startFile, char *ptr, long tokLen,
    /*long lineNum, vstring fileName,*/ vstring errMsg)
{
  char *startLine;
  char *endLine;
  vstring errLine = "";
  vstring errorMsg = "";

  /* 2-Feb-2018 nm */
  vstring fileName = "";
  long lineNum;

  let(&errorMsg, errMsg); /* Prevent deallocation of errMsg */

  /* 2-Feb-2018 nm */
  fileName = getFileAndLineNum(startFile/*=g_sourcePtr*/, ptr, &lineNum);

  /* Get the line with the error on it */
  startLine = ptr;
  while (startLine[0] != '\n' && startLine > startFile) {
    startLine--;
  }
  if (startLine[0] == '\n'
      && startLine != ptr) /* 8/20/04 nm In case of 0-length line */
    startLine++; /* Go to 1st char on line */
  endLine = ptr;
  while (endLine[0] != '\n' && endLine[0] != 0) {
    endLine++;
  }
  endLine--;
  let(&errLine, space(endLine - startLine + 1));
  if (endLine - startLine + 1 < 0) bug(1721);
  memcpy(errLine, startLine, (size_t)(endLine - startLine) + 1);
  errorMessage(errLine, lineNum, ptr - startLine + 1, tokLen, errorMsg,
      fileName, 0, (char)error_);
  print2("\n");
  let(&errLine, "");
  let(&errorMsg, "");
  let(&fileName, ""); /* 2-Feb-2018 nm */
} /* rawSourceError */

/* The global g_sourcePtr is assumed to point to the start of the raw input
     buffer.
   The global g_sourceLen is assumed to be length of the raw input buffer.
   The global g_IncludeCall array is referenced. */
void sourceError(char *ptr, long tokLen, long stmtNum, vstring errMsg)
{
  char *startLine;
  char *endLine;
  vstring errLine = "";
  long lineNum;
  vstring fileName = "";
  vstring errorMsg = "";

  /* 3-May-2017 nm */
  /* Used for the case where a source file section has been modified */
  char *locSourcePtr;
  /*long locSourceLen;*/

  /* 3-May-2017 nm */
  /* Initialize local pointers to raw input source */
  locSourcePtr = g_sourcePtr;
  /*locSourceLen = g_sourceLen;*/

  let(&errorMsg, errMsg); /* errMsg may become deallocated if this function is
                         called with a string function argument (cat, etc.) */

  if (!stmtNum) {
    lineNum = 0;
    goto SKIP_LINE_NUM; /* This isn't a source file parse */
  }
  if (ptr < g_sourcePtr || ptr > g_sourcePtr + g_sourceLen) {
    /* The pointer is outside the raw input buffer, so it must be a
       SAVEd proof or other overwritten section, so there is no line number. */
    /* 3-May-2017 nm */
    /* Reassign the beginning and end of the source pointer to the
       changed section */
    if (g_Statement[stmtNum].labelSectionChanged == 1
         && ptr >= g_Statement[stmtNum].labelSectionPtr
         && ptr <= g_Statement[stmtNum].labelSectionPtr
             + g_Statement[stmtNum].labelSectionLen) {
      locSourcePtr = g_Statement[stmtNum].labelSectionPtr;
      /*locSourceLen = g_Statement[stmtNum].labelSectionLen;*/
    } else if (g_Statement[stmtNum].mathSectionChanged == 1
         && ptr >= g_Statement[stmtNum].mathSectionPtr
         && ptr <= g_Statement[stmtNum].mathSectionPtr
             + g_Statement[stmtNum].mathSectionLen) {
      locSourcePtr = g_Statement[stmtNum].mathSectionPtr;
      /*locSourceLen = g_Statement[stmtNum].mathSectionLen;*/
    } else if (g_Statement[stmtNum].proofSectionChanged == 1
         && ptr >= g_Statement[stmtNum].proofSectionPtr
         && ptr <= g_Statement[stmtNum].proofSectionPtr
             + g_Statement[stmtNum].proofSectionLen) {
      locSourcePtr = g_Statement[stmtNum].proofSectionPtr;
      /*locSourceLen = g_Statement[stmtNum].proofSectionLen;*/
    } else {
      /* ptr points to neither the original source nor a modified section */
      bug(1741);
    }

    lineNum = 0;
    goto SKIP_LINE_NUM;
  }

  /* 9-Jan-2018 nm */
  /*let(&fileName, "");*/ /* No need - already assigned to empty string */
  fileName = getFileAndLineNum(locSourcePtr/*=g_sourcePtr here*/, ptr, &lineNum);

 SKIP_LINE_NUM:
  /* Get the line with the error on it */
  if (lineNum != 0 && ptr > locSourcePtr) {
    startLine = ptr - 1; /* Allows pointer to point to \n. */
  } else {
    /* Special case:  Error message starts at beginning of file or
       the beginning of a changed section. */
    /* Or, it's a non-source statement; must not point to \n. */
    startLine = ptr;
  }


  /**** 3-May-2017 nm Deleted
  /@ Scan back to beginning of line with error @/
  while (startLine[0] != '\n' && (!lineNum || startLine > locSourcePtr)
      /@ ASCII 1 flags start of SAVEd proof @/
      && (lineNum || startLine[0] != 1)
      /@ lineNum is 0 (e.g. no stmt); stop scan at beg. of file
         or beginning of a changed section @/
      && startLine != locSourcePtr) {
  ***/

  /* 3-May-2017 nm */
  /* Scan back to beginning of line with error */
  while (startLine[0] != '\n' && startLine > locSourcePtr) {

    startLine--;
  }
  /* if (startLine[0] == '\n' || startLine[0] == 1) startLine++; */
  /* 3-May-2017 nm */
  if (startLine[0] == '\n') startLine++;

  /* Scan forward to end of line with error */
  endLine = ptr;
  while (endLine[0] != '\n' && endLine[0] != 0) {
    endLine++;
  }
  endLine--;

  /* Save line with error (with no newline on it) */
  let(&errLine, space(endLine - startLine + 1));
  memcpy(errLine, startLine, (size_t)(endLine - startLine) + 1);

  if (!lineNum) {
    /* Not a source file parse */
    errorMessage(errLine, lineNum, ptr - startLine + 1, tokLen, errorMsg,
        NULL, stmtNum, (char)error_);
  } else {
    errorMessage(errLine, lineNum,
        ptr - startLine + 1, tokLen, /* column */
        errorMsg,
        /*g_IncludeCall[i].source_fn,*/
        fileName,
        stmtNum,
        (char)error_ /* severity */);
  }
  let(&errLine, "");
  let(&errorMsg, "");
  let(&fileName, "");
} /* sourceError */


void mathTokenError(long tokenNum /* 0 is 1st one */,
    nmbrString *tokenList, long stmtNum, vstring errMsg)
{
  long i;
  char *fbPtr;
  fbPtr = g_Statement[stmtNum].mathSectionPtr;
  fbPtr = fbPtr + whiteSpaceLen(fbPtr);
  /* Scan past the tokens before the desired one. */
  /* We use the parsed token length rather than tokenLen() to
     account for adjacent tokens with no white space. */
  for (i = 0; i < tokenNum; i++) {
    fbPtr = fbPtr + g_MathToken[tokenList[i]].length;
    fbPtr = fbPtr + whiteSpaceLen(fbPtr);
  }
  sourceError(fbPtr, g_MathToken[tokenList[tokenNum]].length,
      stmtNum, errMsg);
} /* mathTokenError */

vstring shortDumpRPNStack(void) {
  /* The caller must deallocate the returned string. */
  vstring tmpStr = "";
  vstring tmpStr2 = "";
  long i, k, m;

  for (i = 0; i < g_WrkProof.RPNStackPtr; i++) {
     k = g_WrkProof.RPNStack[i]; /* Step */
     let(&tmpStr,space(g_WrkProof.stepSrcPtrNmbr[k]));
     memcpy(tmpStr,g_WrkProof.stepSrcPtrPntr[k],
         (size_t)(g_WrkProof.stepSrcPtrNmbr[k])); /* Label at step */
     let(&tmpStr2,cat(
         tmpStr2,", ","\"",tmpStr,"\" (step ", str((double)k + 1),")",NULL));
  }
  let(&tmpStr2,right(tmpStr2,3));
  if (g_WrkProof.RPNStackPtr == 2) {
    m = instr(1, tmpStr2, ",");
    let(&tmpStr2,cat(left(tmpStr2,m - 1)," and ",
        right(tmpStr2,m + 1),NULL));
  }
  if (g_WrkProof.RPNStackPtr > 2) {
    for (m = (long)strlen(tmpStr2); m > 0; m--) { /* Find last comma */
      if (tmpStr2[m - 1] == ',') break;
    }
    let(&tmpStr2,cat(left(tmpStr2,m - 1),", and ",
        right(tmpStr2,m + 1),NULL));
  }
  if (g_WrkProof.RPNStackPtr == 1) {
    let(&tmpStr2,cat("one entry, ",tmpStr2,NULL));
  } else {
    let(&tmpStr2,cat(str((double)(g_WrkProof.RPNStackPtr))," entries: ",tmpStr2,NULL));
  }
  let(&tmpStr2,cat("RPN stack contains ",tmpStr2,NULL));
  if (g_WrkProof.RPNStackPtr == 0) let(&tmpStr2,"RPN stack is empty");
  let(&tmpStr, "");
  return(tmpStr2);
} /* shortDumpRPNStack */

/* 10/10/02 */
/* ???Todo:  use this elsewhere in mmpars.c to modularize this lookup */
/* Lookup $a or $p label and return statement number.
   Return -1 if not found. */
long lookupLabel(vstring label)
{
  void *voidPtr; /* bsearch returned value */
  long statemNum;
  /* Find the statement number */
  voidPtr = (void *)bsearch(label, g_labelKeyBase, (size_t)g_numLabelKeys,
      sizeof(long), labelSrchCmp);
  if (!voidPtr) {
    return (-1);
  }
  statemNum = (*(long *)voidPtr); /* Statement number */
  if (g_Statement[statemNum].type != a_ && g_Statement[statemNum].type != p_)
      bug(1718);
  return (statemNum);
} /* lookupLabel */


/* Label comparison for qsort */
int labelSortCmp(const void *key1, const void *key2)
{
  /* Returns -1 if key1 < key2, 0 if equal, 1 if key1 > key2 */
  return (strcmp(g_Statement[ *((long *)key1) ].labelName,
      g_Statement[ *((long *)key2) ].labelName));
} /* labelSortCmp */


/* Label comparison for bsearch */
int labelSrchCmp(const void *key, const void *data)
{
  /* Returns -1 if key < data, 0 if equal, 1 if key > data */
  return (strcmp(key,
      g_Statement[ *((long *)data) ].labelName));
} /* labelSrchCmp */


/* Math symbol comparison for qsort */
int mathSortCmp(const void *key1, const void *key2)
{
  /* Returns -1 if key1 < key2, 0 if equal, 1 if key1 > key2 */
  return (strcmp(g_MathToken[ *((long *)key1) ].tokenName,
      g_MathToken[ *((long *)key2) ].tokenName));
}


/* Math symbol comparison for bsearch */
/* Here, key is pointer to a character string. */
int mathSrchCmp(const void *key, const void *data)
{
  /* Returns -1 if key < data, 0 if equal, 1 if key > data */
  return (strcmp(key, g_MathToken[ *((long *)data) ].tokenName));
}


/* Hypotheses and local label comparison for qsort */
int hypAndLocSortCmp(const void *key1, const void *key2)
{
  /* Returns -1 if key1 < key2, 0 if equal, 1 if key1 > key2 */
  return (strcmp( ((struct sortHypAndLoc *)key1)->labelName,
      ((struct sortHypAndLoc *)key2)->labelName));
}


/* Hypotheses and local label comparison for bsearch */
/* Here, key is pointer to a character string. */
int hypAndLocSrchCmp(const void *key, const void *data)
{
  /* Returns -1 if key < data, 0 if equal, 1 if key > data */
  return (strcmp(key, ((struct sortHypAndLoc *)data)->labelName));
}


/* This function returns the length of the white space starting at ptr.
   Comments are considered white space.  ptr should point to the first character
   of the white space.  If ptr does not point to a white space character, 0
   is returned.  If ptr points to a null character, 0 is returned. */
long whiteSpaceLen(char *ptr)
{
  long i = 0;
  char tmpchr;
  char *ptr1;
  while (1) {
    tmpchr = ptr[i];
    if (!tmpchr) return (i); /* End of string */
    if (tmpchr == '$') {
      if (ptr[i + 1] == '(') {
        while (1) {
          /*ptr1 = strchr(ptr + i + 2, '$'); */
          /* in-line code for speed */
          /* (for the lcc-win32 compiler, this speeds it up from 94 sec
              for set.mm read to 4 sec) */
          for (ptr1 = ptr + i + 2; ptr1[0] != '$'; ptr1++) {
            if (ptr1[0] == 0) {
              if ('$' != 0)
                ptr1 = NULL;
              break;
            }
          }
          /* end in-line strchr code */
          if (!ptr1) {
            return(i + (long)strlen(&ptr[i])); /* Unterminated comment - goto EOF */
          }
          if (ptr1[1] == ')') break;
          i = ptr1 - ptr;
        }
        i = ptr1 - ptr + 2;
        continue;
      } else {
        if (ptr[i + 1] == '!') {
          ptr1 = strchr(ptr + i + 2, '\n');
          if (!ptr1) bug(1716);
          i = ptr1 - ptr + 1;
          continue;
        }
        return(i);
      }
    } /* if (tmpchr == '$') */
    if (isgraph((unsigned char)tmpchr)) return (i);
    i++;
  }
  return(0); /* Dummy return - never happens */
} /* whiteSpaceLen */


/* 31-Dec-2017 nm For .mm file splitting */
/* This function is like whiteSpaceLen() except that comments are NOT
   considered white space.  ptr should point to the first character
   of the white space.  If ptr does not point to a white space character, 0
   is returned.  If ptr points to a null character, 0 is returned. */
long rawWhiteSpaceLen(char *ptr)
{
  long i = 0;
  char tmpchr;
  while (1) {
    tmpchr = ptr[i];
    if (!tmpchr) return (i); /* End of string */
    if (isgraph((unsigned char)tmpchr)) return (i);
    i++;
  }
  return(0); /* Dummy return - never happens */
} /* rawWhiteSpaceLen */


/* This function returns the length of the token (non-white-space) starting at
   ptr.  Comments are considered white space.  ptr should point to the first
   character of the token.  If ptr points to a white space character, 0
   is returned.  If ptr points to a null character, 0 is returned.  If ptr
   points to a keyword, 0 is returned.  A keyword ends a token. */
/* Tokens may be of the form "$nn"; this is tolerated (used in parsing
   user math strings in parseMathTokens()).  An (illegal) token of this form
   in the source will be detected earlier, so this won't cause
   syntax violations to slip by in the source. */
long tokenLen(char *ptr)
{
  long i = 0;
  char tmpchr;
  while (1) {
    tmpchr = ptr[i];
    if (tmpchr == '$') {
      if (ptr[i + 1] == '$') { /* '$$' character */
        i = i + 2;
        continue;
      } else {
        /* Tolerate digit after "$" */
        if (ptr[i + 1] >= '0' && ptr[i + 1] <= '9') {
          i = i + 2;
          continue;
        } else {
          return(i); /* Keyword or comment */
        }
      }
    }
    if (!isgraph((unsigned char)tmpchr)) return (i); /* White space or null */
    i++;
  }
  return(0); /* Dummy return (never happens) */
} /* tokenLen */


/* This function returns the length of the token (non-white-space) starting at
   ptr.  Comments are considered white space.  ptr should point to the first
   character of the token.  If ptr points to a white space character, 0
   is returned.  If ptr points to a null character, 0 is returned. */
/* Unlike tokenLen(), keywords are not treated as special.  In particular:
   if ptr points to a keyword, 0 is NOT returned (instead, 2 is returned),
   and a keyword does NOT end a token (which is a relic of days before
   whitespace surrounding a token was part of the spec, but still serves
   a useful purpose in token() for friendlier error detection). */
long rawTokenLen(char *ptr)
{
  long i = 0;
  char tmpchr;
  while (1) {
    tmpchr = ptr[i];
    if (!isgraph((unsigned char)tmpchr)) return (i); /* White space or null */
    i++;
  }
  return(0); /* Dummy return (never happens) */
} /* rawTokenLen */


/* This function returns the length of the proof token starting at
   ptr.  Comments are considered white space.  ptr should point to the first
   character of the token.  If ptr points to a white space character, 0
   is returned.  If ptr points to a null character, 0 is returned.  If ptr
   points to a keyword, 0 is returned.  A keyword ends a token.
   ":" and "?" and "(" and ")" and "=" (25-Jan-2016) are considered tokens. */
long proofTokenLen(char *ptr)
{
  long i = 0;
  char tmpchr;
  if (ptr[0] == ':') return (1); /* The token is a colon */
  if (ptr[0] == '?') return (1); /* The token is a "?" */
  if (ptr[0] == '(') return (1); /* The token is a "(" (compressed proof) */
  if (ptr[0] == ')') return (1); /* The token is a ")" (compressed proof) */
  /* 25-Jan-2016 nm */
  if (ptr[0] == '=') return (1); /* The token is a "=" (/EXPLICIT proof) */
  while (1) {
    tmpchr = ptr[i];
    if (tmpchr == '$') {
      if (ptr[i + 1] == '$') { /* '$$' character */
        i = i + 2;
        continue;
      } else {
        return(i); /* Keyword or comment */
      }
    }
    if (!isgraph((unsigned char)tmpchr)) return (i); /* White space or null */
    if (tmpchr == ':') return(i); /* Colon ends a token */
    if (tmpchr == '?') return(i); /* "?" ends a token */
    if (tmpchr == '(') return(i); /* "(" ends a token */
    if (tmpchr == ')') return(i); /* ")" ends a token */
    if (tmpchr == '=') return(i); /* "=" ends a token */ /* 25-Jan-2016 nm */
    i++;
  }
  return(0); /* Dummy return - never happens */
}


/* 9-Jan-2018 nm */
/* Counts the number of \n between start for length chars.
   If length = -1, then use end-of-string 0 to stop.
   If length >= 0, then scan at most length chars, but stop
       if end-of-string 0 is found. */
long countLines(vstring start, long length) {
  long lines, i;
  lines = 0;
  if (length == -1) {
    i = 0;
    while (1) {
      if (start[i] == '\n') lines++;
      if (start[i] == 0) break;
      i++;
    }
  } else {
    for (i = 0; i < length; i++) {
      if (start[i] == '\n') lines++;
      if (start[i] == 0) break;
    }
  }
  return lines;
} /* countLines */


/* Return (for output) the complete contents of a statement, including all
   white space and comments, from first token through all white space and
   comments after last token. */
/* This allows us to modify the input file with Metamath. */
/* Note: the text near end of file is obtained from g_Statement[g_statements
   + 1] */
/* ???This does not yet implement restoration of the various input files;
      all included files are merged into one. */
/* Caller must deallocated returned string. */
/* reformatFlag= 0: WRITE SOURCE, 1: WRITE SOURCE / REFORMAT,
   2: WRITE SOURCE / WRAP */
/* Note that the labelSection, mathSection, and proofSection do not
   contain keywords ($a, $p,...; $=; $.).  The keywords are added
   by this function when the statement is written. */
/* 31-Dec-2020 nm This must be called in sequence for all statements,
   since the previous statement is needed to populate dollarDpos
   and previousType */
vstring outputStatement(long stmt, /*flag cleanFlag, 3-May-2017 removed */
    flag reformatFlag)
{
  vstring labelSection = "";
  vstring mathSection = "";
  vstring proofSection = "";
  vstring labelSectionSave = "";
  vstring mathSectionSave = "";
  vstring proofSectionSave = "";
  vstring output = "";
  /* flag newProofFlag; */ /* deleted 3-May-2017 nm */
  /* For reformatting: */
  long slen; /* To save local string length */ /* 31-Dec-2020 nm */
  long pos;
  long indent;
  static long dollarDpos = 0;
  static char previousType = illegal_;  /* '?' in mmdata.h */
  long commentStart;
  long commentEnd;
  vstring comment = "";
  vstring str1 = "";
  long length;
  flag nowrapHtml;

  /* For getContribs in-line error insertion to assist massive corrections
  long i;
  vstring ca = "", cd = "", ra = "", rd = "", sa = "", sd = "", md = "";
  long saveWidth;
  */

  /* 31-Dec-2020 nm */
  /* Re-initialize static variables for a second 'write source' */
  if (stmt == 1) {
    previousType = illegal_;  /* '?' in mmdata.h */
    dollarDpos = 0;
  }

  let(&labelSection, space(g_Statement[stmt].labelSectionLen));
  memcpy(labelSection, g_Statement[stmt].labelSectionPtr,
      (size_t)(g_Statement[stmt].labelSectionLen));

  if (stmt == g_statements + 1) return labelSection; /* Special case - EOF */

  /******* 3-May-2017 nm  "/CLEAN" is no longer supported
  /@ 1-Jul-2011 nm @/
  if (cleanFlag) {
    /@ cleanFlag = 1: User is removing theorems with $( [?] $) dates @/
    /@ Most of the WRITE SOURCE / CLEAN processing is done in the
       writeSource() that calls this.  Here, we just remove any
       $( [?} $) date comment missed by that algorithm. @/
    if (labelSection[0] == '\n') { /@ True unless user edited source @/
      pos = instr(1, labelSection, "$( [?] $)");
      if (pos != 0) {
        pos = instr(pos + 9, labelSection, "\n");
        if (pos != 0) {
          /@ Use pos instead of pos + 1 so that we include the \n @/
          let(&labelSection, right(labelSection, pos));
        }
      }
    }
  }
  ************/

  let(&mathSection, space(g_Statement[stmt].mathSectionLen));
  memcpy(mathSection, g_Statement[stmt].mathSectionPtr,
      (size_t)(g_Statement[stmt].mathSectionLen));

  let(&proofSection, space(g_Statement[stmt].proofSectionLen));
  memcpy(proofSection, g_Statement[stmt].proofSectionPtr,
      (size_t)(g_Statement[stmt].proofSectionLen));

  /* 31-Dec-2017 nm */
  let(&labelSectionSave, labelSection);
  let(&mathSectionSave, mathSection);
  let(&proofSectionSave, proofSection);


  /* 12-Jun-2011 nm Added this section to reformat statements to match the
     current set.mm convention */
  if (reformatFlag > 0) {  /* 1 = WRITE SOURCE / FORMAT or 2 = / REWRAP */
    /* Put standard indentation before the ${, etc. */
#define INDENT_FIRST 2
#define INDENT_INCR 2
    indent = INDENT_FIRST + (INDENT_INCR * g_Statement[stmt].scope);
    /* g_Statement[stmt].scope is at start of stmt not end; adjust $} */
    if (g_Statement[stmt].type == rb_) indent = indent - INDENT_INCR;

    /* 9-Jul-2011 nm Added */
    /* Untab the label section */
    if (strchr(labelSection, '\t') != NULL) { /* Only if has tab (speedup) */
      let(&labelSection, edit(labelSection, 2048/*untab*/));
    }
    /* Untab the math section */
    if (strchr(mathSection, '\t') != NULL) { /* Only if has tab (speedup) */
      let(&mathSection, edit(mathSection, 2048/*untab*/));
    }

    /* Reformat the label section */

    /* Remove spaces a end of line */
    /* This is a pretty inefficient loop, but hopefully lots of spaces
       at end of lines is a rare occurrence */
    while (1) {
      pos = instr(1, labelSection, " \n");
      if (pos == 0) break;
      let(&labelSection, cat(left(labelSection, pos - 1),
          right(labelSection, pos + 1), NULL));
    }

    /* Don't allow more than 2 consecutive blank lines */
    while (1) {
      /* Match to 3 or more blank lines */
      pos = instr(1, labelSection, "\n\n\n\n");
      /* If it matches, remove one of the \n and restart loop */
      if (pos == 0) break;
      let(&labelSection, cat(left(labelSection, pos - 1),
          right(labelSection, pos + 1), NULL));
    }

    switch (g_Statement[stmt].type) {
      case lb_: /* ${ */
      case rb_: /* $} */
      case v_: /* $v */
      case c_: /* $c */
      case d_: /* $d */
        /* These 5 cases are for keywords that don't have labels, so that
           labelSection is simply the text before the keyword. */

        /*** 31-Dec-2020 nm ${, $}, $v, $c, $d cases completely rewritten */
        /* Strip any trailing spaces */
        let(&labelSection, edit(labelSection, 128 /*trailing spaces*/));
        slen = (long)strlen(labelSection); /* Save to avoid recomputing */
        /* See if last character is \n; if not, add it */
        /* (If there's no text - just spaces - they've been stripped, and
            leave labelSection as an empty string.) */
        /* We use slen - 1 because native C strings start at index 0. */
        if (slen != 0 && labelSection[slen - 1] != '\n') {
          let(&labelSection, cat(labelSection, "\n", NULL));
          slen++;
        }
        /* Put a blank line between $} and ${ if there is none. */
        if (g_Statement[stmt].type == lb_
            && previousType == rb_) {
          if (slen == 0) {
            /* There's no text (comment) between $} and ${, so make it
               a blank line */
            let(&labelSection, "\n\n");
            slen = 2; /* 2-Aug-2021 */
          } else {
            /* There's text between $} and ${ */
            if (instr(1, labelSection, "\n\n") == 0) {
              /* If there's no blank line, add one (note that code above
                 ensures non-empty labelSection will end with \n, so
                 add just 1 more) */
              let(&labelSection, cat(labelSection, "\n", NULL));
              slen++; /* 2-Aug-2021 */
            }
          } /* if slen == 0 else */
        } /* if $}...${ */
        if (slen == 0) {
          /* 2-Aug-2021 */
          /* If the statement continues on this line, put 2 spaces before it */
          let(&labelSection, cat(labelSection, "  ", NULL));
          slen = 2;
        } else {
          /* Add indentation to end of labelSection i.e. before the keyword */
          /* If there was text (comment) before the keyword on the same line,
             it now has a \n after it, thus the indentation of the keyword will
             be consistent. */
          let(&labelSection, cat(labelSection, space(indent), NULL));
          slen = slen + indent;
        }
        if (g_Statement[stmt].type == d_/*$d*/) {
          /* Try to put as many $d's on one line as will fit.
             First we remove redundant spaces in mathSection. */
          let(&mathSection, edit(mathSection,
              /*4*//*discard \n*/ + 16/*reduce spaces*/));
              /* 31-Dec-2020: No longer discard \n so that user can
                 insert \n to break a huge $d with say >40 variables,
                 which itself can exceed line length. */
          if (strlen(edit(labelSection, 4/*discard \n*/ + 2/*discard spaces*/))
              == 0) /* This and previous $d are separated by spaces
                       and newlines only */
              {
            if (previousType == d_) { /* The previous statement was a $d */
              /* See if the $d will fit on the current line */
              if (dollarDpos + 2 + (signed)(strlen(mathSection)) + 4
                  <= g_screenWidth) {
                let(&labelSection, "  ");  /* 2 spaces between $d's */
                dollarDpos = dollarDpos + 2 + (long)strlen(mathSection) + 4;
              } else {
                /* The $d assembly overflowed; start on new line */
                /* Add 4 = '$d' length + '$.' length */
                dollarDpos = indent + (long)strlen(mathSection) + 4;
                /* Start new line */
                let(&labelSection, cat("\n", space(indent), NULL));
              }
            } else { /* previousType != $d */
              /* If the previous statement type (keyword) was not $d,
                 we want to start the assembly of $d statements here. */
              dollarDpos = indent + (long)strlen(mathSection) + 4;
            } /* if previousType == $d else */
          } else {
            /* There is some text (comment) between this $d and previous,
               so we restart assembling $d groups on this line */
            dollarDpos = indent + (long)strlen(mathSection) + 4;
          } /* if labelSection = spaces and newlines only else */
        } /* if g_Statement[stmt].type == d_ */
        /*** (End of 31-Dec-2020 rewrite) ***/

        break; /* End of ${, $}, $v, $c, $d cases */
      case a_:    /* $a */
      case p_:    /* $p */
        /* Get last $( */
        commentStart = rinstr(labelSection,  "$(");
        /* Get last $) */
        commentEnd = rinstr(labelSection, "$)") + 1;
        if (commentEnd < commentStart) {
          print2("?Make sure syntax passes before running / REWRAP.\n");
          print2("(Forcing a bug check since output may be corrupted.)\n");
          bug(1725);
        }
        if (commentStart != 0) {
          let(&comment, seg(labelSection, commentStart, commentEnd));
        } else {
          /* If there is no comment before $a or $p, add dummy comment */
          let(&comment, "$( PLEASE PUT DESCRIPTION HERE. $)");
        }

        /* 4-Nov-2015 The section below is for a one-time attribution in
           definitions and should be commented out for normal use. */
        /******* TODO: DELETE THIS SOMEDAY *********/
        /*********
        long j;
        if (g_Statement[stmt].type == a_
             && instr(1, comment, "(Contributed") == 0
             && (!strcmp(left(g_Statement[stmt].labelName, 3), "df-")
               || !strcmp(left(g_Statement[stmt].labelName, 3), "ax-"))) {
          let(&str1, "");
          str1 = traceUsage(stmt, 0, 0);
          for (i = 1; i <= g_statements; i++) {
            if (str1[i] == 'Y') break;
          }
          if (i >= g_statements) {
             let(&ca, "??");
             let(&cd, cat("??", "-???", "-????", NULL));
          } else {

            /@ 3-May-2017 nm (not tested because code is commented out) @/
            let(&ca, "");
            ca = getContrib(i, CONTRIBUTOR);
            let(&cd, "");
            cd = getContrib(i, CONTRIB_DATE);
            let (&rd, "");
            rd = getContrib(i, REVISE_DATE);

            /@@@@@@@@@ deleted 3-May-2017
            getContrib(i, &ca, &cd, &ra, &rd, &sa, &sd, &md, 0);
            @@@@@@/

            if (cd[0] == 0) {
              let(&ca, "??");
              getProofDate(i, &cd, &rd);
              if (rd[0]) let(&cd, rd);
              if (cd[0] == 0) {
                let(&cd, cat("??", "-???", "-????", NULL));
              }
            }
          }
          let(&comment, cat(left(comment, (long)strlen(comment) - 2),
              " (Contributed by ", ca, ", ", cd, ".) $)", NULL));
          let(&ca, "");
          let(&cd, "");
          let(&ra, "");
          let(&rd, "");
          let(&sa, "");
          let(&sd, "");
          let(&str1, "");
        }

        if (g_Statement[stmt].type == p_
           && instr(1, comment, "(Contributed") == 0) {
          getProofDate(stmt, &cd, &rd);
          if (rd[0]) let(&cd, rd);
          if (cd[0] == 0) {
            let(&cd, cat("??", "-???", "-????", NULL));
          }

          i = instr(1, comment, "(Revised") - 1;
          if (i <= 0) i = (long)strlen(comment);
          j = instr(1, comment, "(Proof shorten") - 1;
          if (j <= 0) j = (long)strlen(comment);

          if (j < i) i = j;
          if ((long)strlen(comment) - 2 < i) i = (long)strlen(comment) - 2;

          let(&ca, "??");
          let(&comment, cat(left(comment, i - 1),
              " (Contributed by ", ca, ", ", cd, ".) ", right(comment, i),
              NULL));
          let(&ca, "");
          let(&cd, "");
          let(&ra, "");
          let(&rd, "");
          let(&sa, "");
          let(&sd, "");
          let(&str1, "");
        }
        ************/

        let(&labelSection, left(labelSection, commentStart - 1));
        /* Get the last newline before the comment */
        pos = rinstr(labelSection, "\n");

        /* 9-Jul-2011 nm Added */
        /* If previous statement was $e, take out any blank line */
        if (previousType == e_ && pos == 2 && labelSection[0] == '\n') {
          let(&labelSection, right(labelSection, 2));
          pos = 1;
        }

        /* If there is no '\n', insert it (unless first line in file) */
        if (pos == 0 && stmt > 1) {
          let(&labelSection, cat(edit(labelSection, 128 /* trailing spaces */),
              "\n", NULL));
          pos = (long)strlen(labelSection) + 1;
        }

        /* 30-Jun-2020 nm */
        /* If comment has "<HTML>", don't reformat. */
        if (instr(1, comment, "<HTML>") != 0) {
          nowrapHtml = 1;
        } else {
          nowrapHtml = 0;
        }

        /* 30-Jun-2020 nm Added nowrapHtml condition */
        if (nowrapHtml == 0) {

          /* 30-Jun-2020 nm */
          /* This strips off leading spaces before $( (start of comment).  Don't
             do it for <HTML>, since spacing before $( is manual. */
          let(&labelSection, left(labelSection, pos));

          if (reformatFlag == 2) {
            /* If / REWRAP was specified, unwrap and rewrap the line */
            let(&str1, "");
            str1 = rewrapComment(comment);
            let(&comment, str1);
          }

          /* Make sure that start of new lines inside the comment have no
             trailing space (because printLongLine doesn't do this after
             explict break) */
          pos = 0;
          while (1) {
            pos = instr(pos + 1, comment, "\n");
            if (pos == 0) break; /* Beyond last line in comment */
            /* Remove leading spaces from comment line */
            length = 0;
            while (1) {
              if (comment[pos + length] != ' ') break;
              length++;
            }
            /* Add back indent+3 spaces to beginning of line in comment */
            let(&comment, cat(left(comment, pos),
                (comment[pos + length] != '\n')
                    ? space(indent + 3)
                    : "",  /* Don't add indentation if line is blank */
                right(comment, pos + length + 1), NULL));
          }

          /* Reformat the comment to wrap if necessary */
          if (g_outputToString == 1) bug(1726);
          g_outputToString = 1;
          let(&g_printString, "");
          /* 7-Nov-2015 nm For getContribs in-line error insertion to assist
             massive corrections; maybe delete someday */
          /***********
          saveWidth = g_screenWidth;
          g_screenWidth = 9999;
          /@i=getContrib(stmt, &ca, &cd, &ra, &rd, &sa, &sd, &md, 1);@/
          let(&ca, "");
          /@ 3-May-2017 nm @/
          ca = getContrib(stmt, ERROR_CHECK);
          g_screenWidth = saveWidth;
          ************/
          printLongLine(cat(space(indent), comment, NULL),
              space(indent + 3), " ");
          let(&comment, g_printString);
          let(&g_printString, "");
          g_outputToString = 0;
#define ASCII_4 4
          /* Restore ASCII_4 characters put in by rewrapComment() to space */
          length = (long)strlen(comment);
          for (pos = 2; pos < length - 2; pos++) {
             /* For debugging: */
             /* if (comment[pos] == ASCII_4) comment[pos] = '#'; */
             if (comment[pos] == ASCII_4) comment[pos] = ' ';
          }
        } /* if nowrapHtml == 0 */ else {
          /* 30-Jun-2020 nm */
          /* If there was an <HTML> tag, we don't modify the comment. */
          /* However, we need "\n" after "$)" (end of comment), corresponding to
             the one that was put there by printLongLine() in the normal
             non-<HTML> case above */
          let(&comment, cat(comment, "\n", NULL));
        }

        /* Remove any trailing spaces */
        pos = 2;
        while(1) {
          pos = instr(pos + 1, comment, " \n");
          if (!pos) break;
          let(&comment, cat(left(comment, pos - 1), right(comment, pos + 1),
              NULL));
          pos = pos - 2;
        }

        /* Complete the label section */
        let(&labelSection, cat(labelSection, comment,
            space(indent), g_Statement[stmt].labelName, " ", NULL));
        break; /* End of $a, $p cases */
      case e_:    /* $e */
      case f_:    /* $f */
        pos = rinstr(labelSection, g_Statement[stmt].labelName);
        let(&labelSection, left(labelSection, pos - 1));
        pos = rinstr(labelSection, "\n");
        /* If there is none, insert it (unless first line in file) */
        if (pos == 0 && stmt > 1) {
          let(&labelSection, cat(edit(labelSection, 128 /* trailing spaces */),
              "\n", NULL));
          pos = (long)strlen(labelSection) + 1;
        }
        let(&labelSection, left(labelSection, pos));
        /* If previous statement is $d or $e and there is no comment after it,
           discard entire rest of label to get rid of redundant blank lines */
        if ((previousType == d_       /* 31-Dec-2020 nm (simplified) */
              || previousType == e_)
            && instr(1, labelSection, "$(") == 0) {
          let(&labelSection, "\n");
        }
        /* Complete the label section */
        let(&labelSection, cat(labelSection,
            space(indent), g_Statement[stmt].labelName, " ", NULL));
        break; /* End of $e, $f cases */
      default: bug(1727);
    } /* switch (g_Statement[stmt].type) */

    /* Reformat the math section */
    switch (g_Statement[stmt].type) {
      case lb_: /* ${ */
      case rb_: /* $} */
      case v_: /* $v */
      case c_: /* $c */
      case d_: /* $d */
      case a_: /* $a */
      case p_: /* $p */
      case e_: /* $e */
      case f_: /* $f */
        /* Remove blank lines */
        while (1) {
          pos = instr(1, mathSection, "\n\n");
          if (pos == 0) break;
          let(&mathSection, cat(left(mathSection, pos),
              right(mathSection, pos + 2), NULL));
        }

        /* 6-Mar-2016 nm Turn off wrapping of math section.  It should be
           done manually for best readability. */
        /*
        /@ Remove leading and trailing space and trailing new lines @/
        while(1) {
          let(&mathSection, edit(mathSection,
              8 /@ leading sp @/ + 128 /@ trailing sp @/));
          if (mathSection[strlen(mathSection) - 1] != '\n') break;
          let(&mathSection, left(mathSection, (long)strlen(mathSection) - 1));
        }
        let(&mathSection, cat(" ", mathSection, " ", NULL));
                   /@ Restore standard leading/trailing space stripped above @/
        */

        /* Reduce multiple in-line spaces to single space */
        pos = 0;
        while(1) {
          pos = instr(pos + 1, mathSection, "  ");
          if (pos == 0) break;
          if (pos > 1) {
            if (mathSection[pos - 2] != '\n' && mathSection[pos - 2] != ' ') {
              /* It's not at the start of a line, so reduce it */
              let(&mathSection, cat(left(mathSection, pos),
                  right(mathSection, pos + 2), NULL));
              pos--;
            }
          }
        }

        /* 6-Mar-2016 nm Turn off wrapping of math section.  It should be
           done manually for best readability. */
        /*
        /@ Wrap long lines @/
        length = indent + 2 /@ Prefix length - add 2 for keyword ${, etc. @/
            /@ Add 1 for space after label, if $e, $f, $a, $p @/
            + (((g_Statement[stmt].labelName)[0]) ?
                ((long)strlen(g_Statement[stmt].labelName) + 1) : 0);
        if (g_outputToString == 1) bug(1728);
        g_outputToString = 1;
        let(&g_printString, "");
        printLongLine(cat(space(length), mathSection, "$.", NULL),
            space(indent + 4), " ");
        g_outputToString = 0;
        let(&mathSection, left(g_printString, (long)strlen(g_printString) - 3));
            /@ Trim off "$." plus "\n" @/
        let(&mathSection, right(mathSection, length + 1));
        let(&g_printString, "");
        */

        break;
      default: bug(1729);
    }

    /* Set previous state for next statement */
    if (g_Statement[stmt].type == d_) {
      /* dollarDpos is computed in the processing above */
    } else {
      dollarDpos = 0; /* Reset it */
    }
    previousType = g_Statement[stmt].type;
    /* let(&comment, ""); */  /* Deallocate string memory */ /* (done below) */
  } /* if reformatFlag */

  let(&output, labelSection);

  /* Add statement keyword */
  let(&output, cat(output, "$", chr(g_Statement[stmt].type), NULL));

  /* Add math section and proof */
  if (g_Statement[stmt].mathSectionLen != 0) {
    let(&output, cat(output, mathSection, NULL));
    /* newProofFlag = 0; */  /* deleted 3-May-2017 nm */

    if (g_Statement[stmt].type == (char)p_) {
      let(&output, cat(output, "$=", proofSection, NULL));

      /******** deleted 3-May-2017 nm
      if (g_Statement[stmt].proofSectionPtr[-1] == 1) {
        /@ ASCII 1 is flag that line is not from original source file @/
        newProofFlag = 1; /@ Proof is result of SAVE (NEW_)PROOF command @/
      }
    }
    /@ If it's not a source file line, the proof text should supply the
       statement terminator, so that additional text may be added after
       the terminator if desired.  (I.e., date in SAVE NEW_PROOF command) @/
    if (!newProofFlag) let(&output, cat(output, "$.", NULL));
    ********/

    /* 3-May-2017 nm */
    }
    let(&output, cat(output, "$.", NULL));

  }

  /* Added 10/24/03 */
  /* Make sure the line has no carriage-returns */
  if (strchr(output, '\r') != NULL) {

    /* 31-Dec-2017 nm */
    /* We are now using readFileToString, so this should never happen. */
    bug(1758);

    /* This may happen with Cygwin's gcc, where DOS CR-LF becomes CR-CR-LF
       in the output file */
    /* Someday we should investigate the use of readFileToString() in
       mminou.c for the main set.mm READ function, to solve this cleanly. */
    let(&output, edit(output, 8192)); /* Discard CR's */
  }

  /* 31-Dec-2017 nm */
  /* This function is no longer used to supply the output, but just to
     do any reformatting/wrapping.  Now writeSourceToBuffer() builds the
     output source.  So instead, we update the g_Statement[] content with
     any changes, which are read by writeSourceToBuffer() and also saved
     in the g_Statement[] array for any future write source.  Eventually
     we should replace WRITE SOURCE.../REWRAP with a REWRAP(?) command. */
  if (strcmp(labelSection, labelSectionSave)) {
    g_Statement[stmt].labelSectionLen = (long)strlen(labelSection);
    if (g_Statement[stmt].labelSectionChanged == 1) {
      let(&(g_Statement[stmt].labelSectionPtr), labelSection);
    } else {
      /* This is the first time we've updated the label section */
      g_Statement[stmt].labelSectionChanged = 1;
      g_Statement[stmt].labelSectionPtr = labelSection;
      labelSection = ""; /* so that labelSectionPtr won't be deallocated */
    }
  }
  if (strcmp(mathSection, mathSectionSave)) {
    g_Statement[stmt].mathSectionLen = (long)strlen(mathSection);
    if (g_Statement[stmt].mathSectionChanged == 1) {
      let(&(g_Statement[stmt].mathSectionPtr), mathSection);
    } else {
      /* This is the first time we've updated the math section */
      g_Statement[stmt].mathSectionChanged = 1;
      g_Statement[stmt].mathSectionPtr = mathSection;
      mathSection = ""; /* so that mathSectionPtr won't be deallocated */
    }
  }
  /* (I don't see anywhere that proofSection will change.  So make
     it a bug check to force us to look into it.) */
  if (strcmp(proofSection, proofSectionSave)) {
    bug(1757); /* This may not be a bug */
    g_Statement[stmt].proofSectionLen = (long)strlen(proofSection);
    if (g_Statement[stmt].proofSectionChanged == 1) {
      let(&(g_Statement[stmt].proofSectionPtr), proofSection);
    } else {
      /* This is the first time we've updated the proof section */
      g_Statement[stmt].proofSectionChanged = 1;
      g_Statement[stmt].proofSectionPtr = proofSection;
      proofSection = ""; /* so that proofSectionPtr won't be deallocated */
    }
  }

  let(&labelSection, "");
  let(&mathSection, "");
  let(&proofSection, "");
  let(&labelSectionSave, "");
  let(&mathSectionSave, "");
  let(&proofSectionSave, "");
  let(&comment, "");
  let(&str1, "");
  return output; /* The calling routine must deallocate this vstring */
} /* outputStatement */

/* 12-Jun-2011 nm */
/* Unwrap the lines in a comment then re-wrap them according to set.mm
   conventions.  This may be overly aggressive, and user should do a
   diff to see if result is as desired.  Called by WRITE SOURCE / REWRAP.
   Caller must deallocate returned vstring. */
vstring rewrapComment(vstring comment1)
{
  /* Punctuation from mmwtex.c */
#define OPENING_PUNCTUATION "(['\""
/* #define CLOSING_PUNCTUATION ".,;)?!:]'\"_-" */
#define CLOSING_PUNCTUATION ".,;)?!:]'\""
#define SENTENCE_END_PUNCTUATION ")'\""
  vstring comment = "";
  vstring commentTemplate = ""; /* Non-breaking space template */
  long length, pos, i, j;
  vstring ch; /* Pointer only; do not allocate */
  flag mathmode = 0;

  let(&comment, comment1); /* Grab arg so it can be reassigned */

  /* Ignore pre-formatted comments */
  /* if (instr(1, comment, "<PRE>") != 0) return comment; */
  /* 30-Jun-2020 nm This is now done in the calling program */
  /*
  if (instr(1, comment, "<HTML>") != 0) {
    return comment;  /@ 26-Dec-2011 nm @/
  }
  */

  /* Make sure back quotes are surrounded by space */
  pos = 2;
  mathmode = 0;
  while (1) {
    pos = instr(pos + 1, comment, "`");
    if (pos == 0) break;
    mathmode = (flag)(1 - mathmode);
    if (comment[pos - 2] == '`' || comment[pos] == '`') continue;
            /* See if previous or next char is "`"; ignore "``" escape */
    if (comment[pos] != ' ' && comment[pos] != '\n') {
      /* Currently, mmwtex.c doesn't correctly handle broken subscript (_)
         or broken hyphen (-) in the CLOSING_PUNCTUATION, so allow these two as
         exceptions until that is fixed.   E.g. ` a ` _2 doesn't yield
         HTML subscript; instead we need ` a `_2. */
      if (mathmode == 1 || (comment[pos] != '_' && comment[pos] != '-')) {
        /* Add a space after back quote if none */
        let(&comment, cat(left(comment, pos), " ",
            right(comment, pos + 1), NULL));
      }
    }
    if (comment[pos - 2] != ' ') {
      /* Add a space before back quote if none */
      let(&comment, cat(left(comment, pos - 1), " ",
          right(comment, pos), NULL));
      pos++; /* Go past the "`" */
    }
  }

  /* Make sure "~" for labels are surrounded by space */
  if (instr(2, comment, "`") == 0) {  /* For now, just process comments
         not containing math symbols.  More complicated code is needed
         to ignore ~ in math symbols; maybe add it someday. */
    pos = 2;
    while (1) {
      pos = instr(pos + 1, comment, "~");
      if (pos == 0) break;
      if (comment[pos - 2] == '~' || comment[pos] == '~') continue;
              /* See if previous or next char is "~"; ignore "~~" escape */
      if (comment[pos] != ' ') {
        /* Add a space after tilde if none */
        let(&comment, cat(left(comment, pos), " ",
            right(comment, pos + 1), NULL));
      }
      if (comment[pos - 2] != ' ') {
        /* Add a space before tilde if none */
        let(&comment, cat(left(comment, pos - 1), " ",
            right(comment, pos), NULL));
        pos++; /* Go past the "~" */
      }
    }
  }

  /* Change all newlines to space unless double newline */
  /* Note:  it is assumed that blank lines have no spaces
     for this to work; the user must ensure that. */
  length = (long)strlen(comment);
  for (pos = 2; pos < length - 2; pos++) {
    if (comment[pos] == '\n' && comment[pos - 1] != '\n'
        && comment[pos + 1] != '\n')
      comment[pos] = ' ';
  }
  let(&comment, edit(comment, 16 /* reduce spaces */));

  /* Remove spaces and blank lines at end of comment */
  while (1) {
    length = (long)strlen(comment);
    if (comment[length - 3] != ' ') bug(1730);
            /* Should have been syntax err (no space before "$)") */
    if (comment[length - 4] != ' ' && comment[length - 4] != '\n') break;
    let(&comment, cat(left(comment, length - 4),
        right(comment, length - 2), NULL));
  }

  /* Put period at end of comment ending with lowercase letter */
  /* Note:  This will not detect a '~ label' at end of comment.
     However, user should have ended it with a period, and if not the
     label + period is unlikly to be valid and thus will
     usually be detected by 'verify markup'.
     (We could enhace the code here to do that if it becomes a problem.) */
  length = (long)strlen(comment);
  if (islower((unsigned char)(comment[length - 4]))) {
    let(&comment, cat(left(comment, length - 3), ". $)", NULL));
  }

  /* Change to ASCII 4 those spaces where the line shouldn't be
     broken */
  mathmode = 0;
  for (pos = 3; pos < length - 2; pos++) {
    if (comment[pos] == '`') { /* Start or end of math string */
/*
      if (mathmode == 0) {
        if (comment[pos - 1] == ' '
            && strchr(OPENING_PUNCTUATION, comment[pos - 2]) != NULL)
          /@ Keep opening punctuation on same line @/
          comment[pos - 1] = ASCII_4;
      } else {
        if (comment[pos + 1] == ' '
            && strchr(CLOSING_PUNCTUATION, comment[pos + 2]) != NULL)
          /@ Keep closing punctuation on same line @/
          comment[pos + 1] = ASCII_4;
      }
*/
      mathmode = (char)(1 - mathmode);
    }
    if ( mathmode == 1 && comment[pos] == ' ')
      /* We assign comment[] rather than commentTemplate to avoid confusion of
         math with punctuation.  Also, commentTemplate would be misaligned
         anyway due to adding of spaces below. */
      comment[pos] = ASCII_4;
  }

  /* 3-May-2016 nm */
  /* Look for proof discouraged or usage discouraged markup and change their
     spaces to ASCII 4 to prevent line breaks in the middle */
  if (g_proofDiscouragedMarkup[0] == 0) {
    /* getMarkupFlags() in mmdata.c has never been called, so initialize the
       markup strings to their defaults */
    let(&g_proofDiscouragedMarkup, PROOF_DISCOURAGED_MARKUP);
    let(&g_usageDiscouragedMarkup, USAGE_DISCOURAGED_MARKUP);
  }
  pos = instr(1, comment, g_proofDiscouragedMarkup);
  if (pos != 0) {
    i = (long)strlen(g_proofDiscouragedMarkup);
    for (j = pos; j < pos + i - 1; j++) { /* Check 2nd thru penultimate char */
      if (comment[j] == ' ') {
        comment[j] = ASCII_4;
      }
    }
  }
  pos = instr(1, comment, g_usageDiscouragedMarkup);
  if (pos != 0) {
    i = (long)strlen(g_usageDiscouragedMarkup);
    for (j = pos; j < pos + i - 1; j++) { /* Check 2nd thru penultimate char */
      if (comment[j] == ' ') {
        comment[j] = ASCII_4;
      }
    }
  }


  /* Put two spaces after end of sentence and colon */
  ch = ""; /* Prevent compiler warning */
  for (i = 0; i < 4; i++) {
    switch (i) {
      case 0: ch = "."; break;
      case 1: ch = "?"; break;
      case 2: ch = "!"; break;
      case 3: ch = ":";
    }
    pos = 2;
    while (1) {
      pos = instr(pos + 1, comment, ch);
      if (pos == 0) break;
      if (ch[0] == '.' && comment[pos - 2] >= 'A' && comment[pos - 2] <= 'Z')
        continue;  /* Ignore initials of names */
      if (strchr(SENTENCE_END_PUNCTUATION, comment[pos]) != NULL)
        pos++;
      if (comment[pos] != ' ') continue;
      if ((comment[pos + 1] >= 'A' && comment[pos + 1] <= 'Z')
          || strchr(OPENING_PUNCTUATION, comment[pos + 1]) != NULL) {
        /* 7-Aug-2021 nm A change of space to ASCII_4 is not needed, and in fact
           prevents end of sentence e.g. "." from ever appearing at column 79,
           triggering an earlier break that makes line unnecessarily short.
           Contrary to the deleted comment below, there is no problem with
           next line having leading space:  it is removed in mminou.c (search
           for "Remove leading space for neatness" there).  (Note that we use
           ASCII_4 to prevent bad line breaks, then later change them to
           spaces.) */
        /***** 7-Aug-2021 nm Deleted
        comment[pos] = ASCII_4; /@ Prevent break so next line won't have
                                  leading space; instead, break at 2nd space @/
        *****/
        /* Add a second space after end of sentence, which is recommended for
           monospaced (typewriter) fonts to more easily see sentence
           separation. */
        let(&comment, cat(left(comment, pos + 1), " ",
            right(comment, pos + 2), NULL));
      }
    } /* end while */
  } /* next i */

  length = (long)strlen(comment);
  let(&commentTemplate, space(length));
  for (pos = 3; pos < length - 2; pos++) {
    if (comment[pos] == ' ') {
      if (comment[pos - 1] == '~' && comment[pos - 2] != '~') {
        /* Don't break "~ <label>" */
        commentTemplate[pos] = ASCII_4;
      } else if ((comment[pos - 2] == ' '
            || strchr(OPENING_PUNCTUATION, comment[pos - 2]) != NULL)
          && strchr(OPENING_PUNCTUATION, comment[pos - 1]) != NULL) {
        /* Don't break space after opening punctuation */
        commentTemplate[pos] = ASCII_4;
      } else if ((comment[pos + 2] == ' '
            || comment[pos + 2] == '\n' /* Period etc. before line break */
            || comment[pos + 2] == ASCII_4 /* 2-space sentence break
                              done above where 1st space is ASCII_4 */
            || strchr(CLOSING_PUNCTUATION, comment[pos + 2]) != NULL)
          && strchr(CLOSING_PUNCTUATION, comment[pos + 1]) != NULL) {
        /* Don't break space before closing punctuation */
        commentTemplate[pos] = ASCII_4;
/*
      } else if (comment[pos + 2] == ' '
          && strchr(CLOSING_PUNCTUATION, comment[pos + 1]) != NULL) {
        /@ Don't break space after "~ <label>" if followed by punctuation @/
        commentTemplate[pos] = ASCII_4;
      } else if (comment[pos - 2] == ' '
          && strchr(OPENING_PUNCTUATION, comment[pos - 1]) != NULL) {
        /@ Don't break space before "~ <label>" if preceded by punctuation @/
        commentTemplate[pos] = ASCII_4;
      } else if (comment[pos + 1] == '`'
          && strchr(OPENING_PUNCTUATION, comment[pos - 1]) != NULL) {
        /@ Don't break space between punctuation and math start '`' @/
        commentTemplate[pos] = ASCII_4;
      } else if (comment[pos - 1] == '`'
          && strchr(CLOSING_PUNCTUATION, comment[pos + 1]) != NULL) {
        /@ Don't break space between punctuation and math end '`' @/
        commentTemplate[pos] = ASCII_4;
*/
      } else if (comment[pos - 3] == ' ' && comment[pos - 2] == 'p'
          && comment[pos - 1] == '.') {
        /* Don't break " p. nnn" */
        commentTemplate[pos] = ASCII_4;
      }
    }
  }
  commentTemplate[length - 3] = ASCII_4; /* Last space in comment */

  for (pos = 3; pos < length - 2; pos++) {
    /* Transfer the non-breaking spaces from the template to the comment */
    if (commentTemplate[pos] == ASCII_4) comment[pos] = ASCII_4;
  }

  let(&commentTemplate, "");

  return(comment);
} /* rewrapComment */

/* This is a general-purpose function to parse a math token string,
   typically input by the user at the keyboard.  The statemNum is
   needed to provide a context to determine which symbols are active.
   Lack of whitespace is tolerated according to standard rules.
   g_mathTokens must be set to the proper value. */
/* The code in this section is complex because it uses the fast parsing
   method borrowed from parseStatements().  On the other hand, it must set
   up some initial tables by scanning the entire g_MathToken array; this may
   slow it down in some applications. */
/* Warning:  g_mathTokens must be the correct value (some procedures might
   artificially adjust g_mathTokens to add dummy tokens [schemes] to the
   g_MathToken array) */
/* The user text may include existing or new dummy variables of the
   form "?nnn". */
nmbrString *parseMathTokens(vstring userText, long statemNum)
{
  long i, j;
  char *fbPtr;
  long mathStringLen;
  long tokenNum;
  long lowerKey, upperKey;
  long symbolLen, origSymbolLen, g_mathKeyNum;
  void *g_mathKeyPtr; /* bsearch returned value */
  int maxScope;
  flag errorFlag = 0; /* Prevents bad token from being added to output */
  int errCount = 0; /* Cumulative error count */
  vstring tmpStr = "";
  vstring nlUserText = "";


  long *mathTokenSameAs; /* Flag that symbol is unique (for speed up) */
  long *reverseMathKey; /* Map from g_mathTokens to g_mathKey */


  /* Temporary working space */
  long wrkLen;
  nmbrString *wrkNmbrPtr;
  char *wrkStrPtr;

  /* The answer */
  nmbrString *mathString = NULL_NMBRSTRING;

  long maxSymbolLen; /* Longest math symbol (for speedup) */
  flag *symbolLenExists; /* A symbol with this length exists (for speedup) */

  long nmbrSaveTempAllocStack; /* For nmbrLet() stack cleanup */
  long saveTempAllocStack; /* For let() stack cleanup */
  nmbrSaveTempAllocStack = g_nmbrStartTempAllocStack;
  g_nmbrStartTempAllocStack = g_nmbrTempAllocStackTop;
  saveTempAllocStack = g_startTempAllocStack;
  g_startTempAllocStack = g_tempAllocStackTop;

  /* Initialization to avoid compiler warning (should not be theoretically
     necessary) */
  tokenNum = 0;

  /* Add a newline before user text for sourceError() */
  let(&nlUserText, cat("\n", userText, NULL));

  /* Make sure that g_mathTokens has been initialized */
  if (!g_mathTokens) bug(1717);

  /* Set the 'active' flag based on statemNum; here 'active' just means it
     would be in the stack during normal parsing, not the Metamath manual
     definition. */
  for (i = 0; i < g_mathTokens; i++) {
    if (g_MathToken[i].statement <= statemNum && g_MathToken[i].endStatement >=
        statemNum) {
      g_MathToken[i].active = 1;
    } else {
      g_MathToken[i].active = 0;
    }
  }

  /* Initialize flags for g_mathKey array that identify math symbols as
     unique (when 0) or, if not unique, the flag is a number identifying a group
     of identical names */
  mathTokenSameAs = malloc((size_t)g_mathTokens * sizeof(long));
  if (!mathTokenSameAs) outOfMemory("#12 (mathTokenSameAs)");
  reverseMathKey = malloc((size_t)g_mathTokens * sizeof(long));
  if (!reverseMathKey) outOfMemory("#13 (reverseMathKey)");
  for (i = 0; i < g_mathTokens; i++) {
    mathTokenSameAs[i] = 0; /* 0 means unique */
    reverseMathKey[g_mathKey[i]] = i; /* Initialize reverse map to g_mathKey */
  }
  for (i = 1; i < g_mathTokens; i++) {
    if (!strcmp(g_MathToken[g_mathKey[i]].tokenName,
        g_MathToken[g_mathKey[i - 1]].tokenName)) {
      if (!mathTokenSameAs[i - 1]) mathTokenSameAs[i - 1] = i;
      mathTokenSameAs[i] = mathTokenSameAs[i - 1];
    }
  }

  /* Initialize temporary working space for parsing tokens */
  /* Assume the worst case of one token per userText character */
  wrkLen = (long)strlen(userText);
  wrkNmbrPtr = malloc((size_t)wrkLen * sizeof(nmbrString));
  if (!wrkNmbrPtr) outOfMemory("#22 (wrkNmbrPtr)");
  wrkStrPtr = malloc((size_t)wrkLen + 1);
  if (!wrkStrPtr) outOfMemory("#23 (wrkStrPtr)");

  /* Find declared math symbol lengths (used to speed up parsing) */
  maxSymbolLen = 0;
  for (i = 0; i < g_mathTokens; i++) {
    if (g_MathToken[i].length > maxSymbolLen) {
      maxSymbolLen = g_MathToken[i].length;
    }
  }
  symbolLenExists = malloc(((size_t)maxSymbolLen + 1) * sizeof(flag));
  if (!symbolLenExists) outOfMemory("#25 (symbolLenExists)");
  for (i = 0; i <= maxSymbolLen; i++) {
    symbolLenExists[i] = 0;
  }
  for (i = 0; i < g_mathTokens; i++) {
    symbolLenExists[g_MathToken[i].length] = 1;
  }


  g_currentScope = g_Statement[statemNum].scope; /* Scope of the ref. statement */


  /* The code below is indented because it was borrowed from parseStatements().
     We will leave the indentation intact for easier future comparison
     with that code. */

        /* Scan the math section for tokens */
        mathStringLen = 0;
        fbPtr = nlUserText;
        while (1) {
          fbPtr = fbPtr + whiteSpaceLen(fbPtr);
          origSymbolLen = tokenLen(fbPtr);
          if (!origSymbolLen) break; /* Done scanning source line */

          /* Scan for largest matching token from the left */
         nextAdjToken:
          /* maxSymbolLen is the longest declared symbol */
          if (origSymbolLen > maxSymbolLen) {
            symbolLen = maxSymbolLen;
          } else {
            symbolLen = origSymbolLen;
          }
          memcpy(wrkStrPtr, fbPtr, (size_t)symbolLen);
          for (; symbolLen > 0; symbolLen--) {
            /* symbolLenExists means a symbol of this length was declared */
            if (!symbolLenExists[symbolLen]) continue;
            wrkStrPtr[symbolLen] = 0; /* Define end of trial token to look up */
            g_mathKeyPtr = (void *)bsearch(wrkStrPtr, g_mathKey,
                (size_t)g_mathTokens, sizeof(long), mathSrchCmp);
            if (!g_mathKeyPtr) continue; /* Trial token was not declared */
            g_mathKeyNum = (long *)g_mathKeyPtr - g_mathKey; /* Pointer arithmetic! */
            if (mathTokenSameAs[g_mathKeyNum]) { /* Multiply-declared symbol */
              lowerKey = g_mathKeyNum;
              upperKey = lowerKey;
              j = mathTokenSameAs[lowerKey];
              while (lowerKey) {
                if (j != mathTokenSameAs[lowerKey - 1]) break;
                lowerKey--;
              }
              while (upperKey < g_mathTokens - 1) {
                if (j != mathTokenSameAs[upperKey + 1]) break;
                upperKey++;
              }
              /* Find the active symbol with the most recent declaration */
              /* (Note:  Here, 'active' means it's on the stack, not the
                 official def.) */
              maxScope = -1;
              for (i = lowerKey; i <= upperKey; i++) {
                j = g_mathKey[i];
                if (g_MathToken[j].active) {
                  if (g_MathToken[j].scope > maxScope) {
                    tokenNum = j;
                    maxScope = g_MathToken[j].scope;
                    if (maxScope == g_currentScope) break; /* Speedup */
                  }
                }
              }
              if (maxScope == -1) {
                tokenNum = g_mathKey[g_mathKeyNum]; /* Pick an arbitrary one */
                errCount++;
                if (errCount <= 1) { /* Print 1st error only */
                  sourceError(fbPtr, symbolLen, /*stmt*/ 0,
       "This math symbol is not active (i.e. was not declared in this scope).");
                }
                errorFlag = 1;
              }
            } else { /* The symbol was declared only once. */
              tokenNum = *((long *)g_mathKeyPtr);
                  /* Same as: tokenNum = g_mathKey[g_mathKeyNum]; but faster */
              if (!g_MathToken[tokenNum].active) {
                errCount++;
                if (errCount <= 1) { /* Print 1st error only */
                  sourceError(fbPtr, symbolLen, /*stmt*/ 0,
       "This math symbol is not active (i.e. was not declared in this scope).");
                }
                errorFlag = 1;
              }
            } /* End if multiply-defined symbol */
            break; /* The symbol was found, so we are done */
          } /* Next symbolLen */

          if (symbolLen == 0) { /* Symbol was not found */
            /* See if the symbol is a dummy variable name */
            if (fbPtr[0] == '$') {
              symbolLen = tokenLen(fbPtr);
              for (i = 1; i < symbolLen; i++) {
                if (fbPtr[i] < '0' || fbPtr[i] > '9') break;
              }
              symbolLen = i;
              if (symbolLen == 1) {
                symbolLen = 0; /* No # after '$' -- error */
              } else {
                memcpy(wrkStrPtr, fbPtr + 1, (size_t)i - 1);
                wrkStrPtr[i - 1] = 0; /* End of string */
                tokenNum = (long)(val(wrkStrPtr)) + g_mathTokens;
                /* See if dummy var has been declared; if not, declare it */
                if (tokenNum > g_pipDummyVars + g_mathTokens) {
                  declareDummyVars(tokenNum - g_pipDummyVars - g_mathTokens);
                }
              }
            } /* End if fbPtr == '$' */
         } /* End if symbolLen == 0 */


          if (symbolLen == 0) { /* Symbol was not found */
            symbolLen = tokenLen(fbPtr);
            errCount++;
            if (errCount <= 1) { /* Print 1st error only */
              sourceError(fbPtr, symbolLen, /*stmt*/ 0,
      "This math symbol was not declared (with a \"$c\" or \"$v\" statement).");
            }
            errorFlag = 1;
          }

          /* Add symbol to mathString */
          if (!errorFlag) {
            wrkNmbrPtr[mathStringLen] = tokenNum;
            mathStringLen++;
          } else {
            errorFlag = 0;
          }
          fbPtr = fbPtr + symbolLen; /* Move on to next symbol */

          if (symbolLen < origSymbolLen) {
            /* This symbol is not separated from next by white space */
            /* Speed-up: don't call tokenLen again; just jump past it */
            origSymbolLen = origSymbolLen - symbolLen;
            goto nextAdjToken; /* (Instead of continue) */
          }
        } /* End while */


        /* Assign mathString */
        nmbrLet(&mathString, nmbrSpace(mathStringLen));
        for (i = 0; i < mathStringLen; i++) {
          mathString[i] = wrkNmbrPtr[i];
        }

  /* End of unconventionally indented section borrowed from parseStatements() */

  g_startTempAllocStack = saveTempAllocStack;
  g_nmbrStartTempAllocStack = nmbrSaveTempAllocStack;
  if (mathStringLen) nmbrMakeTempAlloc(mathString); /* Flag for dealloc*/

  /* Deallocate temporary space */
  free(mathTokenSameAs);
  free(reverseMathKey);
  free(wrkNmbrPtr);
  free(wrkStrPtr);
  free(symbolLenExists);
  let(&tmpStr, "");
  let(&nlUserText, "");

  return (mathString);

} /* parseMathTokens */


/* 31-Dec-2017 nm For .mm file splitting */
/* Get the next real $[...$] or virtual $( Begin $[... inclusion */
/* This uses the convention of mmvstr.c where beginning of a string
   is position 1.  However, startOffset = 0 means no offset i.e.
   start at fileBuf. */
void getNextInclusion(char *fileBuf, long startOffset, /* inputs */
    /* outputs: */
    long *cmdPos1, long *cmdPos2,
    long *endPos1, long *endPos2,
    char *cmdType, /* 'B' = "$( Begin [$..." through "$( End [$...",
                      'I' = "[$...",
                      'S' = "$( Skip [$...",
                      'E' = Start missing matched End
                      'N' = no include found; includeFn = "" */
    vstring *fileName /* name of included file */
    )
{

/*
cmdType = 'B' or 'E':
      ....... $( Begin $[ prop.mm $] $)   ......   $( End $[ prop.mm $] $) ...
       ^      ^           ^^^^^^^       ^          ^                      ^
  startOffset cmdPos1    fileName  cmdPos2     endPos1              endPos2
                                             (=0 if no End)   (=0 if no End)

   Note: in the special case of Begin, cmdPos2 points _after_ the whitespace
   after "$( Begin $[ prop.mm $] $)" i.e. the whitespace is considered part of
   the Begin command.  The is needed because prop.mm content doesn't
   necessarily start with whitespace.  prop.mm does, however, end with
   whitespace (\n) as enforced by readFileToString().

cmdType = 'I':
      ............... $[ prop.mm $]  ..............
       ^              ^  ^^^^^^^   ^
  startOffset   cmdPos1 fileName  cmdPos2     endPos1=0  endPos2=0

cmdType = 'S':
      ....... $( Skip $[ prop.mm $] $)   ......
       ^      ^          ^^^^^^^      ^
  startOffset cmdPos1    fileName  cmdPos2     endPos1=0  endPos2=0
*/
  char *fbPtr;
  char *tmpPtr;
  flag lookForEndMode = 0; /* 1 if inside of $( Begin, End, Skip... */
  long i, j;

  fbPtr = fileBuf + startOffset;

  while (1) {
    fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr); /* Count $( as a token */
    j = rawTokenLen(fbPtr); /* Treat $(, $[ as tokens */
    if (j == 0) {
      *cmdType = 'N'; /* No include found */
      break;  /* End of file */
    }
    if (fbPtr[0] != '$') {
      fbPtr = fbPtr + j;
      continue;
    }

    /* Process normal include $[ $] */
    if (fbPtr[1] == '[') {
      if (lookForEndMode == 0) {
        /* If lookForEndMode is 1, ignore everything until we find a matching
           "$( End $[..." */
        *cmdPos1 = fbPtr - fileBuf + 1; /* 1 = beginning of file */
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + whiteSpaceLen(fbPtr); /* Comments = whitespace here */
        j = rawTokenLen(fbPtr); /* Should be file name */
        /* Note that mid, seg, left, right do not waste time computing
           the length of the input string fbPtr */
        let(&(*fileName), left(fbPtr, j));
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + whiteSpaceLen(fbPtr); /* Comments = whitespace here */
        j = rawTokenLen(fbPtr);
        if (j == 2/*speedup*/ && !strncmp("$]", fbPtr, (size_t)j)) {
          *cmdPos2 = fbPtr - fileBuf + j + 1;
          *endPos1 = 0;
          *endPos2 = 0;
          *cmdType = 'I';
          return;
        }
        /* TODO - more precise error message */
        print2("?Missing \"$]\" after \"$[ %s\"\n", *fileName);
        fbPtr = fbPtr + j;
        continue; /* Not a completed include */
      } /* if (lookForEndMode == 0) */
      fbPtr = fbPtr + j;
      continue; /* Either not a legal include - error detected later,
             or we're in lookForEndMode */
    /* Process markup-type include inside comment */
    } else if (fbPtr[1] == '(') {
      /* Process comment starting at "$(" */
      if (lookForEndMode == 0) {
        *cmdPos1 = fbPtr - fileBuf + 1;
      } else {
        *endPos1 = fbPtr - fileBuf + 1;
      }
      fbPtr = fbPtr + j;
      fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr); /* comment != whitespace */
      j = rawTokenLen(fbPtr);
      *cmdType = '?';
      if (j == 5/*speedup*/ && !strncmp("Begin", fbPtr, (size_t)j)) {
        /* If lookForEndMode is 1, we're looking for End matching earlier Begin */
        if (lookForEndMode == 0) {
          *cmdType = 'B';
        }
      } else if (j == 4/*speedup*/ && !strncmp("Skip", fbPtr, (size_t)j)) {
        /* If lookForEndMode is 1, we're looking for End matching earlier Begin */
        if (lookForEndMode == 0) {
          *cmdType = 'S';
        }
      } else if (j == 3/*speedup*/ && !strncmp("End", fbPtr, (size_t)j)) {
        /* If lookForEndMode is 0, there was no matching Begin */
        if (lookForEndMode == 1) {
          *cmdType = 'E';
        }
      }
      if (*cmdType == '?') { /* The comment doesn't qualify as $[ $] markup */
        /* Find end of comment and continue */
        goto GET_PASSED_END_OF_COMMENT;
      } else {
        /* It's Begin or Skip or End */
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr);
        j = rawTokenLen(fbPtr);
        if (j != 2 || strncmp("$[", fbPtr, (size_t)j)) {
          /* Find end of comment and continue */
          goto GET_PASSED_END_OF_COMMENT;
        }
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr);  /* comment != whitespace */
        j = rawTokenLen(fbPtr);
        /* Note that mid, seg, left, right do not waste time computing
           the length of the input string fbPtr */
        if (lookForEndMode == 0) {
          /* It's Begin or Skip */
          let(&(*fileName), left(fbPtr, j));
        } else {
          /* It's an End command */
          if (strncmp(*fileName, fbPtr, (size_t)j)) {
            /* But the file name didn't match, so it's not a matching End */
            /* Find end of comment and continue */
            goto GET_PASSED_END_OF_COMMENT;
          }
        }
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr);  /* comment != whitespace */
        j = rawTokenLen(fbPtr);
        if (j != 2 || strncmp("$]", fbPtr, (size_t)j)) {
          /* The token after the file name isn't "$]" */
          /* Find end of comment and continue */
          goto GET_PASSED_END_OF_COMMENT;
        }
        fbPtr = fbPtr + j;
        fbPtr = fbPtr + rawWhiteSpaceLen(fbPtr);  /* comment != whitespace */
        j = rawTokenLen(fbPtr);
        if (j != 2 || strncmp("$)", fbPtr, (size_t)j)) {
          /* The token after the "$]" isn't "$)" */
          /* Find end of comment and continue */
          goto GET_PASSED_END_OF_COMMENT;
        }
        /* We are now at the end of "$( Begin/Skip/End $[ file $] $)" */
        fbPtr = fbPtr + j;
        if (lookForEndMode == 0) {
          *cmdPos2 = fbPtr - fileBuf + 1
            + ((*cmdType == 'B') ? 1 : 0);/*after whitespace for 'B' (see above)*/
          if (*cmdType == 'S') {  /* Skip command; we're done */
            *endPos1 = 0;
            *endPos2 = 0;
            return;
          }
          if (*cmdType != 'B') bug(1742);
          lookForEndMode = 1;
        } else { /* We're at an End */
          if (*cmdType != 'E') bug(1743);
          /*lookForEndMode = 0;*/ /* Not needed since we will return soon */
          *cmdType = 'B'; /* Restore it to B for Begin/End pair */
          *endPos2 = fbPtr - fileBuf + 1;
          return;
        }
        continue; /* We're past Begin; start search for End */
      } /* Begin, End, or Skip */
    } else if (i != i + 1) { /* Suppress "unreachable code" warning for bug trap below */
      /* It's '$' not followed by '[' or '('; j is token length */
      fbPtr = fbPtr + j;
      continue;
    }
    bug(1746); /* Should never get here */
   GET_PASSED_END_OF_COMMENT:
    /* Note that fbPtr should be at beginning of last token found, which
       may be "$)" (in which case i will be 1 from the instr) */

    /* Don't use instr because it computes string length each call */
    /*i = instr(1, fbPtr, "$)");*/ /* Normally this will be fast because we only
        have to find the end of the comment that we're in */
    /* Emulater the instr() */
    tmpPtr = fbPtr;
    i = 0;
    while (1) {

      /* strchr is incredibly slow under lcc - why?
         Is it computing strlen internally maybe? */
      /*
      tmpPtr = strchr(tmpPtr, '$');
      if (tmpPtr == NULL) {
        i = 0;
        break;
      }
      if (tmpPtr[1] == ')') {
        i = tmpPtr - fbPtr + 1;
        break;
      }
      tmpPtr++;
      */

      /* Emulate strchr */
      while (tmpPtr[0] != '$') {
        if (tmpPtr[0] == 0) break;
        tmpPtr++;
      }
      if (tmpPtr[0] == 0) {
        i = 0;
        break;
      }
      if (tmpPtr[1] == ')') {
        i = tmpPtr - fbPtr + 1;
        break;
      }
      tmpPtr++;

    } /* while (1) */

    if (i == 0) {
      /* TODO: better error msg */
      printf("?End of comment not found\n");
      i = (long)strlen(fileBuf); /* Slow, but this is a rare error */
      fbPtr = fileBuf + i; /* Points to null at end of fileBuf */
    } else {
      fbPtr = fbPtr + i + 2 - 1; /* Skip the "$)" - skip 2 characters, then
           back up 1 since the instr result starts at 1 */
    }
    /* continue; */  /* Not necessary since we're at end of loop */
  } /* while (1) */
  if (j != 0) bug(1744); /* Should be at end of file */
  if (lookForEndMode == 1) {
    /* We didn't find an End */
    *cmdType = 'E';
    *endPos1 = 0; *endPos2 = 0;
  } else {
    *cmdType = 'N'; /* no include was found */
    *cmdPos1 = 0; *cmdPos2 = 0; *endPos1 = 0; *endPos2 = 0;
    let(&(*fileName), "");
  }
  return;

} /* getNextInclusion */



/* This function transfers the content of the g_Statement[] array
   to a linear buffer in preparation for creating the output file.
   Any changes such as modified proofs will be updated in the buffer. */
/* The caller is responsible for deallocating the returned string. */
vstring writeSourceToBuffer(void)
{
  long stmt, size;
  vstring buf = "";
  char *ptr;

  /* Compute the size of the buffer */
  /* Note that g_Statement[g_statements + 1] is a dummy statement
     containing the text after the last statement in its
     labelSection */
  size = 0;
  for (stmt = 1; stmt <= g_statements + 1; stmt++) {
    /* Add the sizes of the sections.  When sections don't exist
       (like a proof for a $a statement), the section length is 0. */
    size += g_Statement[stmt].labelSectionLen
        + g_Statement[stmt].mathSectionLen
        + g_Statement[stmt].proofSectionLen;
    /* Add in the 2-char length of keywords, which aren't stored in
       the statement sections */
    switch (g_Statement[stmt].type) {
      case lb_: /* ${ */
      case rb_: /* $} */
        size += 2;
        break;
      case v_: /* $v */
      case c_: /* $c */
      case d_: /* $d */
      case e_: /* $e */
      case f_: /* $f */
      case a_: /* $a */
        size += 4;
        break;
      case p_: /* $p */
        size += 6;
        break;
      case illegal_: /* dummy */
        if (stmt != g_statements + 1) bug(1747);
        /* The labelLen is text after last statement */
        size += 0; /* There are no keywords in g_statements + 1 */
        break;
      default: bug(1748);
    } /* switch (g_Statement[stmt].type) */
  } /* next stmt */

  /* Create the output buffer */
  /* We could have created it with let(&buf, space(size)), but malloc should
     be slightly faster since we don't have to initialize each entry */
  buf = malloc((size_t)(size + 1) * sizeof(char));

  ptr = buf; /* Pointer to keep track of buf location */
  /* Transfer the g_Statement[] array to buf */
  for (stmt = 1; stmt <= g_statements + 1; stmt++) {
    /* Always transfer the label section (text before $ keyword) */
    memcpy(ptr/*dest*/, g_Statement[stmt].labelSectionPtr/*source*/,
        (size_t)(g_Statement[stmt].labelSectionLen)/*size*/);
    ptr += g_Statement[stmt].labelSectionLen;
    switch (g_Statement[stmt].type) {
      case illegal_:
        if (stmt != g_statements + 1) bug(1749);
        break;
      case lb_: /* ${ */
      case rb_: /* $} */
        ptr[0] = '$';
        ptr[1] = g_Statement[stmt].type;
        ptr += 2;
        break;
      case v_: /* $v */
      case c_: /* $c */
      case d_: /* $d */
      case e_: /* $e */
      case f_: /* $f */
      case a_: /* $a */
        ptr[0] = '$';
        ptr[1] = g_Statement[stmt].type;
        ptr += 2;
        memcpy(ptr/*dest*/, g_Statement[stmt].mathSectionPtr/*source*/,
            (size_t)(g_Statement[stmt].mathSectionLen)/*size*/);
        ptr += g_Statement[stmt].mathSectionLen;
        ptr[0] = '$';
        ptr[1] = '.';
        ptr += 2;
        break;
      case p_: /* $p */
        ptr[0] = '$';
        ptr[1] = g_Statement[stmt].type;
        ptr += 2;
        memcpy(ptr/*dest*/, g_Statement[stmt].mathSectionPtr/*source*/,
            (size_t)(g_Statement[stmt].mathSectionLen)/*size*/);
        ptr += g_Statement[stmt].mathSectionLen;
        ptr[0] = '$';
        ptr[1] = '=';
        ptr += 2;
        memcpy(ptr/*dest*/, g_Statement[stmt].proofSectionPtr/*source*/,
            (size_t)(g_Statement[stmt].proofSectionLen)/*size*/);
        ptr += g_Statement[stmt].proofSectionLen;
        ptr[0] = '$';
        ptr[1] = '.';
        ptr += 2;
        break;
      default: bug(1750);
    } /* switch (g_Statement[stmt].type) */
  } /* next stmt */
  if (ptr - buf != size) bug(1751);
  buf[size] = 0; /* End of string marker */
  return buf;
} /* writeSourceToBuffer */


/* 31-Dec-2017 nm */
/* This function creates split files containing $[ $] inclusions, from
   a nonsplit source with $( Begin $[... etc. inclusions */
/* This function calls itself recursively, and after the recursive call
   the fileBuf (=includeBuf) argument is deallocated. */
/* For the top level call, fileName MUST NOT HAVE A DIRECTORY PATH */
/* Note that fileBuf must be deallocated by initial caller.  This will let the
   caller decide whether to say re-use fileBuf to create an unsplit version
   of the .mm file in case the split version generation encounters an error. */
/*                                     TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
/*flag(TODO)*/ void writeSplitSource(vstring *fileBuf, vstring fileName,
    flag noVersioningFlag, flag noDeleteFlag) {
  FILE *fp;
  vstring tmpStr1 = "";
  vstring tmpFileName = "";
  vstring includeBuf = "";
  vstring includeFn = "";
  vstring fileNameWithPath = "";
  long size;
  flag writeFlag;
  long startOffset;
  long cmdPos1;
  long cmdPos2;
  long endPos1;
  long endPos2;
  char cmdType;
  startOffset = 0;
  let(&fileNameWithPath, cat(g_rootDirectory, fileName, NULL));
  while (1) {
    getNextInclusion(*fileBuf, startOffset, /* inputs */
        /* outputs: */
        &cmdPos1, &cmdPos2,
        &endPos1, &endPos2,
        &cmdType, /* 'B' = "$( Begin [$..." through "$( End [$...",
                     'I' = "[$...",
                     'S' = "$( Skip [$...",
                     'E' = Start missing matched End
                     'N' = no include found; includeFn = "" */
        &includeFn /* name of included file */ );
    if (cmdType == 'N') {
      writeFlag = 0;
      /* There are no more includes to expand, so write out the file */
      if (!strcmp(fileName, g_output_fn)) {
        /* We're writing the top-level file - always create new version */
        writeFlag = 1;
      } else {
        /* We're writing an included file */
        /* See if the file already exists */
        let(&tmpStr1, "");
        tmpStr1 = readFileToString(fileNameWithPath, 0/*quiet*/, &size);
        if (tmpStr1 == NULL) {
          tmpStr1 = ""; /* Prevent seg fault */
          /* If it doesn't exist, see if the ~1 version exists */
          let(&tmpFileName, cat(fileNameWithPath, "~1", NULL));
          tmpStr1 = readFileToString(tmpFileName, 0/*quiet*/, &size);
          if (tmpStr1 == NULL) {
            tmpStr1 = ""; /* Prevent seg fault */
            /* Create and write the file */
            writeFlag = 1;
          } else {
            /* See if the ~1 backup file content changed */
            if (strcmp(tmpStr1, *fileBuf)) {
              /* Content is different; write the file */
              writeFlag = 1;
            } else {
              /* Just rename the ~1 version to the main version */
              print2("Recovering \"%s\" from \"%s\"...\n",
                  fileNameWithPath, tmpFileName);
              rename(tmpFileName/*old*/, fileNameWithPath/*new*/);
            }
          } /* if (tmpStr1 == NULL) */
        } else { /* tmpStr1 != NULL */
          /* The include file already exists; see if the content changed */
          if (strcmp(tmpStr1, *fileBuf)) {
            /* Content is different; write the file */
            writeFlag = 1;
          } else {
            /* Just rename the ~1 version to the main version */
            print2("Content of \"%s\" did not change.\n",
                fileNameWithPath);
            rename(tmpFileName/*old*/, fileNameWithPath/*new*/);
          }
        }
      }
      if (writeFlag == 1) {
        fp = fSafeOpen(fileNameWithPath, "w", 0/*noVersioningFlag*/);
        if (fp == NULL) {
          /* TODO: better error msg?  Abort and don't split? */
          print2("?Error: couldn't create the file \"%s\"\n", fileNameWithPath);
          print2("  Make sure any directories needed have been created.\n");
          print2("  Try WRITE SOURCE without / SPLIT to recover your work.\n");
          break;
        } else {
          print2("Writing \"%s\"...\n", fileNameWithPath);
          fprintf(fp, "%s", *fileBuf);
          fclose(fp);
          break;
        }
      } /* if (writeFlag == 1 ) */
      break;
    } else if (cmdType == 'S') {
      /* Change "Skip" to a real inclusion */
      let(&tmpStr1, cat("$[ ", includeFn, " $]", NULL));
      startOffset = cmdPos1 - 1 + (long)strlen(tmpStr1);
      let(&(*fileBuf), cat(left(*fileBuf, cmdPos1 - 1), tmpStr1,
          right(*fileBuf, cmdPos2), NULL));
      continue;
    } else if (cmdType == 'B') {
      /* Extract included file content and call this recursively to process */
      let(&tmpStr1, cat("$[ ", includeFn, " $]", NULL));
      startOffset = cmdPos1 - 1 + (long)strlen(tmpStr1);
      /* We start _after_ the whitespace after cmdPos2 because it wasn't
         in the original included file but was put there by us in
         readSourceAndIncludes() */
      let(&includeBuf, seg(*fileBuf, cmdPos2, endPos1 - 1));
      let(&(*fileBuf), cat(left(*fileBuf, cmdPos1 - 1), tmpStr1,
          right(*fileBuf, endPos2), NULL));
      /* TODO: intercept error from deeper calls? */
      writeSplitSource(&includeBuf, includeFn, noVersioningFlag, noDeleteFlag);
      continue;
    } else if (cmdType == 'I') {
      bug(1752); /* Any real $[ $] should have been converted to commment */
      /* However in theory, user could have faked an assignable description
         if modifiable comments are added in the future...*/
      startOffset = cmdPos2 - 1;
      continue;
    } else if (cmdType == 'E') {
      /* TODO What error message should go here? */
      print2("?Unterminated \"$( Begin $[...\" inclusion markup in \"%s\".",
          fileNameWithPath);
      startOffset = cmdPos2 - 1;
      continue;
    } else {
      /* Should never happen */
      bug(1753);
    }
  } /* while (1) */
  /* Deallocate memory */
  /*let(&(*fileBuf), "");*/ /* Let caller decide whether to do this */
  let(&tmpStr1, "");
  let(&tmpFileName, "");
  let(&includeFn, "");
  let(&includeBuf, "");
  let(&fileNameWithPath, "");
} /* writeSplitSource */


/* 31-Dec-2017 nm */
/* When "write source" does not have the "/split" qualifier, by default
   (i.e. without "/no_delete") the included modules are "deleted" (renamed
   to ~1) since their content will be in the main output file. */
/*flag(TODO)*/ void deleteSplits(vstring *fileBuf, flag noVersioningFlag) {
  FILE *fp;
  vstring includeFn = "";
  vstring fileNameWithPath = "";
  long startOffset;
  long cmdPos1;
  long cmdPos2;
  long endPos1;
  long endPos2;
  char cmdType;
  startOffset = 0;
  while (1) {
    /* We scan the source for all "$( Begin $[ file $] $)...$( End $[ file $]"
       and when found, we "delete" file. */
    getNextInclusion(*fileBuf, startOffset, /* inputs */
        /* outputs: */
        &cmdPos1, &cmdPos2,
        &endPos1, &endPos2,
        &cmdType, /* 'B' = "$( Begin [$..." through "$( End [$...",
                     'I' = "[$...",
                     'S' = "$( Skip [$...",
                     'E' = Start missing matched End
                     'N' = no include found; includeFn = "" */
        &includeFn /* name of included file */ );
    /* We only care about the 'B' command */
    if (cmdType == 'B') {
      let(&fileNameWithPath, cat(g_rootDirectory, includeFn, NULL));
      /* See if the included file exists */
      fp = fopen(fileNameWithPath, "r");
      if (fp != NULL) {
        fclose(fp);
        if (noVersioningFlag == 1) {
          print2("Deleting \"%s\"...\n", fileNameWithPath);
        } else {
          print2("Renaming \"%s\" to \"%s~1\"...\n", fileNameWithPath,
              fileNameWithPath);
        }
        fp = fSafeOpen(fileNameWithPath, "d", noVersioningFlag);
      }
      /* Adjust offset and continue */
      /* We don't go the the normal end of the 'B' because it may
         have other 'B's inside.  Instead, just skip past the Begin. */
      startOffset = cmdPos2 - 1; /* don't use endPos2 */
    } else if (cmdType == 'N') {
      /* We're done */
      break;
    } else if (cmdType == 'S') {
      /* Adjust offset and continue */
      startOffset = cmdPos2 - 1;
    } else if (cmdType == 'E') {
      /* There's a problem, but ignore - should have been reported earlier */
      /* Adjust offset and continue */
      startOffset = cmdPos2 - 1;
    } else if (cmdType == 'I') {
      bug(1755); /* Should never happen */
    } else {
      bug(1756);
    }
    continue;
  } /* while (1) */
  /* Deallocate memory */
  /*let(&(*fileBuf), "");*/ /* Let caller decide whether to do this */
  let(&includeFn, "");
  let(&fileNameWithPath, "");
  return;
} /* deleteSplits */


/* 9-Jan-2018 nm */
/* Get file name and line number given a pointer into the read buffer */
/* The user must deallocate the returned string (file name) */
/* The globals g_IncludeCall structure and g_includeCalls are used */
vstring getFileAndLineNum(vstring buffPtr/*start of read buffer*/,
    vstring currentPtr/*place at which to get file name and line no*/,
    long *lineNum/*return argument*/) {
  long i, smallestOffset, smallestNdx;
  vstring fileName = "";

  /* Make sure it's not outside the read buffer */
  if (currentPtr < buffPtr
      || currentPtr >= buffPtr + g_IncludeCall[1].current_offset) {
    bug(1769);
  }

  /* Find the g_IncludeCall that is closest to currentPtr but does not
     exceed it */
  smallestOffset = currentPtr - buffPtr; /* Start with g_IncludeCall[0] */
  if (smallestOffset < 0) bug(1767);
  smallestNdx = 0; /* Point to g_IncludeCall[0] */
  for (i = 0; i <= g_includeCalls; i++) {
    if (g_IncludeCall[i].current_offset <= currentPtr - buffPtr) {
      if ((currentPtr - buffPtr) - g_IncludeCall[i].current_offset
          <= smallestOffset) {
        smallestOffset = (currentPtr - buffPtr) - g_IncludeCall[i].current_offset;
        smallestNdx = i;
      }
    }
  }
  if (smallestOffset < 0) bug(1768);
  *lineNum = g_IncludeCall[smallestNdx].current_line
      + countLines(buffPtr + g_IncludeCall[smallestNdx].current_offset,
          smallestOffset);
  /* Assign to new string to prevent original from being deallocated */
  let(&fileName, g_IncludeCall[smallestNdx].source_fn);
/*D*//*printf("smallestNdx=%ld smallestOffset=%ld i[].co=%ld\n",smallestNdx,smallestOffset,g_IncludeCall[smallestNdx].current_offset);*/
  return fileName;
} /* getFileAndLineNo */


/* 9-Jan-2018 nm */
/* g_Statement[stmtNum].fileName and .lineNum are initialized to "" and 0.
   To save CPU time, they aren't normally assigned until needed, but once
   assigned they can be reused without looking them up again.  This function
   will assign them if they haven't been assigned yet.  It just returns if
   they have been assigned. */
/* The globals g_Statement[] and g_sourcePtr are used */
void assignStmtFileAndLineNum(long stmtNum) {
  if (g_Statement[stmtNum].lineNum > 0) return; /* Already assigned */
  if (g_Statement[stmtNum].lineNum < 0) bug(1766);
  if (g_Statement[stmtNum].fileName[0] != 0) bug(1770); /* Should be empty string */
  /* We can make a direct string assignment here since previous value was "" */
  g_Statement[stmtNum].fileName = getFileAndLineNum(g_sourcePtr,
      g_Statement[stmtNum].statementPtr, &(g_Statement[stmtNum].lineNum));
  return;
} /* assignStmtFileAndLineNum */



/* This function returns a pointer to a buffer containing the contents of an
   input file and its 'include' calls.  'Size' returns the buffer's size.  */
/* TODO - ability to flag error to skip raw source function */
/* Recursive function that processes a found include */
/* If NULL is returned, it means a serious error occured (like missing file)
   and reading should be aborted. */
/* Globals used:  g_IncludeCall[], g_includeCalls */
char *readInclude(vstring fileBuf, long fileBufOffset,
    /*vstring parentFileName,*/ vstring sourceFileName,
    long *size, long parentLineNum, flag *errorFlag)
{
  long i;
  /*vstring fileBuf = "";*/
  long inclSize;
  /* flag insideLineComment; */ /* obsolete */
  vstring newFileBuf = "";
  vstring inclPrefix = "";
  vstring tmpSource = "";
  vstring inclSource = "";
  vstring oldSource = "";
  vstring inclSuffix = "";

  long startOffset;
  long cmdPos1;
  long cmdPos2;
  long endPos1;
  long endPos2;
  char cmdType;
  long oldInclSize = 0; /* Init to avoid compiler warning */
  long newInclSize = 0; /* Init to avoid compiler warning */
  long befInclLineNum;
  long aftInclLineNum;
  /*long contLineNum;*/
  vstring includeFn = "";
  vstring fullInputFn = "";
  vstring fullIncludeFn = "";
  long alreadyInclBy;
  long saveInclCalls;

  let(&newFileBuf, fileBuf);  /* TODO - can this be avoided for speedup? */
  /* Look for and process includes */
  startOffset = 0;

  while (1) {
    getNextInclusion(newFileBuf, startOffset, /* inputs */
        /* outputs: */
        &cmdPos1, &cmdPos2,
        &endPos1, &endPos2,
        &cmdType, /* 'B' = "$( Begin [$..." through "$( End [$...",
                     'I' = "[$...",
                     'S' = "$( Skip [$...",
           TODO: add error code for missing $]?
                     'E' = Begin missing matched End
                     'N' = no include found; includeFn = "" */
        &includeFn /* name of included file */ );
    /*
    cmdType = 'B' or 'E':
          ....... $( Begin $[ prop.mm $] $)   ......   $( End $[ prop.mm $] $) ...
           ^      ^           ^^^^^^^       ^          ^                      ^
      startOffset cmdPos1    fileName  cmdPos2     endPos1              endPos2
                                                 (=0 if no End)   (=0 if no End)

       Note: in the special case of Begin, cmdPos2 points _after_ the whitespace
       after "$( Begin $[ prop.mm $] $)" i.e. the whitespace is considered part of
       the Begin command.  The is needed because prop.mm content doesn't
       necessarily start with whitespace.  prop.mm does, however, end with
       whitespace (\n) as enforced by readFileToString().

    cmdType = 'I':
          ............... $[ prop.mm $]  ..............
           ^              ^  ^^^^^^^   ^
      startOffset   cmdPos1 fileName  cmdPos2     endPos1=0  endPos2=0

    cmdType = 'S':
          ....... $( Skip $[ prop.mm $] $)   ......
           ^      ^          ^^^^^^^      ^
      startOffset cmdPos1    fileName  cmdPos2     endPos1=0  endPos2=0
    */

    if (cmdType == 'N') break; /* No (more) includes */
    if (cmdType == 'E') {
      /* TODO: Better error msg here or in getNextInclude */
      print2("?Error: \"$( Begin $[...\" without matching \"$( End $[...\"\n");
      startOffset = cmdPos2; /* Get passed the bad "$( Begin $[..." */
      *errorFlag = 1;
      continue;
    }

    /* Count lines between start of last source continuation and end of
       the inclusion, before the newFileBuf is modified */

    if (g_IncludeCall[g_includeCalls].pushOrPop != 1) bug(1764);
    /*
    contLineNum = g_IncludeCall[g_includeCalls].current_line
      + countLines(newFileBuf, ((cmdType == 'B') ? endPos2 : cmdPos2)
          - g_IncludeCall[g_includeCalls].current_offset);\
    */


    /* If we're here, cmdType is 'B', 'I', or 'S' */

    /* Create 2 new includeCall entries before recursive call, so that
       alreadyInclBy will scan entries in proper order (e.g. if this
       include calls itself at a deeper level - weird but not illegal - the
       deeper one should be Skip'd per the Metamath spec). */
    /* This entry is identified by pushOrPop = 0 */
    g_includeCalls++;
    /* We will use two more entries here (include call and return), and
       in parseKeywords() a dummy additional top entry is assumed to exist.
       Thus the comparison must be to 3 less than g_MAX_INCLUDECALLS. */
    if (g_includeCalls >= g_MAX_INCLUDECALLS - 3) {
      g_MAX_INCLUDECALLS = g_MAX_INCLUDECALLS + 20;
/*E*/if(db5)print2("'Include' call table was increased to %ld entries.\n",
/*E*/    g_MAX_INCLUDECALLS);
      g_IncludeCall = realloc(g_IncludeCall, (size_t)g_MAX_INCLUDECALLS *
          sizeof(struct includeCall_struct));
      if (g_IncludeCall == NULL) outOfMemory("#2 (g_IncludeCall)");
    }
    g_IncludeCall[g_includeCalls].pushOrPop = 0;

    /* This entry is identified by pushOrPop = 1 */
    g_includeCalls++;
    g_IncludeCall[g_includeCalls].pushOrPop = 1;
    /* Save the value before recursive calls will increment the global
       g_includeCalls */
    saveInclCalls = g_includeCalls;

    g_IncludeCall[saveInclCalls - 1].included_fn = "";  /* Initialize string */
    let(&(g_IncludeCall[saveInclCalls - 1].included_fn), includeFn); /* Name of the
       file in the inclusion statement e.g. "$( Begin $[ included_fn..." */
    g_IncludeCall[saveInclCalls].included_fn = "";
    let(&g_IncludeCall[saveInclCalls].included_fn,
        sourceFileName); /* Continuation of parent file after this include */


    /* See if includeFn file has already been included */
    alreadyInclBy = -1;
    for (i = 0; i <= saveInclCalls - 2; i++) {
      if (g_IncludeCall[i].pushOrPop == 0
         && !strcmp(g_IncludeCall[i].included_fn, includeFn)) {
        /*
        print2("%s",cat(
            "(File \"",
            g_IncludeCall[g_includeCalls].source_fn,
            "\", referenced at line ",
            str((double)(g_IncludeCall[g_includeCalls].calledBy_line)),
            " in \"",
            g_IncludeCall[g_includeCalls].calledBy_fn,
            "\", has already been included.)\n",NULL));
        */
        alreadyInclBy = i;
        break;
      }
    }
    if (alreadyInclBy == -1) {
      /* This is the first time the included file has been included */
      switch (cmdType) {
        case 'B':
          let(&inclPrefix, seg(newFileBuf, cmdPos1, cmdPos2 - 1)); /* Keep trailing
              \n (or other whitespace) as part of prefix for the special
              case of Begin - cmdPos2 points to char after \n */
          let(&inclSuffix, seg(newFileBuf, endPos1, endPos2 - 1));
          let(&tmpSource, seg(newFileBuf, cmdPos2, endPos1 - 1));  /* Save the
              included source */
          inclSize = endPos1 - cmdPos2; /* Actual included source size */

          /* Get the parent line number up to the inclusion */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
              cmdPos2 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = befInclLineNum - 1;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos2/*start at (cmdPos2+1)th character*/,
              endPos2 - cmdPos2 - 1) + 1;
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum - 1;
          parentLineNum = aftInclLineNum;

          /* Call recursively to expand any includes in the included source */
          /* Use parentLineNum since the inclusion source is in the parent file */
          let(&inclSource, "");
          inclSource = readInclude(tmpSource,
              fileBufOffset + cmdPos1 - 1 + (long)strlen(inclPrefix), /*new offset*/
              /*includeFn,*/ sourceFileName,
              &inclSize /*input/output*/, befInclLineNum, &(*errorFlag));

          oldInclSize = endPos2 - cmdPos1; /* Includes old prefix and suffix */
          /*newInclSize = oldInclSize;*/ /* Includes new prefix and suffix */
          newInclSize = (long)strlen(inclPrefix) + inclSize +
                (long)strlen(inclSuffix); /* Includes new prefix and suffix */
          /* It is already a Begin comment, so leave it alone */
          /* *size = *size; */
          /* Adjust starting position for next inclusion search */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is 0-based but
              cmdPos2 is 1-based */
          break;
        case 'I':
          /* Read the included file */
          let(&fullIncludeFn, cat(g_rootDirectory, includeFn, NULL));
          let(&tmpSource, "");
          tmpSource = readFileToString(fullIncludeFn, 0/*verbose*/, &inclSize);
          if (tmpSource == NULL) {
            /* TODO: print better error msg?*/
            print2(
                /* 23-Jan-2018 nm */
                "?Error: file \"%s%s\" (included in \"%s\") was not found\n",
                fullIncludeFn, g_rootDirectory, sourceFileName);
            tmpSource = "";
            inclSize = 0;
            *errorFlag = 1;
          } else {
            print2("Reading included file \"%s\"... %ld bytes\n",
                fullIncludeFn, inclSize);
          }

          /* Change inclusion command to Begin...End comment */
          let(&inclPrefix, cat("$( Begin $[ ", includeFn, " $] $)\n", NULL));
               /* Note that trailing whitespace is part of the prefix in
                  the special case of Begin, because the included file might
                  not start with whitespace.  However, the included file
                  will always end with whitespace i.e. \n as enforced by
                  readFileToString(). */
          let(&inclSuffix, cat("$( End $[ ", includeFn, " $] $)", NULL));

          /* Get the parent line number up to the inclusion */
          /* TODO: compute aftInclLineNum directly and eliminate befInclLineNum */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
              cmdPos1 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = 0;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos1/*start at (cmdPos1+1)th character*/,
              cmdPos2 - cmdPos1 - 1);
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum;
          parentLineNum = aftInclLineNum;

          /* Call recursively to expand includes in the included source */
          /* Start at line 1 since source is in external file */
          let(&inclSource, "");
          inclSource = readInclude(tmpSource,
              fileBufOffset + cmdPos1 - 1 + (long)strlen(inclPrefix), /*new offset*/
              /*includeFn,*/ includeFn,
              &inclSize /*input/output*/, 1/*parentLineNum*/, &(*errorFlag));

          oldInclSize = cmdPos2 - cmdPos1; /* Includes old prefix and suffix */
          /* "$( Begin $[...$] $)" must have a whitespace
             after it; we use a newline.  readFileToString will ensure a
             newline at its end.  We don't add whitespace after
             "$( End $[...$] $)" but reuse the whitespace after the original
             "$[...$]". */
          let(&newFileBuf, cat(left(newFileBuf, cmdPos1 - 1),
              inclPrefix, inclSource, inclSuffix,
              right(newFileBuf, cmdPos2), NULL));
          *size = *size - (cmdPos2 - cmdPos1) + (long)strlen(inclPrefix)
              + inclSize + (long)strlen(inclSuffix);
          newInclSize = (long)strlen(inclPrefix) + inclSize +
                (long)strlen(inclSuffix); /* Includes new prefix and suffix */
          /* Adjust starting position for next inclusion search (which will
             be at the start of the included file continuing into the remaining
             parent file) */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is
              0-based but cmdPos2 is 1-based */
              /*
              + inclSize  /@ Use instead of strlen for speed @/
              + (long)strlen(inclSuffix) */
              ;  /* -1 since startOffset is 0-based but cmdPos1 is 1-based */
          /* TODO: update line numbers for error msgs */
          break;
        case 'S':
          /* Read the included file */
          let(&fullIncludeFn, cat(g_rootDirectory, includeFn, NULL));
          let(&tmpSource, "");
          tmpSource = readFileToString(fullIncludeFn, 1/*verbose*/, &inclSize);
          if (tmpSource == NULL) {
            /* TODO: print better error msg */
            print2(
                /* 23-Jan-2018 nm */
                "?Error: file \"%s%s\" (included in \"%s\") was not found\n",
                fullIncludeFn, g_rootDirectory, sourceFileName);
            *errorFlag = 1;
            tmpSource = ""; /* Prevent seg fault */
            inclSize = 0;
          }

          /* Change Skip comment to Begin...End comment */
          let(&inclPrefix, cat("$( Begin $[ ", includeFn, " $] $)\n", NULL));
          let(&inclSuffix, cat("$( End $[ ", includeFn, " $] $)", NULL));

          /* Get the parent line number up to the inclusion */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
          /* TODO: compute aftInclLineNum directly and eliminate befInclLineNum */
              cmdPos1 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = 0;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos1/*start at (cmdPos1+1)th character*/,
              cmdPos2 - cmdPos1 - 1);
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum;
          parentLineNum = aftInclLineNum;

          /* Call recursively to expand includes in the included source */
          /* Start at line 1 since source is in external file */
          let(&inclSource, "");
          inclSource = readInclude(tmpSource,
              fileBufOffset + cmdPos1 - 1 + (long)strlen(inclPrefix), /*new offset*/
              /*includeFn,*/ includeFn,
              &inclSize /*input/output*/, 1/*parentLineNum*/, &(*errorFlag));

          oldInclSize = cmdPos2 - cmdPos1; /* Includes old prefix and suffix */
          let(&newFileBuf, cat(left(newFileBuf, cmdPos1 - 1),
              inclPrefix, inclSource, inclSuffix,
              right(newFileBuf, cmdPos2), NULL));
          newInclSize = (long)strlen(inclPrefix) + inclSize +
                (long)strlen(inclSuffix); /* Includes new prefix and suffix */
          *size = *size - (cmdPos2 - cmdPos1) + (long)strlen(inclPrefix)
              + inclSize + (long)strlen(inclSuffix);
          /* Adjust starting position for next inclusion search */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is
              0-based but cmdPos2 is 1-based */
          /* TODO: update line numbers for error msgs */
          break;
        default:
          bug(1745);
      } /* end switch (cmdType) */
    } else {
      /* This file has already been included.  Change Begin and $[ $] to
         Skip.  alreadyInclBy is the index of the previous g_IncludeCall that
         included it. */
      if (!(alreadyInclBy > 0)) bug(1765);
      switch (cmdType) {
        case 'B':
          /* Save the included source temporarily */
          let(&inclSource,
              seg(newFileBuf, cmdPos2, endPos1 - 1));
          /* Make sure it's content matches */
          let(&oldSource, "");
          oldSource = g_IncludeCall[  /* Connect to source for brevity */
                  alreadyInclBy
                  ].current_includeSource;
          if (strcmp(inclSource, oldSource)) {
            /* TODO - print better error msg */
            print2(
        "?Warning: \"$( Begin $[...\" source, with %ld characters, mismatches\n",
                (long)strlen(inclSource));
            print2(
                "  earlier inclusion, with %ld characters.\n",
                (long)strlen(oldSource));
          }
          oldSource = ""; /* Disconnect from source */
          /* We need to delete it from the source and change to Skip */
          let(&inclPrefix, cat("$( Skip $[ ", includeFn, " $] $)", NULL));
          let(&inclSuffix, "");

          /* Get the parent line number up to the inclusion */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
              cmdPos2 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = befInclLineNum;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos2/*start at (cmdPos2+1)th character*/,
              endPos2 - cmdPos2 /*- 1*/);
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum;
          parentLineNum = aftInclLineNum;

          let(&inclSource, ""); /* Final source to be stored - none */
          inclSize = 0; /* Size of just the included source */
          oldInclSize = endPos2 - cmdPos1; /* Includes old prefix and suffix */
          let(&newFileBuf, cat(left(newFileBuf, cmdPos1 - 1),
              inclPrefix,
              right(newFileBuf, endPos2), NULL));
          newInclSize = (long)strlen(inclPrefix); /* Includes new prefix and suffix */
          *size = *size - (endPos2 - cmdPos1) + newInclSize;
          /* Adjust starting position for next inclusion search (which may
             occur inside the source we just included, so don't skip passed
             that source) */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is
              0-based but cmdPos2 is 1-based */
          break;
        case 'I':
          /* Change inclusion command to Skip comment */
          let(&inclPrefix, cat("$( Skip $[ ", includeFn, " $] $)", NULL));
          let(&inclSuffix, "");

          /* Get the parent line number up to the inclusion */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
              cmdPos1 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = befInclLineNum;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos1/*start at (cmdPos1+1)th character*/,
              cmdPos2 - cmdPos1 /*- 1*/);
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum;
          parentLineNum = aftInclLineNum;

          let(&inclSource, ""); /* Final source to be stored - none */
          inclSize = 0; /* Size of just the included source */
          oldInclSize = cmdPos2 - cmdPos1; /* Includes old prefix and suffix */
          let(&newFileBuf, cat(left(newFileBuf, cmdPos1 - 1),
              inclPrefix,
              right(newFileBuf, cmdPos2), NULL));
          newInclSize = (long)strlen(inclPrefix); /* Includes new prefix and suffix */
          *size = *size - (cmdPos2 - cmdPos1) + (long)strlen(inclPrefix);
          /* Adjust starting position for next inclusion search */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is
              0-based but cmdPos2 is 1-based */
          break;
        case 'S':
          /* It is already Skipped, so leave it alone */
          /* *size = *size; */
          /* Adjust starting position for next inclusion search */
          let(&inclPrefix, seg(newFileBuf, cmdPos1, cmdPos2 - 1));
          let(&inclSuffix, "");

          /* Get the parent line number up to the inclusion */
          befInclLineNum = parentLineNum + countLines(
              newFileBuf + startOffset + 1,
              cmdPos1 - 1 - startOffset);
          g_IncludeCall[saveInclCalls - 1].current_line = befInclLineNum;
          aftInclLineNum = befInclLineNum + countLines(newFileBuf
              + cmdPos1/*start at (cmdPos1+1)th character*/,
              cmdPos2 - cmdPos1 /*- 1*/);
          g_IncludeCall[saveInclCalls].current_line = aftInclLineNum - 1;
          parentLineNum = aftInclLineNum;

          let(&inclSource, ""); /* Final source to be stored - none */
          inclSize = 0; /* Size of just the included source */
          oldInclSize = cmdPos2 - cmdPos1; /* Includes old prefix and suffix */
          newInclSize = oldInclSize; /* Includes new prefix and suffix */
          startOffset = cmdPos1 + newInclSize - 1; /* -1 since startOffset is
              0-based but cmdPos2 is 1-based */
          if (startOffset != cmdPos2 - 1) bug(1772);
          break;
        default:
          bug(1745);
      } /* end switch(cmdType) */
    } /* if alreadyInclBy == -1 */

    /* Assign structure with information for subsequent passes and
       (later) error messages */
    g_IncludeCall[saveInclCalls - 1].source_fn = "";  /* Name of the file where the
       inclusion source is located (= parent file for $( Begin $[... etc.) */
    g_IncludeCall[saveInclCalls - 1].current_offset = fileBufOffset + cmdPos1 - 1
        + (long)strlen(inclPrefix) - 1; /* This is the starting character position
            of the included file w.r.t entire source buffer */
    if (alreadyInclBy >= 0 || cmdType == 'B') {
      /* No external file was included, so let the includeFn be the
         same as the source */
      let(&g_IncludeCall[saveInclCalls - 1].source_fn, sourceFileName);
    } else {
      /* It hasn't been included yet, and cmdType is 'I' or 'S', meaning
         that we bring in an external file */
      let(&g_IncludeCall[saveInclCalls - 1].source_fn, includeFn); /* $[ $], Begin,
          or Skip file name for this inclusion */
      /*
      g_IncludeCall[saveInclCalls - 1].current_line = 0; */ /* The line number
          of the start of the included file (=1) */
    }
    g_IncludeCall[saveInclCalls - 1].current_includeSource = inclSource; /* let() not
        needed because we're just assigning a new name to inclSource */
    inclSource = ""; /* Detach from
        g_IncludeCall[saveInclCalls - 1].current_includeSource for later reuse */
    g_IncludeCall[saveInclCalls - 1].current_includeSource = "";
    g_IncludeCall[saveInclCalls - 1].current_includeLength = inclSize; /* Length of the file
        to be included (0 if the file was previously included) */


    /* Initialize a new include call for the continuation of the parent. */
    /* This entry is identified by pushOrPop = 1 */
    g_IncludeCall[saveInclCalls].source_fn = "";  /* Name of the file to be
        included */
    let(&g_IncludeCall[saveInclCalls].source_fn,
        sourceFileName); /* Source file containing
         this inclusion */
    g_IncludeCall[saveInclCalls].current_offset = fileBufOffset + cmdPos1
        + newInclSize - 1;
        /* This is the position of the continuation of the parent */
    g_IncludeCall[saveInclCalls].current_includeSource = ""; /* (Currently) assigned
        only if we may need it for a later Begin comparison */
    g_IncludeCall[saveInclCalls].current_includeLength = 0; /* Length of the file
        to be included (0 if the file was previously included) */

  } /* while (1) */

  /* Deallocate strings */
  let(&inclSource, "");
  let(&tmpSource, "");
  let(&oldSource, "");
  let(&inclPrefix, "");
  let(&inclSuffix, "");
  let(&includeFn, "");
  let(&fullInputFn, "");
  let(&fullIncludeFn, "");

  return newFileBuf;
} /* readInclude */


/* This function returns a pointer to a buffer containing the contents of an
   input file and its 'include' calls.  'Size' returns the buffer's size.  */
/* TODO - ability to flag error to skip raw source function */
/* If NULL is returned, it means a serious error occured (like missing file)
   and reading should be aborted. */
char *readSourceAndIncludes(vstring inputFn /*input*/, long *size /*output*/)
{
  long i;
/*D*//*long j;*/
/*D*//*vstring s=""; */
  vstring fileBuf = "";
  vstring newFileBuf = "";

  vstring fullInputFn = "";
  flag errorFlag = 0;

  /* Read starting file */
  let(&fullInputFn, cat(g_rootDirectory, inputFn, NULL));
  fileBuf = readFileToString(fullInputFn, 1/*verbose*/, &(*size));
  if (fileBuf == NULL) {
    print2(
        "?Error: file \"%s\" was not found\n", fullInputFn);
    fileBuf = "";
    *size = 0;
    errorFlag = 1;
    /* goto RETURN_POINT; */ /* Don't go now so that g_IncludeCall[]
         strings will be initialized.  If error, then blank fileBuf will
         cause main while loop to break immediately after first
         getNextInclusion() call. */
  }
  print2("Reading source file \"%s\"... %ld bytes\n", fullInputFn, *size);

  /* Create a ficticious initial include for the main file (at least 2
     g_IncludeCall structure array entries have been already been allocated
     in initBigArrays() in mmdata.c) */
  g_includeCalls = 0;
  g_IncludeCall[g_includeCalls].pushOrPop = 0; /* 0 means start of included file,
       1 means continuation of including file */
  g_IncludeCall[g_includeCalls].source_fn = "";
  let(&g_IncludeCall[g_includeCalls].source_fn, inputFn); /* $[ $], Begin,
      of Skip file name for this inclusion */
  g_IncludeCall[g_includeCalls].included_fn = "";
  let(&g_IncludeCall[g_includeCalls].included_fn, inputFn); /* $[ $], Begin,
      of Skip file name for this inclusion */
  g_IncludeCall[g_includeCalls].current_offset = 0;  /* This is the starting
      character position of the included file w.r.t entire source buffer */
  g_IncludeCall[g_includeCalls].current_line = 1; /* The line number
      of the start of the included file (=1) or the continuation line of
      the parent file */
  g_IncludeCall[g_includeCalls].current_includeSource = ""; /* (Currently) assigned
      only if we may need it for a later Begin comparison */
  g_IncludeCall[g_includeCalls].current_includeLength = *size; /* Length of the file
      to be included (0 if the file was previously included) */

  /* Create a ficticious entry for the "continuation" after the
     main file, to make error message line searching easier */
  g_includeCalls++;
  g_IncludeCall[g_includeCalls].pushOrPop = 1; /* 0 means start of included file,
       1 means continuation of including file */
  g_IncludeCall[g_includeCalls].source_fn = "";
  /*let(&g_IncludeCall[g_includeCalls].source_fn, inputFn);*/ /* Leave empty;
        there is no "continuation" file for the main file, so no need to assign
        (it won't be used) */
  g_IncludeCall[g_includeCalls].included_fn = "";
  /*let(&g_IncludeCall[g_includeCalls].included_fn, inputFn);*/ /* $[ $], Begin,
      of Skip file name for this inclusion */
  g_IncludeCall[g_includeCalls].current_line = -1; /* Ideally this should be
      countLines(fileBuf), but since it's never used we don't bother to
      call countLines(fileBuf) to save CPU time */
  g_IncludeCall[g_includeCalls].current_includeSource = ""; /* (Currently) assigned
      only if we may need it for a later Begin comparison */
  g_IncludeCall[g_includeCalls].current_includeLength = 0; /* The "continuation"
      of the main file is ficticious, so just set it to 0 length */

  /* Recursively expand the source of an included file */
  newFileBuf = "";
  newFileBuf = readInclude(fileBuf, 0, /*inputFn,*/ inputFn, &(*size),
      1/*parentLineNum*/, &errorFlag);
  g_IncludeCall[1].current_offset = *size;  /* This is the starting
      character position of the included file w.r.t entire source buffer.
      Here, it points to the nonexistent character just beyond end of main file
      (after all includes are expanded).
      Note that readInclude() may change g_includeCalls, so use 1 explicitly. */
  let(&fileBuf, ""); /* Deallocate */
/*D*//*printf("*size=%ld\n",*size);                                       */
/*D*//*for(i=0;i<*size;i++){                                              */
/*D*//*let(&s,"");                                                        */
/*D*//*s=getFileAndLineNum(newFileBuf,newFileBuf+i,&j);                   */
/*D*//*printf("i=%ld ln=%ld fn=%s ch=%c\n",i,j,s,(newFileBuf+i)[0]);  }   */
  if (errorFlag == 1) {
    /* The read should be aborted by the caller. */
    /* Deallocate the strings in the g_IncludeCall[] structure. */
    for (i = 0; i <= g_includeCalls; i++) {
      let(&g_IncludeCall[i].source_fn, "");
      let(&g_IncludeCall[i].included_fn, "");
      let(&g_IncludeCall[i].current_includeSource, "");
      g_includeCalls = -1; /* For the eraseSource() function in mmcmds.c */
    }
    return NULL;
  } else {
/*D*//*for (i=0; i<=g_includeCalls;i++)                                       */
/*D*//*  printf("i=%ld p=%ld f=%s,%s l=%ld o=%ld s=%ld\n",                  */
/*D*//*i,(long)g_IncludeCall[i].pushOrPop,g_IncludeCall[i].source_fn,           */
/*D*//*g_IncludeCall[i].included_fn,g_IncludeCall[i].current_line,              */
/*D*//*g_IncludeCall[i].current_offset,g_IncludeCall[i].current_includeLength); */
    return newFileBuf;
  }

} /* readSourceAndIncludes */

