対話環境を作る
2年ぶりにワード定義部分を実装し、いっそうプログラミング言語らしくなった。しかし、まだ何かが足りない。そう、私の作ったForthは対話的に使うことが出来ない。ファイルを指定して実行するだけでなく、コンソール上での入力を受け付けるようにしたい。
ファイル指定の場合は、
- ファイルを読み込んで構文解析し、内部コードに変換する
- 内部コードを実行する
- 終了
だったのだが、対話環境の場合は、
- ファイル名を指定せずに起動すると、入力待ちのプロンプトを表示する
- 入力した文字列を構文解析し、内部コードに変換する
- 内部コードを実行する
- 再度プロンプトを表示し、入力を待つ
となる。
まず変更するところは、構文解析関数parse()である。これまでparse()は、ファイル名を引数としていたが、これからはForthのプログラムを引数としなければならない。そのため、main()を以下のように変更した。
int main(int argc, char *argv[]) { char cmd_buf[100]; init(); if (argc >= 2) { load_prog(argv[1], cmd_buf); parse(cmd_buf); proc_prog(); } else { while (!quit_enabled) { scanf("%100[^\n]%*[^\n]", &cmd_buf); parse(cmd_buf); proc_prog(); } } return 0; }
cmd_buf[]にファイルから読み込んだプログラムか、または、コンソールで入力されたワードを保存し、それをparse()に渡すようにした。また、parse()で行っていたファイル読み込みは、load_prog()で行う。
/** * Forthで書かれたプログラムをロードする */ void load_prog(char *fname, char *cmd) { FILE *fp; unsigned int idx = 0; fp = fopen(fname, "r"); while ((c = fgetc(fp)) != EOF) { cmd[idx++] = c; } cmd[idx] = '\x0'; }
parse()から呼び出される、get_token()は、対象をファイルから文字列にする。
/** * トークンを切り出す */ void get_token(char **cmd, char *token) { int c; int j = 0; token[0] = '\x0'; /* 英数記号が来るまで読み込む */ c = **cmd; *cmd = *cmd + 1; while (is_separator(c) && **cmd != '\x0') { c = **cmd; *cmd = *cmd + 1; } /* 区切り記号までを読み込む */ while (!is_separator(c) && **cmd != '\x0') { token[j] = c; j++; c = **cmd; *cmd = *cmd + 1; } token[j] = '\x0'; }
なんか複雑になった。これはstrtok()使えばいいような気がする。試してみたが、strtok()は、区切り子が連続して2つ続けてくると、2つ目の区切り子を取り除いてくれない。というのは勘違いで、区切り子の入れ忘れが2箇所あった。get_token()は必要なくなった。
これで、parse()の引数をファイル名からプログラムに変更できた。次は、対話環境である。ファイル指定なしでForthを起動してみる。プロンプトが表示されない。後で表示させよう。そして、"2 3 + ."と入力し、Enterキーを押すが、反応なし。
gdbで調べてみた。
- なぜかscanf()で処理が止まらない
- コマンドラインからの入力を保存したcmd_bufをクリアしていない
- コマンドラインのワードを処理した後、prog_cntをクリアしていない
scanf()が止まらないのは、バッファに'\n'が残っているかららしい。scanf()したあとにgetchar()を入れると、無限ループは止まった。"2 3 + ."と入れて、"5"が表示された。
しかし、次に"6 2 + ."と入れたが、反応はなく、プロンプトが帰ってくるだけだ。これは、
- "2 3 + ."を実行、prog_cntが6の位置になる( = prog[6] = CODE_END)
- "6 2 + ."を実行、prog_cntがCODE_ENDの位置にあるので、即終了となる
が原因だった。これを修正するには、prog_cntの初期位置を記憶して、入力されたワードを実行後、元に戻すようにする。0にしてしまえれば一番簡単だが、ワード定義がある場合、その分prog_cntがずれてしまう。そこで、プログラムカウンタの初期位置を記録する変数を用意する。
int base_prog_cnt; /**< プログラムカウンタの初期位置 */
proc_prog()を呼び出すときは、必ずbase_prog_cntの値に戻す。
void proc_prog(void) { prog_cnt = base_prog_cnt; while (prog[prog_cnt] != CODE_END) {
ワード定義があったときは、その分だけ位置をずらす。
} else if (strcmp(token, ";") == 0) { /* ワード終端コードを追加する */ prog[idx] = CODE_RET; idx++; base_prog_cnt = idx;
上記の変更をして、実行してみた。
$ ./moiforth.exe >2 3 + . cr 5 >6 2 + . cr 8 >
うまくいった。これで対話環境が完成した。