/* Copyright (C) 1993,1994 by the author(s).
 
 This software is published in the hope that it will be useful, but
 WITHOUT ANY WARRANTY for any part of this software to work correctly
 or as described in the manuals. See the ShapeTools Public License
 for details.

 Permission is granted to use, copy, modify, or distribute any part of
 this software but only under the conditions described in the ShapeTools 
 Public License. A copy of this license is supposed to have been given
 to you along with ShapeTools in a file named LICENSE. Among other
 things, this copyright notice and the Public License must be
 preserved on all copies.
*/
/*
 * ShapeTools/shape program - pa.c
 *
 * by Juergen.Nickelsen@cs.tu-berlin.de
 *
 * $Header: parser.c[8.0] Fri Aug  6 20:12:26 1993 nickel@cs.tu-berlin.de frozen $
 */
#ifndef lint
static char *AtFSid = "$Header: parser.c[8.0] Fri Aug  6 20:12:26 1993 nickel@cs.tu-berlin.de frozen $";
#endif

#include <ctype.h>
#include <setjmp.h>
#include "shape.h"
#include "parser.h"
#include "mfiles.h"

/* This variable is exported: */
int parse_errors = 0 ;		/* number of parse_errors */
#ifdef DEBUG
int dump_parsed_definitions ;
#endif

/* local declarations and definitions */

/* several string literals and their length: */
#define STRING_LENGTH(s)	(sizeof(s) - 1)	/* ONLY for literals! */
#define INCLUDE_STRING		"include"
#define INCLUDE_LENGTH		STRING_LENGTH(INCLUDE_STRING)
#define VCLASS_STRING		"vclass"
#define VCLASS_LENGTH		STRING_LENGTH(VCLASS_STRING)
#define VCLASS_SEPARATOR	"::="
#define VCLSEP_LENGTH		STRING_LENGTH(VCLASS_SEPARATOR)
#define RULESEC_STRING		"RULE-SECTION"
#define RULESEC_LENGTH		STRING_LENGTH(RULESEC_STRING)
#define VARSEC_STRING		"VARIANT-SECTION"
#define VARSEC_LENGTH		STRING_LENGTH(VARSEC_STRING)
#define ENDSEC_STRING		"END-"
#define ENDSEC_LENGTH		STRING_LENGTH(ENDSEC_STRING)


/* constants for the type of a top-level line */
#define UNKNOWN_LINE		0 /* not recognized */
#define VCLASS_LINE		1 /* vclass definition */
#define DEPRULE_LINE		2 /* dependency rule */
#define VARDEF_LINE		3 /* variant definition */
#define MACRO_LINE		4 /* macro definition with = */
#define ADDMAC_LINE		5 /* macro definition with += */
#define SUBMAC_LINE		6 /* macro definition with -=  */
#define VARMAC_LINE		7 /* macro definition with := */
#define SELRULE_LINE		8 /* selection rule */
#define RULESEC_LINE		9 /* beginning of rule section */
#define VARSEC_LINE	       10 /* beginning of variant section */
#define INCLUDE_LINE	       11 /* "include" line */
#define DOUBLEC_LINE	       12 /* line with double colon */

/* Constants for parse_selection_rule() and parse_variant_definition():
 * Has definition been found in a SECTION (old style) or not. */
#define IN_SECTION		1
#define NO_SECTION		0

/* possible components of selection rule or variant names, this is
 * essentially [0-9_A-Za-z] */
#define RULE_OR_VAR_NAME_COMPONENTS \
"0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

/* local Functions */

static int recognize_line A((char **linep, struct stringlist **name_list_p)) ;
static void skip_over_rulebody A((void)) ;
static char *get_variant_name A((char **linep)) ;
static struct stringlist *collect_names A((char **linep)) ;
static void parse_rule_or_var_section A((char *line)) ;
static void parse_macro_definition A((char *line, int type,
				struct stringlist *name_list)) ;
static void parse_vclass_definition A((char *line)) ;
static void parse_dependency_rule A((char *line, struct stringlist *name_list)) ;
static int end_of_section A((char *line, char *section)) ;
static void parse_selection_rule A((char *line,
				    struct stringlist *name_list,
				    int in_section)) ;
static void variant_section_body A((char *line)) ;
static void rule_section_body A((char *line)) ;
static void parse_variant_definition A((char *line,
					struct stringlist *name_list,
					int in_section)) ;
static void parse_include_statement A((char *line)) ;
static int rule_or_variant_body A((char *line)) ;
static void clean_from_comment A((char *line)) ;
static void clean_selrule_from_comment A((char *line)) ;

/* local Variables */

static MFILE *currentmf ;
static jmp_buf jmp_toplevel ;


/* int parse_file(filename): toplevel parse funtion */

int parse_file(filename)
char *filename ;
{
    /* It does:
       - read in the specified file with mf_readin()
       - for each line at the top-level do the following:
         - skip over leading blanks
         - look at the first character of the line. It may indicate a
	   comment line, an empty line, or a line beginning with TAB,
	   which is not allowed at top-level.
	 - call recognize_line() to determine the type of a non-empty
	   line. recognize_line() also reads the "names" (target,
	   rule, variant, or macro names) it encounters before seeing
	   a "magic token" which identifies the line.
	 - call the appropriate parse function for this line. This
	   function may read more lines.
	 - if an error is found, a parse function may jump back to the
	   top-level with longjmp() (usually called by parsing_error()).
	   The entry point is saved in jmp_toplevel.

       The current MFILE * and jmp_toplevel are global to this module
       in order to be accessible by the other parse functions. In case
       of an recursive invokation of parse_file() by
       parse_include_statement() they are saved in local variables of
       parse_include_statement(). 
     */

    char *line ;		/* pointer to current line */
    int line_type ;		/* type of line */
    char *line_begin ;		/* save ptr to beginning of line */
    struct stringlist *name_list ; /* list of "names" collected
				    * before type of line is
				    * determined */
#ifdef DEBUG
    dump_parsed_definitions = getenv("SHAPE_PARSER_DUMP") ;
#endif
    
    /* check if standard input is to be parsed */
    if (strcmp(filename, "-")) {
	currentmf = mf_readin(filename) ;
    } else {
	currentmf = mf_readin_stdin() ;
    }

    if (currentmf == NULL) {	/* mf_readin() failed */
	return FALSE ;
    }

    /* jump target for parsing_error() */
    setjmp(jmp_toplevel) ;

    /* top-level loop */
    while ((line = mf_nextline(currentmf))) {
	line_begin = line ;
	line = skip_over_blanks(line) ;
	switch (*line) {	/* look at first character */
	  case '#':		/* comment or RULE- or VARIANT-SECTION */
	    if (*(line + 1) == '%') {
		parse_rule_or_var_section(line) ;
	    }			/* else comment: ignore */
	    break ;

	  case '\t':		/* tab at beginning only in rule or
				 * variant bodies allowed (or line
				 * must be empty) */
	    line = skip_over_whitespace(line) ;
	    if (*line != '\0' && *line != '#') {
		parsing_error("line with tab character at top-level",
			    line, NULL) ;
	    }
	    break ;

	  case '\0':		/* empty line */
	    break ;

	  default:
	    line_type = recognize_line(&line, &name_list) ;

	    /* line now points to first character after magic token,
	     * name_list contains names found up to that point in
	     * correct order.
	     */

	    clean_from_comment(line) ;
	    switch (line_type) {
	      case UNKNOWN_LINE:
		parsing_error("line not recognized", line_begin, NULL) ;
		break ;
	      case VCLASS_LINE:
		parse_vclass_definition(line) ;
		break ;
	      case DEPRULE_LINE:
		parse_dependency_rule(line, name_list) ;
		break ;
	      case VARDEF_LINE:
		parse_variant_definition(line, name_list, NO_SECTION) ;
		break ;
	      case MACRO_LINE:
		parse_macro_definition(line, ' ', name_list) ;
		break ;
	      case ADDMAC_LINE:
		parse_macro_definition(line, '+', name_list) ;
		break ;
	      case SUBMAC_LINE:
		parse_macro_definition(line, '-', name_list) ;
		break ;
	      case VARMAC_LINE:
		parse_macro_definition(line, ':', name_list) ;
		break ;
	      case SELRULE_LINE:
		parse_selection_rule(line, name_list, NO_SECTION) ;
		break ;
	      case INCLUDE_LINE:
		parse_include_statement(line) ;
		break ;
	      case DOUBLEC_LINE:
		parsing_warning("double colon treated as single colon",
			      line, NULL) ;
		parse_dependency_rule(line, name_list) ;
		break ;
	      default:
		fatal("This can't happen", "invalid line type at top-level") ;
	    }
	}
    }

    return TRUE ;
}


/* parse functions for top-level items */

/* parse an include file */
static void parse_include_statement(line)
char *line ;
{
    char *includefilenames ;	/* rest of line after "include" */
    MFILE *save_mf ;		/* to save currentmf in  */
    jmp_buf save_jmpbuf ;	/* to save jmp_toplevel in */
    int retval ;		/* return value of parse_file() */
    struct stringlist *name_list ; /* names of include files to parse */
    struct stringlist *curname ; /* current include file to parse */
    char incpath[PATH_MAX];	/* construct include file name here */
    char *st_env ;		/* ST_ENV environment */

    line = skip_over_whitespace(line) ;
    includefilenames = expandmacro(line) ;
    name_list = collect_names(&includefilenames) ;

    if (name_list == NULL) {
	parsing_warning("name of include file(s) missing", line, line) ;
    } else {
    
	/* save global state */
	memcpy (save_jmpbuf, jmp_toplevel, sizeof(jmp_buf)) ;
	save_mf = currentmf ;

	/* iterate through name_list */
	for (curname = name_list; curname != NULL; curname = curname->next) {
	    retval = parse_file(curname->string) ;
	    if (retval == FALSE) {
		/* try to look for file in standard shape include path */
		if ((st_env = getenv(ST_ENV)) != 0) {
		    strcpy(incpath, st_env) ;
		    strcat(incpath, ST_ENV_SHAPELIB) ;
		    strcat(incpath, "/") ;
		    strcat(incpath, curname->string) ;

		    /* try again */
		    retval = parse_file(incpath) ;
		}
	    }
	    if (retval == FALSE) {
		currentmf = save_mf ;
		parsing_warning("could not open include file",
				line, curname->string) ;
	    }
	}
	free_stringlist(name_list) ;

	/* restore global state */
	currentmf = save_mf ;
	memcpy (jmp_toplevel, save_jmpbuf, sizeof(jmp_buf)) ;
    }
}
    

/* parse dependency rule. Target names are in name_list. */
static void parse_dependency_rule(line, name_list)
char *line ;
struct stringlist *name_list ;
{
    struct stringlist *targets = NULL ;
				/* targets of rule */
    struct stringlist *deps = NULL ;
				/* dependents of rule */
    struct stringlist *macros = NULL ;
				/* macros targets depend on */
    struct stringlist *cmds = NULL ;
				/* shell commands to build */
    
    /* names of targets are in name_list, *if* */
    if (name_list == NULL) {
	parsing_error("dependency rule must begin with target", line, NULL) ;
    }
    targets = name_list ;

    /* dependents are following */
    deps = collect_names(&line) ;
    
    /* line may now point to second ':', then collect macros
     * the target depends on */
    if (*line == ':') {
	line++ ;
	macros = collect_names(&line) ;
    }

    /* line must point to command, comment, or end of line now */
    switch (*line) {
      case ';':
	/* first command line */
	if (skip_over_whitespace(line + 1) != '\0') {
	    cmds = push_stringlist(line + 1, cmds, 0) ;
	}
	break ;
      case '#':			/* "virtual" end of line... */
      case '\0':		/* and the real one */
	break ;
      default:
	parsing_error("Syntax error in dependency rule", line, NULL) ;
    }
    
    /* collect command lines for shell script. The shell script ends
     * on a non-comment line that does not begin with a TAB. */
    while ((line = mf_nextline(currentmf)) != NULL) {
	char *first_nonblank = skip_over_whitespace(line) ;

	if (*line != '\t' && *first_nonblank != '#') {
	    break ;		/* end of shell script */
	}
	/* give only lines with real commands to the shell */
	if (*first_nonblank != '\0' && *first_nonblank != '#') {
	    cmds = push_stringlist(line, cmds, 0) ;
	}
    }

    /* make first non-command line next to be read if ! EOF */
    if (line != NULL) {
	mf_prevline(currentmf) ;
    }

    /* cmds are in still in reverse order! */
    cmds = revert_stringlist(cmds) ;

    queue_dependency_rule(targets, deps, macros, cmds) ;
}



/* collect the names (usually of targets or dependants) from a string.
 * Returned is a stringlist with the names in correct order.
 * Stops at the first character which is neither namecomponent nor
 * whitespace. The pointer linep points to is set to the position
 * where collect_names() stopped collecting.
 */
static struct stringlist *collect_names(linep)
char **linep ;			/* pointer to pointer to string */
{
    struct stringlist *names = NULL ; /* list of names */
    struct stringlist *last = NULL ; /* last member */
    struct stringlist *tmp ;

    while (strlen(*linep)) {	/* something is left */
	*linep = skip_over_whitespace(*linep) ;

	/* stop here if no name is found */
	if (is_no_namecomponent(**linep)) {
	    break ;
	}

	/* append name to list */
	tmp = (struct stringlist *) check_malloc(sizeof(struct stringlist)) ;
	if (last == NULL) {
	    last = names = tmp ;
	} else {
	    last->next = tmp ;
	    last = last->next ;
	}
	last->next = NULL ;
	last->string = get_name(linep) ;
	last->freeable = 1 ;
    }
    return names ;
}


/* parse selection rule */

static void parse_selection_rule(line, name_list, in_section)
char *line ;			/* line with head of rule */
struct stringlist *name_list ;	/* list with name of selection rule */
int in_section ;
{
    sb_ptr sb ;			/* buffer for selection predicates */
    int src_line_no;            /* pos. of sel.rule head in input file */
    char *src_filename_ptr;     /* pointer to name of cur. input file */
    char src_filename_buf[PATH_MAX];
    char *pretty_header;

    /* "pretty-print" selection rule header */
    {
      sb_ptr head_buf;
      struct stringlist *next_str;

      head_buf = check_sbnew (SB_LENGTH);
      for (next_str = name_list; next_str; next_str = next_str->next) {
	head_buf = check_sbcat (head_buf, next_str->string);
      }
      pretty_header = check_strdup (sbstr (head_buf));
      sbfree (head_buf);
    }
    
    /* 
     * consider header syntactically correct, if it either consists
     * of a single name, or a single name, followed by a parenthesized
     * parameter list. In the latter case, it will suffice, if the first
     * non-white-space character after the name is a '(', and the last 
     * character is a ')'.
     */
    {
      register char *open_paren = pretty_header, *close_paren;

      while (open_paren && *open_paren && (*open_paren != '(') &&
	     !isspace (*open_paren)) open_paren++;
      while (open_paren && *open_paren && isspace (*open_paren)) open_paren++;
      close_paren = open_paren;
      while (close_paren && *close_paren) close_paren++;
      if (close_paren && (*close_paren == '\0')
	  && (close_paren > pretty_header)) {
	close_paren--;
	while (*close_paren && isspace(*close_paren)) close_paren--;
      }
      if (open_paren && *open_paren && (*open_paren != '(')) {
	parsing_error ("rule name with optional argument list required",
		       line, NULL);
      }
      if (open_paren && *open_paren && close_paren && *close_paren && 
	  (*close_paren != ')')) {
	parsing_error ("rule name with optional argument list required",
		       line, NULL);
      }
    }
    mf_locate (line, &src_filename_ptr, &src_line_no);
    (void) strcpy (src_filename_buf, src_filename_ptr);

    /* check if text follows */
    line = skip_over_whitespace(line) ;
    if (*line != '\0') {
	parsing_error("newline expected after selection rule header",
		      line, NULL) ;
    }

    sb = check_sbnew(SB_LENGTH) ; /* prepare string buffer for rule body */

    /* collect rule body lines */
    while ((line = mf_nextline(currentmf)) != NULL) {
	if (in_section ?
	        rule_or_variant_body(line)
	      : *line == '\t') {
	    clean_selrule_from_comment(line) ;
	    sb = check_sbcat(sb, line) ;
	} else {
	    break ;
	}
    }

    /* ... and feed them to vbind. */
    queue_selection_rule(pretty_header, sbstr(sb), src_filename_buf,
			 src_line_no);

    free(name_list) ;
    free(sb) ;

    /* recover from look-ahead */
    if (line != NULL) {
	mf_prevline(currentmf) ;
    }
}


static int rule_or_variant_body(line)
char *line ;
{
    /* In a section there are three ytpes of lines:
         1. empty or comment-only line (possibly end of section)
	        --> return FALSE
	 2. body of rule or variant
	        --> return TRUE
         3. header of rule or variant, this is something like
	     "^[ \t]+[_0-9a-zA-Z]+[ \t]+:[ \t]+\(#.*\)*$"
	        --> return FALSE
	 4. vclass definition, beginning with "[ \t]vclass[ \t]"
	 	--> return FALSE
       The choice between 2. and 3. is difficult. I decide to return
       TRUE if the header pattern is not matched.
     */

    char *word_beginning ;	/* beginning of first word in line */

    line = skip_over_whitespace(line) ;
    if (*line == '\0' || *line == '#') {
				/* empty or comment-only */
	return FALSE ;
    }

    word_beginning = line ;
    while (*line != '\0' && strchr(RULE_OR_VAR_NAME_COMPONENTS, *line)) {
				/* skip over possible header */
	line++ ;
    }
    if (line - word_beginning == VCLASS_LENGTH &&
	(*line == '\t' || *line == ' ') &&
	!strncmp(word_beginning, VCLASS_STRING, VCLASS_LENGTH)) {
	/* is a vclass definition */
	return FALSE ;
    }

    line = skip_over_whitespace(line) ;
    if (*line != ':') {
	return TRUE ;		/* does not match header pattern */
    }

    /* if nothing but comment follows, header pattern is matched */
    line = skip_over_whitespace(line + 1) ;
    if (*line == '\0' || *line == '#') {
	return FALSE ;		/* matched */
    }

    return TRUE ;		/* not matched */
}
	



/* parse variant definition */

static void parse_variant_definition(line, name_list, in_section)
char *line ;			/* line with variant name */
struct stringlist *name_list ;	/* must contain only variant name */
int in_section ;
{
    char *variant_name ;
    struct stringlist *macro_list = NULL ;
				/* list of macros for variant */

    if (name_list == NULL || name_list->next != NULL) {
	parsing_error("exactly one variant name required", line, NULL) ;
    }

    /* check if text follows */
    line = skip_over_whitespace(line) ;
    if (*line != '\0') {
	parsing_error("newline expected after variant definition header",
		    line, NULL) ;
    }

    variant_name = name_list->string ;
    free(name_list) ;

    /* feed macro definitions to shape */
    while ((line = mf_nextline(currentmf)) != NULL) {
	if (in_section ?
	        rule_or_variant_body(line)
	      : *line == '\t') {
	    clean_from_comment(line) ;
	    macro_list = push_stringlist(skip_over_whitespace(line),
					 macro_list, 0) ;
	} else {
	    break ;
	}
    }
    queue_variant_definition(variant_name, macro_list) ;

    /* recover from look-ahead */
    if (line != NULL) {
	mf_prevline(currentmf) ;
    }
}


/* parse rule- or variant-section */

static void parse_rule_or_var_section(line)
char *line ;
{
    char *section_name ;	/* pointer to name of section */

				/* functions to parse the body of a
				 * rule resp. variant section */
    void (*body_parser)A((char *line)) ; /* pointer to hold the address
					  * of the appropriate routine */
    char *backup = line ;	/* line before current. Must have an initial
				 * value because it is given as argument to
				 * parsing_error() even if no line has been
				 * read here (in case of EOF) */
    jmp_buf save_jmpbuf ;	/* save state of top-level loop */

    

    line = skip_over_whitespace(line + 2) ;
    section_name = get_name(&line) ; /* get name of section */

    if (!strcmp(section_name, RULESEC_STRING)) {
	/* this is a rule section */
	body_parser = rule_section_body ;
    } else if (!strcmp(section_name, VARSEC_STRING)) {
	/* this is a variant section */
	body_parser = variant_section_body ;
    } else {
	return ;		/* must be comment */
    }

    /* a section has its own top-level, so it needs
     * an own recovery point for parsing_error() */
    memcpy (save_jmpbuf, jmp_toplevel, sizeof(jmp_buf)) ;
    setjmp(jmp_toplevel) ;

    /* section top-level loop */
    do {
	line = mf_nextline(currentmf) ;
	if (line == NULL) {	/* end of file reached */

	    /* restore global state */
	    memcpy (jmp_toplevel, save_jmpbuf, sizeof(jmp_buf)) ;
	    parsing_error("end of file in section", backup, section_name) ;
	}
	backup = line ;
	line = skip_over_whitespace(line) ;

	/* skip empty and comment lines */

	switch (*line) {
	  case '\0':
	  case '#':
	    break ;
	  case '\t':
	    parsing_error("line beginning with TAB at top-level", line,
			  section_name) ;
	    break ;
	  default:
	    (*body_parser)(line) ;
	}
    } while (!end_of_section(line, section_name)) ;
    free(section_name) ;
    
    /* restore global state */
    memcpy (jmp_toplevel, save_jmpbuf, sizeof(jmp_buf)) ;
}


/* parse a body part of a rule section */

static void rule_section_body(line)
char *line ;
{
    struct stringlist *name_list = NULL ;

    /* get name of rule */
    name_list = push_stringlist(get_name(&line), name_list, 1) ;

    line = skip_over_whitespace(line) ;
    if (*line != ':' || *skip_over_whitespace(++line) != '\0') {
	parsing_error("syntax error in selection rule header", line, NULL) ;
    }
    parse_selection_rule(line, name_list, IN_SECTION) ;
}


/* parse a body part of a variant section */

static void variant_section_body(line)
char *line ;
{
    char *name ;		/* variant name or "vclass" */
    struct stringlist *name_list ; /* to feed it to
				    * parse_variant_definition() */

    name = get_name(&line) ;	/* variant name or "vclass" */

    if (!strcmp(name, VCLASS_STRING)) {
	parse_vclass_definition(line) ;
    } else {
	name_list = push_stringlist(name, NULL, 1) ;
	line = skip_over_whitespace(line) ;
	if (*line != ':' || *skip_over_whitespace(++line) != '\0') {
	    parsing_error("syntax error in variant definition header",
			line, NULL) ;
	}
	
	parse_variant_definition(line, name_list, IN_SECTION) ;
    }
}


/* determine if line is the end line of section */

static int end_of_section(line, section)
char *line ;			/* line with possible "#% END-..." */
char *section ;			/* name of section as string */
{
    if (strncmp(line, "#%", 2)) {
	return FALSE ;		/* no "#%" at beginning */
    }

    line = skip_over_whitespace(line + 2) ;
    if (strncmp(line, ENDSEC_STRING, ENDSEC_LENGTH)) {
	return FALSE ;		/* no "END-" */
    }

    if (strncmp(line + 4, section, strlen(section))) {
	return FALSE ;		/* name of appropriate section missing */
    }

    line = skip_over_name(line) ;
    if (*line != '\0' && !isspace(*line)) {
	return FALSE ;		/* no whitespace after name of section */
    }

    line = skip_over_whitespace(line) ;
    if (*line != '\0') {
	parsing_warning("trailing text on end-section line ignored",
		      line, section) ;
    }

    return TRUE ;		/* it IS the end of the section (at last) */
}


/* parse_macro_definition(char *line, char type): parse macro definition */

static void parse_macro_definition(line, type, name_list)
char *line ;
char type ;
struct stringlist *name_list ;
{
  int mac_def_type = DYNAMIC;

  if (name_list == NULL || name_list->next != NULL) {
    parsing_error("exactly one macro name required", line, NULL) ;
  }
  
  switch (type) {
  case '-':
    parsing_error("macros with \"-=\" not yet supported", line, NULL) ;
    break ;
  case ':':
    mac_def_type = ONCE;
    break ;
  case '+':
    mac_def_type = APPEND;
    break ;
  case ' ':
    break ;
  }
  
  define_macro(name_list->string, line, mac_def_type) ;
  free_stringlist(name_list) ;
}


/* parse vclass definition. "vclass" has been eaten by recognize_line() */

static void parse_vclass_definition(line)
char *line ;
{
    char *name ;		/* name of vclass */
    struct stringlist *elems = NULL ; /* list of elements */

    name = get_name(&line) ;	/* get name of vclass */
    line = skip_over_whitespace(line) ;

    if (strncmp(line, VCLASS_SEPARATOR, VCLSEP_LENGTH)) { /* "::=" */
	parsing_error("Syntax error in vclass definition", line, NULL) ;
    }

    line += VCLSEP_LENGTH ;
    line = skip_over_whitespace(line) ;

    if (*line != '(') {
	parsing_error("\"(\" expected in vclass definition", line, NULL) ;
    }

    line++ ;
    /* get first element (at least one must be present) */
    elems = push_stringlist(get_variant_name(&line), elems, 1) ;
    line = skip_over_whitespace(line) ;

    /* continue until ')' is seen */
    while (*line != ')') {
	if (*line == ',') {
	    line = skip_over_whitespace(++line) ;
	    elems = push_stringlist(get_variant_name(&line), elems, 1) ;
	    line = skip_over_whitespace(line) ;
	} else {
	    parsing_error("syntax error in vclass definition", line,
			"\")\" or \",\" expected") ;
	}
    }
    /* feed definition to shape */
    queue_vclass_definition(name, elems) ;
    if (*skip_over_whitespace(++line)) {
	parsing_warning("trailing text after vclass definition ignored",
		      line, NULL) ;
    }
}



/* The following function is different from the other "skip_over_"s
 * because it is intended to throw away information. */
static void skip_over_rulebody()
{
    char *line ;		/* line to be read */

    /* This is only a bad guess. Since the bodies of selection
     * rule definitions in rule sections and of variant definitions
     * in variant sections need not begin with a tab, we may lose here.
     * This will result in lots of complaints about syntactically wrong
     * ruledef and vardef headers (which are no headers).
     * But to make this of somewhat general purpose, I decide
     * to make this tradeoff.
     * For all things outside sections this works ok.
     */
    while ((line = mf_nextline(currentmf)) != NULL
	   && *line == '\t') ;

    if (line != NULL) {
	mf_prevline(currentmf) ;
    }
}



/* parsing_error(char *message, char *ptr, char *option): give
 * error message, skip any lines beginning with tabs and
 * jump to recovery point. extern int parse_errors is incremented. */

void parsing_error(message, ptr, option)
char *message ;			/* error message */
char *ptr ;			/* pointer to error position */
char *option ;			/* additional message */
{
    char *fname ;		/* name of file error occured in */
    int lineno ;		/* number of line */

    parse_errors++ ;		/* increment number of detected errors */

    /* locate position of error */
    if (mf_locate(ptr, &fname, &lineno)) {
	fprintf(stderr, "%s:%d: ", fname, lineno) ;
    }

    /* give message */
    fprintf(stderr, "%s parse error: %s", stProgramName, message) ;
    if (option) {
	fprintf(stderr, " (%s)", option) ;
    }
    fprintf(stderr, "\n") ;

#ifdef DEBUG
    /* determine column where the error was detected */
    line = mf_thisline(currentmf, lineno) ;
    fprintf(stderr, "  Column %d\n", ptr - line) ; /* counts from 0 */
#endif

    /* throw away anything that is not top-level */
    skip_over_rulebody() ;

    /* jump to top-level loop */
    longjmp(jmp_toplevel, 1) ;
}


/* issue a warning */

void parsing_warning(message, ptr, option)
char *message ;			/* warning message */
char *ptr ;			/* pointer to place */
char *option ;			/* additional message */
{
    char *fname ;		/* name of file place is in */
    int lineno ;		/* number of line */

    /* locate place */
    if (mf_locate(ptr, &fname, &lineno)) {
	fprintf(stderr, "%s:%d: ", fname, lineno) ;
    }

    /* issue warning */
    fprintf(stderr, "%s parse warning: %s", stProgramName, message) ;
    if (option) {
	fprintf(stderr, " (%s)", option) ;
    }
    fprintf(stderr, "\n") ;
}


/* tries to recognize the type of a top-level line.
 * *name_list_p contains a list of names found before the token that
 * determines the line type.
 * *linep points to the first character *after* this token.
 * We don't need to recognize RULESEC_LINE or VARSEC_LINE, because
 * these would have been detected earlier in the top-level loop.
 */

static int recognize_line(linep, name_list_p)
char **linep ;
struct stringlist **name_list_p ;
{
    char *line = *linep ;	/* pointer into line */
    struct stringlist *name_list = NULL ; /* list of names read */
    sb_ptr sb ;			/* buffer to collect a name */
    char in_quote = 0 ;		/* if non-zero: current wuoting character */
    int paren_level = 0 ;	/* level of nested parentheses we're in */
    int line_type = UNKNOWN_LINE ; /* don't know yet... */
    
    /* "include" or "vclass" at beginning of line
     * overrides all other line types,
     * so we can return immediately */
    if (!strncmp(line, INCLUDE_STRING, INCLUDE_LENGTH)
	&& isspace(*(line + INCLUDE_LENGTH))) {

	*linep = line + INCLUDE_LENGTH ;
	line_type = INCLUDE_LINE ;
	*name_list_p = NULL ;
	return line_type ;
    } else if (!strncmp(line, VCLASS_STRING, VCLASS_LENGTH)
	       && isspace(*(line + VCLASS_LENGTH))) {
	*linep = line + VCLASS_LENGTH ; 
	line_type = VCLASS_LINE ;
	*name_list_p = NULL ;
	return line_type ;
    }

    /* In the other case we have to look at each character.
     * Each character that is not whitespace or part of the
     * magic token is saved in a string buffer. If whitespace is
     * found, the string buffer is pushed to the name list.
     *
     * This can *not* be done with get_name(), since some of the
     * characters in the magic tokens are valid name components.
     */

    sb = check_sbnew(SB_LENGTH) ; /* prepare string buffer */

    /* iterate over line while we don't know the type */
    while (*line && line_type == 0) {

	/* handle quoted strings */
	if (in_quote) {
	    if (*line == in_quote) {
		in_quote = 0 ;
	    } else {
		sb = check_sbaddc(sb, *line) ;
	    }
	} else {
	    switch (*line) {
	      case '(':
		paren_level++ ;
		sb = check_sbaddc(sb, *line) ;
		break ;
	      case ')':
		if (paren_level <= 0) {
		    parsing_error("unexpected close parentheses", line, NULL) ;
		}
		paren_level-- ;
		sb = check_sbaddc(sb, *line) ;
		break ;
	      case '\'':
	      case '\"':
	      case '`':
		in_quote = *line ;
		break ;
	      default:
		if (paren_level > 0) {
		    sb = check_sbaddc(sb, *line) ;
		} else {
		    switch (*line) {
		      case '#':
			*line = '\0' ;
			break ;
		      case '-':	/* perhaps -= macro */
			if (*(line + 1) == '=') {
			    line_type = SUBMAC_LINE ;
			    line++ ;
			} else {
			    sb = check_sbaddc(sb, *line) ;
			}
			break ;
		      case '+':	/* perhaps += macro */
			if (*(line + 1) == '=') {
			    line_type = ADDMAC_LINE ;
			    line++ ;
			} else {
			    sb = check_sbaddc(sb, *line) ;
			}
			break ;
		      case ':':	/* dependency rule, vclass definition,
				 * := macro, selection rule, or variant
				 * definition */
			switch (*(line + 1)) {
			  case ':': /* vclass or :: rule */
			    if (*(line + 2) == '=') {
				line_type = VCLASS_LINE ;
				line += 2 ;
			    } else {
				line_type = DOUBLEC_LINE ;
				line++ ;
			    }
			    break ;
			  case '=': /* := macro */
			    line_type = VARMAC_LINE ;
			    line++ ;
			    break ;
			  case '+': /* variant definition */
			    line_type = VARDEF_LINE ;
			    line ++ ;
			    break ;
			  case '-': /* selection rule */
			    line_type = SELRULE_LINE ;
			    line++ ;
			    break ;
			  default: /* dependency rule */
			    line_type = DEPRULE_LINE ;
			}
			break ;
		      case '=':	/* normal macro */
			line_type = MACRO_LINE ;
			break ;
		      case ' ':	/* ends a name */
		      case '\t':
			/* if a name is in the string buffer,
			 * save it to the name list */
			if (sblen(sb) > 0) { 
			    /* the string is not unquote()ed since the
			     * quotes are already removed */
			    name_list = push_stringlist(sbstr(sb),
							name_list, 1) ;
			    free(sb) ;
			    /* prepare new string buffer */
			    sb = check_sbnew(SB_LENGTH) ;
			}
			break ;
		      default:
			sb = check_sbaddc(sb, *line) ;
		    }
		}
	    }
	}
	line++ ;
    }

    /* save last name to name list */
    if (sblen(sb) > 0) {
	name_list = push_stringlist(sbstr(sb), name_list, 1) ;
	free(sb) ;
    } else {
	sbfree(sb) ;
    }

    /* name list is backwards yet */
    *name_list_p = revert_stringlist(name_list) ;
    *linep = line ;

    /* any open quotes? */
    if (in_quote) {
	char quote_char[2] ;

	quote_char[0] = in_quote ;
	quote_char[1] = '\0' ;
	free_stringlist(name_list) ;
	parsing_error("non-terminated quoting at end of line",
		      line, quote_char) ;
    }

    /* any open parentheses? */
    if (paren_level > 0) {
	parsing_error("missing close parentheses at end of line", line, NULL) ;
    }

    return line_type ;
}
		
    

/* This function differs from get_name in that commas and parentheses
 * are not allowed in variant names. */
static char *get_variant_name(linep)
char **linep ;
{
    char *name_end, *name_beg ;
    
    name_beg = skip_over_whitespace(*linep) ;
    name_end = skip_over_name_with_stop(name_beg, ",()") ;
    if (name_beg == name_end) {
	parsing_error("name or macro expected", *linep, NULL) ;
    }

    *linep = name_end ;
    return check_strndup(name_beg, name_end - name_beg) ;
}


/* clean trailing comments from a selection rule line
 * quting doesn't matter here, only parentheses
 */
static void clean_selrule_from_comment(line)
char *line ;
{
    int paren_level = 0 ;	/* number of nested () pairs */

    while (*line != '\0') {
	switch (*line) {
	  case '(':
	    paren_level++ ;
	    break ;
	  case ')':
	    paren_level-- ;
	    break ;
	  case '#':
	    if (paren_level <= 0) {
		*line = '\0' ;
		return ;
	    }
	    break ;
	  default: ;
	}
	line++ ;
    }
}

/* clean trailing comments from a line */
static void clean_from_comment(line)
char *line ;
{
    while (*line != '\0') {
	if (*line == '#') {
	    *line = '\0' ;
	    return ;
	}
	if (isspace(*line)) {
	    line = skip_over_whitespace(line) ;
	} else if (is_no_namecomponent(*line)) {
	    line++ ;
	} else {
	    line = skip_over_name(line) ;
	}
    }
}


/* EOF */
