Common Lisp風のLISPを作ってみる（７．プリンター）

my-lisp2もやっとテストができる段階まで来ました。ソースコードを解説して、その後に実際に動かしてみます。
[ソースコード](https://github.com/haruo-wakakusa/my-lisp2/tree/main/chapter07)
# printer.h
プリンターが公開する関数は1つだけです。シンプルですね。
```printer.h
/*
 * 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
/*
 * 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`関数の中身です。
```printer.c
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`関数の中身です。エスケープ文字(`|`や`\`)を使って入力したシンボルはプリンターの出力でもエスケープされるようになっています。
```printer.c
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`関数の中身です。リストがドットリストであっても正しく出力されるようになっています。
```printer.c
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`関数の中身です。文字列の中もエスケープされる文字はエスケープするようになっています。
```printer.c
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`を使います。
```printer.c
#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`と`.`を消す処理が書いてあります。
```printer.c
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
次の一文が追加されています。
```helper.h
char * get_string_string(STRING *string);
```
# helper.c
`get_string_string`関数は文字列オブジェクトの文字列を取得します。
```helper.c
char * get_string_string(STRING *string) {
    return string->str;
}
```
# test.c
`main`関数です。リーダーとプリンターのループを形成しています。
```test.c
/*
 * 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のリポジトリ](https://github.com/haruo-wakakusa/my-lisp2/tree/main)をクローンして、chapter07ディレクトリに移動します。そこで`build.sh`か`build.bat`を実行すればこれまでのコードをビルドすることができます。Windows(Visual Studio), macOS(Xcode), Linuxの３つで試験しているのでいつもの環境でビルドできると思います。Visual Studioでは[Developer Command Prompt for VS 2022]で`build.bat`を実行してください。`clang`ではなく`gcc`でビルドしたい人は`build.sh`の中を書き換えてください。
　プログラムを停止するにはEOFを入力します。コマンドプロンプトでは[Ctrl + Z]を押してからエンターキーを押します。その他の環境では、[Ctrl + D]を押します。