#pragma module gnm "V2.4-000"
//
//  The GNM Compiler
//
//++
//  Copyright 1976, 2005 Hewlett-Packard Development Company, L.P.
//
//  Confidential computer software.  Valid license  from  HP  and/or its
//  subsidiaries required for possession, use, or copying.
//
//  Consistent with FAR 12.211 and 12.212, Commercial Computer Software,
//  Computer Software Documentation,  and  Technical Data for Commercial
//  Items  are  licensed to the U.S. Government under vendor's  standard
//  commercial license.
//
//  Neither HP nor any of its subsidiaries shall be liable for technical
//  or editorial errors or omissions contained herein.   The information
//  in  this document is provided  "as is"  without warranty of any kind
//  and is subject  to  change  without  notice.   The warranties for HP
//  products are set forth in the express  limited  warranty  statements
//  accompanying such products.   Nothing herein should be construed  as
//  constituting an additional warranty.
//--
//
//  GNM provides a mechanism for maintaining a common source file for
//  both OpenVMS messages and for error messages and for the recovery 
//  documentation.
//
//  The GNM compiler translates the contents of the GNM-format input file
//  into an output file suitable for processing by the OpenVMS MESSAGE
//  compiler, and into a second output file intended as input into the
//  OpenVMS DOCUMENT utility.
//
//
//  Modification History:
//
//  03-Jun-2005    Stephen Hoffman (V2.4)
//  Permit .destination message (-only) blocks to contain only the .name and
//  the .message -- allow the .explanation and .user_action to be omitted.
//  This change included changes in the state tables, and the addition of the
//  GNM$CB_DESTMSG routine here.  Adjust the output filename defaulting to
//  use the source filename, with the expected file type.  Resolve parsing
//  of $FAO directives with width specifications, and add various of the !%
//  directives.  Add warnings for specific cases of unimplemented directives.
//  Fix a bug which prevented the COMPONENT keyword text from being displayed
//  within the header of the MSG file.  Display the current date and time
//  within the comments of the output files.
//
//  21-Sep-2004    Stephen Hoffman (V2.3)
//  Add support for the display of the current version, for the GNMVERSION.OPT
//  options file mechanism, and for loading the version string into the output
//  files.  This so that we can quickly identify the version of the tool used.
//  Provide a workaround for current fopen (mis)handling of file specifications
//  including NLA0: as the device specification.
//
//  02-Sep-2004    Stephen Hoffman
//  Fixed a looping bug in the syntax recovery portion of the state tables,
//  updated the documentation and debugging within this module, and modified
//  the DCL build procedure for use with OpenVMS Alpha V7.3-2 and later (the
//  ARCH_DEFS.MAR-style values are now required by the $TRAN macro) and for
//  use with OpenVMS I64.  Also generalized the SDL.COM search.  Modified
//  the FOOMSGDEF.GNM example module for purposes of clarity, and to allow
//  easier direct verification of the correct SDML sort order.  Also tweak
//  the exit path from a failed sys$parse operation; we no longer stackdump
//  when an input or output has gone missing.
//
//  23-Apr-2003    Stephen Hoffman
//  Correctly display errors and exit when no paramters are specified; when
//  the image is erroneously RUN.
//
//  10-Apr-2003    Stephen Hoffman
//  Implement the GNM_COMPONENT logical name and associated handling, and
//  add the COMPONENT keyword in the file.  This allows the caller to easily
//  select the component (facility) text string for the SDML file, without
//  having to rebuild GNM.
//
//  09-Mar-2002    Stephen Hoffman
//  Complete redesign and rewrite of an earlier GNM tool.  This version uses
//  the lib$t[able_]parse routine at the core of its central logic.
//
//
//
//
#include <ctype.h>
#include <descrip.h>
#include <lib$routines.h>
#include <rms.h>
#include <ssdef.h>
#include <starlet.h>
#include <stat.h>
#include <string.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stsdef.h>
#include <time.h>
#include <tpadef.h>
#include "gnmdef.h"
#include <search.h>

#define GNM_MIN_MRS  80
#define GNM_MAX_MRS  256
#define GNM_VERSION  16

typedef int GNMTokenT;
typedef int GNMDestT;

struct GNMNode
    {
    struct GNMNode *LinkListNextNode;
    GNMTokenT Token;
    GNMDestT DestMSG, DestSDML;
    char *MsgName;
    int LineCount;
    int Base;
    int FAOCount;
    char *TextMSG;
    char *TextSDML;
    };

typedef struct GNMNode GNMNodeT;

typedef struct
    {
    struct tpadef *CoreP;
    int LineCount;
    int LastGoodLine;
    int LastBadLine;
    GNMTokenT LastGoodToken;
    int GNMMRS;
    GNMDestT DestMSG, DestSDML;
    GNMNodeT *TreeRootNameOrder;
    GNMNodeT LinkListLineHead, *LinkListLineLast;
    char *ComponentString;
    char *FileGNM, *FileMSG, *FileSDML;
    FILE *ChanGNM, *ChanMSG, *ChanSDML;
    _Bool FileMSGNull, FileSDMLNull;
    int ExitStat;
    struct dsc$descriptor TextGNMD, UpCaseGNMD;
    char VersionString[GNM_VERSION+1];
    char UnknownString[GNM_MAX_MRS+1];
    char CurrentTime[GNM_MAX_MRS+1];
    } GNMContextT;
GNMContextT GNMContext;

#pragma extern_model save
#pragma extern_model strict_refdef
//
//  Curious about these? 
//    ...Go look at the GNMPARSETABLE.MAR state table.
//
extern char GNM$STATE_TABLE;
extern char GNM$KEY_TABLE;
#pragma extern_model globalvalue
//
//  Curious about these? 
//    ...Go look at the gnmversion.opt linker options.
//
extern const char GNM$$K_VERSION_CHAR;
extern const int  GNM$$K_MAJ_VERSION;
extern const int  GNM$$K_MIN_VERSION;
extern const int  GNM$$K_EDIT_VERSION;
#pragma extern_model restore

#define SDML_HEADER_1   "<comment>(This SDML file was created by GNM %s)"
#define SDML_HEADER_2   "<comment>(File created on %s)"
#define SDML_HEADER_3   "<MESSAGE_SECTION><MESSAGE_TYPE>(textident)"
#define SDML_FOOTER     "<ENDMESSAGE_SECTION>"
#define SDML_MSG_MSG    "<MSG>("
#define SDML_MSG_FAC    "<MSG_TEXT>(Facility)"
#define SDML_MSG_EXP    "<MSG_TEXT>(Explanation)"
#define SDML_MSG_USR    "<MSG_TEXT>(User Action)"
#define MSG_HEADER      "! This MSG file was created by GNM %s."
#define MSG_CREATE      "! File created on %s"
#define MSG_FOOTER      ".end\n"
#define UNKNOWN_STRING  "Unknown"

static void
GNM$$LoadMSG( GNMNodeT *Node );
static void
GNM$$LoadSDML( GNMNodeT *Node );
static void
GNM$$UnloadMSG();
static void
GNM$$UnloadSDML();
static void
GNM$$UnloadSDMLNode( GNMNodeT **Node,
    enum visit VisitOrder , long int NodeLevel );
static int
GNM$$SortName( GNMNodeT *, GNMNodeT * );
static void 
GNM$$FileWrite( FILE *Chan, char *Text );
static void
GNM$$WriteMSG( char *Text );
static void
GNM$$UnloadMSGNode( GNMNodeT * );
static void
GNM$$WriteMSGFooter();
static void
GNM$$WriteMSGHeader();
static void
GNM$$WriteSDML( char *Text );
static void
GNM$$LineFeedSDML();
static void
GNM$$WriteSDMLFooter();
static void
GNM$$WriteSDMLHeader();
extern int 
GNM$CB_DESTMSG( struct tpadef *CoreP );
extern int 
GNM$CB_LOADER( struct tpadef *CoreP );
extern int
GNM$CB_READGNM( struct tpadef *CoreP );
extern int 
GNM$CB_SYNTAXERR( struct tpadef *CoreP );
extern int 
GNM$CB_SYNTAXMSG( struct tpadef *CoreP );
static void
GNM$$SecretDecoder( GNMTokenT Token, char * );
static void
GNM$$ShowCore( char *Hdr, struct tpadef *CoreP );
static void
GNM$$ShowNode( char *Hdr, GNMNodeT * );
static void
GNM$$BreakPoint();
static void
GNM$$Version();
static void
GNM$MaximizeErrorExit( int );


//  Open the input and output files.
//
static int
GNM$$OpenFiles()
    {
    struct stat GNMStat;
    int RetStat;

    lib$establish( lib$sig_to_ret );

    //  open the input file and the output files.
    //
    GNMContext.ChanGNM = fopen( GNMContext.FileGNM, "r");
    if ( !GNMContext.ChanGNM )
	{
	printf("Unable to open input file %s\n", GNMContext.FileGNM );
        lib$signal( SS$_BUFFEROVF );
	}

    GNMContext.ChanMSG = fopen( GNMContext.FileMSG, "w");
    if ( !GNMContext.ChanMSG )
	{
	printf("Unable to open output MSG file %s\n", GNMContext.FileMSG );
        lib$signal( SS$_BUFFEROVF );
	}

    GNMContext.ChanSDML = fopen( GNMContext.FileSDML, "w");
    if ( !GNMContext.ChanSDML )
	{
	printf("Unable to open output SDML file %s\n", GNMContext.FileSDML );
        lib$signal( SS$_BUFFEROVF );
	}

    //  Size the record buffer based on the maximum record size read
    //  from the file.  Ensure that the maximum size is within the 
    //  "reasonable" range.  (This code prefers to use the maximum 
    //  record size when the input record size is too small, and will
    //  prefer to signal if the file records are oversized.)
    //
    //  The RMS LRL field ("Longest Record Length"; see XABFHCDEF) would
    //  have been a far better choice to examine here than is the following 
    //  MRS ("Maximum Record Size") check, as MRS tends to be zero more 
    //  often than not.  That said, since MRS is usually returned as zero,
    //  the following code resets the zero MRS value returned by fstat to 
    //  our prefered MRS value and execution continues merrily onward in 
    //  an entirely unpurturbed fashion.  Why wasn't LRL used here?  Well,
    //  right now I'd rather use the C I/O and not direct RMS I/O for the
    //  initial debug and testing, and C I/O unfortunately does not have 
    //  ready access to the LRL value.
    //
    fstat( fileno( GNMContext.ChanGNM ), &GNMStat );

    GNMContext.GNMMRS = GNMStat.st_fab_mrs;
    if ( GNMContext.GNMMRS < GNM_MIN_MRS )
        GNMContext.GNMMRS = GNM_MAX_MRS;
    if ( GNMContext.GNMMRS > GNM_MAX_MRS )
	{
	printf("Input GNM record size %d larger than max permitted (%d)\n", 
	    GNMContext.GNMMRS, GNM_MAX_MRS );
        lib$signal( SS$_BUFFEROVF );
	}

    GNMContext.TextGNMD.dsc$w_length  = GNMContext.GNMMRS;
    GNMContext.TextGNMD.dsc$a_pointer = malloc( GNMContext.GNMMRS + 1 );
    GNMContext.UpCaseGNMD.dsc$w_length  = GNMContext.GNMMRS;
    GNMContext.UpCaseGNMD.dsc$a_pointer = malloc( GNMContext.GNMMRS + 1 );

    return SS$_NORMAL;
    }

//  Close the input and output files.
//
static void
GNM$$CloseFiles()
    {
    fclose( GNMContext.ChanGNM );
    fclose( GNMContext.ChanMSG );
    fclose( GNMContext.ChanSDML );

    return;
    }


//  Process the command line.
//
static int
GNM$$CommandParse( int argc, char **argv, char **envp )
    {
    int RetStat;
    int NullStat1, NullStat2;
    char *GNMDefType = ".GNM";
    char *MSGDefType = ".MSG";
    char *SDMLDefType = ".SDML";
    $DESCRIPTOR( NullDev, "_NLA0:" );
    struct FAB GNMFab = cc$rms_fab;
    struct NAM GNMNam = cc$rms_nam;
    char esa[NAM$C_MAXRSS];
    char MSGNam[NAM$C_MAXRSS];
    char SDMLNam[NAM$C_MAXRSS];

    lib$establish( lib$sig_to_ret );

    if ( argc != 4 )
      {
      printf("GNM.EXE\n");
      printf("\n");
      printf("This tool converts an input file into SDML and MSG\n");
      printf("output files, for subsequent processing of these files\n");
      printf("using the MESSAGE compiler, SDL compiler, and DOCUMENT.\n");
      printf("\n");
      printf("Please set up this tool as an OpenVMS foreign command:\n");
      printf("  $ gnm :== $ddcu:[dir]gnm.exe\n");
      printf("\n");
      printf("Usage:\n");
      printf("  $ gnm gnm-in-file.gnm msg-out-file.msg sdml-out-file.sdml\n");
      printf("Specification of these files is required.\n");
      printf("\n");
      printf("Please see the associated example GNM file for details of\n");
      printf("the syntax and format required within the GNM input file.\n");
      printf("\n");
      return SS$_BADPARAM;
      }

    //  Acquire the GNM input file name, applying defaults.
    //
    GNMFab.fab$l_nam  = &GNMNam;
    GNMFab.fab$l_fop  = FAB$M_NAM;
    GNMFab.fab$l_fna  = argv[1];
    GNMFab.fab$b_fns  = strlen( argv[1] );
    GNMFab.fab$b_dns  = strlen( GNMDefType );
    GNMFab.fab$l_dna  = GNMDefType;
    GNMNam.nam$l_esa  = esa;
    GNMNam.nam$b_ess  = NAM$C_MAXRSS;

    RetStat = sys$parse( &GNMFab );
    if ( !$VMS_STATUS_SUCCESS( RetStat ))
        {
        if ( RetStat == RMS$_DNF )
          RetStat = SS$_BADPARAM;
        lib$signal( RetStat );
        return RetStat;
        }    

    GNMContext.FileGNM = malloc( GNMNam.nam$b_esl + 1 );
    strcpy( GNMContext.FileGNM, esa );
    GNMContext.FileGNM[GNMNam.nam$b_esl] = '\0';

    //  Acquire the MSG output file name, applying defaults.
    //
    memcpy( MSGNam, GNMNam.nam$l_name, GNMNam.nam$b_name );
    MSGNam[GNMNam.nam$b_name] = '\0';
    strcat( MSGNam, MSGDefType );

    GNMFab.fab$l_nam  = &GNMNam;
    GNMFab.fab$l_fop  = FAB$M_NAM;
    GNMFab.fab$l_fna  = argv[2];
    GNMFab.fab$b_fns  = strlen( argv[2] );
    GNMFab.fab$l_dna  = MSGNam;
    GNMFab.fab$b_dns  = strlen( MSGNam );
// //    GNMFab.fab$l_fna  = MSGDef.dsc$a_pointer;
// //    GNMFab.fab$b_fns  = MSGDef.dsc$w_length;
// //    GNMFab.fab$l_dna  = argv[2];
// //    GNMFab.fab$b_dns  = strlen( argv[2] );
    GNMNam.nam$l_esa  = esa;
    GNMNam.nam$b_ess  = NAM$C_MAXRSS;

    RetStat = sys$parse( &GNMFab );
    if ( !$VMS_STATUS_SUCCESS( RetStat ))
        {
        if ( RetStat == RMS$_DNF )
          RetStat = SS$_BADPARAM;
        lib$signal( RetStat );
        return RetStat;
        }    

    //  Work around limitations over in fopen(); that code apparently doesn't expect
    //  to find anything after a null device (NLA0:) specification.  This is based on
    //  empirical evidence on V7.3-2, given that the fopen code will stackdump if we
    //  pass in "NLA0:[mumble]fratz", and it doesn't stackdump if we pass in "NLA0:".
    //
    NullStat1 = NullStat2 = GNMNam.nam$b_dev;
    if ( GNMNam.nam$b_dev == NullDev.dsc$w_length || GNMNam.nam$b_dev == NullDev.dsc$w_length - 1 )
      {
      NullStat1 = strncasecmp( GNMNam.nam$l_dev, NullDev.dsc$a_pointer, NullDev.dsc$w_length );
      NullStat2 = strncasecmp( GNMNam.nam$l_dev, NullDev.dsc$a_pointer + 1, NullDev.dsc$w_length - 1 );
      }
    if ( !NullStat1 || !NullStat2 )
      {
      GNMContext.FileMSGNull = TRUE;
      GNMContext.FileMSG = malloc( NullDev.dsc$w_length + 1 );
      strncpy( GNMContext.FileMSG, NullDev.dsc$a_pointer, NullDev.dsc$w_length  );
      }
    else
      {
      GNMContext.FileMSG = malloc( GNMNam.nam$b_esl + 1 );
      strcpy( GNMContext.FileMSG, esa );
      GNMContext.FileMSG[GNMNam.nam$b_esl] = '\0';
      GNMContext.FileMSGNull = FALSE;
      }


    //  Acquire the SDML output file name, applying defaults.
    //
    memcpy( SDMLNam, GNMNam.nam$l_name, GNMNam.nam$b_name );
    SDMLNam[GNMNam.nam$b_name] = '\0';
    strcat( SDMLNam, SDMLDefType );

    GNMFab.fab$l_nam  = &GNMNam;
    GNMFab.fab$l_fop  = FAB$M_NAM;
    GNMFab.fab$l_fna  = argv[3];
    GNMFab.fab$b_fns  = strlen( argv[3] );
    GNMFab.fab$l_dna  = SDMLNam;
    GNMFab.fab$b_dns  = strlen( SDMLNam );
// //    GNMFab.fab$l_fna  = SDMLDef.dsc$a_pointer;
// //    GNMFab.fab$b_fns  = SDMLDef.dsc$w_length;
// //    GNMFab.fab$l_dna  = argv[3];
// //    GNMFab.fab$b_dns  = strlen( argv[3] );
    GNMNam.nam$l_esa  = esa;
    GNMNam.nam$b_ess  = NAM$C_MAXRSS;

    RetStat = sys$parse( &GNMFab );
    if ( !$VMS_STATUS_SUCCESS( RetStat ))
        {
        if ( RetStat == RMS$_DNF )
          RetStat = SS$_BADPARAM;
        lib$signal( RetStat );
        return RetStat;
        }    

    //  Same work-around as discussed above.
    //
    NullStat1 = NullStat2 = GNMNam.nam$b_dev;
    if ( GNMNam.nam$b_dev == NullDev.dsc$w_length || GNMNam.nam$b_dev == NullDev.dsc$w_length - 1 )
      {
      NullStat1 = strncasecmp( GNMNam.nam$l_dev, NullDev.dsc$a_pointer, NullDev.dsc$w_length );
      NullStat2 = strncasecmp( GNMNam.nam$l_dev, NullDev.dsc$a_pointer + 1, NullDev.dsc$w_length - 1 );
      }
    if ( !NullStat1 || !NullStat2 )
      {
      GNMContext.FileSDMLNull = TRUE;
      GNMContext.FileSDML = malloc( NullDev.dsc$w_length + 1 );
      strncpy( GNMContext.FileSDML, NullDev.dsc$a_pointer, NullDev.dsc$w_length  );
      }
    else
      {
      GNMContext.FileSDML = malloc( GNMNam.nam$b_esl + 1 );
      strcpy( GNMContext.FileSDML, esa );
      GNMContext.FileSDML[GNMNam.nam$b_esl] = '\0';
      GNMContext.FileSDMLNull = FALSE;
      }

    return SS$_NORMAL;
    }


//  Diagnostic tool; displays the current contents of the core
//  data structure for the lib$table_parse routine.
//
static void
GNM$$ShowText( char *Txt )
    {
#ifdef DEBUG
    printf("%s\n", Txt );
#endif
    }


//  Diagnostic tool; displays the current contents of the core
//  data structure for the lib$table_parse routine.
//
static void
GNM$$ShowNode( char *Hdr, GNMNodeT *NodeP )
    {
#ifdef DEBUG
    int TruncLen;

    printf("Node ........... %s\n", Hdr );
    printf("     Address.... %08.8x\n", (int) NodeP );
    printf("     Name....... %s\n", NodeP->MsgName );
#endif
    return;
    }

//  Diagnostic tool; displays the current contents of the core
//  data structure for the lib$table_parse routine.
//
static void
GNM$$ShowCore( char *Hdr, struct tpadef *CoreP )
    {
#ifdef DEBUG
    int TruncLen;

    printf("Core ........... %s\n", Hdr );
    printf("     Address.... %08.8x\n", (int) CoreP );
    printf("     Count...... %08.8x\n", CoreP->tpa$l_count );
    printf("     Options.... %08.8x\n", CoreP->tpa$l_options );
    printf("     StringCnt.. %08.8x\n", CoreP->tpa$l_stringcnt );
    TruncLen = ( CoreP->tpa$l_stringcnt > 40 ) ? 40 : CoreP->tpa$l_stringcnt;
    if ( TruncLen )
        printf("     StringPtr.. %08.8x %*.*s\n", 
	    CoreP->tpa$l_stringptr,
	    TruncLen, TruncLen, CoreP->tpa$l_stringptr );
    else
        printf("     StringPtr.. %08.8x [null stringptr]\n" );
    printf("     TokenCnt... %08.8x\n", CoreP->tpa$l_tokencnt );
    TruncLen = ( CoreP->tpa$l_tokencnt > 40 ) ? 40 : CoreP->tpa$l_tokencnt;
    if ( TruncLen )
        printf("     TokenPtr... %08.8x %*.*s\n", 
            CoreP->tpa$l_tokenptr,
	    TruncLen, TruncLen, CoreP->tpa$l_tokenptr );
    else
        printf("     TokenPtr... %08.8x [null tokenptr]\n" );
    printf("     Param...... %08.8x\n", CoreP->tpa$l_param );
#endif
    return;
    }

//  Initialize the lib$table_parse core structure.
//
static void
GNM$$InitContext()
    {
    time_t FileCreationTime;
    size_t FindTheNewLine;

    GNM$$ShowText( "GNM$$InitContext" );

    memset( &GNMContext, 0, sizeof( GNMContextT ));

    if ( !(int) GNMContext.CoreP )
	GNMContext.CoreP = (void *) calloc( 1, sizeof( struct tpadef ));

    GNM$$ShowCore( "ResetCore", GNMContext.CoreP );

    GNMContext.ExitStat = SS$_NORMAL;

    memset( (void *) GNMContext.CoreP, 0, sizeof( struct tpadef ));

    GNM$$Version();

    strcpy( GNMContext.UnknownString, UNKNOWN_STRING );

    GNMContext.CoreP->tpa$l_count = TPA$K_COUNT0;
    /* GNMContext.CoreP->tpa$l_count = 
        ( sizeof( struct tpadef ) / sizeof(unsigned long int) ); */

    GNMContext.DestMSG = TRUE;
    GNMContext.DestSDML = TRUE;

    time( &FileCreationTime );
    strcpy( GNMContext.CurrentTime, ctime( &FileCreationTime ) );
    FindTheNewLine = strlen( GNMContext.CurrentTime ) - 1;
    GNMContext.CurrentTime[FindTheNewLine] = '\0';

    return;
    }


//  The main routine.  Sets up, parses, and unloads the data.
//
main( int argc, char **argv, char **envp )
    {
    int RetStat;
    int RetStatX;
    char *DefFacNam = "Unknown, The Unknown Facility";
    char *LogFacNam;

    GNM$$InitContext();

    GNMContext.LinkListLineLast = &GNMContext.LinkListLineHead;

    //  Establish a default value for the component text, and allow
    //  it to be overrided by a logical name or by a GNM keyword.
    //
    LogFacNam = getenv("GNM_COMPONENT");
    GNMContext.ComponentString = LogFacNam ? LogFacNam : DefFacNam;

    RetStat = GNM$$CommandParse( argc, argv, envp );
    if (!$VMS_STATUS_SUCCESS( RetStat ))
        return RetStat;

    RetStat = GNM$$OpenFiles();
    if (!$VMS_STATUS_SUCCESS( RetStat ))
        return RetStat;

    RetStat = GNM$CB_READGNM( GNMContext.CoreP );
    if (!$VMS_STATUS_SUCCESS( RetStat ))
	return RetStat;

    RetStat = lib$table_parse( 
	GNMContext.CoreP,
	&GNM$STATE_TABLE, 
	&GNM$KEY_TABLE );
    if (!$VMS_STATUS_SUCCESS( RetStat ))
	{
	RetStatX = GNM$CB_SYNTAXMSG( GNMContext.CoreP );
	return RetStat;
	}

    GNM$$UnloadMSG();

    GNM$$UnloadSDML();

    GNM$$CloseFiles();

    return GNMContext.ExitStat;
    }

//  Based on examination of the data structures used for the parse, 
//  find the data.  (The parse operation uses uppercase data, we 
//  want to use the original data here.)
//
static void
GNM$$CopyFrom( GNMNodeT *Node, char **From, int *FromLen )
    {
    int UpCaseOffset;
    char *UpCaseBaseP, *UpCaseCurrP;
    int TextLen;
    char *TextBaseP, *TextCurrP;

    *From = NULL;
    *FromLen = 0;

    UpCaseBaseP = (void *) GNMContext.UpCaseGNMD.dsc$a_pointer;
    UpCaseCurrP = (void *) GNMContext.CoreP->tpa$l_stringptr;
    UpCaseOffset = UpCaseCurrP - UpCaseBaseP;

    TextBaseP = (void *) GNMContext.TextGNMD.dsc$a_pointer;
    TextCurrP = TextBaseP + UpCaseOffset;
    TextLen = (int) GNMContext.CoreP->tpa$l_stringcnt;

    *From = TextCurrP;
    *FromLen = TextLen;

    return;
    }

//
//
static void
GNM$$CopyMSG( GNMNodeT *Node )
    {
    GNMTokenT Token = Node->Token;
    char To[GNM_MAX_MRS + 20];
    int IntraAngular = FALSE;
    int PostComma = FALSE;
    char *From;
    char *FromLetter;
    int FromLen;
    int i, j;
    int BangCount = 0;
    int EatLeadingSpaces = TRUE;

    lib$establish( lib$sig_to_ret );

    GNM$$CopyFrom( Node, &From, &FromLen );

    for ( i = j = 0; i < FromLen; i++ )
        {
	//  The following test catches most cases where we would 
	//  overfill the output buffer.
	//
	if ( j > GNM_MAX_MRS )
	    lib$signal( SS$_BUGCHECK );

        //  If we are just starting, there can be no spaces...
	//
        if ( EatLeadingSpaces && isspace( From[i] ))
	    continue;
        else
            EatLeadingSpaces = FALSE;

        //  If we are IntraAngular, there can be no spaces...
	//
        if ( IntraAngular && isspace( From[i] ))
	    continue;

        //  If we are in angles, the comma is the delimiter...
	//
        if ( IntraAngular && From[i] == ',' )
	    {
	    PostComma = TRUE;
	    continue;
	    }

        //  If we see a closing angle, we are no longer IntraAngular...
	//
        if ( IntraAngular && From[i] == '>' )
	    {
	    IntraAngular = FALSE;
	    PostComma = FALSE;
	    continue;
	    }

        //  If we see an angle, we become IntraAngular...
	//
        if ( From[i] == '<' )
            {
            IntraAngular = TRUE;
	    PostComma = FALSE;
            continue;
            }

        //  If we are in angles and have not seen the comma, ignore...
	//
        if ( IntraAngular && !PostComma )
	    continue;

        //  If we are not IntraAngular or if if we are PostComma, 
	//  copy most characters.  Translate braces into angles, 
	//  since there are various uses for angles in the message 
	//  file, including a set of $fao directives that need them.
	//
	//  (The format of the "if" statement used here deliberately
	//  matches the format of the same statement over in CopySDML 
	//  -- with the second part of the test reversed, of course -- 
	//  though here, PostComma can only be TRUE if (and only if)
	//  IntraAngular is also TRUE, so the second reference to 
	//  IntraAngular here is not strictly necessary.  Over in 
	//  CopySDML of course, PostComma can be (and is) FALSE 
	//  outside of IntraAngular.  Thus the reference is needed.)
	//
        if ( !IntraAngular || ( IntraAngular && PostComma ))
	    {

	    //  If we are looking at a directive, the following hack
	    //  is a guess whether there will be a parameter associated
	    //  with the $fao directive, or not.  It is a *guess*.
	    //  The following most definitely does not work with the
	    //  repeat count directive.
            //
	    if ( PostComma )
		{

		//  A bang indicates a a directive, but we do need
		//  to contend with the double-bang syntax.
		//
		if (( From[i] == '!' ) && ( From[i-1] != '!' ))
		    {
                    FromLetter = From + i + 1;

                    //  Advance past any punctuation and any leading digits.
                    //
                    while ( isdigit( FromLetter[0] ) ||
			FromLetter[0] == '(' || FromLetter[0] == ')' ||
			FromLetter[0] == '@' || FromLetter[0] == '#' )
			{
			if ( FromLetter[0] == '#' )
                          {
                          printf("Warning: $FAO repeat count (#) on line %d not supported by GNM.\n", Node->LineCount );
                          GNM$MaximizeErrorExit( SS$_UNSUPPORTED );
                          }

                        FromLetter++;
                        }

		    //  Detect some of the directives we don't deal with,
		    //  and report the problem to the caller.
		    //
		    if (( FromLetter[0] == '%' ) &&
                      (( FromLetter[1] == 'C' ) ||
                      ( FromLetter[1] == 'E' ) ||
                      ( FromLetter[1] == 'F' )))
                       {
                       printf("Warning: $FAO !%%C, !%%E, or !%%F on line %d not supported by GNM.\n", Node->LineCount );
                       GNM$MaximizeErrorExit( SS$_UNSUPPORTED );
                       }

		    //  Bang followed by a % can require zero or one
		    //  parameter.  Here are the !% directives that
		    //  expect one parameter.
		    //
		    if (( FromLetter[0] == '%' ) &&
                      (( FromLetter[1] == 'T' ) ||
                      ( FromLetter[1] == 'U' ) ||
                      ( FromLetter[1] == 'D' ) ||
                      ( FromLetter[1] == 'I' )))
			BangCount++;


		    //  Bang followed by a letter usually requires 
		    //  (at least) one parameter.
		    //
		    if ( isalpha( FromLetter[0] ))
			BangCount++;

		    //  Bang-AD and Bang-AF require a second parameter
		    //
		    if (( toupper( FromLetter[0] ) == 'A' ) &&
			(( toupper( FromLetter[1] ) == 'D' ) || 
			( toupper( FromLetter[1] ) == 'F' )))
		    BangCount++;
		    }
		}
            switch ( From[i] )
	        {
		case '{':
		    To[j++] = '<';
		    break;
		case '}':
		    To[j++] = '>';
		    break;
		default:
		    To[j++] = From[i];
		    break;
		}
	    }
        }

    Node->FAOCount = BangCount;
    To[j++] = '\0';
    Node->TextMSG = malloc( j + 1 );
    strcpy( Node->TextMSG, To );

    return;
    }

//
//
static void
GNM$$CopySDML( GNMNodeT *Node )
    {
    GNMTokenT Token = Node->Token;
    char To[GNM_MAX_MRS + 20];
    int IntraAngular = FALSE;
    int PostComma = FALSE;
    char *From;
    int FromLen;
    int i, j;
    char *Emphasis = "<EMPHASIS>(";
    int StuffLen;
    int EatLeadingSpaces = TRUE;

    lib$establish( lib$sig_to_ret );

    GNM$$CopyFrom( Node, &From, &FromLen );

    for ( i = j = 0; i < FromLen; i++ )
        {
	//  The following test catches most cases where we would 
	//  overfill the output buffer.
	//
	if ( j > GNM_MAX_MRS )
	    lib$signal( SS$_BUGCHECK );

        //  If we are just starting, there can be no spaces...
	//
        if ( EatLeadingSpaces && isspace( From[i] ))
	    continue;
        else
            EatLeadingSpaces = FALSE;

        //  If we are IntraAngular, there can be no spaces...
	//
        if ( IntraAngular && isspace( From[i] ))
	    continue;

        //  If we are in angles, the comma is the delimiter...
	//
        if ( IntraAngular && ( From[i] == ',' ))
	    {
	    PostComma = TRUE;
	    continue;
	    }

        //  If we see a closing angle, we are no longer IntraAngular...
	//
        if ( IntraAngular && ( From[i] == '>' ))
	    {
	    IntraAngular = FALSE;
	    PostComma = FALSE;
            To[j++] = ')';
	    continue;
	    }

        //  If we are in angles and have seen the comma, ignore...
	//
        if ( IntraAngular && PostComma )
	    continue;

        //  If we see an angle, we become IntraAngular...
	//
        if ( From[i] == '<' )
            {
            IntraAngular = TRUE;
	    PostComma = FALSE;
            StuffLen = strlen( Emphasis );
	    memcpy( &(To[j]), Emphasis, StuffLen );
            j += StuffLen;
            continue;
            }

        //  If we are not IntraAngular or if if we are not PostComma, 
	//  copy (most) characters directly.  Translate braces into 
	//  angles, since there are various uses for angles in the 
	//  SDML file.
	//
        if ( !IntraAngular || ( IntraAngular && !PostComma ))
	    {
            switch ( From[i] )
	        {
		case '{':
		    To[j++] = '<';
		    break;
		case '}':
		    To[j++] = '>';
		    break;
		default:
		    To[j++] = From[i];
		    break;
		}
	    }
        }
    To[j++] = '\0';
    Node->TextSDML = malloc( j + 1 );
    strcpy( Node->TextSDML, To );

    return;
    }

//  Format and load the current context into a storage node,
//  for eventual output into the MSG message file.
//
static void
GNM$$LoadMSG( GNMNodeT *Node )
    {
    int RetStat;
    GNMTokenT Token = Node->Token;

    switch ( Token )
	{
        case GNM$K_COPYRIGHT:
        case GNM$K_EXPLAN:
        case GNM$K_EXPLAN_MORE:
	case GNM$K_FACILITY:
	case GNM$K_IDENT:
	case GNM$K_MESSAGE:
	case GNM$K_TITLE:
        case GNM$K_USRACT:
        case GNM$K_USRACT_MORE:
	    GNM$$CopyMSG( Node );
	    break;
	default:
	    break;
	}

    return;
    }

//  Format and load the current context into a storage node,
//  for eventual output into the SDML document file.
//
static void
GNM$$LoadSDML( GNMNodeT *Node )
    {
    int RetStat;
    GNMTokenT Token = Node->Token;

    switch ( Token )
	{
        case GNM$K_COMPONENT:
        case GNM$K_COPYRIGHT:
        case GNM$K_EXPLAN:
        case GNM$K_EXPLAN_MORE:
	case GNM$K_FACILITY:
	case GNM$K_IDENT:
	case GNM$K_MESSAGE:
	case GNM$K_TITLE:
        case GNM$K_USRACT:
        case GNM$K_USRACT_MORE:
	    GNM$$CopySDML( Node );
	    break;
	default:
	    break;
	}

    return;
    }

//  Unloads the contents of the node list into the MSG file.
//
static void
GNM$$UnloadMSG()
    {
    int RetStat;
    GNMNodeT *Node = &GNMContext.LinkListLineHead;
    char *MsgNam, *MsgTxt;

    GNM$$ShowText( "GNM$$UnloadMSG" );

    GNM$$WriteMSGHeader();

    while ( Node )
        {
        if ( Node->DestMSG )
            GNM$$UnloadMSGNode( Node );
        Node = Node->LinkListNextNode;
        }

    GNM$$WriteMSGFooter();

    return;
    }


//  Writes information into the specified file.
//
static void 
GNM$$FileWrite( FILE *Chan, char *Text )
    {
    int RetStat;

    RetStat = fputs( Text, Chan );
    if ( RetStat == EOF)
        {
        printf("Unexpected error on fputs().\n");
        printf("Error writing to output file.\n");
        printf("Check for file error or protection error.\n");
        printf("Issuing SS$_BUGCHECK $exit.\n");
        RetStat = sys$exit( SS$_BUGCHECK );
        }
    return;
    }

//  Write the contents of the specified message node into the
//  MSG file.  Can traverse the node linkages as required.
//
static void
GNM$$UnloadMSGNode( GNMNodeT *Node )
    {
    GNMNodeT *NodeNext;
    GNMTokenT Token = Node->Token;
    char TmpText[GNM_MAX_MRS];

    GNM$$ShowText( "GNM$$UnloadMSGNode" );

    switch ( Token )
	{
        case GNM$K_COMMENT:
        case GNM$K_COPYRIGHT:
	    sprintf( TmpText, "! %s\n", Node->TextMSG );
	    GNM$$WriteMSG( TmpText );
	    break;
        case GNM$K_COMPONENT:
	    sprintf( TmpText, "! %s\n", GNMContext.ComponentString );
	    GNM$$WriteMSG( TmpText );
	    break;
        case GNM$K_ERROR:
	    GNM$$WriteMSG(".severity error\n");
	    break;
        case GNM$K_FATAL:
	    GNM$$WriteMSG(".severity fatal\n");
            break;
        case GNM$K_INFORMATIONAL:
	    GNM$$WriteMSG(".severity informational\n");
            break;
        case GNM$K_SUCCESS:
	    GNM$$WriteMSG(".severity success\n");
            break;
        case GNM$K_WARNING:
	    GNM$$WriteMSG(".severity warning\n");
            break;
        case GNM$K_END:
	    GNM$$WriteMSG(".end\n");
            break;
        case GNM$K_NAME:
	    NodeNext = Node->LinkListNextNode;
	    if ( NodeNext->FAOCount )
		sprintf( TmpText, "%-18s \"%s\"/fao=%d\n", 
		    Node->MsgName, NodeNext->TextMSG, NodeNext->FAOCount );
	    else
		sprintf( TmpText, "%-18s\"%s\"\n", 
		    Node->MsgName, NodeNext->TextMSG );
	    GNM$$WriteMSG( TmpText );
            break;
        case GNM$K_MESSAGE:
            break;
        case GNM$K_BASE:
	    sprintf( TmpText, ".base %d\n", Node->Base );
	    GNM$$WriteMSG( TmpText );
            break;
        case GNM$K_FACILITY:
	    sprintf( TmpText, ".facility %s\n", Node->TextMSG );
	    GNM$$WriteMSG( TmpText );
            break;
        case GNM$K_IDENT:
	    sprintf( TmpText, ".ident %s\n", Node->TextMSG );
	    GNM$$WriteMSG( TmpText );
            break;
        case GNM$K_PAGE:
	    GNM$$WriteMSG(".page\n");
            break;
        case GNM$K_TITLE:
	    sprintf( TmpText, ".title %s\n", Node->TextMSG );
	    GNM$$WriteMSG( TmpText );
            break;
	default:
            break;
	}

    return;
    }

//  Write a line of text into the MSG file...
//
static void
GNM$$WriteMSG( char *Text )
    {
    GNM$$FileWrite( GNMContext.ChanMSG, Text );
    return;
    }

//  Write a linefeed into the MSG file...
//
static void
GNM$$LineFeedMSG()
    {
    char *LineFeed = "\n";
    GNM$$FileWrite( GNMContext.ChanMSG, LineFeed );
    return;
    }

//  Write the line(s) associated with the message file header
//  Embed the GNM version string in the associated comment text.
//
static void
GNM$$WriteMSGHeader()
    {
    char HeaderString[GNM_MIN_MRS];

    sprintf( HeaderString, MSG_HEADER, GNMContext.VersionString );
    GNM$$FileWrite( GNMContext.ChanMSG, HeaderString );
    GNM$$LineFeedMSG();

    sprintf( HeaderString, MSG_CREATE, GNMContext.CurrentTime );
    GNM$$FileWrite( GNMContext.ChanMSG, HeaderString );
    GNM$$LineFeedMSG();

    return;
    }

//  Write the line(s) associated with the message file footer
//
static void
GNM$$WriteMSGFooter()
    {
    GNM$$FileWrite( GNMContext.ChanMSG, MSG_FOOTER );
    GNM$$LineFeedMSG();
    return;
    }

//  Write a record into the SDML file...
//
static void
GNM$$WriteSDML( char *Text )
    {
    GNM$$FileWrite( GNMContext.ChanSDML, Text );
    return;
    }

//  Write a linefeed into the SDML file...
//
static void
GNM$$LineFeedSDML()
    {
    char *LineFeed = "\n";
    GNM$$FileWrite( GNMContext.ChanSDML, LineFeed );
    return;
    }

//  Write the line(s) associated with the SDML file header.
//  Embed the GNM version string in the associated comment text.
//
static void
GNM$$WriteSDMLHeader()
    {
    char HeaderString[GNM_MIN_MRS];

    sprintf( HeaderString, SDML_HEADER_1, GNMContext.VersionString );
    GNM$$FileWrite( GNMContext.ChanSDML, HeaderString );
    GNM$$LineFeedSDML();

    sprintf( HeaderString, SDML_HEADER_2, GNMContext.CurrentTime );
    GNM$$FileWrite( GNMContext.ChanSDML, HeaderString );
    GNM$$LineFeedSDML();

    GNM$$FileWrite( GNMContext.ChanSDML, SDML_HEADER_3 );
    GNM$$LineFeedSDML();
    return;
    }

//  Write the line(s) associated with the SDML file footer
//
static void
GNM$$WriteSDMLFooter()
    {
    GNM$$FileWrite( GNMContext.ChanSDML, SDML_FOOTER );
    GNM$$LineFeedSDML();
    return;
    }

//  Unload the tree into the SDML output file...
//
static void
GNM$$UnloadSDML()
    {
    int RetStat;

    GNM$$ShowText( "GNM$$UnloadSDML" );

    GNM$$WriteSDMLHeader();

    twalk( (void *) GNMContext.TreeRootNameOrder, GNM$$UnloadSDMLNode );

    GNM$$WriteSDMLFooter();

    return;
    }

//  Extract the contents of a specific node into the SDML file...
//  This routine is called (repeatedly) by the twalk() routine...
//
static void
GNM$$UnloadSDMLNode( GNMNodeT **NodeArg,
    enum visit VisitOrder , long int NodeLevel )
    {
    int AreWeThereYet = FALSE;
    GNMNodeT *Node, *NodeNext;
    char SDMLBuffer[GNM_MAX_MRS+1];

    NodeNext = Node = *NodeArg;

    //  If we should not unload this node, well, don't.
    //
    if ( !Node->DestSDML )
        return;

    //  Since we store the (sorted) data in a binary tree, the tree
    //  traversal can return some tree nodes more than once.  The
    //  following test selects (only) those nodes returned during 
    //  the pre-order traversal -- including the leaf nodes -- for 
    //  further processing.
    //
    switch ( VisitOrder )
        {

	//  Nodes of interest...
        //
        case leaf:
            GNM$$ShowNode( "GNM$$UnloadSDMLNode node leaf", Node );
            break;
        case postorder:
            GNM$$ShowNode( "GNM$$UnloadSDMLNode postorder", Node );
            break;

	//  The currently-uninteresting nodes...
        //

        case endorder:
            GNM$$ShowNode( "GNM$$UnloadSDMLNode endorder", Node );
	    return;
        case preorder:
            GNM$$ShowNode( "GNM$$UnloadSDMLNode node preorder", Node );
	    return;
        default:
            GNM$$ShowNode( "GNM$$UnloadSDMLNode default", Node );
	    return;

        //  The unused ...
        //
        // //        case preorder:
        // //            GNM$$ShowNode( "GNM$$UnloadSDMLNode node preorder", Node );
        // //            break;
        // //        case postorder:
        // //            GNM$$ShowNode( "GNM$$UnloadSDMLNode postorder", Node );
        // //            return;

        }


    //  As we only store the NAME nodes in the binary tree, we must
    //  then walk the linked list chain for the remainder of the data 
    //  associated with each NAME node.  (This means we don't have to
    //  keep a second set of linkages, we can use the same linkages
    //  we needed for processing the output into the message file.)
    //
    switch ( Node->Token )
        {
	case GNM$K_NAME:
	    GNM$$LineFeedSDML();
            strcpy( SDMLBuffer, SDML_MSG_MSG );
            strcat( SDMLBuffer, Node->MsgName );
            strcat( SDMLBuffer, "\\" );
            Node = Node->LinkListNextNode;
            Node->TextSDML ? strcat( SDMLBuffer, Node->TextSDML ) : strcat( SDMLBuffer, GNMContext.UnknownString );
            strcat( SDMLBuffer, ")" );
	    GNM$$WriteSDML( SDMLBuffer );
	    GNM$$LineFeedSDML();
            strcpy( SDMLBuffer, SDML_MSG_FAC );
	    GNM$$WriteSDML( SDMLBuffer );
            strcpy( SDMLBuffer, GNMContext.ComponentString );
	    GNM$$WriteSDML( SDMLBuffer );
	    GNM$$LineFeedSDML();
            strcpy( SDMLBuffer, SDML_MSG_EXP );
	    GNM$$WriteSDML( SDMLBuffer );
	    GNM$$LineFeedSDML();
            while ( ((int) Node->LinkListNextNode ) &&
		(( Node->LinkListNextNode->Token == GNM$K_EXPLAN ) ||
                ( Node->LinkListNextNode->Token == GNM$K_EXPLAN_MORE ))
		)
                {
                Node = Node->LinkListNextNode;
                Node->TextSDML ? strcpy( SDMLBuffer, Node->TextSDML ) : strcpy( SDMLBuffer, GNMContext.UnknownString );
                GNM$$WriteSDML( SDMLBuffer );
		GNM$$LineFeedSDML();
                }
            strcpy( SDMLBuffer, SDML_MSG_USR );
	    GNM$$WriteSDML( SDMLBuffer );
	    GNM$$LineFeedSDML();
            while ( ((int) Node->LinkListNextNode ) &&
		(( Node->LinkListNextNode->Token == GNM$K_USRACT ) ||
                ( Node->LinkListNextNode->Token == GNM$K_USRACT_MORE ))
		)
                {
                Node = Node->LinkListNextNode;
                Node->TextSDML ? strcpy( SDMLBuffer, Node->TextSDML ) : strcpy( SDMLBuffer, GNMContext.UnknownString );
                GNM$$WriteSDML( SDMLBuffer );
		GNM$$LineFeedSDML();
                }
	    break;
	default:
	    break;
	}

    return;
    }

//  Allocate and initialize a new node...
//
GNMNodeT *
GNM$$NewNode( GNMTokenT Token )
    {
    GNMNodeT *Node;
    char *StringPtr = (void *) GNMContext.TextGNMD.dsc$a_pointer;
    int StringCnt = GNMContext.TextGNMD.dsc$w_length;
    int RestOfStringCnt = 0;
    char *TextPtr;

    Node = calloc( 1, sizeof( GNMContextT ) + 1 );
    Node->Token = Token;
    Node->LineCount = GNMContext.LineCount;

    //  The following sets the destination (output) routing flags,
    //  and loads the various node-specific details...
    //
    switch( Token )
        {
	case GNM$K_DEST_BOTH:
	    GNMContext.DestMSG = TRUE;
	    GNMContext.DestSDML = TRUE;
	    break;
	case GNM$K_DEST_SDML:
	    GNMContext.DestMSG = FALSE;
	    GNMContext.DestSDML = TRUE;
	    break;
	case GNM$K_DEST_MSG:
	    GNMContext.DestMSG = TRUE;
	    GNMContext.DestSDML = FALSE;
	    break;
	case GNM$K_COMPONENT:
GNM$$BreakPoint();
            RestOfStringCnt = GNMContext.CoreP->tpa$l_tokencnt + GNMContext.CoreP->tpa$l_stringcnt;
	    GNMContext.ComponentString = (void *) malloc( RestOfStringCnt + 1  );
            TextPtr = StringPtr + strlen( StringPtr ) - RestOfStringCnt;
	    memcpy(
		GNMContext.ComponentString,
		(void *) TextPtr,
		RestOfStringCnt );
	    GNMContext.ComponentString[RestOfStringCnt] = '\0';
	    GNMContext.CoreP->tpa$l_stringcnt = 0;
            break;
	case GNM$K_NAME:
	    Node->DestMSG = GNMContext.DestMSG;
	    Node->DestSDML = GNMContext.DestSDML;
	    Node->MsgName =
		(void *) malloc( GNMContext.CoreP->tpa$l_tokencnt + 1 );
	    memcpy(
		Node->MsgName,
		(void *) GNMContext.CoreP->tpa$l_tokenptr,
		GNMContext.CoreP->tpa$l_tokencnt );
	    Node->MsgName[GNMContext.CoreP->tpa$l_tokencnt] = '\0';
            break;
	case GNM$K_BASE:
	    Node->DestMSG = GNMContext.DestMSG;
	    Node->DestSDML = GNMContext.DestSDML;
	    Node->Base = GNMContext.CoreP->tpa$l_number;
	    break;
        default:
	    Node->DestMSG = GNMContext.DestMSG;
	    Node->DestSDML = GNMContext.DestSDML;
	    break;
        }

    Node->DestMSG = GNMContext.DestMSG;
    Node->DestSDML = GNMContext.DestSDML;

    if ( Node->DestMSG )
	GNM$$LoadMSG( Node );
    if ( Node->DestSDML )
        GNM$$LoadSDML( Node );

    return Node;
    }

//  Generic routine used (by the parser) to load nodes...
//
extern int 
GNM$CB_LOADER( struct tpadef *CoreP )
    {
    int RetStat;
    GNMNodeT *Node;
    GNMNodeT *NodeMatched;
    GNMTokenT Token = CoreP->tpa$l_param;
    char DecodedTokenName[GNM_MIN_MRS];

    //  debug
    //
    GNM$$ShowText( "GNM$CB_LOADER" );
    GNM$$SecretDecoder( Token, DecodedTokenName );
    GNM$$ShowText( DecodedTokenName );

    //  Save some stuff useful when displaying details of syntax errors...
    //
    GNMContext.LastGoodLine = GNMContext.LineCount;
    GNMContext.LastGoodToken = Token;

    Node = GNM$$NewNode( Token );

    //  We only sort the nodes containing the .name entries.
    //  This tree will eventually be twalk-traversed, and the 
    //  results used (only) for generating the SDML output.
    //
    if ( Token == GNM$K_NAME )
        NodeMatched = tsearch( 
            Node,
            (void *) &GNMContext.TreeRootNameOrder,
            GNM$$SortName );

    //  Update the forward linkages in the current last record and
    //  in the context block to reference the new now-last record.
    //  The node linkages are used both for SDML and MSG output.
    //
    (GNMContext.LinkListLineLast)->LinkListNextNode = Node;
    GNMContext.LinkListLineLast = Node;
   
    RetStat = GNM$CB_READGNM( GNMContext.CoreP );
    if (!$VMS_STATUS_SUCCESS( RetStat ))
	return RetStat;

    return SS$_NORMAL;
    }


//  Diagnostic tool; displays the text translation of the token
//
static void
GNM$$SecretDecoder( GNMTokenT Token, char *DecodedName )
    {
    switch ( Token )
        {
        case GNM$K_COMMENT:
	    strcpy( DecodedName, "COMMENT");
	    break;
        case GNM$K_COMPONENT:
	    strcpy( DecodedName, "COMPONENT");
	    break;
        case GNM$K_COPYRIGHT:
	    strcpy( DecodedName, "COPYRIGHT");
	    break;
        case GNM$K_ERROR:
	    strcpy( DecodedName, "ERROR");
	    break;
        case GNM$K_FATAL:
	    strcpy( DecodedName, "FATAL");
	    break;
        case GNM$K_INFORMATIONAL:
	    strcpy( DecodedName, "INFORMATIONAL");
	    break;
        case GNM$K_SUCCESS:
	    strcpy( DecodedName, "SUCCESS");
	    break;
        case GNM$K_WARNING:
	    strcpy( DecodedName, "WARNING");
	    break;
        case GNM$K_END:
	    strcpy( DecodedName, "END");
	    break;
        case GNM$K_NAME:
	    strcpy( DecodedName, "NAME");
	    break;
        case GNM$K_MESSAGE:
	    strcpy( DecodedName, "MESSAGE");
	    break;
        case GNM$K_BASE:
	    strcpy( DecodedName, "BASE");
	    break;
        case GNM$K_FACILITY:
	    strcpy( DecodedName, "FACILITY");
	    break;
        case GNM$K_IDENT:
	    strcpy( DecodedName, "IDENT");
	    break;
        case GNM$K_PAGE:
	    strcpy( DecodedName, "PAGE");
	    break;
        case GNM$K_TITLE:
	    strcpy( DecodedName, "TITLE");
	    break;
        case GNM$K_EXPLAN:
	    strcpy( DecodedName, "EXPLAN");
	    break;
        case GNM$K_EXPLAN_MORE:
	    strcpy( DecodedName, "EXPLAN_MORE");
	    break;
        case GNM$K_USRACT:
	    strcpy( DecodedName, "USRACT");
	    break;
        case GNM$K_USRACT_MORE:
	    strcpy( DecodedName, "USRACT_MORE");
	    break;
        case GNM$K_DEST_BOTH:
	    strcpy( DecodedName, "DEST_BOTH");
	    break;
        case GNM$K_DEST_SDML:
	    strcpy( DecodedName, "DEST_SDML");
	    break;
        case GNM$K_DEST_MSG:
	    strcpy( DecodedName, "DEST_MSG");
	    break;
	default: 
	    sprintf( DecodedName, "Unknown[0x0%x]", Token );
	    break;
	}
    return;
    }

//  Remove the comment characters from the string
//
static int
GNM$$Decomment(char *Text )
    {
    int i;
    int IntraAngular = FALSE;

    for ( i = 0; i < strlen( Text ); i++ )
	{
	if ( Text[i] == '<' ) 
            IntraAngular = TRUE;
	if ( Text[i] == '>' ) 
            IntraAngular = FALSE;
	if (( Text[i] == '!' ) && (!IntraAngular))
	    {
	    Text[i] = '\0';
	    break;
	    }
	}

    //  Return the length of the decommented string, or a zero.  If
    //  we believe we are still within angle brackets, we zero out.
    //
    if ( IntraAngular )
	return 0;
    else
	return i;
    }

//  Trim superfluous characters from the GNM string
//
static int
GNM$$TrimString(char *Text )
    {
    int TextLen = strlen( Text );
    int FlabIdx, TrimIdx;
    char *TextFlab, *TextTrim;
    int CharI;

    TextFlab = TextTrim = Text;

    //  Back over the null, and trim off any trailing whitespace and
    //  trailing control characters.
    //
    for (; TextLen > 0; TextLen-- )
	{
	CharI = (int) TextFlab[TextLen - 1];
	if ( !isspace( CharI ) && !iscntrl( CharI ) )
	    break;
	}

    //  If there is nothing left of the input string, leave now.
    //
    if ( !TextLen )
	{
	TextTrim[0] = '\0';
        return 0;
	}

    //  If we are here, we know there is at least something of 
    //  interest left in the string.  Trim any leading whitespace 
    //  and any leading control characters, and continue onward.
    //
    for ( FlabIdx = 0; FlabIdx < TextLen; FlabIdx++ )
	{
	CharI = (int) TextFlab[FlabIdx];
	if ( !isspace( CharI ) && !iscntrl( CharI ) )
	    break;
	}

    //  Compress any embedded whitespace sequences down to a single ASCII
    //  space character, and also expunge any lurking control characters.
    //  Note that FlabIdx will start this for-loop with a non-zero value 
    //  (only) when there is leading whitespace, as the value of FlabIdx
    //  is established by the previous for-loop, and not by this loop.
    //  Note that the iscntrl follows the primary isspace check, because 
    //  a tab character counts as both a space and as a control character.
    //
    for (TrimIdx = 0; FlabIdx < TextLen; FlabIdx++ )
	{
	if ( (isspace(TextFlab[FlabIdx])) && (!isspace(TextFlab[FlabIdx+1])) )
	    TextTrim[TrimIdx++] = ' ';
	if ( iscntrl( TextFlab[FlabIdx] ))
	    continue;
	if (!isspace( TextFlab[FlabIdx] ))
	    TextTrim[TrimIdx++] = TextFlab[FlabIdx];
	}

    //  Terminate the pastuerized processed cheese flavored string product.
    //
    TextTrim[TrimIdx] = '\0';

    return TrimIdx;
    }


//  Read the next record from the GNM (input) file...
//
extern int
GNM$CB_READGNM( struct tpadef *CoreP )
    {
    int RetStat;
    int i;
    char *UpCaseGNMP;
    char *TextGNMP;
    char *GarbageP;
    int GarbageLen;
    size_t TextGNMLen;

#ifdef DEBUG
    GNM$$ShowText( "GNM$CB_READGNM");
    GNM$$ShowCore( "READGNM Old", CoreP );
#endif

    //  Initialize some values used in the loop...
    //
    UpCaseGNMP = (void *) GNMContext.UpCaseGNMD.dsc$a_pointer;
    TextGNMP = (void *) GNMContext.TextGNMD.dsc$a_pointer;
    TextGNMLen = (size_t) GNMContext.TextGNMD.dsc$w_length;

    do
	{
        //  Increment the input record count
        //
        GNMContext.LineCount++;

        //  Read a record into the central buffer
        //
        GarbageP = fgets( 
	    (void *) TextGNMP, 
	    (size_t) TextGNMLen, 
	    GNMContext.ChanGNM );

	//  If fgets returneth sans data, then we are done.
	//
	if ( !(int) GarbageP )
	    {
            GNM$$ShowText( "GNM fgets() fatal error");
	    return SS$_BADPARAM;
	    }

	//  Trim any flab (leading and trailing and multiple spaces, 
	//  control characters, etc) lurking within the record string.  
	//  If thyne record begoneth whence thyne flabeth be trimmeth, 
	//  fetcheth thyne record anew.
	//
        GarbageLen = GNM$$Decomment( TextGNMP );
	if ( GarbageLen )
	    GarbageLen = GNM$$TrimString( (void *) TextGNMP );
	} while ( !GarbageLen );

    for ( i = 0; i < strlen( TextGNMP ); i++ )
	GNMContext.UpCaseGNMD.dsc$a_pointer[i] = toupper(TextGNMP[i]);
    GNMContext.UpCaseGNMD.dsc$a_pointer[i] = '\0';

#ifdef DEBUG
    // debug code...
    //
    printf("GNM Text   %s\n", 
	GNMContext.TextGNMD.dsc$a_pointer );
    printf("GNM Upcase %s\n", 
	GNMContext.UpCaseGNMD.dsc$a_pointer );
#endif

    //  Reinitialize the pointers in the lib$table_parse data structure
    //
    GNMContext.CoreP->tpa$l_stringptr = 
	(int) GNMContext.UpCaseGNMD.dsc$a_pointer;
    GNMContext.CoreP->tpa$l_stringcnt = 
	strlen( GNMContext.UpCaseGNMD.dsc$a_pointer );
    GNMContext.CoreP->tpa$l_tokenptr = 0;
    GNMContext.CoreP->tpa$l_tokencnt = 0;
    GNM$$ShowCore( "READGNM New", CoreP );

    return SS$_NORMAL;
    }

//  Display the text of a syntax error
//
extern int 
GNM$CB_SYNTAXERR( struct tpadef *CoreP )
    {
    int RetStat;
    char *TokStr =  (void *) GNMContext.CoreP->tpa$l_tokenptr;
    size_t TokStrLen =  GNMContext.CoreP->tpa$l_tokencnt;
    char DecodedTokenName[GNM_MIN_MRS];

    GNM$$ShowText( "GNM$CB_SYNTAXERR");

    if ( GNMContext.LastBadLine == GNMContext.LineCount )
        return SS$_NORMAL;
    GNMContext.LastBadLine = GNMContext.LineCount;

    return SS$_NORMAL;
    }


//  Confirm the current state of the parsing; ensure the output
//  processing is being sent (only) to the message file.  This
//  check is part of the logic that allows message-only output
//  to optionally omit the .explanation and .user_action text.
//
extern int 
GNM$CB_DESTMSG( struct tpadef *CoreP )
    {
    int RetStat;
    char *TokStr =  (void *) GNMContext.CoreP->tpa$l_tokenptr;
    size_t TokStrLen =  GNMContext.CoreP->tpa$l_tokencnt;

    GNM$$ShowText( "GNM$CB_DESTMSG");

    if ( !GNMContext.DestMSG || GNMContext.DestSDML )
      return SS$_BADPARAM;

    return SS$_NORMAL;
    }


//  Display the text of a syntax error
//
extern int 
GNM$CB_SYNTAXMSG( struct tpadef *CoreP )
    {
    int RetStat;
    char *TokStr =  (void *) GNMContext.CoreP->tpa$l_tokenptr;
    size_t TokStrLen =  GNMContext.CoreP->tpa$l_tokencnt;
    char DecodedTokenName[GNM_MIN_MRS];

    GNM$$ShowText( "GNM$CB_SYNTAXMSG");

    //  Prevent us from displaying the same syntax error message 
    //  more than once -- this also means we permit and we detect
    //  at most one error per source line.
    //
    if ( GNMContext.LastBadLine == GNMContext.LineCount )
        return SS$_NORMAL;
    GNMContext.LastBadLine = GNMContext.LineCount;

    GNM$MaximizeErrorExit( SS$_BUGCHECK );

    printf("GNM has detected a syntax error.\n" );
    printf("  The syntax error is likely between lines %d and %d inclusive.\n", 
        GNMContext.LastGoodLine, GNMContext.LineCount );
    GNM$$SecretDecoder( GNMContext.LastGoodToken, DecodedTokenName );
    printf("  The last good token seen was %s on line %d.\n", DecodedTokenName, GNMContext.LastGoodLine );
    printf("  Scanning forward; attempting to recover position.\n" );
    printf("  Scanning forward for .NAME, .END or the End Of File.\n" );
    return SS$_NORMAL;
    }


//  The following is used to sort name strings within the binary tree
//
static int
GNM$$SortName( GNMNodeT *ThisMsgNode,  GNMNodeT *ThatMsgNode )
    {
    int RetStat;

    RetStat = strcasecmp( ThisMsgNode->MsgName, ThatMsgNode->MsgName );

#ifdef DEBUG
    GNM$$ShowNode( "GNM$$SortName ThisMsgNode", ThisMsgNode );
    GNM$$ShowNode( "GNM$$SortName ThatMsgNode", ThatMsgNode );
    if ( RetStat < 0 ) printf("This < That\n\n");
    if ( RetStat > 0 ) printf("This > That\n\n");
    if ( !RetStat )    printf("This = That\n\n");
#endif

    return RetStat;
    }
static void
GNM$$BreakPoint()
    {
    return;
    }


//  The following derives a text-format version string from the data stored in
//  the linker options file.   The trailing null -- given that the context data
//  is zero-initialized -- isn't strictly necessary.  The abort protects against
//  what should never happen; against a version string buffer overflow error.
//  (Oh, for the existence of an implementation of `snprintf' or some such in
//  the C standard; of sprinf, printf and the other variants -- with output
//  string maximum-length limit arguments.  Until then, we abort() on error.)
//
static void
GNM$$Version()
  {
  int RetStat;
  int VersStringLen;

  sprintf( GNMContext.VersionString, "%c%d.%d-%d",
    GNM$$K_VERSION_CHAR, GNM$$K_MAJ_VERSION, GNM$$K_MIN_VERSION, GNM$$K_EDIT_VERSION );

  VersStringLen = strlen( GNMContext.VersionString );
  if ( VersStringLen > GNM_VERSION )
    {
    printf("Fatal internal version string buffer overflow error; aborting...\n");
    abort();
    }

  GNMContext.VersionString[VersStringLen] = '\0';

  return;
  }


//  If the caller wants to record an error more severe than one we already have recorded,
//  do so now.  Otherwise, preserve the existing (and more severe) error exit status.
//  Adjust the local severity values to ensure errors are always larger than successes.
//
static void
GNM$MaximizeErrorExit( int ErrStat )
    {
    int SeverityOld, SeverityNew;

    SeverityOld = $VMS_STATUS_SUCCESS( GNMContext.ExitStat ) ?
	$VMS_STATUS_SEVERITY( GNMContext.ExitStat ) : $VMS_STATUS_SEVERITY( GNMContext.ExitStat ) + 7;
    SeverityNew = $VMS_STATUS_SUCCESS( ErrStat ) ?
	$VMS_STATUS_SEVERITY( ErrStat ) : $VMS_STATUS_SEVERITY( ErrStat ) + 7;

    if ( SeverityOld < SeverityNew )
      GNMContext.ExitStat = ErrStat;

    return;
    }

