my-lisp2もやっとテストができる段階まで来ました。ソースコードを解説して、その後に実際に動かしてみます。
ソースコード
printer.h
プリンターが公開する関数は1つだけです。シンプルですね。
/*
* printer.h
*/
#ifndef PRINTER_H_
#define PRINTER_H_
#include <stdio.h>
void printer_print(FILE *stream, void *obj);
#endif
printer.c
インクルード文とプロトタイプ宣言です。Visual Studio(C++)でビルドする時はコマンドラインオプションでMSVC
を定義します。_GNU_SOURCE
はGNU拡張の関数を使うときに指定します。
/*
* printer.c
*/
#ifndef MSVC
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "../chapter02/type.h"
#include "helper.h"
#include "printer.h"
static void print_symbol(FILE *stream, void *obj);
static void print_list(FILE *stream, void *obj);
static void print_string(FILE *stream, void *obj);
static void print_number(FILE *stream, void *obj);
printer_print
関数の中身です。
void printer_print(FILE *stream, void *obj) {
HEADER *h;
if (!obj) {
fprintf(stream, "[!!!NULL POINTER!!!]");
return;
}
h = (HEADER *)obj;
switch (h->type) {
case TYPE_SYMBOL:
print_symbol(stream, obj);
break;
case TYPE_CONS:
print_list(stream, obj);
break;
case TYPE_STRING:
print_string(stream, obj);
break;
case TYPE_NUMBER:
print_number(stream, obj);
break;
default:
fprintf(stderr, "未実装のコードに到達しました\n");
}
}
print_symbol
関数の中身です。エスケープ文字(|
や\
)を使って入力したシンボルはプリンターの出力でもエスケープされるようになっています。
static void print_symbol(FILE *stream, void *obj) {
SYMBOL *symbol = (SYMBOL *)obj;
char *p;
int multiple_escape_is_needed = 0;
for (p = get_symbol_string(symbol); *p != '\0'; ++p) {
if (*p >= 'a' && *p <= 'z') {
multiple_escape_is_needed = 1;
break;
}
switch (*p) {
case '(':
case ')':
case '\'':
case ';':
case '\"':
case '`':
case ',':
case '#':
case '\\':
case '|':
case ' ':
case '\t':
case '\n':
multiple_escape_is_needed = 1;
goto LOOPEND1;
default:
;
}
}
LOOPEND1:
if (!multiple_escape_is_needed) {
fprintf(stream, "%s", get_symbol_string(symbol));
} else {
fputc('|', stream);
for (p = get_symbol_string(symbol); *p != '\0'; ++p) {
switch (*p) {
case '\\':
fputs("\\\\", stream);
break;
case '|':
fputs("\\|", stream);
break;
default:
fputc(*p, stream);
}
}
fputc('|', stream);
}
}
print_list
関数の中身です。リストがドットリストであっても正しく出力されるようになっています。
static void print_list(FILE *stream, void *obj) {
CONS *cons1 = (CONS *)obj;
fputc('(', stream);
printer_print(stream, cons1->car);
obj = cons1->cdr;
while (1) {
if (obj == NIL) {
break;
} else if (!consp(obj)) {
fprintf(stream, " . ");
printer_print(stream, obj);
break;
}
cons1 = (CONS *)obj;
fputc(' ', stream);
printer_print(stream, cons1->car);
obj = cons1->cdr;
}
fputc(')', stream);
}
print_string
関数の中身です。文字列の中もエスケープされる文字はエスケープするようになっています。
static void print_string(FILE *stream, void *obj) {
STRING *string = (STRING *)obj;
char *p;
fputc('"', stream);
for (p = get_string_string(string); *p != '\0'; ++p) {
switch (*p) {
case '\"':
fputs("\\\"", stream);
break;
case '\\':
fputs("\\\\", stream);
break;
default:
fputc(*p, stream);
}
}
fputc('"', stream);
}
Visual Studio(C++)の場合に限って、asprintf
関数を定義しています。ちなみにこれはcopilotで出力されたコードです。というのは、この関数はGNU拡張の関数なのでVisual StudioのCLRの中には入っていないためです。Visual Studioではasprintf
の代わりに_vscprintf
を使います。
#ifdef MSVC
int asprintf(char **strp, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int size = _vscprintf(fmt, args) + 1; /* _vscprintf doesn't count terminating '\0' */
int result;
*strp = (char *)malloc(size);
if (*strp == NULL) {
va_end(args);
return -1;
}
result = vsprintf(*strp, fmt, args);
va_end(args);
return result;
}
#endif
print_number
関数の中身です。my-lisp2ではdouble
型を使いますが、小数点以下の余計な0
と.
を消す処理が書いてあります。
static void print_number(FILE *stream, void *obj) {
NUMBER *number = (NUMBER *)obj;
char *str = 0;
char *s;
int has_dot = 0;
asprintf(&str, "%lf", number->num);
for (s = str; *s != '\0'; ++s) {
if (*s == '.') {
has_dot = 1;
break;
}
}
if (!has_dot) {
fprintf(stream, "%s", str);
free(str);
} else {
while (*s != '\0') ++s;
for (--s; *s != '.'; --s) {
if (*s != '0') break;
*s = '\0';
}
if (*s == '.') *s = '\0';
fprintf(stream, "%s", str);
free(str);
}
}
helper.h
次の一文が追加されています。
char * get_string_string(STRING *string);
helper.c
get_string_string
関数は文字列オブジェクトの文字列を取得します。
char * get_string_string(STRING *string) {
return string->str;
}
test.c
main
関数です。リーダーとプリンターのループを形成しています。
/*
* test.c
*/
#include <stdio.h>
#include "../chapter05/state.h"
#include "../chapter06/reader.h"
#include "printer.h"
int main(void) {
reader_initialize();
while (1) {
printf("> ");
void *obj = reader_read(stdin);
if (!obj) {
if (state == STATE_EXIT) {
reader_free();
return 0;
} else if (state == STATE_ERROR) {
state = STATE_NORMAL;
continue;
} else {
fprintf(stderr, "未実装のコードに到達しました\n");
continue;
}
}
printer_print(stdout, obj);
fputc('\n', stdout);
}
return 0;
}
動かしてみよう
my-lisp2のリポジトリをクローンして、chapter07ディレクトリに移動します。そこでbuild.sh
かbuild.bat
を実行すればこれまでのコードをビルドすることができます。Windows(Visual Studio), macOS(Xcode), Linuxの3つで試験しているのでいつもの環境でビルドできると思います。Visual Studioでは[Developer Command Prompt for VS 2022]でbuild.bat
を実行してください。clang
ではなくgcc
でビルドしたい人はbuild.sh
の中を書き換えてください。
プログラムを停止するにはEOFを入力します。コマンドプロンプトでは[Ctrl + Z]を押してからエンターキーを押します。その他の環境では、[Ctrl + D]を押します。