/***********************************************************************
*                                                                      *
*               This software is part of the ast package               *
*           Copyright (c) 1984-2006 AT&T Knowledge Ventures            *
*                      and is licensed under the                       *
*                  Common Public License, Version 1.0                  *
*                      by AT&T Knowledge Ventures                      *
*                                                                      *
*                A copy of the License is available at                 *
*            http://www.opensource.org/licenses/cpl1.0.txt             *
*         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
*                                                                      *
*              Information and Software Systems Research               *
*                            AT&T Research                             *
*                           Florham Park NJ                            *
*                                                                      *
*                 Glenn Fowler <gsf@research.att.com>                  *
*                                                                      *
***********************************************************************/
#pragma prototyped
/*
 * Glenn Fowler
 * AT&T Research
 *
 * makefile lexical scanner and parser
 *
 * interleaved recursive parse() and expand() nesting supported
 *
 * NOTE: readline() from file does double copy to pp->ip and sp
 *	 it should be possible to go directly to sp
 */

#include "make.h"
#include "options.h"

#include <sig.h>

#define PARSEDEPTH	64		/* maximum parse stack depth	*/

#define PS1		"make> "	/* primary query prompt		*/
#define PS2		">>>>> "	/* secondary query prompt	*/

#define OP_ASSERT	(1<<0)		/* assertion statement		*/
#define OP_ASSIGN	(1<<1)		/* assignment statement		*/
#define OP_EMPTY	(1<<2)		/* no operator found		*/
#define OP_STATEMENT	((1<<3)-1)	/* statement type mask		*/

#define OP_ACTION	(1<<3)		/* operator takes action	*/
#define OP_APPEND	(1<<4)		/* append assignment		*/
#define OP_AUXILIARY	(1<<5)		/* auxiliary assignment		*/
#define OP_EXPAND	(1<<6)		/* expand lhs and rhs		*/
#define OP_STATE	(1<<7)		/* state assignment		*/

#define CON_kept	(1<<0)		/* already kept part of block	*/
#define CON_skip	(1<<1)		/* skip this part of block	*/
#define CON_stash	(1<<2)		/* file loop body lines stashed	*/

#define CON_if		(1<<3)		/* if block			*/
#define CON_elif	(1<<4)		/* not used in flags		*/
#define CON_else	(1<<5)		/* had else			*/
#define CON_end		(1<<6)		/* not used in flags		*/
#define CON_for		(1<<7)		/* for loop block		*/
#define CON_while	(1<<8)		/* while loop block		*/
#define CON_eval	(1<<9)		/* eval block			*/

#define CON_break	(1<<10)		/* loop break			*/
#define CON_continue	(2<<10)		/* loop continue		*/
#define CON_error	(3<<10)		/* error message output		*/
#define CON_exit	(4<<10)		/* exit				*/
#define CON_let		(5<<10)		/* let expression		*/
#define CON_local	(6<<10)		/* local var declaration	*/
#define CON_print	(7<<10)		/* standard output message	*/
#define CON_read	(8<<10)		/* read				*/
#define CON_return	(9<<10)		/* return			*/
#define CON_rules	(10<<10)	/* rules			*/
#define CON_set		(11<<10)	/* set options			*/

#define PUSHLOCAL(p)	do{for(p=pp->local;p;p=p->next)if(!(p->newv.property&V_scope))p->bucket->value=(char*)p->oldv;}while(0)
#define POPLOCAL(p)	do{for(p=pp->local;p;p=p->next)if(!(p->newv.property&V_scope))p->bucket->value=(char*)&p->newv;}while(0)

#define freelocal(x)	do{x->next=freelocals;freelocals=x;}while(0)
#define newlocal(x)	do{if(x=freelocals)freelocals=x->next;else x=newof(0,Local_t,1,0);}while(0)

struct Local_s; typedef struct Local_s Local_t;

struct Local_s				/* local variable state		*/
{
	Hash_bucket_t*	bucket;		/* table.var hash bucket	*/
	Var_t*		oldv;		/* old variable value		*/
	Var_t		newv;		/* new variable info		*/
	Local_t*	next;		/* next in list			*/
	int		line;		/* declaration line number	*/
};

typedef struct Control_s		/* flow control block stack	*/
{
	int		flags;		/* block flags			*/
	union
	{
	char*		buffer;		/* loop body buffer		*/
	size_t		offset;		/* loop body ip offset		*/
	}		body;
	int		line;		/* loop line number		*/
	union
	{
	struct
	{
	char*		test;		/* while loop test		*/
	int		free;		/* free test			*/
	}		w;
	struct
	{
	Var_t*		var;		/* for loop variable		*/
	char**		args;		/* for loop arg vector		*/
	Sfio_t*		tmp;		/* for loop arg tmp		*/
	Sfio_t*		vec;		/* for loop args tmp		*/
	}		f;
	}		loop;
} Control_t;

typedef struct Parseinfo_s		/* recursive parse state stack	*/
{
	Control_t*	cp;		/* control block pointer	*/
	Control_t*	block;		/* control block lo water	*/

	/* the rest are implicitly initialized */

	int		argc;		/* local argc			*/
	char*		name;		/* current level input name	*/
	char*		here;		/* <<? termination string	*/
	Sfio_t*		fp;		/* input file pointer		*/
	Sfio_t*		ip;		/* readline write string	*/
	Sfio_t*		scoped;		/* scoped options/assignments	*/
	char*		bp;		/* input buffer pointer		*/
	char*		stashget;	/* loop body stash get		*/
	char*		pushback;	/* line pushback pointer	*/
	Local_t*	local;		/* local variables		*/
	int		checkhere;	/* <<? offset			*/
	int		line;		/* prev level input line number	*/
	int		splice;		/* splice line			*/
	short		indent;		/* active indentation level	*/
	unsigned char	eval;		/* eval block level		*/
	unsigned char	status;		/* action return status		*/
	unsigned int	newline:1;	/* \n at *bp replaced by 0	*/
	unsigned int	prompt:1;	/* interactive input with prompt*/
	unsigned int	stashput:1;	/* put lines in stash		*/
} Parseinfo_t;

/*
 * WARNING: getline() uses the first keyword char
 */

static Namval_t		controls[] =	/* control keywords		*/
{
	"break",	CON_break,
	"continue",	CON_continue,
	"elif",		CON_elif,
	"else",		CON_else,
	"end",		CON_end,
	"error",	CON_error,
	"eval",		CON_eval,
	"exit",		CON_exit,
	"for",		CON_for,
	"if",		CON_if,
	"let",		CON_let,
	"local",	CON_local,
	"print",	CON_print,
	"read",		CON_read,
	"return",	CON_return,
	"rules",	CON_rules,
	"set",		CON_set,
	"while",	CON_while,
};

static Control_t	constack[PARSEDEPTH * 3];

static Parseinfo_t	parsestack[PARSEDEPTH] = { &constack[0], &constack[0] };
static Parseinfo_t*	pp = &parsestack[0];

static Local_t*		freelocals;

/*
 * unwind the parse stack to level on errors
 */

void
unparse(int level)
{
	register Local_t*	lcl;
	register Local_t*	olcl;
	register Control_t*	cp;
	register Rule_t*	r;

	if (pp >= &parsestack[elementsof(parsestack)])
		pp = &parsestack[elementsof(parsestack) - 1];
	while (pp > &parsestack[level])
	{
		if ((r = getrule(pp->name)) && r->active)
		{
			r->active = r->active->previous;
			if (r->status == UPDATE)
				r->status = EXISTS;
		}
		if (pp->fp)
		{
			if (pp->fp != sfstdin)
				sfclose(pp->fp);
			sfstrclose(pp->ip);
		}
		if (lcl = pp->local)
		{
			pp->local = 0;
			while (lcl)
			{
				lcl->bucket->value = (char*)lcl->oldv;
				olcl = lcl;
				lcl = lcl->next;
				freelocal(olcl);
			}
		}
		for (cp = pp->cp; cp > pp->block; cp--)
			if (cp->flags & CON_for)
			{
				if (cp->loop.f.vec)
				{
					sfstrclose(cp->loop.f.vec);
					sfstrclose(cp->loop.f.tmp);
				}
			}
			else if (cp->flags & CON_while)
			{
				if (pp->cp->loop.w.free)
					free(pp->cp->loop.w.test);
			}
		pp--;
	}
}

/*
 * declare a single local variable t
 * redefinitions on same line>0 get value 1
 * if (flags & V_append) then old value is retained
 * if (flags & V_scope) then value survives PUSHLOCAL/POPLOCAL
 */

static void
declare(char* t, int line, long flags)
{
	register Var_t*		v;
	register Local_t*	p;
	register char*		s;
	register char*		d;

	if (d = strchr(t, '='))
		*d = 0;
	if (!(v = getvar(t)))
	{
		for (s = t; *s; s++)
			if (istype(*s, C_TERMINAL))
				error(3, "%s: invalid local variable name", t);
		v = setvar(t, null, 0);
	}
	else if (!(flags & V_scope))
	{
		/*
		 * check for duplicate declarations
		 */

		for (p = pp->local; p; p = p->next)
			if (v->name == p->newv.name)
			{
				if (d)
					*d++ = '=';
				else if (line && p->line == line)
					d = "1";
				else
					d = null;
				p->line = line;
				setvar(v->name, d, 0);
				return;
			}
	}
	newlocal(p);
	p->next = pp->local;
	pp->local = p;
	p->oldv = v;
	p->newv.name = v->name;
	p->newv.property &= ~(V_readonly|V_scope);
	p->newv.property |= (v->property & V_auxiliary) | (flags & V_scope);
	if (!p->newv.value || (p->newv.property & V_import))
	{
		p->newv.value = newof(0, char, MINVALUE + 1, 0);
		p->newv.length = MINVALUE;
		p->newv.property &= ~V_import;
		p->newv.property |= V_free;
	}
	p->bucket = hashlast(table.var);
	p->bucket->value = (char*)&p->newv;
	p->line = line;
	if (d)
	{
		*d++ = '=';
		setvar(v->name, d, 0);
	}
	else if (flags & V_append)
		setvar(v->name, v->value, 0);
	else
		*p->newv.value = 0;
}

/*
 * declare local variables using tmp string xp
 * s is first expanded
 * 2 funtion argument styles supported
 *	(formal ...) actual ...
 *	-[n] actual
 */

static void
local(Sfio_t* xp, char* v)
{
	register char*	t;
	register char*	a;
	long		top;
	int		argn;
	Control_t*	cp;
	Local_t*	p;
	int		argc = 0;
	int		optional = 0;
	char*		argv = 0;
	char*		formal = 0;
	Sfio_t*		ap = 0;

	for (cp = pp->cp; cp > pp->block; cp--)
		if (cp->flags & (CON_for|CON_while))
		{
			for (p = pp->local; p; p = p->next)
				p->line = 0;
			break;
		}
	if (t = strchr(v, '\n'))
		*t = 0;
	top = sfstrtell(xp);
	expand(xp, v);
	sfputc(xp, 0);
	v = sfstrseek(xp, top, SEEK_SET);
	if (t)
		*t = '\n';
	while (t = getarg(&v, NiL))
	{
		if (!ap && *t == '-')
		{
			ap = sfstropen();
			if (!argc)
				argc = 1;
			argn = strtol(t + 1, NiL, 0);
		}
		else if (!ap && *t == '(')
		{
			ap = sfstropen();
			argv = t + strlen(t) - 1;
			if (*argv != ')')
				error(3, "%s: missing ) in formal argument list", t);
			*argv = 0;
			argv = t + 1;
			argc = 1;
			argn = 0;
			formal = getarg(&argv, NiL);
		}
		else
		{
			if (argv)
			{
				if (!formal)
					error(3, "%s: only %d actual argument%s expected", t, argc - 1, argc == 2 ? null : "s");
				if ((a = getarg(&argv, NiL)) && !optional && streq(a, "..."))
				{
					optional = 1;
					if (*v)
						*(v - 1) = ' ';
					if (!(a = getarg(&argv, NiL)))
						v = null;
				}
				argc++;
				sfprintf(ap, "%s=%s", formal, t);
				t = sfstruse(ap);
				formal = a;
			}
			else if (argc)
			{
				sfprintf(ap, "%d=%s", argc++, t);
				t = sfstruse(ap);
			}
			declare(t, error_info.line, 0);
		}
	}
	if (argc)
	{
		if (formal)
		{
			a = getarg(&argv, NiL);
			if (!optional && (!a || !streq(a, "...")))
				error(3, "%s: actual argument expected", formal, argc - 1);
			formal = a;
			while (formal)
			{
				declare(formal, error_info.line, 0);
				formal = getarg(&argv, NiL);
			}
		}
		pp->argc = argc - 1;
		while (argc <= argn)
		{
			sfprintf(ap, "%d", argc++);
			declare(sfstruse(ap), error_info.line, 0);
		}
		sfstrclose(ap);
	}
}

/*
 * copy the local argc into internal.val
 */

void
argcount(void)
{
	sfprintf(internal.val, "%d", pp->argc);
}

/*
 * compute next for loop iteration variable
 */

static int
iterate(void)
{
	register char*		p;
	register Var_t*		v = pp->cp->loop.f.var;

	if (!(p = *pp->cp->loop.f.args++))
		return 0;

	/*
	 * NOTE: this may bypass checks in setvar()
	 */

	if (!(v->property & V_import) && v->value && strlen(p) <= v->length)
		strcpy(v->value, p);
	else
	{
		if (v->property & V_free)
		{
			v->property &= ~V_free;
			free(v->value);
		}
		v->value = p;
		v->property |= V_import;
	}
	debug((-6, "assignment: lhs=`%s' rhs=`%s'", v->name, v->value));
	return 1;
}

/*
 * check for pp directives
 */

static void
directive(register char* s)
{
	register char*	t;
	register int	n;
	Rule_t*		r;
	Stat_t		st;

	static Sfio_t*	file;

	while (isspace(*++s));
	if (isdigit(*s))
	{
		n = 0;
		while (isdigit(*s))
			n = n * 10 + *s++ - '0';
		error_info.line = n - 1;
		while (isspace(*s))
			s++;
		if (*s++ == '"')
		{
			if (!file)
				file = sfstropen();
			while (*s && *s != '"')
				sfputc(file, *s++);
			t = sfstruse(file);
			if (*s == '"')
			{
				if (!*t)
					error_info.file = pp->name;
				else
				{
					pathcanon(error_info.file = t, 0);
					state.init++;
					r = makerule(t);
					state.init--;
					if (!r->time && !(r->property & P_dontcare))
					{
						if (stat(t, &st))
							r->property |= P_dontcare;
						else
						{
							r->time = tmxgetmtime(&st);
							compref(r, COMP_INCLUDE);
						}
					}
				}
			}
		}
	}
	else
	{
		for (t = s; *s && !isspace(*s); s++);
		if (*s)
			*s++ = 0;
		if (streq(t, "rules"))
			rules(s);
		else if (!state.preprocess && strmatch(t, "assert|define|elif|else|endif|endmac|error|if|ifdef|ifndef|include|line|macdef|pragma|unassert|undef|warning"))
		{
			state.preprocess = 1;
			punt(0);
		}
	}
}

/*
 * read line from file or buffer
 *
 * `\<newline>' splices the current and next lead line
 * `#...<newline>' comments (preceded by space) are stripped from files
 *
 * return value for file input placed in pp->ip
 */

static char*
readline(int lead)
{
	register char*	s;
	register char*	t;
	register char*	f;
	register int	c;
	register int	n;
	register int	q;
	Rule_t*		r;
	int		start;
	int		here;
	int		m;
	long		line;
	Sfio_t*		sps[2];

	trap();
	if (s = pp->pushback)
	{
		pp->pushback = 0;
		if (pp->fp)
		{
			if (lead > 0 && (t = sfstrseek(pp->ip, 0, SEEK_CUR) - 1) > s && *(t - 1) == '\\')
			{
				sfstrseek(pp->ip, -2, SEEK_CUR);
				line = s - sfstrbase(pp->ip);
				goto pushback;
			}
			return s;
		}
		else if (t = pp->bp = strchr(s, '\n'))
		{
			if (lead > 0 && t > s && *(t - 1) == '\\')
				*(t - 1) = *t = ' ';
			else
			{
				pp->newline = 1;
				*pp->bp = 0;
				return s;
			}
		}
		else
			return s;
	}
	else
		s = pp->bp;
	if (pp->fp)
	{
		if (s = pp->stashget)
		{
			*s++ = '\n';
			if (t = strchr(s, '\n'))
				*(pp->stashget = t) = 0;
			else
				pp->stashget = 0;
			return s;
		}
		if (pp->stashput)
		{
			sfstrseek(pp->ip, -1, SEEK_CUR);
			sfputc(pp->ip, '\n');
		}
		else
			sfstrseek(pp->ip, 0, SEEK_SET);
		line = sfstrtell(pp->ip);
	pushback:
		do
		{
			trap();
			if (r = getrule(external.makeprompt))
				maketop(r, P_dontcare|P_foreground, NiL);
			if (pp->prompt)
				error(ERROR_PROMPT, lead > 0 && pp->cp == pp->block ? PS1 : PS2);
			if (pp->fp != sfstdin || !state.coshell || !state.coshell->outstanding)
				break;
			sps[0] = pp->fp;
			sps[1] = state.coshell->msgfp;
			if ((n = sfpoll(sps, 2, -1)) <= 0)
				continue;
			if (sps[0] == state.coshell->msgfp || n > 1 && sps[1] == state.coshell->msgfp)
				while (block(1));
		} while (sps[0] != pp->fp && (n <= 1 || sps[1] != pp->fp));
		here = pp->checkhere = 0;
		n = q = 0;
		for (;;)
		{
			switch (c = sfgetc(pp->fp))
			{
			case EOF:
			eof:
				if (q == COMMENT)
					error(lead > 0 ? 2 : 1, "EOF in %c comment", q);
				else if (q)
					error(lead > 0 ? 2 : 1, "EOF in %c...%c quote starting at line %d", q, q, start);
				else if (sfstrtell(pp->ip) > line)
					error(lead > 0 ? 2 : 1, "file does not end with newline");
				if (sfstrtell(pp->ip) > line)
				{
					sfputc(pp->ip, 0);
					return sfstrbase(pp->ip) + line;
				}
				return 0;
			case '\r':
				if ((c = sfgetc(pp->fp)) != '\n')
				{
					if (c != EOF)
						sfungetc(pp->fp, c);
					c = '\r';
					break;
				}
				/*FALLTHROUGH*/
			case '\n':
			newline:
				error_info.line++;
				if (!q || q == COMMENT)
				{
					t = sfstrseek(pp->ip, 0, SEEK_CUR);
					s = sfstrbase(pp->ip) + line;
					while (t > s && (*(t - 1) == ' ' || *(t - 1) == '\t'))
						t--;
					sfstrseek(pp->ip, t - sfstrbase(pp->ip), SEEK_SET);
					sfputc(pp->ip, 0);
					s = sfstrbase(pp->ip) + line;
					if (*s == COMMENT)
					{
						directive(s);
						*s = 0;
						sfstrseek(pp->ip, line + 1, SEEK_SET);
					}
					return s;
				}
				if (pp->prompt)
					error(ERROR_PROMPT, PS2);
				break;
			case '\\':
				switch (c = sfgetc(pp->fp))
				{
				case EOF:
					sfputc(pp->ip, '\\');
					goto eof;
				case '\r':
					if ((c = sfgetc(pp->fp)) != '\n')
					{
						if (c != EOF)
							sfungetc(pp->fp, c);
						sfputc(pp->ip, '\\');
						c = '\r';
						break;
					}
					/*FALLTHROUGH*/
				case '\n':
					if (lead > 0)
					{
						error_info.line++;
						if (pp->prompt)
							error(ERROR_PROMPT, PS2);
						continue;
					}
					pp->splice = error_info.line + 2;
					sfungetc(pp->fp, '\n');
					c = '\\';
					break;
				default:
					sfputc(pp->ip, '\\');
					break;
				}
				break;
			case '"':
			case '\'':
				if (c == q)
					q = 0;
				else if (!q)
				{
					q = c;
					start = error_info.line;
				}
				break;
			case '/':
				if (!q && (sfstrtell(pp->ip) == line || isspace(*(sfstrseek(pp->ip, 0, SEEK_CUR) - 1))))
					switch (c = sfgetc(pp->fp))
					{
					case EOF:
						sfputc(pp->ip, '/');
						goto eof;
					case '*':
						start = ++error_info.line;
					mid:
						for (;;) switch (sfgetc(pp->fp))
						{
						case EOF:
						comeof:
							error(2, "EOF in /*...*/ comment starting at line %d", start);
							goto end;
						case '\n':
							error_info.line++;
							sfputc(pp->ip, '\n');
							if (pp->prompt)
								error(ERROR_PROMPT, PS2);
							break;
						case '/':
							for (;;) switch (c = sfgetc(pp->fp))
							{
							case EOF:
								goto comeof;
							case '*':
								if (error_info.line > start)
									error(2, "/* in /*...*/ comment starting at line %d", start);
								break;
							case '/':
								break;
							default:
								sfungetc(pp->fp, c);
								goto mid;
							}
						case '*':
							for (;;) switch (c = sfgetc(pp->fp))
							{
							case EOF:
								goto comeof;
							case '*':
								break;
							case '/':
								goto end;
							default:
								sfungetc(pp->fp, c);
								goto mid;
							}
						}
					end:
						error_info.line--;
						c = ' ';
						break;
					default:
						sfungetc(pp->fp, c);
						c = '/';
						break;
					}
				break;
			case COMMENT:
				if (!q && sfstrtell(pp->ip) > line && isspace(*(sfstrseek(pp->ip, 0, SEEK_CUR) - 1)))
					for (q = c;;)
						switch (sfgetc(pp->fp))
						{
						case EOF:
							goto eof;
						case '\\':
							switch (sfgetc(pp->fp))
							{
							case EOF:
								goto eof;
							case '\\':
								sfungetc(pp->fp, '\\');
								break;
							case '\n':
								if (!lead)
									goto newline;
								error_info.line++;
								sfputc(pp->ip, '\n');
								if (pp->prompt)
									error(ERROR_PROMPT, PS2);
								break;
							}
							continue;
						case '\n':
							goto newline;
						}
				break;
			case '(':
			case '[':
			case '{':
				if (!q)
					n++;
				break;
			case ')':
			case ']':
			case '}':
				if (!q && n)
					n--;
				break;
			case '<':
				if (!q && !n && !pp->checkhere && (m = sfstrtell(pp->ip) - line))
				{
					if (m == here)
						pp->checkhere = 1;
					else
						here = m + 1;
				}
				break;
			}
			sfputc(pp->ip, c);
		}
	}
	else if (s)
	{
		error_info.line++;
		if (pp->newline)
		{
			pp->newline = 0;
			*s++ = '\n';
		}
		if (f = strchr(s, '\n'))
		{
			if (lead > 0 && f > s && *(f - 1) == '\\')
			{
				/*
				 * NOTE: `\\n' are permanently eliminated *each pass*
				 *	 line counts are preserved by adding newlines
				 *	 after sliding the spliced segment(s)
				 */

				n = 1;
				t = f - 1;
				for (;;)
				{
					if (!(c = *++f))
					{
						f = 0;
						*t = 0;
						break;
					}
					else if (c != '\n')
						*t++ = c;
					else if (*(f - 1) != '\\')
					{
						f = t + 1;
						while (n--)
						{
							*t++ = ' ';
							*t++ = '\n';
						}
						break;
					}
					else
					{
						n++;
						t--;
					}
				}
			}
		}
		if (pp->bp = f)
		{
			*f = 0;
			pp->newline = 1;
		}
		return s;
	}
	else
		return 0;
}

/*
 * structured flow control line input into sp
 *
 * lead identifies a lead line (as opposed to an action line)
 * term is the line terminator char placed in sp
 * 0 return on EOF
 */

static int
getline(Sfio_t* sp, int lead, int term)
{
	register int	c;
	register char*	s;
	register char*	t;
	register int	indent;
	Time_t		tm;
	long		n;
	int		i;
	char*		e;
	char*		lin;
	char*		tok;
	Control_t*	cp;
	Namval_t*	nv;

	if (pp->here && !lead)
	{
		t = pp->here;
		pp->here = 0;
		while ((s = sfgetr(pp->fp, '\n', 1)) && !streq(s, t))
			sfputr(sp, s, term);
		if (!s)
			error(3, "here document terminator \"%s\" not found", t);
		free(t);
		return sfstrtell(sp) != 0;
	}
 again:
	while (s = lin = readline(lead))
	{
		indent = 0;
		if (lead > 0 && !(pp->cp->flags & CON_skip))
			pp->indent = SHRT_MAX;
		while (indent <= pp->indent)
		{
			if (*s == '\t')
			{
				s++;
				indent += state.tabstops - indent % state.tabstops;
			}
			else if (*s == ' ')
			{
				static int	warned;

				for (t = s; isspace(*t); t++);
				if (!*t)
					goto again;
				if (!warned)
				{
					error(1, "<space> indentation may be non-portable");
					warned = 1;
				}
				while (*s == ' ')
				{
					s++;
					indent++;
				}
			}
			else
				break;
		}
		if (lead > 0 && indent <= pp->indent && (c = *s))
		{
			pp->indent = indent;
			if (c == 'b' || c == 'c' || c == 'e' || c == 'f' || c == 'i' || c == 'l' || c == 'p' || c == 'r' || c == 's' || c == 'w')
			{
				for (t = s; istype(*t, C_VARIABLE2); t++);
				if (istype(c = *t, C_SEP))
				{
					*t = 0;
					i = (nv = (Namval_t*)strsearch(controls, elementsof(controls), sizeof(*controls), stracmp, s, NiL)) ? nv->value : 0;
					for (*t = c; isspace(*t); t++);
					c = pp->cp->flags;
					/*UNDENT*/
	if (i)
	{
		debug((-7, "%s:%d:test: `%s'", error_info.file, error_info.line, s));
		switch (i)
		{

		case CON_if:
			if (++pp->cp >= &constack[elementsof(constack)])
				error(3, "if nesting too deep");
			pp->cp->flags = CON_if;
			if (c & CON_skip)
				pp->cp->flags |= CON_kept | CON_skip;
			else if (expr(sp, t))
				pp->cp->flags |= CON_kept;
			else
				pp->cp->flags |= CON_skip;
			continue;

		case CON_else:
			if (!(c & CON_if))
				error(3, "no matching if for else");
			if (*t != 'i' || *(t + 1) != 'f' || !isspace(*(t + 2)))
			{
				if (c & CON_else)
					error(1, "only one else per if");
				if (*t)
					error(1, "%s: tokens after else ignored", t);
				pp->cp->flags |= CON_else;
				if (c & CON_kept)
					pp->cp->flags |= CON_skip;
				else
				{
					pp->cp->flags |= CON_kept;
					pp->cp->flags &= ~CON_skip;
				}
				continue;
			}
			t++;
			while (isspace(*++t));
			/*FALLTHROUGH*/

		case CON_elif:
			if (!(c & CON_if))
				error(3, "no matching if for elif");
			if (c & CON_else)
				error(1, "elif after else");
			if ((c & CON_kept) || !expr(sp, t))
				pp->cp->flags |= CON_skip;
			else
			{
				pp->cp->flags |= CON_kept;
				pp->cp->flags &= ~CON_skip;
			}
			continue;

		case CON_end:
			if (pp->cp <= pp->block)
				error(3, "unmatched end");
			if (*t)
				error(1, "%s: tokens after end ignored", t);
			if (c & CON_eval)
				pp->eval--;
			else if (!(c & CON_skip) || (c & (CON_for | CON_while)) && !(c & CON_kept))
			{
				pp->cp->flags &= ~(CON_kept | CON_skip);
				if ((c & CON_while) && expr(sp, pp->cp->loop.w.test) || (c & CON_for) && iterate())
				{
					if (pp->newline)
					{
						pp->newline = 0;
						*pp->bp = '\n';
					}
					if (pp->fp)
					{
						if (pp->stashget)
							*pp->stashget = '\n';
						pp->stashget = sfstrbase(pp->ip) + pp->cp->body.offset;
					}
					else
						pp->bp = pp->cp->body.buffer;
					error_info.line = pp->cp->line;
					continue;
				}
			}
			if (c & CON_for)
			{
				if (pp->cp->loop.f.vec)
				{
					sfstrclose(pp->cp->loop.f.vec);
					sfstrclose(pp->cp->loop.f.tmp);
				}
			}
			if (c & CON_while)
			{
				if (pp->cp->loop.w.free)
					free(pp->cp->loop.w.test);
			}
			if (c & CON_stash)
				pp->stashput = 0;
			pp->cp--;
			continue;

		case CON_for:
			if (++pp->cp >= &constack[elementsof(constack)])
				error(3, "for nesting too deep");
			pp->cp->flags = CON_for;
			pp->cp->loop.f.vec = 0;
			if (c & CON_skip)
				pp->cp->flags |= CON_kept | CON_skip;
			else
			{
				Sfio_t*	tp;

				pp->cp->loop.f.tmp = tp = sfstropen();
				expand(tp, t);
				tok = sfstruse(tp);
				if (!(s = getarg(&tok, NiL)))
					error(3, "for loop variable omitted");
				pp->cp->loop.f.vec = tp = sfstropen();
				do putptr(tp, t = getarg(&tok, NiL)); while (t);
				if (*(pp->cp->loop.f.args = (char**)sfstrseek(tp, 0, SEEK_SET)))
				{
					pp->cp->loop.f.var = setvar(s, null, 0);
					if (!iterate())
						pp->cp->flags |= CON_kept | CON_skip;
					else
					{
						if (pp->fp)
						{
							if (!pp->stashput)
							{
								pp->stashput = 1;
								pp->cp->flags |= CON_stash;
								sfstrseek(pp->ip, 0, SEEK_SET);
								sfputc(pp->ip, 0);
							}
							pp->cp->body.offset = pp->stashget ? (pp->stashget - sfstrbase(pp->ip)) : (sfstrtell(pp->ip) - 1);
						}
						else if (!pp->bp)
							error(3, "for loop body expected");
						else
							pp->cp->body.buffer = pp->bp + 1;
						pp->cp->line = error_info.line;
					}
				}
				else
					pp->cp->flags |= CON_kept | CON_skip;
			}
			continue;

		case CON_while:
			if (++pp->cp >= &constack[elementsof(constack)])
				error(3, "while nesting too deep");
			pp->cp->flags = CON_while;
			pp->cp->loop.w.free = 0;
			if ((c & CON_skip) || !expr(sp, t))
				pp->cp->flags |= CON_kept | CON_skip;
			else
			{
				pp->cp->loop.w.test = t;
				if (pp->fp)
				{
					if (!pp->stashput)
					{
						pp->stashput = 1;
						pp->cp->flags |= CON_stash;
						sfstrseek(pp->ip, 0, SEEK_SET);
						sfputc(pp->ip, 0);
						pp->cp->loop.w.test = strdup(t);
						pp->cp->loop.w.free = 1;
					}
					pp->cp->body.offset = pp->stashget ? (pp->stashget - sfstrbase(pp->ip)) : (sfstrtell(pp->ip) - 1);
				}
				else if (!pp->bp)
					error(3, "while loop body expected");
				else
					pp->cp->body.buffer = pp->bp + 1;
				pp->cp->line = error_info.line;
			}
			continue;

		case CON_break:
		case CON_continue:
			if (!(pp->cp->flags & CON_skip))
			{
				c = *t ? expr(sp, t) : 1;
				for (cp = pp->cp; cp > pp->block; cp--)
				{
					if (cp <= pp->block)
						error(3, "%s outside of loop", s);
					cp->flags |= CON_skip | CON_kept;
					if ((cp->flags & (CON_for | CON_while)) && --c <= 0)
					{
						if (i == CON_continue)
							cp->flags &= ~CON_kept;
						break;
					}
				}
			}
			continue;

		case CON_return:
			if (!(pp->cp->flags & CON_skip))
			{
				if (state.frame->target->property & P_functional)
				{
					n = sfstrtell(sp);
					expand(sp, t);
					s = sfstrbase(sp) + n;
					t = sfstrseek(sp, 0, SEEK_CUR);
					while (t > s && isspace(*(t - 1)))
						t--;
					sfstrseek(sp, t - sfstrbase(sp), SEEK_SET);
					sfputc(sp, 0);
					setvar(state.frame->target->name, sfstrseek(sp, n, SEEK_SET), 0);
					debug((-5, "%s returns `%s'", state.frame->target->name, sfstrseek(sp, 0, SEEK_CUR)));
				}
				else if (*t)
				{
					if ((tm = timenum(t, &e)) != TMX_NOTIME && *e)
					{
						if ((n = expr(sp, t)) == -1)
							tm = TMX_NOTIME;
						else
							tm = tmxsns(n, 0);
					}
					if (tm == TMX_NOTIME)
					{
						pp->status = FAILED;
						debug((-5, "return fail"));
					}
					else if (tm)
					{
						pp->status = TOUCH;
						internal.internal->time = tm;
						debug((-5, "return [%s]", timestr(internal.internal->time)));
					}
					else
					{
						pp->status = EXISTS;
						debug((-5, "return no update"));
					}
				}
				else
					debug((-5, "return normal"));
				for (cp = pp->cp; cp >= pp->block; cp--)
					cp->flags |= CON_skip | CON_kept;
			}
			continue;

		case CON_eval:
			if (++pp->cp >= &constack[elementsof(constack)])
				error(3, "eval nesting too deep");
			pp->cp->flags = CON_eval | (c & (CON_kept | CON_skip));
			pp->eval++;
			continue;

		case CON_rules:
			if (!(pp->cp->flags & CON_skip))
				rules(t);
			continue;

		case CON_let:
			if (!(pp->cp->flags & CON_skip))
				expr(sp, t);
			continue;

		case CON_local:
			if (!(pp->cp->flags & CON_skip))
				local(sp, t);
			continue;

		case CON_error:
			if (!(pp->cp->flags & CON_skip))
			{
				n = sfstrtell(sp);
				expand(sp, t);
				sfputc(sp, 0);
				i = strtol(sfstrseek(sp, n, SEEK_SET), &tok, 0);
				for (t = tok; isspace(*t); t++);
				c = error_info.line;
				if (i > 0 && !(i & 040))
					error_info.line = 0;
				error(i, "%s", t);
				error_info.line = c;
			}
			continue;

		case CON_exit:
			if (!(pp->cp->flags & CON_skip))
			{
				n = sfstrtell(sp);
				expand(sp, t);
				sfputc(sp, 0);
				i = strtol(sfstrseek(sp, n, SEEK_SET), &tok, 0);
				finish(i);
			}
			continue;

		case CON_print:
		case CON_read:
			if (!(pp->cp->flags & CON_skip))
			{
				int		d;
				int		m;
				int		x;
				char*		a;
				char*		f;

				n = sfstrtell(sp);
				expand(sp, t);
				sfputc(sp, 0);
				a = sfstrseek(sp, n, SEEK_SET);
				d = i == CON_print;
				f = 0;
				n = '\n';
				/*UNDENT...*/

	for (;;)
	{
		for (t = a; *t == ' '; t++);
		if ((x = *t) != '-' && x != '+')
			break;
		a = t;
		t = getarg(&a, NiL) + 1;
		if (*(t - 1) == *t && !*(t + 1))
		{
			for (t = a; *t == ' '; t++);
			break;
		}
		for (;;)
		{
			switch (m = *t++)
			{
			case 0:
				break;
			case 'f':
				if (*t)
					f = t;
				else if (!(f = getarg(&a, NiL)))
					error(3, "-f: format argument expected");
				break;
			case 'i':
			case 'o':
			case 'p':
				if (!*t && !(t = getarg(&a, NiL)))
				{
					error(3, "-%c: file argument expected", m);
					t = "-";
				}
				if (state.io[d])
				{
					if (state.io[d] != sfstdin && state.io[d] != sfstdout && state.io[d] != sfstderr)
						sfclose(state.io[d]);
					state.io[d] = 0;
				}
				if (!streq(t, "-"))
					switch (m)
					{
					case 'i':
						if (!(state.io[d] = sfopen(NiL, t, x == '+' ? "rw" : "r")))
							error(ERROR_SYSTEM|2, "%s: cannot read", t);
						break;
					case 'o':
						if (!(state.io[d] = sfopen(NiL, t, x == '+' ? "a" : "w")))
							error(ERROR_SYSTEM|2, "%s: cannot write", t);
						break;
					case 'p':
						if (!(state.io[d] = sfpopen(NiL, t, "rw")))
							error(ERROR_SYSTEM|2, "%s: cannot connect to coprocess", t);
						else
							sfdcslow(state.io[d]);
						break;
					}
				if (!state.io[d])
					switch (d)
					{
					case 0:
						state.io[d] = sfstdin;
						break;
					case 1:
						state.io[d] = sfstdout;
						break;
					case 2:
						state.io[d] = sfstderr;
						break;
					}
				else
				{
					if (m != 'p')
						sfsetbuf(state.io[d], NiL, 0);
					fcntl(sffileno(state.io[d]), F_SETFD, FD_CLOEXEC);
				}
				break;
			case 'n':
				n = -1;
				continue;
			case 'u':
				if (!*t && !(t = getarg(&a, NiL)))
					error(3, "-u: unit argument expected");
				switch (c = *t++)
				{
				case 'm':
					if ((state.io[d = elementsof(state.io) - 1] = state.mam.out) && *state.mam.label)
						sfputr(state.io[d], state.mam.label, -1);
					break;
				case '0': case '1': case '2': case '3': case '4':
				case '5': case '6': case '7': case '8': case '9':
					d = c - '0';
					break;
				default:
					error(2, "-u: unit [0-9m] expected");
					break;
				}
				continue;
			default:
				error(3, "-%c: unknown option", *(t - 1));
				continue;
			}
			break;
		}
	}

				/*...INDENT*/
				if (i == CON_print)
				{
					if (state.io[d] && (sfset(state.io[d], 0, 0) & SF_WRITE))
					{
						if (f)
							strprintf(state.io[d], f, t, 0, n);
						else
							sfputr(state.io[d], t, n);
					}
					else if (*t || n != -1)
						error(2, "unit %d not open for writing", d);
				}
				else if (n != -1 || *t)
				{
					if (state.io[d] && (sfset(state.io[d], 0, 0) & SF_READ))
					{
						if (!*(a = t) || !(t = getarg(&a, NiL)) || getarg(&a, NiL))
							error(2, "one variable argument expected");
						else
						{
							if (f)
								error(1, "read format ignored");
							sfset(state.io[d], SF_IOINTR, 1);
							if (!(f = sfgetr(state.io[d], '\n', 1)))
								f = null;
							setvar(t, f, 0);
						}
					}
					else
						error(2, "unit %d not open for reading", d);
				}
			}
			continue;

		case CON_set:
			if (!(pp->cp->flags & CON_skip))
			{
				n = sfstrtell(sp);
				expand(sp, t);
				sfputc(sp, 0);
				set(sfstrseek(sp, n, SEEK_SET), 1, pp->scoped);
			}
			continue;
		}
	}
					/*INDENT*/
				}
			}
		}
#if DEBUG
		if (pp->cp->flags & CON_skip)
			debug((-8, "%s:%d:skip: `%s'", error_info.file, error_info.line, s));
		else
			debug((-7, "%s:%d:data: `%s'", error_info.file, error_info.line, s));
#endif
		if (!(pp->cp->flags & CON_skip))
		{
			for (t = s; isspace(*t); t++);
			if (*t)
			{
				if (!lead && indent <= pp->indent)
				{
					if (pp->newline)
					{
						pp->newline = 0;
						*pp->bp = '\n';
					}
					pp->pushback = lin;
					return 0;
				}
				break;
			}
			else if (!lead && pp->fp == sfstdin)
				return 0;
			else if (pp->splice == error_info.line)
				break;
			else if (!lead)
			{
				sfputr(sp, s, term);
				return 1;
			}
		}
	}
	if (!s)
		return 0;
	switch (i = pp->eval)
	{
	case 0:
		sfputr(sp, s, term);
		break;
	case 1:
		expand(sp, s);
		sfputc(sp, term);
		break;
	default:
		{
			Sfio_t*	tp;
			Sfio_t*	xp;

			tp = sfstropen();
			for (;;)
			{
				xp = (i & 1) ? sp : tp;
				expand(xp, s);
				if (--i <= 0)
					break;
				s = sfstruse(xp);
			}
			sfstrclose(tp);
			sfputc(sp, term);
		}
		break;
	}
	return 1;
}

/*
 * makefile statement parser
 *
 * the statement grammar is
 *
 *	<lhs> <op> <rhs> <act>
 *
 * <lhs>, <rhs> and <act> are placed in sp
 * <op> determines which components are expanded when read
 *
 *	<op>	<lhs>	<rhs>	<act>
 *	-----------------------------
 *	 =	  0	  0	  -
 *	 :=	  1	  1	  -
 *	 +=	  1	  1	  -
 *	 :	  1	  1	  0
 *	:op:	  0	  0	  0
 *
 * an action <act> is not expanded when read
 * actions are indented by a number of <tab> chars
 * the indent tabs are stripped from each line in the action
 * the first line not indented by this amount terminates the action
 * ideally only '\t' should be used although ' ' are ok using state.tabstops
 * the statement operator flags OP_* are returned
 */

static int
statement(Sfio_t* sp, char** lhs, Rule_t** opr, char** rhs, char** act)
{
	register int	c;
	register char*	s;
	register char*	t;
	char*		b;
	char*		p;
	char*		brace = 0;
	char*		ecarb = 0;
	int		item = 0;
	int		op = 0;
	int		nest = 0;
	int		paren = 0;
	int		quote = 0;
	long		rhs_pos = -1;
	long		act_pos = -1;
	long		lin_pos;

	if (!getline(sp, 1, 0))
		return op;
	*opr = 0;
	b = s = sfstrbase(sp);
	while (c = *s++)
	{
		if (c == '\\')
		{
			if (*s)
				s++;
		}
		else if (c == quote)
			quote = 0;
		else if (c == '"' || c == '\'')
			quote = c;
		else if (!quote)
		{
			if (brace)
			{
				if (c == '{')
					nest++;
				else if (c == '}' && !--nest)
				{
					for (p = s; isspace(*p); p++);
					if (*p || brace > b && *(brace - 1) == '$')
					{
						brace = 0;
						continue;
					}
					ecarb = s - 1;
				}
			}
			else if (c == '(')
				paren++;
			else if (c == ')' && !paren--)
				error(3, "to many )'s");
			else if (!paren)
			{
				if (c == '{')
				{
					brace = s - 1;
					nest++;
				}
				else if (c == '}')
					error(3, "unbalanced {...}");
				else if (c == ':')
				{
					if (item < 2 && *s == '=')
					{
						t = s + 1;
						op = OP_ASSIGN|OP_EXPAND;
						break;
					}
					t = s;
					if (istype(*t, C_ID1))
						while (istype(*++t, C_ID2));
					if (*t++ == ':')
					{
						c = *t;
						*t = 0;
						if (!(*opr = getrule(s - 1)) || !((*opr)->property & P_operator))
						{
							Sfio_t*	tmp;

							tmp = sfstropen();
							*(t - 1) = 0;
							sfprintf(tmp, "%s%s", s, external.source);
							*(t - 1) = ':';
							op = readfile(sfstruse(tmp), COMP_INCLUDE|COMP_DONTCARE, NiL);
							sfstrclose(tmp);
							if (!op || !(*opr = getrule(s - 1)) || !((*opr)->property & P_operator))
							{
								*opr = internal.op;
								error(1, "operator %s not defined", s - 1);
							}
						}
						*t = c;
						op = OP_ASSERT|OP_ACTION|OP_EXPAND;
						if (((*opr)->property & P_ignore))
							op &= ~OP_EXPAND;
						break;
					}
					t = s;
					op = OP_ASSERT|OP_ACTION|OP_EXPAND;
					break;
				}
				else if (item < 2)
				{
					if (isspace(c))
					{
						item++;
						while (isspace(*s))
							s++;
						if ((c = *s) != ':' && c != '+' && c != '&' && c != '=')
							item++;
					}
					else if (c == '+')
					{
						if (*s == '=')
						{
							t = s + 1;
							op = OP_ASSIGN|OP_APPEND|OP_EXPAND;
							break;
						}
					}
					else if (c == '&')
					{
						if (*s == '=')
						{
							t = s + 1;
							op = OP_ASSIGN|OP_EXPAND|OP_AUXILIARY;
							break;
						}
					}
					else if (c == '=')
					{
						if (*s == '=')
						{
							t = s + 1;
							op = OP_ASSIGN|OP_STATE;
							break;
						}
						t = s;
						op = OP_ASSIGN;
						break;
					}
				}
			}
		}
	}
	if (quote)
		error(1, "missing closing %c quote", quote);
	else if (paren)
		error(1, "%d closing )%s missing from line", paren, paren == 1 ? null : "'s");
	if (op)
	{
		if (op & OP_EXPAND)
		{
			Sfio_t*	tmp;

			tmp = sfstropen();
			*(s - 1) = 0;
			sfputr(tmp, sfstrbase(sp), 0);
			rhs_pos = sfstrtell(tmp);
			sfputr(tmp, t, 0);
			sfstrseek(sp, 0, SEEK_SET);
			expand(sp, sfstrseek(tmp, 0, SEEK_SET));
			sfputc(sp, 0);
			sfstrseek(tmp, rhs_pos, SEEK_SET);
			rhs_pos = sfstrtell(sp);
			expand(sp, sfstrseek(tmp, 0, SEEK_CUR));
			sfputc(sp, 0);
			sfstrclose(tmp);
			s = t = sfstrbase(sp) + rhs_pos;
			p = sfstrseek(sp, 0, SEEK_CUR) - 1;
		}
		else
			p = t + strlen(t);
		while (p > t && isspace(*(p - 1)))
			p--;
		*p = 0;
		while (isspace(*t))
			t++;
		rhs_pos = t - sfstrbase(sp);
		t = sfstrbase(sp);
		for (s--; s > t && isspace(*(s - 1)); s--);
		*s = 0;
	}
	else
	{
		op = OP_EMPTY;
		p = s - 1;
		s = sfstrbase(sp);
		while (p > s && isspace(*(p - 1)))
			p--;
		*p = 0;
	}
	if (brace)
	{
		for (t = brace; t > s && isspace(*(t - 1)); t--);
		*t = 0;
		if (ecarb)
		{
			*ecarb = 0;
			nest = 0;
		}
		if (p > ++brace)
			*p++ = '\n';
		for (;;)
		{
			if (!*brace)
			{
				*brace = ' ';
				*(brace + 1) = 0;
				break;
			}
			else if (!isspace(*brace) || *brace == '\n')
				break;
			brace++;
		}
		act_pos = brace - sfstrbase(sp);
		sfstrseek(sp, p - sfstrbase(sp), SEEK_SET);
		quote = 0;
		while (nest)
		{
			lin_pos = sfstrtell(sp);
			if (!getline(sp, -1, '\n'))
			{
				error(2, "unbalanced {...} action");
				break;
			}
			s = sfstrbase(sp) + lin_pos;
			t = sfstrseek(sp, 0, SEEK_CUR);
			while (s < t)
			{
				if ((c = *s++) == '\\')
				{
					if (s < t)
						s++;
				}
				else if (c == quote)
					quote = 0;
				else if (c == '"' || c == '\'')
					quote = c;
				else if (!quote)
				{
					if (c == '{')
						nest++;
					else if (c == '}' && !--nest)
					{
						p = s - 1;
						while (s < t && isspace(*s))
							s++;
						if (s < t)
							error(2, "tokens after closing } ignored");
						t = sfstrbase(sp) + act_pos;
						while (p > t && isspace(*(p - 1)))
							p--;
						*p = 0;
						if (!*t)
							act_pos = -1;
						break;
					}
				}
			}
		}
	}
	else if (op & OP_ACTION)
	{
		if (pp->checkhere)
		{
			t = b = sfstrbase(sp) + rhs_pos;
			nest = quote = 0;
			for (;;)
			{
				switch (*t++)
				{
				case 0:
					break;
				case '"':
				case '\'':
					if (*(t - 1) == quote)
						quote = 0;
					else if (!quote)
						quote = *(t - 1);
					continue;
				case '(':
				case '[':
				case '{':
					if (!quote)
						nest++;
					continue;
				case ')':
				case ']':
				case '}':
					if (!quote)
						nest--;
					continue;
				case '<':
					if (!nest && !quote && *t == '<')
					{
						s = t - 1;
						while (isspace(*++t));
						if (*t)
						{
							p = t;
							while (*++t)
								if (isspace(*t))
								{
									*t = 0;
									break;
								}
							pp->here = strdup(p);
							while (--s >= b && isspace(*s));
							*(s + 1) = 0;
						}
						break;
					}
					continue;
				default:
					continue;
				}
				break;
			}
			if (!*b)
				rhs_pos = -1;
		}
		act_pos = ++p - sfstrbase(sp);
		sfstrseek(sp, act_pos, SEEK_SET);
		while (getline(sp, 0, '\n'));
		t = sfstrbase(sp) + act_pos;
		s = sfstrseek(sp, 0, SEEK_CUR);
		for (;;)
		{
			if (s <= t)
			{
				act_pos = -1;
				break;
			}
			if (*--s != '\n')
			{
				*(s + 1) = 0;
				break;
			}
		}
	}
	*lhs = sfstrseek(sp, 0, SEEK_SET);
	*rhs = (rhs_pos >= 0) ? sfstrbase(sp) + rhs_pos : null;
	*act = (act_pos >= 0) ? sfstrbase(sp) + act_pos : null;
	return op;
}

static const Namval_t	nametypes[] =
{
	"altstate",	NAME_altstate,
	"assignment",	NAME_assignment,
	"context",	NAME_context,
	"dynamic",	NAME_dynamic,
	"glob",		NAME_glob,
	"identifier",	NAME_identifier,
	"intvar",	NAME_intvar,
	"option",	NAME_option,
	"path",		NAME_path,
	"staterule",	NAME_staterule,
	"statevar",	NAME_statevar,
	"variable",	NAME_variable,
};

/*
 * parse a basic assertion statement
 */

static void
assertion(char* lhs, Rule_t* opr, char* rhs, char* act, int op)
{
	register char*		s;
	register Rule_t*	r;
	register List_t*	p;
	register List_t*	q;
	int			c;
	int			i;
	int			n;
	int			isactive;
	Rule_t*			x;
	Rule_t*			joint;
	Var_t*			v;
	List_t*			jointail;
	List_t*			prereqs;
	char*			name;
	struct					/* prereq attributes	*/
	{
		Rule_t		rule;		/* rule attributes	*/
		int		op;		/* assertion op		*/
	}			*att, clr, set;

	if (opr)
	{
		debug((-6, "operator: lhs=`%s' %s rhs=`%s' act=`%-.1024s'", lhs, opr->name, rhs, act));
		apply(opr, lhs, rhs, act, CO_ALWAYS|CO_LOCAL|CO_URGENT);
		return;
	}
	if (internal.assert_p->prereqs && (opr = associate(internal.assert_p, NiL, lhs, NiL)) && opr->prereqs && (opr = opr->prereqs->rule) && (opr->property & P_operator) && !opr->uname)
	{
		s = opr->uname = opr->name;
		opr->name = lhs;
		apply(opr, lhs, rhs, act, CO_ALWAYS|CO_LOCAL|CO_URGENT);
		opr->name = s;
		opr->uname = 0;
		return;
	}
	debug((-6, "assertion: lhs=`%s' rhs=`%-.1024s' act=`%-.1024s'", lhs, rhs, act));

	/*
	 * special check for internal.query
	 */

	if (getrule(lhs) == internal.query)
	{
		c = 0;
		if (!(s = getarg(&rhs, NiL)))
			interpreter(NiL);
		else if (*s == '-' && !*(s + 1))
			while (s = getarg(&rhs, NiL))
		{
			if (streq(s, "buckets"))
			{
				c = 0;
				hashdump(NiL, HASH_BUCKET);
			}
			else if (streq(s, "hash"))
			{
				c = 0;
				hashdump(NiL, 0);
			}
			else if (streq(s, "jobs"))
			{
#if DEBUG
				c = 1;
				error(0, null);
				dumpjobs(2);
#else
				error(2, "%s: implemented in DEBUG==1 version", s);
#endif
			}
			else if (streq(s, "nametype"))
			{
				while (s = getarg(&rhs, NiL))
				{
					n = nametype(s, NiL);
					sfprintf(sfstdout, "%16s", s);
					for (i = 0; i < elementsof(nametypes); i++)
						if (n & nametypes[i].value)
							sfprintf(sfstdout, " %s", nametypes[i].name);
					sfputc(sfstdout, '\n');
				}
				break;
			}
			else if (streq(s, "rules"))
			{
				c = 1;
				state.ruledump = 1;
				dump(sfstderr, 0);
				state.ruledump = 0;
			}
			else if (streq(s, "stack"))
			{
				Parseinfo_t*	sp = pp;
				Local_t*	lp;

				c = 1;
				while (--sp > &parsestack[0])
				{
					sfprintf(sfstderr, "\n%s:\n", sp->name);
					for (lp = sp->local; lp; lp = lp->next)
						sfprintf(sfstderr, " %15s=%s [%s]\n", lp->oldv->name, lp->newv.value, lp->oldv->value);
				}
			}
			else if (streq(s, "variables"))
			{
				c = 1;
				state.vardump = 1;
				dump(sfstderr, 0);
				state.vardump = 0;
			}
			else if (streq(s, "view"))
			{
				if (state.maxview)
				{
					sfprintf(sfstderr, "\n");
					for (c = 0; c <= state.maxview; c++)
						sfprintf(sfstderr, "[%d] %2d %s\n", c, state.view[c].pathlen, state.view[c].path);
					c = 1;
				}
			}
			else
				error(1, "%s: options are {buckets,hash,jobs,view}", s);
		}
		else
			while (s)
			{
				if (r = getrule(s))
				{
					c = 1;
					dumprule(sfstderr, r);
				}
				if (v = getvar(s))
				{
					c = 1;
					sfprintf(sfstderr, "\n");
					dumpvar(sfstderr, v);
					if (r = staterule(VAR, NiL, s, 0))
						dumprule(sfstderr, r);
				}
				s = getarg(&rhs, NiL);
			}
		if (c)
			sfprintf(sfstderr, "\n");
		return;
	}

	/*
	 * construct the prerequsite list and attributes
	 */

	x = 0;
	zero(clr);
	zero(set);
	set.op = op;
	if (!*rhs)
		set.op |= A_target;
	p = q = 0;
	while (s = getarg(&rhs, &set.op))
	{
		/*
		 * <ATTRNAME><attribute> names (and sets) the attribute
		 * <ATTRSET><attribute> sets the attribute
		 * <ATTRCLEAR><attribute> clears the attribute
		 */

		if (((c = *s) == ATTRSET || c == ATTRCLEAR) && *(s + 1))
		{
			*s = ATTRNAME;
			r = getrule(s);
			*s = c;
			if (!r)
				r = getrule(s + 1);
			if (!r || !(r->property & P_attribute))
			{
				if (r)
				{
					Flags_t		m = 0;

					/*
		 			 * user controlled dynamic
					 * staterule attributes
					 */

					if (r == internal.entries)
						m = D_entries;
					else if (r == internal.member)
						m = D_member;
					else if (r == internal.regular)
						m = D_regular;
					else if (r == internal.scanned)
						m = D_scanned;
					if (m)
					{
						if (c == ATTRCLEAR)
						{
							set.op |= A_negate;
							att = &clr;
						}
						else
							att = &set;
						att->rule.dynamic |= m;
						continue;
					}
				}
				r = makerule(s);
			}
		}
		else
			r = makerule(s);
		if (r->property & P_attribute)
		{
			if (c == ATTRCLEAR)
			{
				set.op |= A_negate;
				att = &clr;
			}
			else
				att = &set;

			/*
			 * assertion attributes
			 */

			if (r == internal.clear)
				att->op |= A_clear;
			else if (r == internal.copy)
				att->op |= A_copy;
			else if (r == internal.delete)
				att->op |= A_delete;
			else if (r == internal.insert)
				att->op |= A_insert;
			else if (r == internal.null)
				att->op |= A_null;
			else if (r == internal.special)
				att->op |= A_special;

			/*
			 * attributes not propagated by merge()
			 *
			 * NOTE: internal.make->make is cleared in immediate()
			 */

			else if (r == internal.attribute)
				att->rule.property |= P_attribute;
			else if (r == internal.immediate)
				att->rule.property |= P_immediate;
			else if (r == internal.make)
				att->rule.property |= P_make;
			else if (r == internal.op)
				att->rule.property |= P_operator;
			else if (r == internal.readonly)
				att->rule.property |= P_readonly;
			else if (r == internal.scan)
				att->op |= A_scan;
			else if (r == internal.semaphore)
			{
				if (att->rule.semaphore < UCHAR_MAX)
					att->rule.semaphore++;
				else
					error(1, "%s: maximum semaphore count is %d", r->name, UCHAR_MAX - 1);
			}
			else if (r == internal.state)
				att->rule.property |= P_state;
			else if (r == internal.target)
				att->rule.property |= P_target;
			else if (r == internal.use)
				att->rule.property |= P_use;

			/*
			 * merge() handles the rest
			 */

			else
				merge(r, &att->rule, MERGE_ATTR|MERGE_FORCE);
		}
		else
		{
			if ((set.op & (A_metarule|A_special)) == A_metarule || !*s)
			{
				if (!x)
					x = r;
				else if (!*s)
					error(1, "multiple prerequisite patterns in metarule assertion");
				else
				{
					while (c = *s++)
					{
						if (c == '%')
							sfputr(internal.tmp, "$(%)", -1);
						else
							sfputc(internal.tmp, c);
					}
					r = makerule(sfstruse(internal.tmp));
				}
			}
			else if (set.op & A_scope)
			{
				r->dynamic |= D_scope;
				set.rule.dynamic |= D_hasscope;
			}
			if (!(set.rule.dynamic & D_dynamic) && !(r->dynamic & D_scope) && isdynamic(r->name))
				set.rule.dynamic |= D_dynamic;
			if (r->property & P_use)
				merge(r, &set.rule, MERGE_ATTR);
			if (!p)
				p = q = cons(r, NiL);
			else
				q = q->next = cons(r, NiL);
			if (!(r->dynamic & D_scope))
				set.op |= A_target;
		}
	}
	prereqs = p;
	if (*act || (set.op & (A_null|A_target)) || (set.rule.property & (P_make|P_local)) == (P_make|P_local))
		set.rule.property |= P_target;
	joint = (set.rule.property & P_joint) ? internal.joint : 0;

	/*
	 * assert each target
	 */

	name = getarg(&lhs, &set.op);
	while (name)
	{
		r = makerule(name);
		if (joint)
		{
			if (joint == internal.joint)
			{
				joint = catrule(internal.joint->name, ".", name, 1);
				joint->property |= P_joint|P_readonly|P_virtual;
				jointail = joint->prereqs = cons(r, NiL);
			}
			else
				jointail = jointail->next = cons(r, NiL);
		}
		if ((set.op & (A_metarule|A_special)) == A_metarule)
		{
			Rule_t*		in;
			Rule_t*		out;

			in = 0;
			if (*name == ATTRNAME)
			{
				for (s = name + 1; istype(*s, C_ID1|C_ID2); s++);
				if (*s == ATTRNAME && s > (name + 1) && (c = *++s))
				{
					*s = 0;
					in = getrule(name);
					*s = c;
				}
			}
			if (in)
			{
				/*
				 * pattern association rule
				 */

				if (*s != '%' || *(s + 1) != ATTRNAME || !(x = getrule(s + 1)) || !(x->property & P_attribute))
					x = makerule(s);
				addprereq(in, x, *(s + strlen(s) - 1) == '%' ? PREREQ_APPEND : PREREQ_INSERT);
				if (set.op & A_negate)
				{
					*name = ATTRCLEAR;
					merge(&clr.rule, makerule(name), MERGE_ATTR|MERGE_FORCE);
					*name = ATTRNAME;
				}
				name = getarg(&lhs, &set.op);
			}
			else
			{
				/*
				 * metarule assertion
				 */

				for (p = prereqs, prereqs = q = 0; p; q = p, p = p->next)
				{
					if (p->rule == x)
					{
						/*
						 * rhs pattern
						 */

						in = p->rule;
						if (q)
							q->next = p->next;
					}
					else if (!prereqs)
						prereqs = p;
				}
				c = ((set.op & A_clear) && !prereqs && !*act) ? PREREQ_DELETE : PREREQ_APPEND;
				out = *(r->name + 1) ? r : 0;

				/*
				 * update the metarule intermediate prerequisite graph
				 */

				if (in)
				{
					r = metarule(in->name, r->name, 1);
					if (out)
					{
						metaclose(in, out, c);
						if (name = getarg(&lhs, &set.op))
						{
							addprereq(metainfo('S', in->name, out->name, 1), out, c);
							do
							{
								x = makerule(name);
								addprereq(metainfo('P', in->name, x->name, 1), out, c);
								addprereq(metainfo('S', in->name, out->name, 1), x, c);
								metaclose(in, x, c);
							} while (name = getarg(&lhs, &set.op));
						}
					}
					else
						addprereq(metainfo((set.rule.property & P_terminal) ? 'T' : 'N', NiL, NiL, 1), in, c);
				}
				else
				{
					if (out && c != PREREQ_DELETE)
						addprereq(internal.metarule, out, PREREQ_LENGTH);
					name = getarg(&lhs, &set.op);
				}
			}
		}
		else
		{
			name = getarg(&lhs, &set.op);
			if (!internal.main->prereqs && !state.global && !(set.rule.property & P_operator) && !(set.op & A_special) && !special(r) && !special(&set.rule))
			{
				internal.main->prereqs = cons(r, NiL);
				internal.main->dynamic &= ~D_compiled;
			}
		}
		if ((r->property & P_readonly) || (r->property & P_staterule) && !istype(*(r->name + 1), C_ID1))
		{
			if (r == internal.readonly)
				continue; /* drop this in 2000 */
			if (pp->fp != sfstdin)
			{
				error(2, "%s: %s atom cannot appear as target", r->name, (r->property & P_readonly) ? "readonly" : "staterule");
				continue;
			}
			else if (r->property & P_readonly)
				error(1, "%s: modifying readonly atom", r->name);
		}
		if (!((r->property|set.rule.property) & P_immediate) && (r->status == UPDATE || r->status == MAKING))
		{
			if (*act || (set.op & A_null))
			{
				error(2, "%s: cannot reset active target action", r->name);
				continue;
			}
			if (set.op & (A_clear|A_copy|A_delete|A_insert))
			{
				error(2, "%s: cannot reorder active target prerequisites", r->name);
				continue;
			}
			isactive = 1;
		}
		else
			isactive = 0;
		if (set.op & (A_clear|A_copy))
		{
			int	dynamic;
			int	property;

			if (r->property & P_attribute)
			{
				error(2, "%s: atom cannot be cleared", r->name);
				continue;
			}
			dynamic = r->dynamic & (D_compiled);
			property = r->property & (P_state|P_staterule|P_statevar);
			if ((r->property & P_metarule) && (prereqs || *act || (set.op & A_null)))
				property |= r->property & (P_metarule|P_use);
			s = r->uname && !(r->property & P_state) ? r->uname : r->name;
			if (!(x = (r->property & P_state) ? rulestate(r, 0) : staterule(RULE, r, NiL, 0)) || r->prereqs != x->prereqs)
				freelist(r->prereqs);
			if (set.op & A_copy)
			{
				if (!prereqs || prereqs->next)
				{
					error(2, "%s: 1-1 copy only", r->name);
					continue;
				}
				x = prereqs->rule;
				*r = *x;
				r->name = s;
				r->uname = 0;
				r->prereqs = listcopy(x->prereqs);
				r->dynamic &= ~D_cached;
				r->dynamic |= dynamic;
				r->property |= property;
				continue;
			}
			zero(*r);
			r->name = s;
			r->dynamic = dynamic;
			r->property = property;
			if (r->property & P_state)
				state.savestate = 1;
		}
		if (set.op & A_delete)
		{
			for (p = prereqs; p; p = p->next)
				addprereq(r, p->rule, PREREQ_DELETE);
			negate(&set.rule, r);
			r->dynamic &= ~D_cached;
			continue;
		}
		if (set.op & A_null)
			r->action = null;
		if (prereqs)
		{
			p = name ? listcopy(prereqs) : prereqs;
			if ((set.op & A_insert) && (r->property & (P_joint|P_target)) == (P_joint|P_target))
				r->prereqs->next = append(p, r->prereqs->next);
			else
			{
				if (isactive && (set.rule.dynamic & D_dynamic))
				{
					r->dynamic |= D_dynamic;
					q = r->prereqs;
					r->prereqs = p;
					dynamic(r);
					p = r->prereqs;
					r->prereqs = q;
				}
				r->prereqs = (set.op & A_insert) ? append(p, r->prereqs) : append(r->prereqs, p);
			}
			remdup(r->prereqs);
			if (r->property & P_state)
				state.savestate = 1;
		}

		/*
		 * check action
		 */

		if (*act)
		{
			if (!(s = r->action) || !streq(act, s))
				r->action = strdup(act); /* XXX: possible leak */
			if (s && r->action != s && !state.user && !(set.rule.property & P_operator) && !(set.op & A_special) && !special(r) && !special(&set.rule))
				error(1, "multiple actions for %s", r->name);
		}

		/*
		 * assign attributes
		 */

		merge(&set.rule, r, MERGE_ATTR|MERGE_FORCE);
		if (set.op & A_negate)
			negate(&clr.rule, r);

		/*
		 * attributes not handled by merge()
		 */

		if (!isactive && ((set.rule.dynamic & D_dynamic) || isdynamic(r->name)))
			r->dynamic |= D_dynamic;
		if (((set.rule.property|clr.rule.property) & P_functional) && !(r->property & P_state) && ((v = getvar(r->name)) || (v = setvar(r->name, null, 0))))
		{
			if (set.rule.property & P_functional)
				v->property |= V_functional;
			else
				v->property &= ~V_functional;
		}
		if (set.rule.property & P_immediate)
			r->property |= (P_ignore|P_immediate);
		if (set.rule.property & P_operator)
		{
			s = r->name;
			if (*s == ':' && istype(*++s, C_ID1))
				while (istype(*++s, C_ID2));
			if (*s == ':' && !*++s)
				r->property |= P_operator;
			else
				error(2, "%s: invalid operator name", r->name);
		}
		if (set.rule.property & P_readonly)
			r->property |= P_readonly;
		if (set.rule.semaphore)
			r->semaphore = set.rule.semaphore + 1;
		if ((set.rule.property & P_target) && !((clr.rule.property | r->property) & P_target))
		{
			r->property |= P_target;
			if (state.targetcontext && (s = strrchr(r->name, '/')))
				makerule(s + 1)->dynamic |= D_context;
		}
		if ((set.rule.property & P_use) && (!(r->property & P_attribute) || !r->attribute))
			r->property |= P_use;

		/*
		 * user controlled dynamic staterule attributes
		 */

		if ((set.rule.dynamic | clr.rule.dynamic) & ~D_dynamic)
		{
			r->dynamic |= (set.rule.dynamic & ~D_dynamic);
			r->dynamic &= ~(clr.rule.dynamic & ~D_dynamic);
			if (x = staterule(RULE, r, NiL, 0))
			{
				x->dynamic |= (set.rule.dynamic & ~D_dynamic);
				x->dynamic &= ~(clr.rule.dynamic & ~D_dynamic);
				if (clr.rule.dynamic & D_scanned)
					x->property |= P_force;
			}
			if (x = staterule(PREREQS, r, NiL, 0))
			{
				x->dynamic |= (set.rule.dynamic & ~D_dynamic);
				x->dynamic &= ~(clr.rule.dynamic & ~D_dynamic);
				if (clr.rule.dynamic & D_scanned)
					x->property |= P_force;
			}
		}

		/*
		 * these are done after attributes have been assigned
		 */

		if ((set.rule.property & P_attribute) && !(r->property & P_attribute))
		{
			r->property |= P_attribute;
			if (!(r->property & P_use))
			{
				if (internal.attribute->attribute << 1)
				{
					r->dynamic |= D_index;
					r->attribute = internal.attribute->attribute;
					internal.attribute->attribute <<= 1;
					addprereq(internal.attribute, r, PREREQ_APPEND);
				}
				else
					error(1, "%s: too many named attributes", r->name);
			}
		}
		if (joint)
			r->prereqs = cons(joint, r->prereqs);
		if ((set.op & A_scan) && !r->scan)
		{
			if (internal.scan->scan == SCAN_MAX)
				error(1, "%s: too many scan strategies", r->name);
			else
			{
				r->dynamic |= D_index;
				r->property |= P_attribute;
				r->scan = internal.scan->scan++;
				addprereq(internal.scan, r, PREREQ_APPEND);
			}
		}
		if (!state.init && !state.readonly && (!state.op && state.reading || !(r->property & P_immediate)))
			r->dynamic &= ~D_compiled;
		r->dynamic &= ~D_cached;

		/*
		 * do immediate actions right away
		 */

		if (r->property & P_immediate)
			immediate(r);
	}
}

/*
 * parse an assignment statement
 */

static void
assignment(char* lhs, int op, char* rhs)
{
	register Rule_t*	r;
	register char*		s;
	register int		n;
	Var_t*			v;

	if (internal.assign_p->prereqs && (r = associate(internal.assign_p, NiL, lhs, NiL)) && r->prereqs && (r = r->prereqs->rule) && (r->property & P_operator) && !r->uname)
	{
		s = r->uname = r->name;
		r->name = (op & OP_APPEND) ? "+=" : (op & OP_AUXILIARY) ? "&=" : (op & OP_STATE) ? "==" : "=";
		apply(r, lhs, rhs, NiL, CO_ALWAYS|CO_LOCAL|CO_URGENT);
		r->name = s;
		r->uname = 0;
		return;
	}
	debug((-6, "assignment: lhs=`%s' %s%srhs=`%-.1024s'", lhs, (op & OP_APPEND) ? "[append] " : null, (op & OP_STATE) ? "[state] " : null, rhs));
	if (!(s = getarg(&lhs, NiL)))
		error(1, "variable name missing in assignment");
	else
	{
		if (getarg(&lhs, NiL))
			error(1, "only one variable per assignment");
		n = 0;
		if (op & OP_APPEND)
			n |= V_append;
		if (op & OP_AUXILIARY)
			n |= V_auxiliary;
		if (op & OP_STATE)
			n |= V_scan;
		if (pp->scoped)
		{
			declare(s, NiL, n|V_scope);
			v = setvar(s, rhs, n);
			if (state.localview && (!(r = staterule(VAR, NiL, s, 0)) || !(r->property & P_parameter)))
			{
				Sfio_t*	tmp;

				tmp = sfstropen();
				expand(tmp, rhs);
				localvar(NiL, v, sfstruse(tmp), V_local_D);
				sfstrclose(tmp);
			}
		}
		else
			setvar(s, rhs, n);
	}
}

/*
 * invoke or verify rules s
 */

void
rules(char* s)
{
	register char*	t;
	register char*	e;

	if (e = strchr(s, '\n'))
		*e = 0;
	sfputr(internal.tmp, s, 0);
	if (e)
		*e = '\n';
	s = sfstruse(internal.tmp);
	if (!(t = getarg(&s, NiL)))
		t = null;
	if (state.rules)
	{
		edit(internal.nam, t, DELETE, KEEP, DELETE);
		edit(internal.wrk, state.rules, DELETE, KEEP, DELETE);
		if (strcmp(sfstruse(internal.nam), sfstruse(internal.wrk)))
			error(3, "%s: incompatible with current base rules %s", t, state.rules);
	}
	else if (t == null)
		state.rules = null;
	else
		state.rules = makerule(t)->name;
	if (t != null && (t = getarg(&s, NiL)))
		error(3, "%s: invalid base rule argument", t);
	state.explicitrules = 1;
}

/*
 * external PUSHLOCAL()
 */

void*
pushlocal(void)
{
	register Local_t*	p;

	PUSHLOCAL(p);
	return (void*)pp->local;
}

/*
 * external POPLOCAL()
 * pos is return value of previous pushlocal()
 */

void
poplocal(void* pos)
{
	register Local_t*	p;
	register Local_t*	t;

	p = (Local_t*)pos;
	while (pp->local != p)
	{
		pp->local->bucket->value = (char*)pp->local->oldv;
		t = pp->local;
		pp->local = pp->local->next;
		freelocal(t);
	}
	POPLOCAL(p);
}

static long	makeexpr(const char*, char**, void*);

/*
 * <var>
 * <var> = <expression>
 * [ <var> = ] <quote> ... <quote>
 */

static char*
nextarg(char* s, char** p, char** end, long* val)
{
	register char*	arg;
	register int	c;
	char*		var;
	char*		varend;
	char		buf[10];
	long		n;

	if ((c = *s) && c != MARK_QUOTE && c != '"')
	{
		if (!istype(*s, C_VARIABLE1))
			error(3, "argument expected in expression [%s]", s);
		for (var = s++; istype(*s, C_VARIABLE2); s++);
		varend = s;
		while (isspace(*s))
			s++;
		if (*s != '=' || *(s + 1) == '=')
		{
			c = *varend;
			*varend = 0;
			arg = getval(var, VAL_PRIMARY|VAL_AUXILIARY);
			*varend = c;
			*p = s;

			/*
			 * determine if in string or numeric context
			 */

			if ((*s == '!' || *s == '=') && *(s + 1) == '=')
			{
				s++;
				while (isspace(*++s));
				if (*s == MARK_QUOTE)
				{
					*end = s;
					return arg;
				}
			}
			if (!*arg)
				*val = 0;
			else
			{
				*val = strtol(arg, &s, 0);
				if (*s)
					*val = 1;
			}
			return 0;
		}
		while (isspace(*++s));
	}
	else
		var = 0;
	if ((c = *s) == MARK_QUOTE)
	{
		for (arg = ++s; *s && *s != c; s++);
		*end = s;
		if (*s)
			while (isspace(*++s));
	}
	else if (c == '"')
	{
		for (arg = ++s; *s && *s != c; s++)
			if (*s == '\\' && *(s + 1))
				s++;
		*end = s;
		if (*s)
			while (isspace(*++s));
	}
	else if (var)
	{
		sfsprintf(arg = buf, sizeof(buf), "%ld", *val = strexpr(s, &s, makeexpr, NiL));
		end = 0;
	}
	else
		error(3, "string argument expected in expression");
	if (var)
	{
		c = *varend;
		*varend = 0;
		if (end)
		{
			n = **end;
			**end = 0;

			/*
			 * XXX: this handles the symptom but not the bug
			 */

			if (*arg == '"' && !*(arg + 1))
				arg++;
		}
		debug((-6, "assignment: lhs=`%s' rhs=`%s'", var, arg));
		setvar(var, arg, 0);
		*varend = c;
		if (end)
			**end = n;
	}
	*p = s;
	return end ? arg : 0;
}

/*
 * supplementary make expression evaluator for strexpr()
 *
 *	"..." == "..."		strmatch()
 *	"..." != "..."		strmatch()
 *	"..." <  "..."		strcoll()
 *	"..." <= "..."		strcoll()
 *	"..." >  "..."		strcoll()
 *	"..." >= "..."		strcoll()
 *	<var>			"<value>"
 *	<var> = "..."		"..."
 *	( <var> = "..." )	"..."
 *	<var> = <expression>	<expression> ? "1" : ""
 *
 * NOTE: '"' translated to MARK_QUOTE by expr()
 */

static long
makeexpr(const char* cs, char** p, void* handle)
{
	char*	s = (char*)cs;
	int	c;
	int	q;
	int	c1;
	int	c2;
	int	m1;
	int	m2;
	char*	paren;
	char*	s1;
	char*	s2;
	char*	e1;
	char*	e2;
	long	n;

	NoP(handle);
	if (!s)
		error(3, "%s in expression", *p);
	else if (!(s1 = nextarg(s, &s, &e1, &n)))
		/* n == expression value */;
	else
	{
		if ((c = *s) == ')')
		{
			paren = s;
			e2 = s;
			while (isspace(*++s));
		}
		else
			paren = 0;
		if (!(c = *s) || c != '<' && c != '>' && (*(s + 1) != '=' || c != '!' && c != '='))
		{
			n = e1 > s1;
			if (paren)
				s = paren;
		}
		else
		{
			q = *++s != '=';
			while (isspace(*++s));
			if (!(s2 = nextarg(s, &s, &e2, &n)))
				n = 0;
			else
			{
				c1 = *e1;
				*e1 = 0;
				c2 = *e2;
				*e2 = 0;
				if (state.context)
				{
					if (*s1 == MARK_CONTEXT && *(e1 - 1) == MARK_CONTEXT)
					{
						*(e1 - 1) = 0;
						m1 = 1;
					}
					else
						m1 = 0;
					if (*s2 == MARK_CONTEXT && *(e2 - 1) == MARK_CONTEXT)
					{
						*(e2 - 1) = 0;
						m2 = 1;
					}
					else
						m2 = 0;
				}
				switch (c)
				{
				case '>':
					n = strcoll(s1, s2) >= q;
					break;
				case '<':
					n = strcoll(s2, s1) >= q;
					break;
				case '!':
					n = !strmatch(s1, s2);
					break;
				default:
					n = strmatch(s1, s2);
					break;
				}
				if (state.context)
				{
					if (m1)
						*(e1 - 1) = MARK_CONTEXT;
					if (m2)
						*(e2 - 1) = MARK_CONTEXT;
				}
				*e1 = c1;
				*e2 = c2;
			}
		}
		if (paren)
		{
			if (!*s)
				s--;
			*s = ')';
		}
	}
	*p = s;
	return n;
}

/*
 * expression evaluation on s using temporary string xp
 * '"' temporarily converted to MARK_QUOTE
 * s is first expanded
 */

long
expr(Sfio_t* xp, register char* s)
{
	register char*	t;
	register int	p;
	register char**	v;
	int		c;
	long		top;
	char*		restore[PARSEDEPTH];

	v = restore;
	t = s;
	p = 0;
	for (;;)
		switch (*t++)
		{
		case '(':
			if (p)
				p++;
			break;
		case ')':
			if (p)
				p--;
			break;
		case '"':
			if (p <= 1)
			{
				p = !p;
				if (v < &restore[elementsof(restore)])
					*(*v++ = t - 1) = MARK_QUOTE;
			}
			break;
		case '\\':
			if (*t++)
				break;
			/*FALLTHROUGH*/
		case 0:
		case '\n':
			c = *--t;
			*t = 0;
			top = sfstrtell(xp);
			expand(xp, s);
			sfputc(xp, 0);
			while (v > restore)
				**--v = '"';
			*t = c;
			return strexpr(sfstrseek(xp, top, SEEK_SET), NiL, makeexpr, NiL);
		}
}

/*
 * error exit during interpreter()
 */

static void
exit_interpreter(int code)
{
	NoP(code);
	sfsync(sfstdout);
	sfsync(sfstderr);
	longjmp(state.resume.label, 1);
	finish(code);
}

/*
 * interactive query loop
 */

void
interpreter(char* msg)
{
	int		level;
	void		(*errexit)(int);
	Frame_t		frame;
	Label_t		resume;

	if (msg)
		error(0, "\n%s\n", msg);
	level = pp - &parsestack[0];
	errexit = error_info.exit;
	error_info.exit = exit_interpreter;
	zero(frame);
	frame.target = internal.query;
	frame.parent = state.frame;
	frame.previous = frame.target->active;
	state.frame = frame.target->active = &frame;
	state.keepgoing |= 2;
	resume = state.resume;
	if (setjmp(state.resume.label))
	{
		unparse(level);
		state.hold = 0;
		state.frame = frame.target->active = &frame;
		sfclrlock(sfstdin);
	}
	else
		state.interpreter++;
	parse(sfstdin, NiL, "query", NiL);
	state.interpreter--;
	state.resume = resume;
	state.keepgoing &= 1;
	frame.target->active = frame.previous;
	state.frame = frame.parent;
	sfclrlock(sfstdin);
	error_info.exit = errexit;
	if (msg)
		error(0, "\n");
}

/*
 * read and parse file fp or line buffer bp
 * non-zero returned if target to be updated
 */

int
parse(Sfio_t* fp, char* bp, char* name, Sfio_t* scoped)
{
	register int		op;
	register Local_t*	lcl;
	char*			lhs;
	char*			rhs;
	char*			act;
	char*			alt;
	Rule_t*			opr;
	Local_t*		olcl;
	Sfio_t*			buf;
	Sfio_t*			tmp;

	if (pp->newline)
		*pp->bp = '\n';
	else if (!pp->fp && pp->bp && !*pp->bp)
	{
		error(1, "parse: early pop");
		return 0;
	}
	PUSHLOCAL(lcl);
	if (++pp >= &parsestack[elementsof(parsestack)])
		error(3, "input nesting too deep");
	if ((pp->block = (pp - 1)->cp + 1) >= &constack[elementsof(constack)])
		error(3, "control block nesting too deep");
#if DEBUG
	message((fp ? -2 : -7, "reading %s", name));
#else
	if (fp)
		message((-2, "reading %s", name));
#endif

	/*
	 * push the parse stack
	 */

	if (pp->fp = fp)
	{
		pp->ip = sfstropen();
		pp->prompt = fp == sfstdin && isatty(sffileno(fp)) && isatty(sffileno(sfstderr));
		if (fp == sfstdin)
			sfdcslow(fp);
	}
	else
	{
		pp->bp = bp;
		pp->prompt = 0;
	}
	pp->name = error_info.file = name;
	pp->line = error_info.line;
	error_info.line = 0;
	pp->argc = 0;
	pp->stashget = 0;
	pp->stashput = 0;
	pp->pushback = 0;
	pp->eval = 0;
	pp->indent = 0;
	pp->newline = 0;
	pp->splice = 0;
	pp->status = UPDATE;
	pp->cp = pp->block;
	pp->cp->flags = 0;
	if (pp->scoped = scoped)
		pp->local = (pp - 1)->local;

	/*
	 * statement parse loop
	 */

	buf = sfstropen();
	for (;;)
	{
		op = statement(buf, &lhs, &opr, &rhs, &act);
		if (trap() && !op)
			continue;
		if (!op)
			break;
		switch (op & OP_STATEMENT)
		{

		case OP_ASSERT:
			assertion(lhs, opr, rhs, act, 0);
			break;

		case OP_ASSIGN:
			assignment(lhs, op, rhs);
			break;

		case OP_EMPTY:
			if (*lhs || *act)
			{
				tmp = sfstropen();
				if (*lhs)
				{
					expand(tmp, lhs);
					rhs = lhs = sfstruse(tmp);
					sfputc(internal.nam, '.');
					while ((op = *rhs++) && !isspace(op))
						sfputc(internal.nam, islower(op) ? toupper(op) : isupper(op) ? tolower(op) : op);
					rhs--;
					alt = sfstruse(internal.nam);
				}
				else
				{
					sfputr(tmp, internal.always->name, -1);
					rhs = lhs = alt = sfstruse(tmp);
				}
				if (!(opr = getrule(alt)) || !(opr->property & (P_attribute|P_functional|P_immediate)))
				{
					if (pp->fp == sfstdin)
					{
						if ((op = *++alt) == 'P' && (!*(alt + 1) || !strcmp(alt, "PRINT")))
						{
							while (isspace(*rhs))
								rhs++;
							sfputr(sfstdout, rhs, '\n');
							sfstrclose(tmp);
							break;
						}
						if (op == 'Q' && (!*(alt + 1) || !strcmp(alt, "QUIT")))
						{
							if (*rhs)
								finish(strtol(rhs, NiL, 0));
							sfstrclose(tmp);
							goto quit;
						}
					}
					else if (!*act)
						error(3, "no operator on line");
					if (*act)
					{
						if (*rhs)
						{
							*rhs++ = 0;
							while (isspace(*rhs))
								rhs++;
						}
						opr = 0;
					}
					else
					{
						opr = internal.query;
						rhs = lhs;
					}
				}
				if (opr)
				{
					if (opr->property & P_functional)
					{
						while (isspace(*rhs))
							rhs++;
						maketop(opr, 0, rhs);
						sfstrclose(tmp);
						break;
					}
					sfputr(internal.nam, opr->name, -1);
					alt = sfstruse(internal.nam);
					if (opr->property & P_immediate)
						lhs = alt;
					else
					{
						lhs = rhs;
						rhs = alt;
					}
				}
				assertion(lhs, NiL, rhs, act, A_special);
				sfstrclose(tmp);
			}
			break;

#if DEBUG
		default:
			error(PANIC, "invalid statement type %d", op);
#endif
		}
	}
 quit:
	sfstrclose(buf);

	/*
	 * pop the parse stack
	 */

	if (pp->cp > pp->block)
		error(3, "missing %d closing end statement%s", pp->cp - pp->block, pp->cp - pp->block == 1 ? null : "s");
	if (pp->fp)
		sfstrclose(pp->ip);
	else if (pp->newline)
		*pp->bp = '\n';
	if (scoped)
	{
		(pp - 1)->local = pp->local;
		pp->local = 0;
	}
	else if (lcl = pp->local)
	{
		pp->local = 0;
		while (lcl)
		{
			lcl->bucket->value = (char*)lcl->oldv;
			olcl = lcl;
			lcl = lcl->next;
			freelocal(olcl);
		}
	}
	error_info.line = pp->line;
	pp--;
	if (pp->newline)
		*pp->bp = 0;
	POPLOCAL(lcl);
	error_info.file = pp->name;
#if DEBUG
	message((fp ? -2 : -7, "popping %s", name));
#else
	if (fp)
		message((-2, "popping %s", name));
#endif
	return (pp + 1)->status;
}

char*
parsefile(void)
{
	register Parseinfo_t*	pi;

	if (state.loading)
		return state.loading;
	for (pi = pp; pi >= &parsestack[0]; pi--)
		if (pi->fp)
			return pi->name;
	return error_info.file;
}
