0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Common Lisp風のLISPを作ってみる(7.プリンター)

Posted at

my-lisp2もやっとテストができる段階まで来ました。ソースコードを解説して、その後に実際に動かしてみます。
ソースコード

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のリポジトリをクローンして、chapter07ディレクトリに移動します。そこでbuild.shbuild.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]を押します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?