14 #include <readline/readline.h>
15 #ifdef HAVE_LIBVNCSERVER
17 #endif /* HAVE_LIBVNCSERVER */
22 #include "hw_lem1802.h"
23 #include "hw_keyboard.h"
26 * shell-like driver for dcpu16 core
27 * provides a basic interface to control a single emulation instance
29 * Justin Wind <justin.wind@gmail.com>
30 * 2012 04 10 - implementation started
31 * 2012 04 12 - cleanup, better shell loop
32 * 2012 05 12 - support v1.7 style devices
35 * handle quotes in shell command parsing
36 * use readline/history.h, since we're using readline anyhow
37 * ncurses windowing or something, for future display capabilities
40 static const char * const src_id_
= "$Id$";
42 /* global invocation options */
49 /* global run state, first sigint caught will drop out of run loop and back into shell */
50 static volatile unsigned int running_
= 0;
52 void sigint_handler_(int sig
) {
57 #define VERBOSE_PRINTF(...) do { if (opt_.verbose) printf(__VA_ARGS__); } while (0)
59 #define DEBUG_PRINTF(...) do { if (opt_.verbose > 4) fprintf(stderr, __VA_ARGS__); } while (0)
61 #define DEBUG_PRINTF(...) do { } while (0)
65 void usage_(char *prog
, unsigned int full
) {
66 FILE *f
= full
? stdout
: stderr
;
67 char *x
= strrchr(prog
, '/');
73 fprintf(f
, "%s -- dcpu16 emulator core shell\n\n",
76 fprintf(f
, "Usage: %s [-v] [file]\n",
80 fprintf(f
, "\nOptions:\n"
81 "\t [file] -- ram image to load initially\n"
82 "\t -v -- prints slightly more information while operating\n"
83 "\t -h -- this screen\n");
85 fprintf(f
, "\n%78s\n", src_id_
);
90 /* flense a buffer into a newly-allocated argument list */
92 int buf_tok_vect_(char ***v
, int *c
, char *buf
) {
93 const char *sep
= " \t";
94 const char *quot
= "\"'`";
95 const size_t v_grow
= 32;
100 *v
= malloc(v_sz
* sizeof **v
);
102 fprintf(stderr
, "%s():%s\n", "malloc", strerror(errno
));
106 for ( (*v
)[*c
] = strqtok_r(buf
, sep
, '\\', quot
, &qt
, &st
);
108 (*v
)[*c
] = strqtok_r(NULL
, sep
, '\\', quot
, &qt
, &st
)
112 if ((size_t)(*c
) == v_sz
) {
113 void *tmp_ptr
= realloc(*v
, (v_sz
+ v_grow
) * sizeof **v
);
114 if (tmp_ptr
== NULL
) {
115 fprintf(stderr
, "%s():%s\n", "realloc", strerror(errno
));
128 resets the vm if addr is zero then
129 loads an image from filename into ram starting at addr
132 int file_load_(struct dcpu16
*vm
, char *filename
, DCPU16_WORD addr
) {
139 f
= fopen(filename
, "rb");
141 fprintf(stderr
, "%s('%s'):%s\n", "fopen", filename
, strerror(errno
));
145 r
= fread(vm
->ram
+ addr
, sizeof(DCPU16_WORD
), DCPU16_RAM
- addr
, f
);
146 VERBOSE_PRINTF("read %zu words", r
);
147 if (addr
) VERBOSE_PRINTF(" starting at 0x%04x", addr
);
148 VERBOSE_PRINTF("\n");
151 fprintf(stderr
, "%s('%s'):%s\n", "fread", filename
, strerror(errno
));
158 * print raw ram contents from start to stop
161 void dump_ram_(struct dcpu16
*vm
, DCPU16_WORD start
, DCPU16_WORD end
) {
163 const unsigned int n
= 8; /* words per line */
167 for (i
= start
, j
= 0; i
<= end
; i
++, j
++) {
169 printf("0x%04x:\t", i
);
170 printf(" %04x%s", vm
->ram
[i
], (j
% n
) == (n
- 1) ? "\n" : "");
172 if ((j
% n
) != (n
- 1))
178 print the current state of the machine
179 shows current cycle count, registers, and next instruction
182 void state_print_(struct dcpu16
*vm
) {
188 for (i
= 0; i
< 8; i
++)
189 printf(" %s:0x%04x", dcpu16_reg_names
[i
], vm
->reg
[i
]);
192 printf("(0x%08llx) %2s:0x%04x %2s:0x%04x %2s:0x%04x %2s:0x%04x [%2s]:",
194 dcpu16_reg_names
[DCPU16_REG_EX
], vm
->reg
[DCPU16_REG_EX
],
195 dcpu16_reg_names
[DCPU16_REG_SP
], vm
->reg
[DCPU16_REG_SP
],
196 dcpu16_reg_names
[DCPU16_REG_PC
], vm
->reg
[DCPU16_REG_PC
],
197 dcpu16_reg_names
[DCPU16_REG_IA
], vm
->reg
[DCPU16_REG_IA
],
200 dcpu16_disassemble_print(vm
, vm
->reg
[DCPU16_REG_PC
]);
205 #ifdef HAVE_LIBVNCSERVER
206 static struct dynamic_array rfbScreens_
;
207 /* wups, kbdAddEvent isn't null by default, so I guess track associations externally */
208 struct rfb_instance_
{
209 rfbScreenInfoPtr screen
;
210 struct dcpu16_hw
*attached_display
;
211 struct dcpu16_hw
*attached_keyboard
;
214 enum rfbscreen_next_what_
{
218 /* locate next rfb not paired to requested type */
220 struct rfb_instance_
*rfbScreen_next_available_(enum rfbscreen_next_what_ what
, struct dynamic_array
*rfbScreens
, int argc
, char *argv
[]) {
222 struct rfb_instance_ new_instance
, *s
;
223 struct packed_args_
{
226 } parg
= { argc
, argv
};
228 for (i
= 0; i
< rfbScreens
->entries
; i
++) {
229 struct dcpu16_hw
*attached
;
231 s
= (struct rfb_instance_
*)DYNARRAY_ITEM(*rfbScreens
, i
);
233 case NEXT_DISPLAY
: attached
= s
->attached_display
; break;
234 case NEXT_KEYBOARD
: attached
= s
->attached_keyboard
; break;
236 if (attached
== NULL
)
240 /* no available rfb, create new */
241 if (dcpu16_hw_module_lem1802
.ctl(NULL
, "new_rfbScreen", &parg
, &new_instance
.screen
)) {
242 fprintf(stderr
, "failed to allocate new rfbScreen\n");
246 new_instance
.attached_display
= NULL
;
247 new_instance
.attached_keyboard
= NULL
;
249 new_instance
.screen
->port
+= rfbScreens
->entries
;
250 new_instance
.screen
->ipv6port
+= rfbScreens
->entries
;
252 DEBUG_PRINTF("%s>> port:%u\n", __func__
, new_instance
.screen
->port
);
254 s
= dynarray_add(rfbScreens
, &new_instance
);
260 /* begin serving a screen */
261 void rfbScreen_start(rfbScreenInfoPtr s
) {
263 rfbRunEventLoop(s
, -1, TRUE
);
265 #endif /* HAVE_LIBVNCSERVER */
268 Here follows the various commands the shell can execute.
270 At invocation, a command function will have already had its
271 number of arguments vetted, but will need command-specific
272 argument verifications done.
274 The arg_vector contains the command as the first entry, and
275 as such, arg_count will always be at least 1.
276 However, the args_min and args_max entries in struct command_
277 only refer to the counts of arguments, not the entries in the
285 int (*func
)(struct dcpu16
*, int c
, char **v
);
286 void (*help
)(FILE *f
, unsigned int);
289 #define COMMAND_IMPL(x) static int command_##x##_(struct dcpu16 *vm, int arg_count, char **arg_vector)
290 #define COMMAND_HELP(x) static void command_##x##_help_(FILE *f, unsigned int summary)
291 #define COMMAND_ENTRY(x, y, z) { #x, y, z, command_##x##_, command_##x##_help_ }
295 (void)vm
, (void)arg_count
, (void)arg_vector
;
300 fprintf(f
, "\tquit\n");
303 fprintf(f
, "Exits the emulator.\n");
307 COMMAND_IMPL(reset
) {
308 (void)arg_count
, (void)arg_vector
;
311 printf("initialized\n");
315 COMMAND_HELP(reset
) {
316 fprintf(f
, "\treset\n");
319 fprintf(f
, "Clears and reinitializes emulator.\n");
323 COMMAND_IMPL(verbosity
) {
325 (void)vm
, (void)arg_count
;
327 l
= str_to_word(arg_vector
[1]);
329 fprintf(stderr
, "invalid level\n");
337 COMMAND_HELP(verbosity
) {
338 fprintf(f
, "\tverbosity level\n");
341 fprintf(f
, "sets the verbosity level\n");
348 addr
= str_to_word(arg_vector
[2]);
350 fprintf(stderr
, "address '%s' is not a valid word: %s\n", arg_vector
[2], strerror(errno
));
355 if (file_load_(vm
, arg_vector
[1], addr
)) {
356 fprintf(stderr
, "failed to load '%s'\n", arg_vector
[1]);
359 printf("loaded '%s'", arg_vector
[1]);
360 if (addr
) printf(" starting at 0x%04x", addr
);
366 fprintf(f
, "\tload file [addr]\n");
369 fprintf(f
, "Load binary image from 'file' into ram.\n");
377 for (i
= 1; i
< arg_count
; i
++) {
378 addr
[i
-1] = str_to_word(arg_vector
[i
]);
380 fprintf(stderr
, "address '%s' is not a valid word: %s\n", arg_vector
[i
], strerror(errno
));
384 if (arg_count
< 2) addr
[0] = vm
->reg
[DCPU16_REG_PC
];
385 if (arg_count
< 3) addr
[1] = addr
[0];
387 if (addr
[1] < addr
[0]) {
388 fprintf(stderr
, "\t'addr_start' must be before addr_end\n");
392 dump_ram_(vm
, addr
[0], addr
[1]);
397 fprintf(f
, "\tdump [addr_start [addr_end]]\n");
400 fprintf(f
, "Displays contents of ram from addr_start to addr_end.\n");
404 COMMAND_IMPL(disassemble
) {
408 for (i
= 1; i
< arg_count
; i
++) {
409 addr
[i
-1] = str_to_word(arg_vector
[i
]);
411 fprintf(stderr
, "address '%s' is not a valid word: %s\n", arg_vector
[i
], strerror(errno
));
415 if (arg_count
< 2) addr
[0] = vm
->reg
[DCPU16_REG_PC
];
416 if (arg_count
< 3) addr
[1] = addr
[0];
418 if (addr
[1] < addr
[0]) {
419 fprintf(stderr
, "\t'addr_start' must be before addr_end\n");
423 for (i
= addr
[0]; i
<= addr
[1]; /* */ ) {
424 printf("0x%04x: ", i
);
425 i
+= dcpu16_disassemble_print(vm
, i
);
431 COMMAND_HELP(disassemble
) {
432 fprintf(f
, "\tdisassemble [addr_start [addr_end]]\n");
435 fprintf(f
, "Displays contents of ram parsed into instructions.\n");
440 unsigned long count
= 1;
443 if (arg_count
== 2) {
445 count
= strtoul(arg_vector
[1], &ep
, 0);
447 || !(*arg_vector
[1] && *ep
== '\0') ) {
448 fprintf(stderr
, "count '%s' is not a valid number: %s\n", arg_vector
[1], strerror(errno
));
453 fprintf(stderr
, "count must be positive\n");
459 dcpu16_disassemble_print(vm
, vm
->reg
[DCPU16_REG_PC
]);
463 if (count
> 1 && opt_
.verbose
)
470 fprintf(f
, "\tstep [count]\n");
473 fprintf(f
, "Executes the next instruction, or the next count instructions.\n");
483 /* check if addr is a register */
484 for (addr
= 0; dcpu16_reg_names
[addr
]; addr
++) {
485 if (strcasecmp(arg_vector
[1], dcpu16_reg_names
[addr
]) == 0)
488 if (addr
< DCPU16_REG__NUM
) {
491 addr
= str_to_word(arg_vector
[1]);
493 fprintf(stderr
, "address '%s' is not a valid word: %s\n", arg_vector
[1], strerror(errno
));
499 value
= str_to_word(arg_vector
[2]);
501 fprintf(stderr
, "address '%s' is not a valid word: %s\n", arg_vector
[2], strerror(errno
));
511 fprintf(f
, "\tset addr value\n");
514 fprintf(f
, "Sets addr to value.");
517 #define NANOSECONDS_PER_CYCLE 10000
518 #define MIN_NANOSLEEP 31000
520 struct sigaction act
;
521 long long run_cycle_start
, run_cycle_end
;
522 long long cycle_start
, cycles_to_wait
;
524 struct timespec ts_run_start
, ts_run_end
, ts_run_diff
;
525 struct timespec ts_cycle_start
, ts_cycle_end_target
, ts_cycle_end
, ts_cycle_waste
, ts_cycle_rem
;
526 const struct timespec ts_cycle_time
= { .tv_sec
= 0, .tv_nsec
= NANOSECONDS_PER_CYCLE
};
528 (void)arg_count
, (void)arg_vector
;
531 gettimespecofday(&ts_run_start
);
532 run_cycle_start
= vm
->cycle_
;
534 memset(&act
, 0, sizeof act
);
535 act
.sa_handler
= sigint_handler_
;
536 act
.sa_flags
= SA_RESETHAND
;
538 if (sigaction(SIGINT
, &act
, NULL
)) {
539 fprintf(stderr
, "%s():%s\n", "sigaction", strerror(errno
));
544 gettimespecofday(&ts_cycle_start
);
545 ts_cycle_end_target
= ts_cycle_start
;
547 cycle_start
= vm
->cycle_
;
550 if (opt_
.verbose
> 1)
552 else if (opt_
.verbose
) {
553 dcpu16_disassemble_print(vm
, vm
->reg
[DCPU16_REG_PC
]);
557 /* how many cycles did this instr use? */
558 cycles_to_wait
= vm
->cycle_
- cycle_start
;
560 /* each cycle wants to take 10 microseconds */
561 while (cycles_to_wait
--)
562 timespec_add(&ts_cycle_end_target
, &ts_cycle_time
);
564 /* how much of that did we spend already */
565 gettimespecofday(&ts_cycle_end
);
567 /* do we have time to kill? */
568 if (timespec_subtract(&ts_cycle_waste
, &ts_cycle_end_target
, &ts_cycle_end
) == 0) {
569 /* nanosleep doesn't interfere with libvncserver, unlike usleep */
570 if (ts_cycle_waste
.tv_sec
== 0 && ts_cycle_waste
.tv_nsec
>= MIN_NANOSLEEP
)
571 while ( nanosleep(&ts_cycle_waste
, &ts_cycle_rem
) )
572 ts_cycle_waste
= ts_cycle_rem
;
574 /* negative, we've already blown our time */
575 DEBUG_PRINTF("cycle time overrun %ld.%09lds\n", ts_cycle_waste
.tv_sec
, ts_cycle_waste
.tv_nsec
);
580 gettimespecofday(&ts_cycle_end
);
581 timespec_subtract(&ts_cycle_rem
, &ts_cycle_end_target
, &ts_cycle_end
);
582 DEBUG_PRINTF("projected end: %ld.%09ld actual end: %ld.%09ld diff: %ld.%09ld\n",
583 ts_cycle_end_target
.tv_sec
, ts_cycle_end_target
.tv_nsec
,
584 ts_cycle_end
.tv_sec
, ts_cycle_end
.tv_nsec
,
585 ts_cycle_rem
.tv_sec
, ts_cycle_rem
.tv_nsec
);
590 run_cycle_end
= vm
->cycle_
;
591 gettimespecofday(&ts_run_end
);
592 timespec_subtract(&ts_run_diff
, &ts_run_end
, &ts_run_start
);
593 VERBOSE_PRINTF("ran %lld cycles in %ld.%09lds\n",
594 run_cycle_end
- run_cycle_start
,
595 ts_run_diff
.tv_sec
, ts_run_diff
.tv_nsec
);
597 printf("interrupted...\n");
602 fprintf(f
, "\trun\n");
605 fprintf(f
, "Begins executing continuously.\n"
606 "May be interrupted with SIGINT.\n");
609 static const char * const display_filename_default_
=
612 #else /* HAVE_LIBPNG */
614 #endif /* HAVE_LIBPNG */
616 COMMAND_IMPL(display
) {
617 struct dcpu16_hw
*hw
;
618 const char *renderer
= arg_vector
[1];
619 const char *renderer_arg
= NULL
;
623 renderer_arg
= arg_vector
[2];
625 hw
= dcpu16_hw_new(vm
, &dcpu16_hw_module_lem1802
, NULL
);
627 fprintf(stderr
, "failed to initialize new display\n");
631 /* handle per-renderer setup of data.. */
632 /* FIXME: these are awkward */
633 if (strcmp(renderer
, "pnm") == 0) {
634 renderer_data
= (void *)(renderer_arg
? renderer_arg
: display_filename_default_
);
638 if (strcmp(renderer
, "png") == 0) {
639 renderer_data
= (void *)(renderer_arg
? renderer_arg
: display_filename_default_
);
641 #endif /* HAVE_LIBPNG */
643 #ifdef HAVE_LIBVNCSERVER
644 if (strcmp(renderer
, "vnc") == 0) {
646 char *argv
[] = { "vm-dcpu16", NULL
};
647 struct rfb_instance_
*s
;
649 s
= rfbScreen_next_available_(NEXT_DISPLAY
, &rfbScreens_
, argc
, argv
);
651 fprintf(stderr
, "failed to initialize vnc\n");
656 if (dcpu16_hw_ctl(hw
, "associate_rfbScreen", s
->screen
, NULL
)) {
657 fprintf(stderr
, "failed to configure display/vnc");
661 s
->attached_display
= hw
;
662 rfbScreen_start(s
->screen
);
663 renderer_data
= s
->screen
;
665 DEBUG_PRINTF("attached display to rfb (frameBuffer:%p)\n", s
->screen
->frameBuffer
);
667 #endif /* HAVE_LIBVNCSERVER */
669 dcpu16_hw_ctl(hw
, "renderer", (char *)renderer
, NULL
);
670 dcpu16_hw_ctl(hw
, "renderer_data", renderer_data
, NULL
);
672 if (dcpu16_hw_attach(vm
, hw
)) {
673 fprintf(stderr
, "failed to attach new display\n");
680 COMMAND_HELP(display
) {
687 fprintf(f
, "\tdisplay renderer [renderer data]\n");
690 fprintf(f
, "Attaches new display unit, using 'renderer' as back-end output.\n"
693 fprintf(f
, "Supported renderers:\n");
697 if (dcpu16_hw_module_lem1802
.ctl(NULL
, "renderers_iter", &iter
, &renderer
)) {
698 fprintf(stderr
, "error fetching next renderer\n");
701 if (iter
== NULL
|| renderer
.name
== NULL
)
704 fprintf(f
, "\t%s %s\n", renderer
.name
, renderer
.args
);
708 COMMAND_IMPL(keyboard
) {
709 struct dcpu16_hw
*hw
;
711 (void)arg_count
, (void)arg_vector
;
713 hw
= dcpu16_hw_new(vm
, &dcpu16_hw_module_keyboard
, NULL
);
715 fprintf(stderr
, "failed to initialize new keyboard\n");
719 #ifdef HAVE_LIBVNCSERVER
720 struct rfb_instance_
*s
;
722 char *argv
[] = { "vm-dcpu16", NULL
};
724 s
= rfbScreen_next_available_(NEXT_KEYBOARD
, &rfbScreens_
, argc
, argv
);
726 fprintf(stderr
, "failed to initialize vnc\n");
730 if (dcpu16_hw_ctl(hw
, "associate_rfbScreen", s
->screen
, NULL
)) {
731 fprintf(stderr
, "failed to configure keyboard/vnc\n");
735 s
->attached_keyboard
= hw
;
737 if (dcpu16_hw_attach(vm
, hw
)) {
738 fprintf(stderr
, "failed to attach new keyboard\n");
742 #endif /* HAVE_LIBVNCSERVER */
746 COMMAND_HELP(keyboard
) {
747 fprintf(f
, "\tkeyboard\n");
750 fprintf(f
, "Attaches new keyboard unit.\n");
753 /* gather all these together into a searchable table */
755 /* help command gets some assistance in declarations */
759 static struct command_ command_table_
[] = {
760 COMMAND_ENTRY(help
, 0, -1),
761 COMMAND_ENTRY(quit
, 0, -1),
762 COMMAND_ENTRY(verbosity
, 1, 1),
763 COMMAND_ENTRY(load
, 1, 2),
764 COMMAND_ENTRY(dump
, 0, 2),
765 COMMAND_ENTRY(disassemble
, 0, 2),
766 COMMAND_ENTRY(step
, 0, 1),
767 COMMAND_ENTRY(run
, 0, 0),
768 COMMAND_ENTRY(set
, 2, 2),
769 COMMAND_ENTRY(reset
, 0, 0),
770 COMMAND_ENTRY(display
, 1, 2),
771 COMMAND_ENTRY(keyboard
, 0, 0),
772 { NULL
, 0, 0, NULL
, NULL
}
779 if (arg_count
== 2) {
780 for (c
= command_table_
; c
->func
; c
++) {
781 if (strcasecmp(arg_vector
[1], c
->name
) == 0) {
790 for (c
= command_table_
; c
->func
; c
++) {
797 fprintf(f
, "\thelp [command]\n");
800 fprintf(f
, "Displays a list of available commands, or detailed help on a specific command.\n");
804 void msg_verbose_filter_(unsigned int level
, char *fmt
, ...) {
805 static const char * const levels
[] = { "error", "info", "debug" };
806 FILE *f
= (level
<= MSG_ERROR
) ? stderr
: stdout
;
809 if (level
+ 2 > opt_
.verbose
)
812 if (level
< sizeof levels
/ sizeof *levels
)
813 fprintf(f
, "[%s %s] ", "dcpu16", levels
[level
]);
815 fprintf(f
, "[%s %u] ", "dcpu16", level
);
818 vfprintf(f
, fmt
, ap
);
826 int main(int argc
, char **argv
) {
827 const char prompt_fmt
[] = "PC:%04x> ";
830 char *line
, *line_prev
;
831 char **tok_v
, **tok_v_prev
;
832 int tok_c
, tok_c_prev
;
835 while ( (c
= getopt(argc
, argv
, "hv")) != EOF
) {
853 dcpu16_msg_set_default(msg_verbose_filter_
);
855 if ((vm
= dcpu16_new()) == NULL
) {
856 fprintf(stderr
, "could not allocate new dcpu16 instance\n");
857 exit(EX_UNAVAILABLE
);
860 #ifdef HAVE_LIBVNCSERVER
861 if (dynarray_init(&rfbScreens_
, sizeof(struct rfb_instance_
), 4)) {
862 fprintf(stderr
, "could not allocate rfb container\n");
863 exit(EX_UNAVAILABLE
);
865 #endif /* HAVE_LIBVNCSERVER */
868 if (file_load_(vm
, *argv
, 0)) {
869 fprintf(stderr
, "couldn't load '%s'\n", *argv
);
874 /* show state, read commands */
875 for (line
= line_prev
= NULL
,
876 tok_v
= tok_v_prev
= NULL
,
877 tok_c
= tok_c_prev
= 0,
878 snprintf(prompt
, sizeof prompt
, prompt_fmt
, vm
->reg
[DCPU16_REG_PC
]),
881 (line
= readline(prompt
));
884 snprintf(prompt
, sizeof prompt
, prompt_fmt
, vm
->reg
[DCPU16_REG_PC
]),
886 const char whitespace
[] = " \t";
891 /* skip whitespaces */
892 line_start
= line
+ strspn(line
, whitespace
);
895 /* a new command, remember previous for possible repetition */
897 /* turn new line into new arg array */
898 if (buf_tok_vect_(&tok_v
, &tok_c
, line_start
)) {
899 fprintf(stderr
, "failed to process command\n");
903 /* and keep track if it all for the next time around */
904 if (line_prev
) free(line_prev
);
907 if (tok_v_prev
) free(tok_v_prev
);
911 /* blank new command, but no prior command to repeat? ask again */
912 if (tok_v_prev
== NULL
|| tok_v_prev
[0] == NULL
|| *(tok_v_prev
[0]) == '\0') {
917 /* otherwise discard new line and promote prior */
924 /* look up command */
925 for (c
= command_table_
; c
->name
; c
++) {
926 if (strcasecmp(tok_v
[0], c
->name
) == 0) {
927 if (c
->args_min
> tok_c
- 1) {
928 fprintf(stderr
, "%s: not enough arguments\n", c
->name
);
934 && tok_c
- 1 > c
->args_max
) {
935 fprintf(stderr
, "%s: too many arguments\n", c
->name
);
940 r
= c
->func(vm
, tok_c
, tok_v
);
948 fprintf(stderr
, "didn't recognize '%s'\n", tok_v
[0]);
951 printf("\nfinished\n");