/** * Convert a line number to an instruction pointer * * @private * @param program dbref * @param whatline the line to find an instruction pointer for * @return pointer to starting instruction for the given line */ static struct inst * linenum_to_pc(dbref program, int whatline) { int siz; struct inst *code; code = PROGRAM_CODE(program); siz = PROGRAM_SIZ(program); for (int i = 0; i < siz; i++) { if (code[i].line == whatline) { return (code + i); } } return (NULL); }
/** * Convert a function name to an instruction pointer * * @private * @param program the program dbref * @param name the procedure name to look for * @return pointer to start of procedure */ static struct inst * funcname_to_pc(dbref program, const char *name) { int siz; struct inst *code; code = PROGRAM_CODE(program); siz = PROGRAM_SIZ(program); for (int i = 0; i < siz; i++) { if ((code[i].type == PROG_FUNCTION) && !strcasecmp(name, code[i].data.mufproc->procname)) { return (code + i); } } return (NULL); }
/** * List the functions in a program * * Optionally provide a filter string that will match functions using * equalstr * * @see equalstr * * You cannot pass NULL into arg, you can pass empty string if you wish to * show all functions. * * @private * @param player the person to show the list of functions to * @param program the program to list functions from * @param arg a filter string or "" */ static void list_program_functions(dbref player, dbref program, char *arg) { struct inst *ptr; int count; ptr = PROGRAM_CODE(program); count = PROGRAM_SIZ(program); notify_nolisten(player, "*function words*", 1); while (count-- > 0) { if (ptr->type == PROG_FUNCTION) { if (ptr->data.mufproc) { if (!*arg || equalstr(arg, ptr->data.mufproc->procname)) { notify_nolisten(player, ptr->data.mufproc->procname, 1); } } } ptr++; } notify_nolisten(player, "*done*", 1); }
/** * Implementation of the MUF debugger * * This implements the command parsing for the MUF debugger. It also clears * temporary bookmarks if this was triggered from a temporary one. * * This relies on some static globals, so it is not threadsafe. If the * 'prim' debugger command is ever used to trigger the MUF debugger somehow * in a recursive fashion, and then you call 'prim' again, it will probably * cause havock. * * @param descr the descriptor of the debugging player * @param player the debugging player * @param program the program we are debugging * @param text the input text from the user * @param fr the current frame pointer * @return boolean true if the program should exit, false if not */ int muf_debugger(int descr, dbref player, dbref program, const char *text, struct frame *fr) { char cmd[BUFFER_LEN]; char buf[BUFFER_LEN]; char buf2[BUFFER_LEN]; char *ptr, *ptr2, *arg; struct inst *pinst; int i, j, cnt; static struct inst primset[5]; static struct muf_proc_data temp_muf_proc_data = { "__Temp_Debugger_Proc", 0, 0, NULL }; /* * Basic massaging of the input - clearing spaces, finding the * argument. */ skip_whitespace(&text); strcpyn(cmd, sizeof(cmd), text); ptr = cmd; remove_ending_whitespace(&ptr); for (arg = cmd; *arg && !isspace(*arg); arg++) ; if (*arg) *arg++ = '\0'; /* Empty command means repeat last command, if available */ if (!*cmd && fr->brkpt.lastcmd) { strcpyn(cmd, sizeof(cmd), fr->brkpt.lastcmd); } else { free(fr->brkpt.lastcmd); if (*cmd) fr->brkpt.lastcmd = strdup(cmd); } /* delete triggering breakpoint, if it's only temp. */ j = fr->brkpt.breaknum; if (j >= 0 && fr->brkpt.temp[j]) { for (j++; j < fr->brkpt.count; j++) { fr->brkpt.temp[j - 1] = fr->brkpt.temp[j]; fr->brkpt.level[j - 1] = fr->brkpt.level[j]; fr->brkpt.line[j - 1] = fr->brkpt.line[j]; fr->brkpt.linecount[j - 1] = fr->brkpt.linecount[j]; fr->brkpt.pc[j - 1] = fr->brkpt.pc[j]; fr->brkpt.pccount[j - 1] = fr->brkpt.pccount[j]; fr->brkpt.prog[j - 1] = fr->brkpt.prog[j]; } fr->brkpt.count--; } fr->brkpt.breaknum = -1; /** * @TODO This giant if statement is pretty gnarly; we'd be better * of looping over an array of callbacks to break this up * nicer and make it more extensible. */ if (!strcasecmp(cmd, "cont")) { /* Nothing to do -- this will continue to next breakpoint */ } else if (!strcasecmp(cmd, "finish")) { if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot finish because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = fr->system.top - 1; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = program; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "stepi")) { i = atoi(arg); if (!i) i = 1; if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot stepi because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = -1; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = i; fr->brkpt.prog[j] = NOTHING; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "step")) { i = atoi(arg); if (!i) i = 1; if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot step because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = -1; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = i; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = NOTHING; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "nexti")) { i = atoi(arg); if (!i) i = 1; if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot nexti because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = fr->system.top; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = i; fr->brkpt.prog[j] = program; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "next")) { i = atoi(arg); if (!i) i = 1; if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot next because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = fr->system.top; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = i; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = program; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "exec")) { if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot finish because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } if (!(pinst = funcname_to_pc(program, arg))) { notify_nolisten(player, "I don't know a function by that name.", 1); add_muf_read_event(descr, player, program, fr); return 0; } if (fr->system.top >= STACK_SIZE) { notify_nolisten(player, "That would exceed the system stack size for this program.", 1); add_muf_read_event(descr, player, program, fr); return 0; } fr->system.st[fr->system.top].progref = program; fr->system.st[fr->system.top++].offset = fr->pc; fr->pc = pinst; j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = fr->system.top - 1; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = program; fr->brkpt.bypass = 1; return 0; } else if (!strcasecmp(cmd, "prim")) { /* * @TODO The way this works is a little funky. It looks like * it would be possible to cause some weird havoc if we * manage to run a primitive that in turn triggers muf_debugger * * I am uncertain if this is possible; looks like the only * way it could happen is by typing 'prim debugger_break' * but I don't know much about about how muf_debugger is * triggered. Some digging should be done to make this * safe. * * Even better would be to not use statics for this somehow * without introducing a memory leak. (tanabi) */ if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Cannot finish because there are too many breakpoints set.", 1); add_muf_read_event(descr, player, program, fr); return 0; } if (!primitive(arg)) { notify_nolisten(player, "I don't recognize that primitive.", 1); add_muf_read_event(descr, player, program, fr); return 0; } if (fr->system.top >= STACK_SIZE) { notify_nolisten(player, "That would exceed the system stack size for this program.", 1); add_muf_read_event(descr, player, program, fr); return 0; } primset[0].type = PROG_FUNCTION; primset[0].line = 0; primset[0].data.mufproc = &temp_muf_proc_data; primset[0].data.mufproc->vars = 0; primset[0].data.mufproc->args = 0; primset[0].data.mufproc->varnames = NULL; primset[1].type = PROG_PRIMITIVE; primset[1].line = 0; primset[1].data.number = get_primitive(arg); primset[2].type = PROG_PRIMITIVE; primset[2].line = 0; primset[2].data.number = IN_RET; /* primset[3].data.number = primitive("EXIT"); */ fr->system.st[fr->system.top].progref = program; fr->system.st[fr->system.top++].offset = fr->pc; fr->pc = &primset[1]; j = fr->brkpt.count++; fr->brkpt.temp[j] = 1; fr->brkpt.level[j] = -1; fr->brkpt.line[j] = -1; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = &primset[2]; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = program; fr->brkpt.bypass = 1; fr->brkpt.dosyspop = 1; return 0; } else if (!strcasecmp(cmd, "break")) { add_muf_read_event(descr, player, program, fr); if (fr->brkpt.count >= MAX_BREAKS) { notify_nolisten(player, "Too many breakpoints set.", 1); return 0; } if (number(arg)) { i = atoi(arg); } else { if (!(pinst = funcname_to_pc(program, arg))) { notify_nolisten(player, "I don't know a function by that name.", 1); return 0; } else { i = pinst->line; } } if (!i) i = fr->pc->line; j = fr->brkpt.count++; fr->brkpt.temp[j] = 0; fr->brkpt.level[j] = -1; fr->brkpt.line[j] = i; fr->brkpt.linecount[j] = -2; fr->brkpt.pc[j] = NULL; fr->brkpt.pccount[j] = -2; fr->brkpt.prog[j] = program; notify_nolisten(player, "Breakpoint set.", 1); return 0; } else if (!strcasecmp(cmd, "delete")) { add_muf_read_event(descr, player, program, fr); i = atoi(arg); if (!i) { notify_nolisten(player, "Which breakpoint did you want to delete?", 1); return 0; } if (i < 1 || i > fr->brkpt.count) { notify_nolisten(player, "No such breakpoint.", 1); return 0; } j = i - 1; for (j++; j < fr->brkpt.count; j++) { fr->brkpt.temp[j - 1] = fr->brkpt.temp[j]; fr->brkpt.level[j - 1] = fr->brkpt.level[j]; fr->brkpt.line[j - 1] = fr->brkpt.line[j]; fr->brkpt.linecount[j - 1] = fr->brkpt.linecount[j]; fr->brkpt.pc[j - 1] = fr->brkpt.pc[j]; fr->brkpt.pccount[j - 1] = fr->brkpt.pccount[j]; fr->brkpt.prog[j - 1] = fr->brkpt.prog[j]; } fr->brkpt.count--; notify_nolisten(player, "Breakpoint deleted.", 1); return 0; } else if (!strcasecmp(cmd, "breaks")) { notify_nolisten(player, "Breakpoints:", 1); for (i = 0; i < fr->brkpt.count; i++) { ptr = unparse_breakpoint(fr, i); notify_nolisten(player, ptr, 1); } notify_nolisten(player, "*done*", 1); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "where")) { i = atoi(arg); muf_backtrace(player, program, i, fr); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "stack")) { notify_nolisten(player, "*Argument stack top*", 1); i = atoi(arg); if (!i) i = STACK_SIZE; ptr = ""; for (j = fr->argument.top; j > 0 && i-- > 0;) { cnt = 0; do { strcpyn(buf, sizeof(buf), ptr); ptr = insttotext(NULL, 0, &fr->argument.st[--j], buf2, sizeof(buf2), 4000, program, 1); cnt++; } while (!strcasecmp(ptr, buf) && j > 0); if (cnt > 1) notifyf(player, " [repeats %d times]", cnt); if (strcasecmp(ptr, buf)) notifyf(player, "%3d) %s", j + 1, ptr); } notify_nolisten(player, "*done*", 1); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "list") || !strcasecmp(cmd, "listi")) { int startline, endline; add_muf_read_event(descr, player, program, fr); if ((ptr2 = (char *) strchr(arg, ','))) { *ptr2++ = '\0'; } else { ptr2 = ""; } if (!*arg) { if (fr->brkpt.lastlisted) { startline = fr->brkpt.lastlisted + 1; } else { startline = fr->pc->line; } endline = startline + 15; } else { if (!number(arg)) { if (!(pinst = funcname_to_pc(program, arg))) { notify_nolisten(player, "I don't know a function by that name. (starting arg, 1)", 1); return 0; } else { startline = pinst->line; endline = startline + 15; } } else { if (*ptr2) { endline = startline = atoi(arg); } else { startline = atoi(arg) - 7; endline = startline + 15; } } } if (*ptr2) { if (!number(ptr2)) { if (!(pinst = funcname_to_pc(program, ptr2))) { notify_nolisten(player, "I don't know a function by that name. (ending arg, 1)", 1); return 0; } else { endline = pinst->line; } } else { endline = atoi(ptr2); } } i = (PROGRAM_CODE(program) + PROGRAM_SIZ(program) - 1)->line; if (startline > i) { notify_nolisten(player, "Starting line is beyond end of program.", 1); return 0; } if (startline < 1) startline = 1; if (endline > i) endline = i; if (endline < startline) endline = startline; notify_nolisten(player, "Listing:", 1); if (!strcasecmp(cmd, "listi")) { for (i = startline; i <= endline; i++) { pinst = linenum_to_pc(program, i); if (pinst) { notifyf_nolisten(player, "line %d: %s", i, (i == fr->pc->line) ? show_line_prims(program, fr->pc, STACK_SIZE, 1) : show_line_prims(program, pinst, STACK_SIZE, 0)); } } } else { list_proglines(player, program, fr, startline, endline); } fr->brkpt.lastlisted = endline; notify_nolisten(player, "*done*", 1); return 0; } else if (!strcasecmp(cmd, "quit")) { notify_nolisten(player, "Halting execution.", 1); return 1; } else if (!strcasecmp(cmd, "trace")) { add_muf_read_event(descr, player, program, fr); if (!strcasecmp(arg, "on")) { fr->brkpt.showstack = 1; notify_nolisten(player, "Trace turned on.", 1); } else if (!strcasecmp(arg, "off")) { fr->brkpt.showstack = 0; notify_nolisten(player, "Trace turned off.", 1); } else { notifyf_nolisten(player, "Trace is currently %s.", fr->brkpt.showstack ? "on" : "off"); } return 0; } else if (!strcasecmp(cmd, "words")) { list_program_functions(player, program, arg); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "print")) { debug_printvar(player, program, fr, arg); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "push")) { push_arg(player, fr, arg); add_muf_read_event(descr, player, program, fr); return 0; } else if (!strcasecmp(cmd, "pop")) { add_muf_read_event(descr, player, program, fr); if (fr->argument.top < 1) { notify_nolisten(player, "Nothing to pop.", 1); return 0; } fr->argument.top--; CLEAR(fr->argument.st + fr->argument.top); notify_nolisten(player, "Stack item popped.", 1); return 0; } else if (!strcasecmp(cmd, "help")) { do_helpfile(player, tp_file_man_dir, tp_file_man, "debugger_commands", ""); return 0; } else { notify_nolisten(player, "I don't understand that debugger command. Type 'help' for help.", 1); add_muf_read_event(descr, player, program, fr); return 0; } return 0; }
/** * This is for showing line listings in "instruction" format. * * This returns a single line of code's primitives. It uses a static buffer, * so be careful with it. Not multi-threaded. * * @param program the program to show listings for * @param pc the program counter where the listing starts * @param maxprims the maximum primitives * @param markpc do a special mark when listing the 'pc' line * @return pointer to static buffer containing primitives */ char * show_line_prims(dbref program, struct inst *pc, int maxprims, int markpc) { static char buf[BUFFER_LEN]; static char buf2[BUFFER_LEN]; int maxback; int thisline = pc->line; struct inst *code, *end, *linestart, *lineend; code = PROGRAM_CODE(program); end = code + PROGRAM_SIZ(program); buf[0] = '\0'; /* * This code is to determine the end of the line. There can be multiple * primitives on the same line. This series of loops finds the end * of the line, so we can iterate over the primitives between start and * end to form our return string. */ for (linestart = pc, maxback = maxprims; linestart > code && linestart->line == thisline && linestart->type != PROG_FUNCTION && --maxback; --linestart) ; if (linestart->line < thisline) ++linestart; for (lineend = pc + 1, maxback = maxprims; lineend < end && lineend->line == thisline && lineend->type != PROG_FUNCTION && --maxback; ++lineend) ; if (lineend >= end || lineend->line > thisline || lineend->type == PROG_FUNCTION) --lineend; if (lineend - linestart >= maxprims) { if (pc - (maxprims - 1) / 2 > linestart) linestart = pc - (maxprims - 1) / 2; if (linestart + maxprims - 1 < lineend) lineend = linestart + maxprims - 1; } if (linestart > code && (linestart - 1)->line == thisline) strcpyn(buf, sizeof(buf), "..."); /* * We're in the right position; loop from linestart til lineend, * putting together text versions of the instructions. */ while (linestart <= lineend) { if (strlen(buf) < BUFFER_LEN / 2) { if (*buf) strcatn(buf, sizeof(buf), " "); if (pc == linestart && markpc) { strcatn(buf, sizeof(buf), " {{"); strcatn(buf, sizeof(buf), insttotext(NULL, 0, linestart, buf2, sizeof(buf2), 30, program, 1)); strcatn(buf, sizeof(buf), "}} "); } else { strcatn(buf, sizeof(buf), insttotext(NULL, 0, linestart, buf2, sizeof(buf2), 30, program, 1)); } } else { break; } linestart++; } if (lineend < end && (lineend + 1)->line == thisline) strcatn(buf, sizeof(buf), " ..."); return buf; }
void disassemble(dbref player, dbref program) { struct inst *curr; struct inst *codestart; int i; char buf[BUFFER_LEN]; codestart = curr = PROGRAM_CODE(program); if (!PROGRAM_SIZ(program)) { notify(player, "Nothing to disassemble!"); return; } for (i = 0; i < PROGRAM_SIZ(program); i++, curr++) { switch (curr->type) { case PROG_PRIMITIVE: if (curr->data.number >= BASE_MIN && curr->data.number <= BASE_MAX) snprintf(buf, sizeof(buf), "%d: (line %d) PRIMITIVE: %s", i, curr->line, base_inst[curr->data.number - BASE_MIN]); else snprintf(buf, sizeof(buf), "%d: (line %d) PRIMITIVE: %d", i, curr->line, curr->data.number); break; case PROG_MARK: snprintf(buf, sizeof(buf), "%d: (line %d) MARK", i, curr->line); break; case PROG_STRING: snprintf(buf, sizeof(buf), "%d: (line %d) STRING: \"%s\"", i, curr->line, curr->data.string ? curr->data.string->data : ""); break; case PROG_ARRAY: snprintf(buf, sizeof(buf), "%d: (line %d) ARRAY: %d items", i, curr->line, curr->data.array ? curr->data.array->items : 0); break; case PROG_FUNCTION: snprintf(buf, sizeof(buf), "%d: (line %d) FUNCTION: %s, VARS: %d, ARGS: %d", i, curr->line, curr->data.mufproc->procname ? curr->data.mufproc->procname : "", curr->data.mufproc->vars, curr->data.mufproc->args); break; case PROG_LOCK: snprintf(buf, sizeof(buf), "%d: (line %d) LOCK: [%s]", i, curr->line, curr->data.lock == TRUE_BOOLEXP ? "TRUE_BOOLEXP" : unparse_boolexp(0, curr->data.lock, 0)); break; case PROG_INTEGER: snprintf(buf, sizeof(buf), "%d: (line %d) INTEGER: %d", i, curr->line, curr->data.number); break; case PROG_FLOAT: snprintf(buf, sizeof(buf), "%d: (line %d) FLOAT: %.17g", i, curr->line, curr->data.fnumber); break; case PROG_ADD: snprintf(buf, sizeof(buf), "%d: (line %d) ADDRESS: %d", i, curr->line, (int)(curr->data.addr->data - codestart)); break; case PROG_TRY: snprintf(buf, sizeof(buf), "%d: (line %d) TRY: %d", i, curr->line, (int)(curr->data.call - codestart)); break; case PROG_IF: snprintf(buf, sizeof(buf), "%d: (line %d) IF: %d", i, curr->line, (int)(curr->data.call - codestart)); break; case PROG_JMP: snprintf(buf, sizeof(buf), "%d: (line %d) JMP: %d", i, curr->line, (int)(curr->data.call - codestart)); break; case PROG_EXEC: snprintf(buf, sizeof(buf), "%d: (line %d) EXEC: %d", i, curr->line, (int)(curr->data.call - codestart)); break; case PROG_OBJECT: snprintf(buf, sizeof(buf), "%d: (line %d) OBJECT REF: %d", i, curr->line, curr->data.number); break; case PROG_VAR: snprintf(buf, sizeof(buf), "%d: (line %d) VARIABLE: %d", i, curr->line, curr->data.number); break; case PROG_SVAR: snprintf(buf, sizeof(buf), "%d: (line %d) SCOPEDVAR: %d (%s)", i, curr->line, curr->data.number, scopedvar_getname_byinst(curr, curr->data.number)); break; case PROG_SVAR_AT: snprintf(buf, sizeof(buf), "%d: (line %d) FETCH SCOPEDVAR: %d (%s)", i, curr->line, curr->data.number, scopedvar_getname_byinst(curr, curr->data.number)); break; case PROG_SVAR_AT_CLEAR: snprintf(buf, sizeof(buf), "%d: (line %d) FETCH SCOPEDVAR (clear optim): %d (%s)", i, curr->line, curr->data.number, scopedvar_getname_byinst(curr, curr->data.number)); break; case PROG_SVAR_BANG: snprintf(buf, sizeof(buf), "%d: (line %d) SET SCOPEDVAR: %d (%s)", i, curr->line, curr->data.number, scopedvar_getname_byinst(curr, curr->data.number)); break; case PROG_LVAR: snprintf(buf, sizeof(buf), "%d: (line %d) LOCALVAR: %d", i, curr->line, curr->data.number); break; case PROG_LVAR_AT: snprintf(buf, sizeof(buf), "%d: (line %d) FETCH LOCALVAR: %d", i, curr->line, curr->data.number); break; case PROG_LVAR_AT_CLEAR: snprintf(buf, sizeof(buf), "%d: (line %d) FETCH LOCALVAR (clear optim): %d", i, curr->line, curr->data.number); break; case PROG_LVAR_BANG: snprintf(buf, sizeof(buf), "%d: (line %d) SET LOCALVAR: %d", i, curr->line, curr->data.number); break; case PROG_CLEARED: snprintf(buf, sizeof(buf), "%d: (line ?) CLEARED INST AT %s:%d", i, (char *) curr->data.addr, curr->line); default: snprintf(buf, sizeof(buf), "%d: (line ?) UNKNOWN INST", i); } notify(player, buf); } }