【入門】プログラミング言語を自作する方法②|トークナイザを実装して文字列を分解する(C言語)

自作プログラミング言語 C言語

はじめに

こちらの記事は前回の続きとなっております。前回まだの方はぜひこちらもどうぞ。前回はインタプリタを実装しましたが、今回はトークナイザを導入し、コードを構造的に解析する第一歩を解説します。
前回は文字列をそのまま、バイトコードにコンパイルして、VMが実行してましたが、ついにトークナイザ(字句解析器)を作成しました。

前回の実装では、文字列をそのまま解析していたため、命令が増えるたびに if 文が増えていき、非常に可読性が悪い状態でした。

例えば「push」「add」などの命令ごとに文字列比較を行う必要があり、処理が複雑になりやすい問題がありました。

今回トークナイザを導入したことで、文字列を「意味のある単位(トークン)」に分割できるようになりました。

ただし現段階では、トークンに分解しただけで、まだ命令として解釈していないため、
前回と大きな違いを感じにくいかもしれません。

しかし次にパーサを実装することで、このトークン列をもとに構文解析が可能になり、
よりシンプルで拡張しやすい実装に進化します。

文字列 → トークナイザ → トークン列 → パーサ → 命令

↓こちらが現在作成中の言語になります。

GitHub - yu-corder/goemon-src
Contribute to yu-corder/goemon-src development by creating an account on GitHub.

実装

前回実装した、コンパイラです。ファイルを1行ごと読み込んでいって、指定した文字列だったら、bytecodeに格納していきます。命令の数だけ、ifを書くため、かなりカオスなプログラムになっていました。

 FILE *src = fopen("examples/study.goe", "r");
    if (!src) { perror("Goemon's scroll (study.goe) not found"); return 1; }

    int bytecode[1024];
    int count = 0;
    char line[256];

    //ここから指定した文字列を探して、bytecodeに格納していく。
   //字句解析器がないため、指定した文字列分だけ、if文を書くため、カオスに、、、
    while (fgets(line, sizeof(line), src)) {
        if (strncmp(line, "push", 4) == 0) {
            int val = atoi(line + 5);
            bytecode[count++] = OP_PUSH;
            bytecode[count++] = val;
        } else if (strstr(line, "add")) {
            bytecode[count++] = OP_ADD;

今回実装したミニトークナイザです。

typedef enum {
    TK_PUSH,
    TK_INPUT,
    TK_STORE,
    TK_LOAD,
    TK_GE,
    TK_JZ,
    TK_HALT,
    TK_PRINTS,
    TK_NUMBER,
    TK_STRING,
    TK_IDENT,
    TK_COLON,
    TK_EOF,
} TokenKind;

//構造体(意味のある文字列ごとにまとめる)
typedef struct {
    TokenKind kind;
    int val;
    char str[256];
} Token;

//ここでファイルごと一括で読み取り、分解する
Token tokens[256];
int tokenize (char *p) {
    int i = 0;
    while(*p) {
        if (isspace(*p)) { p++; continue;}

        if (isdigit(*p)) {
            tokens[i].kind = TK_NUMBER;
            tokens[i].val = strtol(p, &p, 10);
            i++;
            continue;
        }

        if (strncmp(p, "push", 4) == 0 && isspace(p[4])) {
            tokens[i++].kind = TK_PUSH;
            p += 4;
            continue;
        } 

        if (*p == '"') {
            p++;
            int len = 0;
            while (*p != '"' && *p != '\0') {
                tokens[i].str[len++] = *p++;
            }
            tokens[i].str[len] = '\0';
            tokens[i].kind = TK_STRING;
            p++;
            i++;
            continue;
        }

        if (isalpha(*p) || *p == '_') {
            int len = 0;
            while (isalnum(*p) || *p == '_') {
                tokens[i].str[len++] = *p++;
            }
            tokens[i].str[len] = '\0';

            if (*p == ':') {
                tokens[i].kind = TK_IDENT;
                i++;
                tokens[i++].kind = TK_COLON;
                p++;
            } else {
                tokens[i++].kind = TK_IDENT;
            }
            continue;
        }
        //不明な文字が入力された場合はエラーとして検出するようにします。
        printf("不明な文字ですぞ: %c\n", *p);
        p++;
    }
    tokens[i].kind = TK_EOF;
    return i;
}

現段階のtokenize関数になります。なんかあんまり、前回と変わってなくね?と思うかもしれませんが、現段階だとトークナイザしかないため、あまり実感湧きませんが、パーサを作ることによって、真価を発揮します。

int main() {
    char *src = read_file("examples/study.goe");
    //tokenize関数を呼ぶ
    int count = tokenize(src);
    parse(tokens);
    printf("絶景かな! Compiled study.goe to study.gb\n");
    return 0;
}

goemonプログラムが下記の内容だったときは分解結果は次のような結果になります。

入力コード

push 10
push 20

↓

トークン列

TK_PUSH
TK_NUMBER(10)
TK_PUSH
TK_NUMBER(20)

このトークン列をもとに、次回はパーサで命令として解釈していきます。

最後に

今回はかなり短めですが、次回パーサを作っていい感じのところをお見せできればと思います。一応この自作言語の最終目標ですが、自作言語を使って簡単なCGI を作るのが目標です。

最後までご覧いただきありがとうございました。次回はいよいよパーサを実装し、
トークンを命令として解釈できるようにしていきます。

次回の記事も投稿しました。見ていただけると嬉しいです。

コメント

タイトルとURLをコピーしました