/*************************************************************************
* TinyFugue - programmable mud client
* Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005, 2006-2007 Ken Keys
*
* TinyFugue (aka "tf") is protected under the terms of the GNU
* General Public License. See the file "COPYING" for details.
************************************************************************/
static const char RCSid[] = "$Id: expand.c,v 35004.232 2007/01/13 23:12:39 kkeys Exp $";
/********************************************************************
* Fugue macro text interpreter
*
* Written by Ken Keys
* Interprets macro statements, performing substitutions for positional
* parameters, variables, macro bodies, and expressions.
********************************************************************/
#include "tfconfig.h"
#include <math.h>
#include "port.h"
#include "tf.h"
#include "util.h"
#include "pattern.h"
#include "search.h"
#include "tfio.h"
#include "macro.h"
#include "signals.h" /* interrupted() */
#include "socket.h" /* send_line() */
#include "keyboard.h" /* pending_line, handle_input_line() */
#include "parse.h"
#include "expand.h"
#include "expr.h"
#include "cmdlist.h"
#include "command.h"
#include "variable.h"
typedef struct { void *ptr; opcode_t op; } opcmd_t;
Value *user_result = NULL; /* result of last user command */
int recur_count = 0; /* expansion nesting count */
const char *current_command = NULL;
const conString *argstring = NULL; /* command argument string */
Arg *tf_argv = NULL; /* shifted command argument vector */
int tf_argc = 0; /* shifted command/function arg count */
int argtop = 0; /* top of function argument stack */
keyword_id_t block = 0; /* type of current block */
Value *val_zero = NULL;
Value *val_one = NULL;
Value *val_blank = NULL;
static const char *oplabel_table[256];
static int cmdsub_count = 0; /* cmdsub nesting count */
#define KEYWORD_LENGTH 6 /* length of longest keyword */
static const char *keyword_table[] = {
"BREAK", "DO", "DONE", "ELSE", "ELSEIF", "ENDIF", "EXIT", "IF",
"LET", "RESULT", "RETURN", "SET", "SETENV", "TEST", "THEN", "WHILE"
};
/* A JUMP with a negative arg is a placeholder: ENDIF_PLACEHOLDER is for
* jumping to ENDIF, all other negative values jump to the DONE of the
* (-arg)th enclosing WHILE loop. Any negative args left at runtime must
* be because of a BREAK outside of a WHILE loop, so they will jump to
* end-of-program.
*/
#define ENDIF_PLACEHOLDER -999999
static keyword_id_t keyword_parse(Program *prog);
static int list(Program *prog, int subs);
static int statement(Program *prog, int subs);
static int slashsub(Program *prog, String *dest);
static int backsub(Program *prog, String *dest);
static const char *error_text(Program *prog);
static int macsub(Program *prog, int in_expr);
static int cmdsub(Program *prog, int in_expr);
static int percentsub(Program *prog, int subs, String **destp);
static int expand(Program *prog, String *dest, int subs, opcmd_t *opcmdp);
#define is_end_of_statement(p) ((p)[0] == '%' && is_statend((p)[1]))
#define is_end_of_cmdsub(p) (cmdsub_count && *(p) == ')')
void init_expand(void)
{
val_zero = newint(0);
val_one = newint(1);
val_blank = newSstr(blankline);
#define defopcode(name, num, optype, argtype, flag) \
oplabel_table[(num)|(OPF_##flag)] = #name;
#include "opcodes.h"
}
static void prog_free_tail(Program *prog, int start)
{
int i;
for (i = start; i < prog->len; i++) {
switch (op_arg_type(prog->code[i].op)) {
case OPA_STRP:
if (prog->code[i].arg.str)
conStringfree(prog->code[i].arg.str);
break;
case OPA_VALP:
if (prog->code[i].arg.val)
freeval(prog->code[i].arg.val);
break;
}
}
prog->len = start;
}
void prog_free(Program *prog)
{
prog_free_tail(prog, 0);
if (prog->code) FREE(prog->code);
conStringfree(prog->src);
FREE(prog);
}
const char *oplabel(opcode_t op)
{
static char buf[2] = "?";
if (op >= 0x20 && op < 0x7F) {
buf[0] = op;
return buf;
}
return oplabel_table[op & OPLABEL_MASK];
}
static void inst_dump(const Program *prog, int i, char prefix)
{
String *buf;
opcode_t op;
Value *val;
conString *constr;
BuiltinCmd *cmd;
(buf = Stringnew(NULL, 0, 0))->links++;
Sprintf(buf, "%c%5d: ", prefix, i);
op = prog->code[i].op;
Sappendf(buf, "%04X ", op);
if (!oplabel(op)) {
Sappendf(buf, "%8s ", "");
} else {
Sappendf(buf, "%-8s ", oplabel(op));
}
switch(op_arg_type(op)) {
case OPA_INT:
Sappendf(buf, "%d", prog->code[i].arg.i);
break;
case OPA_CHAR:
Sappendf(buf, "%c", prog->code[i].arg.c);
break;
case OPA_STRP:
constr = prog->code[i].arg.str;
Sappendf(buf, constr ? "\"%S\"" : "NULL", constr);
break;
case OPA_CMDP:
cmd = prog->code[i].arg.cmd;
Sappendf(buf, "%s", cmd->name);
break;
case OPA_VALP:
if (!(val = prog->code[i].arg.val)) {
Stringcat(buf, "NULL");
break;
}
switch (val->type & TYPES_BASIC) {
case TYPE_ID: Sappendf(buf, "ID %s", val->name); break;
case TYPE_FUNC: Sappendf(buf, "FUNC %s", val->name); break;
case TYPE_CMD: Sappendf(buf, "CMD %s", val->name); break;
case TYPE_STR: Sappendf(buf, "STR \"%S\"", valstr(val));
if (val->type & TYPE_REGEX)
Stringcat(buf, " (RE)");
if (val->type & TYPE_EXPR)
Stringcat(buf, " (EXPR)");
if (val->type & TYPE_ATTR)
Stringcat(buf, " (ATTR)");
break;
case TYPE_ENUM: Sappendf(buf, "ENUM \"%S\"", valstr(val)); break;
case TYPE_POS: Sappendf(buf, "POS %S", valstr(val)); break;
case TYPE_INT: Sappendf(buf, "INT %S", valstr(val)); break;
case TYPE_DECIMAL: Sappendf(buf, "DECIMAL %S", valstr(val)); break;
case TYPE_DTIME: Sappendf(buf, "DTIME %S", valstr(val)); break;
case TYPE_ATIME: Sappendf(buf, "ATIME %S", valstr(val)); break;
case TYPE_FLOAT: Sappendf(buf, "FLOAT %S", valstr(val)); break;
default: Sappendf(buf, "? %S", valstr(val)); break;
}
break;
}
buf->attrs = getattrvar(prefix == 'c' ? VAR_cecho_attr : VAR_iecho_attr);
tfputline(CS(buf), tferr);
Stringfree(buf);
}
static void prog_dump(Program *prog)
{
int i;
for (i = 0; i < prog->len; i++)
inst_dump(prog, i, 'c');
}
struct Value *handle_eval_command(String *args, int offset)
{
int c, subflag = SUB_MACRO;
const char *ptr;
startopt(CS(args), "s:");
while ((c = nextopt(&ptr, NULL, NULL, &offset))) {
switch (c) {
case 's':
if ((subflag = enum2int(ptr, 0, enum_sub, "-s")) < 0)
return shareval(val_zero);
break;
default:
return shareval(val_zero);
}
}
if (!macro_run(CS(args), offset, NULL, 0, subflag, "\bEVAL"))
return shareval(val_zero);
return_user_result();
}
/* A command with the same name as a builtin was called; execute the macro if
* there is one and builtin was not explicitly required, otherwise execute
* the builtin. */
static int execute_command(BuiltinCmd *cmd, String *args, int offset,
int builtin_only)
{
if (!builtin_only && cmd->macro) {
current_command = cmd->name;
return do_macro(cmd->macro, args, offset, USED_NAME, 0);
} else if (cmd->func) {
current_command = cmd->name;
set_user_result((*cmd->func)(args, offset));
return 1;
}
/* eprefix depends on current_command still referring to caller */
do_hook(H_NOMACRO, "!/%s is disabled in this copy of tf", "%s", cmd->name);
return 0;
}
/* A command with a name not matching a builtin was called. Call the macro. */
static int execute_macro(const char *name, unsigned int hash, String *args,
int offset)
{
Macro *macro;
if (!(macro = find_hashed_macro(name, hash))) {
do_hook(H_NOMACRO, "!%s: no such command or macro", "%s", name);
return 0;
}
current_command = name;
return do_macro(macro, args, offset, USED_NAME, 0);
}
static String *execute_start(const conString *args, const char **old_cmdp)
{
String *str;
(str = Stringdup(args))->links++;
*old_cmdp = current_command;
return str;
}
static int execute_end(const char *old_command, int truth, String *args)
{
int error = 0;
if (pending_line && !read_depth) { /* "/dokey newline" and not in read() */
current_command = NULL;
error += !handle_input_line();
}
current_command = old_command;
if (!truth) {
truth = !valbool(user_result);
set_user_result(newint(truth));
}
Stringfree(args);
return !error;
}
/* handle_command
* Execute a single command line that has already been expanded.
* orig_cmd_line will not be written into.
*/
int handle_command(const conString *orig_cmd_line)
{
String *cmd_line;
const char *old_command, *name;
BuiltinCmd *cmd = NULL;
int error = 0, truth = 1;
int offset, end;
offset = 0;
if (!orig_cmd_line->data[offset] || is_space(orig_cmd_line->data[offset]))
return 0;
cmd_line = execute_start(orig_cmd_line, &old_command);
name = cmd_line->data + offset;
while (cmd_line->data[offset] && !is_space(cmd_line->data[offset]))
offset++;
if (cmd_line->data[offset]) cmd_line->data[offset++] = '\0';
while (is_space(cmd_line->data[offset]))
offset++;
for (end = cmd_line->len - 1; is_space(cmd_line->data[end]); end--);
Stringtrunc(cmd_line, end+1);
while (*name == '!') {
truth = !truth;
name++;
}
if (*name == '@') {
name++;
if (!(cmd = find_builtin_cmd(name))) {
eprintf("%s: not a builtin command", name);
error++;
} else {
error += !execute_command(cmd, cmd_line, offset, TRUE);
}
} else {
if ((cmd = find_builtin_cmd(name))) {
error += !execute_command(cmd, cmd_line, offset, FALSE);
} else {
error += !execute_macro(name, macro_hash(name), cmd_line, offset);
}
}
error += !execute_end(old_command, truth, cmd_line);
return !error;
}
/* stringvec
* Allocates *<vector> and fills it with locations of start and end of each
* word in <str>, starting at <offset>. <vecsize> is a guess of the number of
* words there will be, or -1. Returns number of words found, or -1 for error.
* Freeing *<vector> is the caller's responsibility.
*/
static int stringvec(const String *str, int offset, Arg **vector, int vecsize)
{
int count = 0;
char *start, *next;
const char *end;
if (vecsize <= 0) vecsize = 10;
if (!(*vector = (Arg *)MALLOC(vecsize * sizeof(Arg)))) {
eprintf("Not enough memory for %d word vector", vecsize);
return -1;
}
for (next = str->data + offset; *next; count++) {
if (count == vecsize) {
*vector = (Arg*)XREALLOC((char*)*vector, sizeof(Arg)*(vecsize+=10));
if (!*vector) {
FREE(*vector);
*vector = NULL;
eprintf("Not enough memory for %d word vector", vecsize);
return -1;
}
}
start = stringarg(&next, &end);
(*vector)[count].start = start - str->data;
(*vector)[count].end = end - str->data;
}
return count;
}
Program *compile_tf(conString *src, int srcstart, int subs, int is_expr,
int optimize)
{
Program *prog;
prog = MALLOC(sizeof(Program));
if (!prog) {
eprintf("out of memory");
return NULL;
}
prog->len = 0;
prog->size = 0;
prog->code = NULL;
(prog->src = src)->links++;
prog->srcstart = srcstart;
prog->mark = src->data + srcstart;
prog->optimize = optimize_user ? optimize : 0;
ip = src->data + srcstart;
if (is_expr) {
if (expr(prog)) {
if (!*ip) {
if (cecho > invis_flag) prog_dump(prog);
return prog;
}
parse_error(prog, "expression", "end of expression");
}
} else {
if (list(prog, subs)) {
if (!*ip) {
if (cecho > invis_flag) prog_dump(prog);
return prog;
}
parse_error(prog, "macro", "end of statement");
}
}
prog_free(prog);
return NULL;
}
int macro_run(conString *body, int bodystart, String *args, int offset,
int subs, const char *name)
{
Program *prog;
int result;
if (!(prog = compile_tf(body, bodystart, subs, 0, 0))) return 0;
result = prog_run(prog, args, offset, name, 0);
prog_free(prog);
return result;
}
static int do_parmsub(String *dest, int first, int last, int *emptyp)
{
int from_stack = argtop, to_stack = !dest, result = 1;
int orig_len, i;
Value *val = NULL;
if (first < 0 || last < first || last >= tf_argc) {
*emptyp = 1;
} else if (from_stack) {
if (to_stack && first == last) {
(val = stack[argtop - tf_argc + first])->count++;
*emptyp = (val->type & TYPE_STR) && val->sval->len <= 0;
} else {
if (to_stack)
(dest = Stringnew(NULL, 0, 0))->links++;
orig_len = dest->len;
for (i = first; ; i++) {
SStringcat(dest, valstr(stack[argtop-tf_argc+i]));
if (i == last) break;
Stringadd(dest, ' ');
}
*emptyp = dest->len <= orig_len;
}
} else {
if (to_stack)
(dest = Stringnew(NULL, 0, 0))->links++;
orig_len = dest->len;
SStringoncat(dest, argstring, tf_argv[first].start,
tf_argv[last].end - tf_argv[first].start);
*emptyp = dest->len <= orig_len;
}
if (to_stack) {
if (!val)
val = dest && dest->len ? newSstr(CS(dest)) : shareval(val_blank);
result = pushval(val);
if (dest) Stringfree(dest);
}
return result;
}
static void do_mecho(const Program *prog, int i)
{
/* XXX is invis_flag set correctly at runtime? */
if (prog->code[i].start && prog->code[i].end) {
tfprintf(tferr, "%S%.*s%A", do_mprefix(),
prog->code[i].end - prog->code[i].start, prog->code[i].start,
mecho_attr);
}
}
Value *prog_interpret(const Program *prog, int in_expr)
{
Value *val, *val2, *result = NULL;
String *str, *tbuf;
conString *constr;
int cip, stackbot, no_arg;
int empty; /* for varsub default */
opcode_t op;
int first, last, n;
int instruction_count = 0;
String *buf;
const char *cstr, *old_cmd;
struct tf_frame {
TFILE *orig_tfin, *orig_tfout; /* restored when done */
TFILE *local_tfin, *local_tfout;/* restored after pipes */
TFILE *inpipe, *outpipe; /* pipes between commands */
} first_frame, *frame;
#if 0
TFILE **filep;
#define which_tfile_p(c) \
(c=='i' ? &tfin : c=='o' ? &tfout : c=='e' ? &tferr : NULL)
#endif
frame = &first_frame;
frame->local_tfin = frame->orig_tfin = tfin;
frame->local_tfout = frame->orig_tfout = tfout;
frame->inpipe = frame->outpipe = NULL;
(buf = Stringnew(NULL, 0, 0))->links++;
stackbot = stacktop;
for (cip = 0; cip < prog->len; cip++) {
if (exiting) break;
if (interrupted()) {
eprintf("Macro execution interrupted.");
goto prog_interpret_exit;
}
if (max_instr > 0 && instruction_count > max_instr) {
eprintf("instruction count exceeded %max_instr (%d).", max_instr);
goto prog_interpret_exit;
}
instruction_count++;
op = prog->code[cip].op;
if (mecho > invis_flag) do_mecho(prog, cip);
if (iecho > invis_flag) inst_dump(prog, cip, 'i');
if (op_type_is(op, EXPR)) {
if (!reduce(op, prog->code[cip].arg.i))
goto prog_interpret_exit;
continue;
}
#define setup_next_io() \
do { \
if (!frame->inpipe) { \
frame->local_tfin = tfin; /* save any change */ \
} else { \
tfclose(frame->inpipe); /* close inpipe */ \
} \
if (!frame->outpipe) { \
frame->local_tfout = tfout; /* save any change */ \
tfin = frame->local_tfin; /* no pipe into next cmd */ \
frame->inpipe = NULL; /* no pipe into next cmd */ \
} else { \
tfin = frame->inpipe = frame->outpipe; /* pipe into next cmd */ \
frame->outpipe = NULL; \
tfout = frame->local_tfout; /* no pipe out of next cmd */ \
} \
} while (0)
switch (op) {
case OP_PIPE:
tfout = frame->outpipe = tfopen(NULL, "q");
break;
case OP_EXECUTE:
constr = prog->code[cip].arg.str;
if ((no_arg = !constr)) constr = CS(buf);
handle_command(constr);
if (no_arg) Stringtrunc(buf, 0);
setup_next_io();
break;
case OP_ARG:
constr = prog->code[cip].arg.str;
if ((no_arg = !constr)) constr = CS(buf);
/* no_arg and constr will be used by BUILTIN, MACRO, SET, ... */
break;
case OP_LET:
case OP_SET:
case OP_SETENV:
/* no_arg and constr were set by OP_ARG */
do_set(prog->code[cip].arg.val->name,
prog->code[cip].arg.val->u.hash,
constr, 0, op == OP_SETENV, op == OP_LET);
if (no_arg) Stringtrunc(buf, 0);
break;
case OP_BUILTIN: case OP_NBUILTIN:
case OP_COMMAND: case OP_NCOMMAND:
/* no_arg and constr were set by OP_ARG */
str = execute_start(constr, &old_cmd); /*XXX optimize: don't dup buf */
execute_command(prog->code[cip].arg.cmd, str, 0,
opnum_eq(op, OP_BUILTIN));
execute_end(old_cmd, !(op & OPF_NEG), str);
if (no_arg) Stringtrunc(buf, 0);
setup_next_io();
break;
case OP_MACRO: case OP_NMACRO:
/* no_arg and constr were set by OP_ARG */
str = execute_start(constr, &old_cmd); /*XXX optimize: don't dup buf */
execute_macro(prog->code[cip].arg.val->name,
prog->code[cip].arg.val->u.hash, str, 0);
execute_end(old_cmd, !(op & OPF_NEG), str);
if (no_arg) Stringtrunc(buf, 0);
setup_next_io();
break;
case OP_SEND:
constr = prog->code[cip].arg.str;
if ((no_arg = !constr)) constr = CS(buf);
if (constr->len || !snarf) {
#if 0
if (/*(subs == SUB_MACRO) &&*//*XXX*/ (mecho > invis_flag))
tfprintf(tferr, "%S%s%S%A", do_mprefix(), "SEND: ", constr,
mecho_attr);
#endif
if (!do_hook(H_SEND, NULL, "%S", constr)) {
set_user_result(newint(send_line(constr->data, constr->len,
TRUE)));
}
}
if (no_arg) Stringtrunc(buf, 0);
setup_next_io();
break;
case OP_APPEND:
constr = prog->code[cip].arg.str;
if (!constr) {
SStringcat(buf, valstr(popval()));
freeval(opd(0));
} else {
SStringcat(buf, constr);
}
break;
#define jumpaddr(prog) \
((prog->code[cip].arg.i < 0) ? prog->len : prog->code[cip].arg.i - 1)
case OP_JUMP:
cip = jumpaddr(prog);
break;
case OP_JZ:
if (!valbool(popval()))
cip = jumpaddr(prog);
freeval(opd(0));
break;
case OP_JNZ:
if (valbool(popval()))
cip = jumpaddr(prog);
freeval(opd(0));
break;
case OP_JRZ:
if (!valbool(user_result))
cip = jumpaddr(prog);
break;
case OP_JRNZ:
if (valbool(user_result))
cip = jumpaddr(prog);
break;
case OP_JNEMPTY:
/* empty was set by one of the varsub operators */
if (!empty)
cip = jumpaddr(prog);
break;
case OP_EXPR:
/* Evaulate the expression contained in buf and push its value */
val = expr_value(buf->data);
Stringtrunc(buf, 0);
val = val ? valval(val) : shareval(val_zero);
if (!pushval(val)) goto prog_interpret_exit;
break;
case OP_RETURN:
case OP_RESULT:
if ((val = prog->code[cip].arg.val))
val->count++;
else
val = popval();
set_user_result(valval(val));
if (op == OP_RESULT && !argtop) {
constr = valstr(val);
oputline(constr ? constr : blankline);
}
cip = prog->len - 1;
setup_next_io();
break;
case OP_TEST:
if ((val = prog->code[cip].arg.val))
val->count++;
else
val = popval();
set_user_result(valval(val));
setup_next_io();
break;
case OP_PUSHBUF:
if (!pushval(newptr(buf))) /* XXX optimize */
goto prog_interpret_exit;
(buf = Stringnew(NULL, 0, 0))->links++;
break;
case OP_POPBUF:
Stringfree(buf);
buf = valptr(popval()); /* XXX optimize */
freeval(opd(0));
break;
case OP_CMDSUB:
if (!pushval(newptr(frame))) /* XXX optimize */
goto prog_interpret_exit;
frame = XMALLOC(sizeof(*frame));
frame->local_tfout = tfout = tfopen(NULL, "q");
frame->local_tfin = tfin;
frame->inpipe = frame->outpipe = NULL;
break;
#if 0
case OP_POPFILE:
filep = which_tfile_p(prog->code[cip].arg.c);
tfclose(*filep);
*filep = (TFILE*)valptr(popval()); /* XXX optimize */
freeval(opd(0));
break;
#endif
case OP_ACMDSUB:
case OP_PCMDSUB:
if (op_is_push(op))
(tbuf = Stringnew(NULL, 0, 0))->links++;
else
tbuf = buf;
first = 1;
while ((constr = dequeue((tfout)->u.queue))) {
if (!((constr->attrs & F_GAG) && gag)) {
if (!first) {
Stringadd(tbuf, ' ');
if (tbuf->charattrs) tbuf->charattrs[tbuf->len] = 0;
}
first = 0;
SStringcat(tbuf, constr);
}
conStringfree(constr);
}
tfclose(tfout);
FREE(frame);
frame = valptr(popval()); /* XXX optimize */
tfout = frame->outpipe ? frame->outpipe : frame->local_tfout;
tfin = frame->inpipe ? frame->inpipe : frame->local_tfin;
freeval(opd(0));
if (op_is_push(op)) {
if (!pushval(newSstr(CS(tbuf))))
goto prog_interpret_exit;
Stringfree(tbuf);
}
break;
case OP_PBUF:
constr = CS(buf);
buf = valptr(popval()); /* XXX optimize */
freeval(opd(0));
if (!pushval(newSstr(constr)))
goto prog_interpret_exit;
conStringfree(constr);
break;
case OP_AMAC:
case OP_PMAC:
if ((constr = prog->code[cip].arg.str)) {
constr->links++;
} else {
constr = CS(buf);
buf = valptr(popval()); /* XXX optimize */
freeval(opd(0));
}
if (!(cstr = macro_body(constr->data))) {
tfprintf(tferr, "%% macro not defined: %S", constr);
}
if (op_is_push(op)) {
if (!pushval(newstr(cstr ? cstr : "", -1)))
goto prog_interpret_exit;
} else if (cstr) {
Stringcat(buf, cstr);
}
if (mecho > invis_flag)
tfprintf(tferr, "%S$%S --> %s%A", do_mprefix(), constr, cstr,
mecho_attr);
conStringfree(constr);
break;
case OP_AVAR:
(val = (prog->code[cip].arg.val))->count++;
if (!(val2 = hgetnearestvarval(val))) {
constr = blankline;
if (patmatch(&looks_like_special_sub_ic, NULL, val->name)) {
char upper[64];
int i;
for (i = 0; i < sizeof(upper) && val->name[i]; i++)
upper[i] = ucase(val->name[i]);
wprintf("\"%%{%s}\" is a variable substitution, "
"and is not the same as special substitution "
"\"%%{%.*s}\".", val->name, i, upper);
}
} else {
constr = valstr(val2);
}
empty = !constr->len;
SStringcat(buf, constr);
freeval(val);
break;
case OP_PVAR:
(val = (prog->code[cip].arg.val))->count++;
(val = valval(val))->count++;
if (!pushval(val))
goto prog_interpret_exit;
empty = (val->type & TYPE_STR && !val->sval->len);
freeval(val);
break;
case OP_AREG:
empty = (regsubstr(buf, prog->code[cip].arg.i) <= 0);
break;
case OP_PREG:
str = Stringnew(NULL, 0, 0);
empty = (regsubstr(str, prog->code[cip].arg.i) <= 0);
if (empty) Stringcat(str, "");
if (!pushval(newSstr(CS(str))))
goto prog_interpret_exit;
break;
case OP_APARM:
first = prog->code[cip].arg.i - 1;
if (!do_parmsub(buf, first, first, &empty))
goto prog_interpret_exit;
break;
case OP_PPARM:
first = prog->code[cip].arg.i - 1;
if (!do_parmsub(NULL, first, first, &empty))
goto prog_interpret_exit;
break;
case OP_AXPARM:
if (!do_parmsub(buf, prog->code[cip].arg.i, tf_argc - 1, &empty))
goto prog_interpret_exit;
break;
case OP_PXPARM:
if (!do_parmsub(NULL, prog->code[cip].arg.i, tf_argc - 1, &empty))
goto prog_interpret_exit;
break;
case OP_ALPARM:
first = tf_argc - prog->code[cip].arg.i;
if (!do_parmsub(buf, first, first, &empty))
goto prog_interpret_exit;
break;
case OP_PLPARM:
first = tf_argc - prog->code[cip].arg.i;
if (!do_parmsub(NULL, first, first, &empty))
goto prog_interpret_exit;
break;
case OP_ALXPARM:
last = tf_argc - prog->code[cip].arg.i - 1;
if (!do_parmsub(buf, 0, last, &empty))
goto prog_interpret_exit;
break;
case OP_PLXPARM:
last = tf_argc - prog->code[cip].arg.i - 1;
if (!do_parmsub(NULL, 0, last, &empty))
goto prog_interpret_exit;
break;
case OP_APARM_CNT:
empty = 0;
Sappendf(buf, "%d", tf_argc);
break;
case OP_PPARM_CNT:
empty = 0;
if (!pushval(newint(tf_argc)))
goto prog_interpret_exit;
break;
case OP_ARESULT:
empty = 0;
SStringcat(buf, valstr(user_result));
break;
case OP_PRESULT:
empty = 0;
if (!pushval(user_result))
goto prog_interpret_exit;
user_result->count++;
break;
case OP_ACMDNAME:
if (!(empty = !(current_command && *current_command != '\b')))
Stringcat(buf, current_command);
break;
case OP_PCMDNAME:
if ((empty = !(current_command && *current_command != '\b')))
val = shareval(val_blank);
else
val = newstr(current_command, -1);
if (!pushval(val))
goto prog_interpret_exit;
break;
case OP_APARM_ALL:
if (!do_parmsub(buf, 0, tf_argc - 1, &empty))
goto prog_interpret_exit;
break;
case OP_PPARM_ALL:
if (!do_parmsub(NULL, 0, tf_argc - 1, &empty))
goto prog_interpret_exit;
break;
case OP_APARM_RND:
n = tf_argc ? RRAND(0, tf_argc - 1) : -1;
if (!do_parmsub(buf, n, n, &empty))
goto prog_interpret_exit;
break;
case OP_PPARM_RND:
n = tf_argc ? RRAND(0, tf_argc - 1) : -1;
if (!do_parmsub(NULL, n, n, &empty))
goto prog_interpret_exit;
break;
case OP_DUP: /* duplicate the ARGth item from the top of the stack */
(val = opd(prog->code[cip].arg.i))->count++;
if (!pushval(val)) goto prog_interpret_exit;
break;
case OP_POP: /* argument is ignored */
freeval(popval());
break;
case OP_PUSH: /* push (Value*)ARG onto stack */
(val = (prog->code[cip].arg.val))->count++;
if (!pushval(val)) goto prog_interpret_exit;
break;
case OP_ENDIF:
case OP_DONE:
case OP_NOP:
/* no op: place holders for mecho pointers */
break;
default:
internal_error(__FILE__, __LINE__, "invalid opcode 0x%04X at %d",
op, cip);
goto prog_interpret_exit;
}
}
if (stacktop == stackbot + in_expr) {
if (in_expr) {
result = popval();
} else {
/* Note: a macro prog may never set user_result, but have no
* errors either; in that case we must set user_result here. */
if (!user_result) user_result = shareval(val_blank);
result = user_result;
}
} else {
eprintf("expression stack underflow or dirty (%+d)",
stacktop - (stackbot + in_expr));
}
if (frame != &first_frame)
internal_error(__FILE__, __LINE__, "invalid frame");
prog_interpret_exit:
frame = &first_frame; /* if loop aborted, frame may be wrong */
tfin = frame->orig_tfin;
tfout = frame->orig_tfout;
while (stacktop > stackbot)
freeval(popval());
Stringfree(buf);
return result;
}
int prog_run(const Program *prog, const String *args, int offset,
const char *name, int kbnumlocal)
{
Arg *true_argv = NULL; /* unshifted argument vector */
int saved_cmdsub, saved_argc, saved_argtop;
Arg *saved_argv;
const conString *saved_argstring;
const char *saved_command;
TFILE *saved_tfin, *saved_tfout, *saved_tferr;
List scope[1];
if (++recur_count > max_recur && max_recur) {
eprintf("recursion count exceeded %max_recur (%d)", max_recur);
recur_count--;
return 0;
}
saved_command = current_command;
saved_cmdsub = cmdsub_count;
saved_argstring = argstring;
saved_argc = tf_argc;
saved_argv = tf_argv;
saved_argtop = argtop;
saved_tfin = tfin;
saved_tfout = tfout;
saved_tferr = tferr;
if (name) current_command = name;
cmdsub_count = 0;
pushvarscope(scope);
if (kbnumlocal) {
/* TODO: make this local %kbnum const */
setlocalintvar("kbnum", kbnumlocal);
}
if (args) {
argstring = (const conString*)args;
tf_argc = stringvec(args, offset, &tf_argv, 20);
true_argv = tf_argv;
argtop = 0;
}
/* else, leave argstring, tf_argv, and tv_argc alone, so /eval body
* inherits positional parameters */
if (tf_argc >= 0) {
if (!prog_interpret(prog, !name)) set_user_result(NULL);
}
if (true_argv) FREE(true_argv);
popvarscope();
tfin = saved_tfin;
tfout = saved_tfout;
tferr = saved_tferr;
cmdsub_count = saved_cmdsub;
tf_argc = saved_argc;
tf_argv = saved_argv;
argstring = saved_argstring;
argtop = saved_argtop;
current_command = saved_command;
recur_count--;
return !!user_result;
}
String *do_mprefix(void)
{
STATIC_BUFFER(buffer);
int i;
Stringtrunc(buffer, 0);
for (i = 0; i < recur_count + cmdsub_count; i++)
SStringcat(buffer, mprefix);
Stringadd(buffer, ' ');
if (current_command) {
if (*current_command == '\b') {
Sappendf(buffer, "%s: ", current_command+1);
} else {
Sappendf(buffer, "/%s: ", current_command);
}
}
return buffer;
}
#define keyword_mprefix() \
{ \
if (mecho > invis_flag) \
tfprintf(tferr, "%S/%s%A", do_mprefix(), keyword_label(block), mecho_attr);\
}
static void code_mark(Program *prog, const char *mark)
{
int i;
prog->mark = mark;
for (i = prog->len - 1; i >= 0; i--) {
if (prog->code[i].start && !prog->code[i].end) {
prog->code[i].end = mark;
break;
}
}
}
static void vcode_add(Program *prog, opcode_t op, int use_mark, va_list ap)
{
Instruction *inst;
/* The -1 in the condition, and zeroing of instructions, are to allow
* the use of comefrom() on code that hasn't been emitted yet. */
if (prog->len >= prog->size - 1) {
prog->size += 20;
prog->code = XREALLOC(prog->code, prog->size * sizeof(Instruction));
memset(&prog->code[prog->size - 20], 0, 20 * sizeof(Instruction));
}
inst = &prog->code[prog->len];
prog->len++;
inst->start = inst->end = NULL;
inst->op = op;
switch (op_arg_type(op)) {
case OPA_INT: inst->arg.i = va_arg(ap, int); break;
case OPA_CHAR: inst->arg.c = (char)va_arg(ap, int); break;
case OPA_STRP: inst->arg.str = va_arg(ap, conString *); break;
case OPA_VALP: inst->arg.val = va_arg(ap, Value *); break;
case OPA_CMDP: inst->arg.cmd = va_arg(ap, BuiltinCmd *); break;
default: inst->arg.i = 0;
}
if (prog->mark && use_mark) {
inst->start = prog->mark;
prog->mark = NULL;
}
if (!prog->optimize)
return;
#if 1 /* optimizer */
#define inst_is_const(inst) \
((inst)->op == OP_PUSH && (inst)->arg.val->type != TYPE_ID)
while (1) {
if (inst->comefroms) {
/* Something jumps to this instruction, so we can't consolidate it
* with any instruction before it. */
return;
}
switch (op_type(inst->op)) {
case OPT_EXPR:
/* An expression operator with no side effects and compile-time
* constant operands can be reduced at compile time.
*/
/* e.g. {PUSH 3; PUSH 4; +;} to {PUSH 7} */
if (!op_has_sideeffect(inst->op)) {
int i, n;
int old_stacktop = stacktop;
n = inst->arg.i;
for (i = prog->len - n - 1; i < prog->len - 1; i++) {
if (!inst_is_const(prog->code+i) || prog->code[i+1].comefroms)
return;
}
for (i = prog->len - n - 1; i < prog->len - 1; i++) {
if (!pushval(prog->code[i].arg.val))
goto const_expr_error;
prog->code[i].arg.val = NULL;
}
if (!reduce(inst->op, n))
goto const_expr_error;
prog->len -= n;
inst = &prog->code[prog->len - 1];
/* inst->op = OP_PUSH; */ /* already true */
inst->arg.val = popval();
continue;
const_expr_error:
while (stacktop > old_stacktop)
freeval(popval());
return /* 0 */; /* XXX */
}
return;
case OPT_CTRL:
if (prog->len <= 1)
return;
if (inst->op == OP_APPEND && inst->arg.str) {
/* {APPEND "x";} */
if (inst->arg.str->len == 0) {
/* {APPEND "";} to {} */
prog->len--;
inst--;
continue;
} else if (inst[-1].op == OP_APPEND && inst[-1].arg.str) {
/* e.g. {APPEND "x"; APPEND "y";} to {APPEND "xy";} */
String *str;
prog->len--;
inst--;
/* XXX optimize - don't need to dup inst->arg.str */
str = SStringcat(Stringdup(inst->arg.str), inst[1].arg.str);
conStringfree(inst[0].arg.str);
conStringfree(inst[1].arg.str);
(inst[0].arg.str = CS(str))->links++;
/* inst->op = OP_APPEND; */ /* already true */
continue;
}
} else if (inst->op == OP_APPEND && !inst->arg.str) {
/* {APPEND NULL;} */
if (op_type_is(inst[-1].op, SUB) && op_is_push(inst[-1].op)) {
/* e.g. {PPARM 1; APPEND NULL;} to {APARM 1;} */
prog->len--;
inst--;
inst->op |= OPF_APP;
continue;
} else if (inst_is_const(inst-1)) {
/* e.g. {PUSH "x"; APPEND NULL;} to {APPEND "x";} */
Value *val = inst[-1].arg.val;
prog->len--;
inst--;
inst->op = inst[1].op;
(inst->arg.str = valstr(val))->links++;
freeval(val);
continue;
}
} else if (op_arg_type_is(inst->op, STRP) && !inst->arg.str &&
inst[-1].op == OP_APPEND && inst[-1].arg.str)
{
/* e.g. {APPEND string; MACRO NULL;} to {MACRO string;} */
/* but only if not preceeded by other append operators */
if (prog->len > 2 &&
(inst[-2].op == OP_APPEND /* could be {APPEND NULL;} */ ||
(op_type_is(inst[-2].op, SUB) && op_is_append(inst[-2].op))))
{
return;
}
prog->len--;
inst--;
inst->op = inst[1].op;
/* keep inst->arg.str */
continue;
} else if (op_arg_type_is(inst->op, VALP) && !inst->arg.val &&
inst_is_const(inst-1))
{
/* e.g., {PUSH val; TEST NULL;} to {TEST val;} */
prog->len--;
inst--;
inst->op = inst[1].op;
/* inst->arg.val = inst[1].arg.val; */ /* already true */
continue;
}
return;
case OPT_JUMP:
if (prog->len <= 1)
return;
if (opnum_eq(inst->op, OP_JZ)) {
if (inst[-1].op == '!') {
/* e.g., {! 1; JZ x;} to {JNZ x;} */
prog->len--;
inst--;
inst->op = inst[1].op ^ OPF_NEG;
inst->arg = inst[1].arg;
continue;
} else if (inst_is_const(inst-1)) {
/* e.g., {PUSH INT 0; JZ x;} to {JUMP x;} */
/* e.g., {PUSH INT 1; JZ x;} to {} */
int flag;
prog->len--;
inst--;
flag = valbool(inst->arg.val);
freeval(inst->arg.val);
if (!flag == !(inst[1].op & OPF_NEG)) {
inst->op = OP_JUMP;
inst->arg.i = inst[1].arg.i;
} else {
prog->len--;
inst--;
}
continue;
}
}
return;
default:
/* sub of a macro whose name is compile-time constant */
if (opnum_eq(inst->op, OP_PMAC) &&
!inst->arg.str && prog->len > 2 && inst[-2].op == OP_PUSHBUF &&
inst[-1].op == OP_APPEND)
{
/* e.g. {PUSHBUF; APPEND name; PMAC NULL} to {PMAC name} */
inst[-2].op = inst->op;
inst[-2].arg.str = inst[-1].arg.str;
prog->len -= 2;
continue;
}
if (inst->op == OP_PBUF && !inst->arg.str && prog->len > 2 &&
inst[-2].op == OP_PUSHBUF && inst[-1].op == OP_APPEND)
{
/* {PUSHBUF; APPEND text; PBUF NULL} to {PUSH text} */
inst[-2].op = OP_PUSH;
inst[-2].arg.val = newSstr(inst[-1].arg.str);
prog->len -= 2;
continue;
}
return;
}
}
#endif /* optimizer */
}
void code_add(Program *prog, opcode_t op, ...)
{
va_list ap;
va_start(ap, op);
vcode_add(prog, op, 1, ap);
va_end(ap);
}
static void code_add_nomecho(Program *prog, opcode_t op, ...)
{
va_list ap;
va_start(ap, op);
vcode_add(prog, op, 0, ap);
va_end(ap);
}
void eat_newline(Program *prog)
{
while (ip[0] == '\\' && ip[1] == '\n') {
loadstart++;
for (ip += 2; is_space(*ip); ip++);
}
}
void eat_space(Program *prog)
{
while (is_space(*ip)) ip++;
eat_newline(prog);
}
static inline const char *keyword_label(keyword_id_t id)
{
return keyword_table[id - BREAK];
}
static void unexpected(keyword_id_t innerblock, keyword_id_t outerblock)
{
/* THEN and DO blocks don't start with a /then or /do if they belong to a
* /if () or /while (), so "THEN block" and "DO block" would be confusing
* to user. */
switch (outerblock) {
case THEN:
eprintf("unexpected /%s in IF body", keyword_label(innerblock));
break;
case DO:
eprintf("unexpected /%s in WHILE body", keyword_label(innerblock));
break;
default:
eprintf("unexpected /%s in %s block", keyword_label(innerblock),
outerblock ? keyword_label(outerblock) : "outer");
}
}
static int list(Program *prog, int subs)
{
keyword_id_t oldblock;
int is_a_command, is_a_condition, is_special;
int failed = 0, result = 0;
const char *stmtstart;
int is_pipe = 0;
int block_start, jump_point, oldlen;
/* Do NOT strip leading space here. This allows user to type and send
* lines with leading spaces (but completely empty lines are handled
* by handle_input_line()). During expansion, spaces AFTER a "%;"
* or keyword will be skipped.
*/
if (block == WHILE || block == IF) {
block_start = prog->len;
}
do /* while (*ip) */ {
if (subs >= SUB_NEWLINE) {
while (is_space(*ip) || (ip[0] == '\\' && is_space(ip[1])))
++ip;
eat_newline(prog);
}
is_special = is_a_command = is_a_condition = FALSE;
/* Lines begining with one "/" are tf commands. Lines beginning
* with multiple "/"s have the first removed, and are sent to server.
*/
if ((subs > SUB_LITERAL) && (*ip == '/') && (*++ip != '/')) {
is_a_command = TRUE;
oldblock = block;
if (subs >= SUB_KEYWORD) {
stmtstart = ip;
is_special = block = keyword_parse(prog);
}
} else if ((subs > SUB_LITERAL) &&
(block == IF || block == ELSEIF || block == WHILE))
{
if (*ip == '(') {
is_a_condition = TRUE;
oldblock = block;
ip++; /* skip '(' */
if (!expr(prog)) goto list_exit;
if (*ip != ')') {
parse_error(prog, "condition", "operator or ')'");
goto list_exit;
}
++ip; /* skip ')' */
eat_space(prog);
if (is_end_of_statement(ip)) {
wprintf("\"%2.2s\" following \"/%s (...)\" "
"sends blank line to server, "
"which is probably not what was intended.",
ip, keyword_label(block));
}
block = (block == WHILE) ? DO : THEN;
} else if (*ip) {
wprintf("statement starting with %s in /%s "
"condition sends text to server, "
"which is probably not what was intended.",
error_text(prog), keyword_label(block));
}
}
if (is_a_command || is_a_condition) {
switch(block) {
case WHILE:
if (subs == SUB_KEYWORD)
subs = SUB_NEWLINE;
if (!list(prog, subs)) failed = 1;
else if (block == WHILE) {
parse_error(prog, "macro", "/do");
failed = 1;
} else if (block == DO) {
parse_error(prog, "macro", "/done");
failed = 1;
}
block = oldblock;
if (failed) goto list_exit;
break;
case DO:
if (oldblock != WHILE) {
unexpected(block, oldblock);
block = oldblock;
goto list_exit;
}
oldlen = prog->len;
code_add(prog, is_a_condition ? OP_JZ : OP_JRZ, -1);
code_mark(prog, ip);
if (prog->len > oldlen) /* code was emitted */
jump_point = prog->len - 1;
else /* code was completely optimized away */
jump_point = - 1;
continue;
case BREAK:
{
int levels =
(!*ip || is_end_of_statement(ip) || is_end_of_cmdsub(ip)) ?
1 : strtoint(ip, &ip);
if (levels <= 0) {
parse_error(prog, "/BREAK", "positive integer");
block = oldblock;
goto list_exit;
}
code_add_nomecho(prog, OP_JUMP, -levels);
block = oldblock;
if (is_end_of_statement(ip)) {
ip += 2;
eat_space(prog);
}
code_mark(prog, ip); /* XXX ??? */
continue;
}
case DONE:
if (oldblock != DO) {
unexpected(block, oldblock);
block = oldblock;
goto list_exit;
}
code_add_nomecho(prog, OP_JUMP, block_start);
/* fill in the jump address for each BREAK */
{
int i;
for (i = block_start; i < prog->len; i++) {
if (op_type_is(prog->code[i].op, JUMP) &&
prog->code[i].arg.i < 0 &&
prog->code[i].arg.i != ENDIF_PLACEHOLDER)
{
if (prog->code[i].arg.i == -1)
prog->code[i].arg.i = prog->len;
else
prog->code[i].arg.i++;
}
}
}
code_add(prog, OP_DONE, 0);
comefrom(prog, jump_point, prog->len - 1);
eat_space(prog);
code_mark(prog, ip);
result = 1; goto list_exit;
case IF:
if (subs == SUB_KEYWORD)
subs = SUB_NEWLINE;
if (!list(prog, subs)) {
failed = 1;
} else if (block == IF || block == ELSEIF) {
parse_error(prog, "macro", "/then");
failed = 1;
} else if (block == THEN || block == ELSE) {
parse_error(prog, "macro", "/endif");
failed = 1;
}
block = oldblock;
if (failed) goto list_exit;
code_mark(prog, ip);
break;
case THEN:
if (oldblock != IF && oldblock != ELSEIF) {
unexpected(block, oldblock);
block = oldblock;
goto list_exit;
}
code_add_nomecho(prog, is_a_condition ? OP_JZ : OP_JRZ,
ENDIF_PLACEHOLDER);
code_mark(prog, ip);
if (prog->len > 0 &&
op_type_is(prog->code[prog->len - 1].op, JUMP))
{
jump_point = prog->len - 1;
} else { /* jump was completely optimized away */
jump_point = -1;
}
continue;
case ELSEIF:
case ELSE:
if (oldblock != THEN) {
unexpected(block, oldblock);
block = oldblock;
goto list_exit;
}
code_add_nomecho(prog, OP_JUMP, ENDIF_PLACEHOLDER);
comefrom(prog, jump_point, prog->len);
eat_space(prog);
if (block == ELSE && is_end_of_statement(ip)) {
wprintf("\"%2.2s\" following \"/%s\" "
"sends blank line to server, "
"which is probably not what was intended.",
ip, keyword_label(block));
}
continue;
case ENDIF:
if (oldblock != THEN && oldblock != ELSE) {
unexpected(block, oldblock);
block = oldblock;
goto list_exit;
}
eat_space(prog);
/* fill in the jump address after each THEN block */
{
int i;
for (i = block_start; i < prog->len; i++) {
if (op_type_is(prog->code[i].op, JUMP) &&
prog->code[i].arg.i == ENDIF_PLACEHOLDER)
{
prog->code[i].arg.i = prog->len;
}
}
}
code_add(prog, OP_ENDIF); /* no-op, to hold mecho pointers */
result = 1; goto list_exit;
case LET:
case SET:
case SETENV:
{
String *dest;
Value *val;
const char *start = ip, *end;
while (is_space(*start)) start++;
end = spanvar(start);
ip = end;
if (setdelim(&ip) < 1) {
/* "invalid" name may expand to valid name at runtime */
goto noncontrol;
}
(dest = Stringnew(NULL, 1, 0))->links++;
if (!(result = expand(prog, dest, subs, NULL)))
goto list_exit;
val = newid(start, end - start);
code_add(prog, OP_ARG, result == 1 ? dest : NULL);
switch (block) {
case LET: code_add(prog, OP_LET, val); break;
case SET: code_add(prog, OP_SET, val); break;
case SETENV: code_add(prog, OP_SETENV, val); break;
default: /* impossible */ break;
}
block = oldblock;
break;
}
case RETURN:
case RESULT:
if (cmdsub_count) {
eprintf("/%s may be called only directly from a macro, "
"not in $() command substitution.",
keyword_label(block));
goto list_exit;
}
/* FALL THROUGH */
case TEST:
{
Value *val = NULL;
String *dest;
/* len=1 forces data allocation (expected by prog_interpret) */
(dest = Stringnew(NULL, 1, 0))->links++;
result = expand(prog, dest, subs, NULL);
if (!result)
goto list_exit;
if (result == 2) {
/* expr is generated at runtime, must defer compilation */
code_add(prog, OP_EXPR, NULL);
} else {
/* expression is fixed, can compile it now */
const char *saved_ip = ip;
int exprstart = prog->len; /* start of expr code in prog */
ip = dest->data;
eat_space(prog);
if (!*ip) {
val = shareval(val_zero);
} else if (!expr(prog)) {
prog_free_tail(prog, exprstart); /* rm bad expr code */
val = shareval(val_zero);
} else if (*ip) {
parse_error(prog, "expression", "end of expression");
prog_free_tail(prog, exprstart); /* rm bad expr code */
val = shareval(val_zero);
}
ip = saved_ip; /* XXX need to restore line number too */
/* XXX is this safe? prog may have debug ptrs into dest. */
Stringfree(dest);
}
switch (block) {
case TEST: code_add(prog, OP_TEST, val); break;
case RETURN: code_add(prog, OP_RETURN, val); break;
case RESULT: code_add(prog, OP_RESULT, val); break;
default: /* impossible */ break;
}
block = oldblock;
break;
}
default:
noncontrol:
/* not a control statement */
ip = stmtstart - 1;
is_special = 0;
block = oldblock;
if (!statement(prog, subs)) goto list_exit;
break;
}
} else /* !(is_a_command || is_a_condition) */ {
if (is_pipe) {
eprintf("Piping input to a server command is not allowed.");
goto list_exit;
}
if (!statement(prog, subs))
goto list_exit;
}
is_pipe = (ip[0] == '%' && ip[1] == '|'); /* this stmnt pipes to next */
if (is_pipe) {
Instruction tmp;
if (!is_a_command) {
eprintf("Piping output of a server command is not allowed.");
goto list_exit;
} else if (is_special) {
eprintf("Piping output of a special command is not allowed.");
goto list_exit;
}
/* OP_PIPE needs to go BEFORE the exec operator, so we use
* code_add() to add it, then swap the last two instructions. */
code_add(prog, OP_PIPE);
tmp = prog->code[prog->len - 1];
prog->code[prog->len - 1] = prog->code[prog->len - 2];
prog->code[prog->len - 2] = tmp;
}
if (is_end_of_cmdsub(ip)) {
break;
} else if (is_end_of_statement(ip)) {
ip += 2;
eat_space(prog);
code_mark(prog, ip);
} else if (*ip) {
const char *expect;
switch (is_special) {
case IF: expect = "end of statement after /endif"; break;
case WHILE: expect = "end of statement after /done"; break;
default: expect = "end of statement"; break;
}
parse_error_suggest(prog, "macro", expect, "Possibly missing '%;'");
goto list_exit;
}
} while (*ip);
code_mark(prog, ip);
result = 1;
if (is_pipe) {
eprintf("'%|' must be followed by another command.");
result = 0;
}
list_exit:
return !!result;
}
const char **keyword(const char *id)
{
return (const char **)bsearch((void*)id, (void*)keyword_table,
sizeof(keyword_table)/sizeof(char*), sizeof(char*), cstrstructcmp);
}
static keyword_id_t keyword_parse(Program *prog)
{
const char **result, *start, *end;
char buf[KEYWORD_LENGTH+1];
start = (*ip == '@') ? ip + 1 : ip;
if (!is_keystart(*start)) return 0; /* fast heuristic */
end = start + 1;
while (*end && !is_space(*end) && *end != '%' && *end != ')') {
if (++end - start > KEYWORD_LENGTH)
return 0; /* too long to be keyword */
}
/* XXX stupid copy */
strncpy(buf, start, end - start);
buf[end - start] = '\0';
if (!(result = keyword(buf)))
return 0;
for (ip = end; is_space(*ip); ip++);
return BREAK + (result - keyword_table);
}
/* percentsub() and dollarsub() append to the compile-time buffer *destp if
* source is compile-time constant; otherwise, it emits {APPEND *destp},
* creates a new compile-time buffer, and emits code for the sub.
*/
static int percentsub(Program *prog, int subs, String **destp)
{
int result = 1;
if (*ip == '%') {
while (*ip == '%') Stringadd(*destp, *ip++);
} else if (!*ip || is_space(*ip)) {
/* "% " and "%" at end of line are left alone */
Stringadd(*destp, '%');
} else if (subs >= SUB_FULL) {
if ((*destp)->len) {
code_add(prog, OP_APPEND, *destp);
(*destp = Stringnew(NULL, 0, 0))->links++;
}
result = varsub(prog, 0, 0);
} else {
Stringadd(*destp, '%');
}
return result;
}
int dollarsub(Program *prog, String **destp)
{
int result = 1;
if (*ip == '$' && destp) {
while (*ip == '$') Stringadd(*destp, *ip++);
} else {
if (destp && (*destp)->len) {
code_add(prog, OP_APPEND, *destp);
(*destp = Stringnew(NULL, 0, 0))->links++;
}
result = ((*ip == '[') ? exprsub(prog, !destp) :
(*ip == '(') ? cmdsub(prog, !destp) :
macsub(prog, !destp));
}
return result;
}
/* returns: 0=error, 1=static, 2=dynamic */
static int expand(Program *prog, String *dest, int subs, opcmd_t *opcmdp)
{
const char *start;
int error = 0;
int orig_len = prog->len;
while (*ip) {
eat_newline(prog);
/* XXX The '\\' test used to be >= SUB_NEWLINE when this code was
* part of statement(), but when test/return/result were implemented
* as real keywords, things like
* /test echo('\\.')
* gave the wrong results. Changing it to SUB_FULL fixed that problem,
* but it fails to generate a 'legal escapes' warning for
* /test echo('\.')
* and may have caused other problems I haven't discovered yet.
*/
if (*ip == '\\' && subs >= SUB_FULL) {
++ip;
if (!backsub(prog, dest)) { error++; break; }
} else if (*ip == '/' && subs >= SUB_FULL) {
++ip;
if (!slashsub(prog, dest)) { error++; break; }
} else if (*ip == '%' && subs >= SUB_NEWLINE) {
if (is_end_of_statement(ip)) {
while (dest->len && is_space(dest->data[dest->len-1]))
Stringtrunc(dest, dest->len-1); /* nuke spaces before %; */
break;
}
++ip;
if (!percentsub(prog, subs, &dest)) { error++; break; }
if (prog->len != orig_len) opcmdp = NULL;
} else if (*ip == '$' && subs >= SUB_FULL) {
++ip;
if (!dollarsub(prog, &dest)) { error++; break; }
if (prog->len != orig_len) opcmdp = NULL;
} else if (subs >= SUB_FULL && is_end_of_cmdsub(ip)) {
break;
} else {
/* is_statmeta() is much faster than all those if statements. */
start = ip++;
while (*ip && !is_statmeta(*ip) && !(is_space(*ip) && opcmdp))
ip++;
SStringoncat(dest, prog->src, start - prog->src->data, ip - start);
if (opcmdp && (!*ip || is_space(*ip) || is_end_of_statement(ip))) {
/* command name is constant, can be resolved at compile time */
int i, truth = 1;
for (i = 0; dest->data[i] == '!'; i++)
truth = !truth;
opcmdp->op = (dest->data[i] == '@') ? (++i, OP_BUILTIN) :
OP_COMMAND;
if ((opcmdp->ptr = find_builtin_cmd(dest->data + i))) {
/* BUILTIN or COMMAND: ptr is CMD builtin */
} else if (opcmdp->op == OP_BUILTIN) {
eprintf("%s: not a builtin command", dest->data + i);
error++;
break;
} else {
/* MACRO: ptr is ID name */
Value *val = newid(dest->data + i, dest->len - i);
if (dest->data[i] == '#') /* macro_hash() */
val->u.hash = atoi(dest->data + i + 1);
opcmdp->ptr = val;
opcmdp->op = OP_MACRO;
}
Stringtrunc(dest, 0);
while (is_space(*ip)) { ip++; }
if (!truth)
opcmdp->op |= OPF_NEG;
opcmdp = NULL;
}
}
}
if (error) {
Stringfree(dest);
return 0; /* error */
} else if (prog->len == orig_len) {
return 1; /* static */
} else {
/* text is generated at runtime */
if (dest->len) {
code_add(prog, OP_APPEND, dest);
} else {
Stringfree(dest);
}
dest = NULL;
return 2; /* dynamic */
}
}
static int statement(Program *prog, int subs)
{
String *dest;
int result;
opcmd_t opcmd, *opcmdp = NULL;
opcmd.ptr = NULL;
if (ip[0] != '/' || subs <= SUB_LITERAL) {
opcmd.op = OP_SEND;
} else if (ip[1] == '/') {
ip++;
opcmd.op = OP_SEND;
} else {
ip++;
opcmd.op = OP_EXECUTE;
opcmdp = &opcmd;
}
/* len=1 forces dest->data to be allocated (expected by prog_interpret) */
(dest = Stringnew(NULL, 1, 0))->links++;
result = expand(prog, dest, subs, opcmdp);
if (!result)
return 0;
if (opcmd.ptr) {
code_add(prog, OP_ARG, result == 1 ? dest : NULL);
code_add(prog, opcmd.op, opcmd.ptr);
} else {
code_add(prog, opcmd.op, result == 1 ? dest : NULL);
}
return result;
}
static int slashsub(Program *prog, String *dest)
{
if (*ip == '/' && oldslash)
while (*ip == '/') Stringadd(dest, *ip++);
else
Stringadd(dest, '/');
return 1;
}
static const char *error_text(Program *prog)
{
STATIC_BUFFER(buf);
if (*ip) {
const char *end = ip + 1;
if (is_alnum(*ip) || is_quote(*ip) || *ip == '/') {
while (is_alnum(*end)) end++;
}
Sprintf(buf, "'%.*s'", end - ip, ip);
return buf->data;
} else {
return "end of body";
}
}
void parse_error(Program *prog, const char *type, const char *expect)
{
eprintf("%s syntax error: expected %s, found %s.",
type, expect, error_text(prog));
}
void parse_error_suggest(Program *prog, const char *type, const char *expect,
const char *suggestion)
{
eprintf("%s syntax error: expected %s, found %s. (%s)",
type, expect, error_text(prog), suggestion);
}
int exprsub(Program *prog, int in_expr)
{
int result = 0;
ip++; /* skip '[' */
eat_space(prog);
if (!expr(prog)) return 0;
if (!*ip || is_end_of_statement(ip)) {
eprintf("unmatched $[");
} else if (*ip != ']') {
parse_error(prog, "expression", "operator or ']'");
} else {
if (!in_expr)
code_add(prog, OP_APPEND, NULL);
++ip;
result = 1;
}
return result;
}
static int cmdsub(Program *prog, int in_expr)
{
int result;
const char *saved_mark;
code_add(prog, OP_CMDSUB);
code_add(prog, OP_PUSHBUF, 0);
cmdsub_count++;
ip++; /* skip '(' */
saved_mark = prog->mark;
prog->mark = ip;
result = list(prog, SUB_MACRO);
prog->mark = saved_mark;
cmdsub_count--;
code_add(prog, OP_POPBUF, 0);
code_add(prog, in_expr ? OP_PCMDSUB : OP_ACMDSUB);
if (*ip != ')') {
eprintf("unmatched (");
return 0;
}
ip++;
return result;
}
static int macsub(Program *prog, int in_expr)
{
const char *s;
int bracket;
String *name;
code_add(prog, OP_PUSHBUF, 0);
(name = Stringnew(NULL, 0, 0))->links++;
if ((bracket = (*ip == '{'))) ip++;
while (*ip) {
if (*ip == '\\') {
++ip;
if (!backsub(prog, name)) goto macsub_err;
} else if (is_end_of_statement(ip) || is_end_of_cmdsub(ip)) {
break;
} else if (*ip == '/') {
++ip;
if (!slashsub(prog, name)) goto macsub_err;
} else if (*ip == '}') {
/* note: in case of "%{var-$mac}", we break even if !bracket. */
/* Users shouldn't use '}' in macro names anyway. */
break;
} else if (!bracket && is_space(*ip)) {
break;
} else if (*ip == '$') {
if (ip[1] == '$') {
while(*++ip == '$') Stringadd(name, *ip);
} else {
if (!bracket) break;
else Stringadd(name, *ip++);
}
} else if (*ip == '%') {
++ip;
if (!percentsub(prog, SUB_FULL, &name)) goto macsub_err;
} else {
for (s = ip++; *ip && !is_punct(*ip) && !is_space(*ip); ip++);
SStringoncat(name, prog->src, s - prog->src->data, ip - s);
}
}
if (bracket) {
if (*ip != '}') {
eprintf("unmatched ${");
goto macsub_err;
} else ip++;
} else if (*ip == '$') {
ip++;
}
code_add(prog, OP_APPEND, name);
code_add(prog, in_expr ? OP_PMAC : OP_AMAC, NULL);
return 1;
macsub_err:
Stringfree(name);
return 0;
}
static int backsub(Program *prog, String *dest)
{
if (is_digit(*ip)) {
char c = strtochr(ip, &ip);
Stringadd(dest, mapchar(c));
} else if (!backslash) {
Stringadd(dest, '\\');
} else if (*ip) {
Stringadd(dest, *ip++);
}
return 1;
}
int varsub(Program *prog, int sub_warn, int in_expr)
{
int result = 0;
const char *start, *contents;
int bracket, except = FALSE, ell = FALSE, pee = FALSE, star = FALSE, n = -1;
String *selector;
String *dest = NULL;
static int sub_warned = 0;
(selector = Stringnew(NULL, 0, 0))->links++;
contents = ip;
if ((bracket = (*ip == '{'))) ip++;
if (ip[0] == '#' && (!bracket || ip[1] == '}')) {
++ip;
code_add(prog, in_expr ? OP_PPARM_CNT : OP_APARM_CNT);
} else if (ip[0] == '?' && (!bracket || ip[1] == '}')) {
++ip;
code_add(prog, in_expr ? OP_PRESULT : OP_ARESULT);
} else {
if (is_digit(*ip)) {
start = ip;
n = strtoint(ip, &ip);
} else {
if ((except = (*ip == '-'))) {
++ip;
}
start = ip;
if ((ell = (*ip == 'L')) || (pee = (*ip == 'P')) ||
(star = (*ip == '*')))
{
++ip;
}
if (!star && is_digit(*ip)) {
n = strtoint(ip, &ip);
}
}
/* This is strange, for backward compatibility. Some examples:
* "%{1}x" == parameter "1" followed by literal "x".
* "%1x" == parameter "1" followed by literal "x".
* "%{1x}" == bad substitution.
* "%{L}x" == parameter "L" followed by literal "x".
* "%Lx" == variable "Lx".
* "%{Lx}" == variable "Lx".
* "%{L1}x" == parameter "L1" followed by literal "x".
* "%L1x" == parameter "L1" followed by literal "x".
* "%{L1x}" == variable "L1x".
*/
Stringtrunc(selector, 0);
if ((n < 0 && !star) || (bracket && (ell || pee))) {
/* is non-special, or could be non-special if followed by alnum_ */
if (is_alnum(*ip) || (*ip == '_')) {
ell = pee = FALSE;
n = -1;
do ip++; while (is_alnum(*ip) || *ip == '_');
Stringncpy(selector, start, ip - start);
}
}
if (star) {
code_add(prog, in_expr ? OP_PPARM_ALL : OP_APARM_ALL);
} else if (pee) {
if (n < 0) n = 1;
code_add(prog, in_expr ? OP_PREG : OP_AREG, n);
n = -1;
} else if (ell) {
if (n < 0) n = 1;
if (except)
code_add(prog, in_expr ? OP_PLXPARM : OP_ALXPARM, n);
else
code_add(prog, in_expr ? OP_PLPARM : OP_ALPARM, n);
except = 0; /* handled */
} else if (n > 0) {
if (except)
code_add(prog, in_expr ? OP_PXPARM : OP_AXPARM, n);
else
code_add(prog, in_expr ? OP_PPARM : OP_APARM, n);
except = 0; /* handled */
} else if (n == 0) {
code_add(prog, in_expr ? OP_PCMDNAME : OP_ACMDNAME);
} else if (strcmp(selector->data, "R") == 0) {
code_add(prog, in_expr ? OP_PPARM_RND : OP_APARM_RND);
} else if (strcmp(selector->data, "PL") == 0) {
code_add(prog, in_expr ? OP_PREG : OP_AREG, -1);
} else if (strcmp(selector->data, "PR") == 0) {
code_add(prog, in_expr ? OP_PREG : OP_AREG, -2);
} else {
Value *val = newid(selector->data, selector->len);
if (in_expr) {
code_add(prog, *ip == '-' ? OP_PVAR : OP_PUSH, val);
} else {
code_add(prog, OP_AVAR, val);
}
}
if (except) { /* unhandled leading '-' */
eprintf("illegal character '-' in substitution");
goto varsub_exit;
}
}
if (*ip == '-') {
int jump_point;
++ip;
code_add(prog, OP_JNEMPTY, -1);
jump_point = prog->len - 1;
if (in_expr) {
code_add(prog, OP_POP);
code_add(prog, OP_PUSHBUF, NULL);
}
(dest = Stringnew(NULL, 0, 0))->links++;
while (*ip) {
if (is_end_of_statement(ip) || is_end_of_cmdsub(ip)) {
break;
} else if (bracket && *ip == '}') {
break;
} else if (!bracket && is_space(*ip)) {
break;
} else if (*ip == '%') {
++ip;
if (!percentsub(prog, SUB_FULL, &dest)) goto varsub_exit;
} else if (*ip == '$') {
++ip;
if (!dollarsub(prog, &dest)) goto varsub_exit;
} else if (*ip == '/') {
++ip;
if (!slashsub(prog, dest)) goto varsub_exit;
} else if (*ip == '\\') {
if (ip[1] == '\n') {
eat_newline(prog);
} else {
++ip;
if (!backsub(prog, dest)) goto varsub_exit;
}
} else {
for (start = ip++; *ip && is_alnum(*ip); ip++);
SStringoncat(dest, prog->src, start - prog->src->data,
ip - start);
}
}
if (dest->len) {
code_add(prog, OP_APPEND, dest);
dest = NULL;
}
if (in_expr) {
code_add(prog, OP_PBUF, NULL);
}
comefrom(prog, jump_point, prog->len);
}
if (bracket) {
if (*ip != '}') {
if (!*ip) eprintf("unmatched { or bad substitution");
else eprintf("unmatched { or illegal character '%c'", *ip);
goto varsub_exit;
} else ip++;
}
result = 1;
if (sub_warn & (!sub_warned || pedantic)) {
sub_warned = 1;
wprintf("\"%%%.*s\" substitution in expression is legal, "
"but can be confusing. Try using \"{%.*s}\" instead.",
ip-contents, contents,
(ip-bracket)-(contents+bracket), contents+bracket);
}
varsub_exit:
if (dest) Stringfree(dest);
Stringfree(selector);
return result;
}
struct Value *handle_shift_command(String *args, int offset)
{
int count;
int error;
count = (args->len - offset) ? atoi(args->data + offset) : 1;
if (count < 0) return shareval(val_zero);
if ((error = (count > tf_argc))) count = tf_argc;
tf_argc -= count;
if (tf_argv) { /* true if macro was called as command, not as function */
tf_argv += count;
}
return newint(!error);
}
#if USE_DMALLOC
void free_expand()
{
freeval(user_result);
freeval(val_blank);
freeval(val_one);
freeval(val_zero);
}
#endif