トッププログラミング言語 > 字句解析の基礎

字句解析

1.字句解析とは

ソースコードをプログラム上の最小単位に分割することを字句解析(Lexical Analysis)と言う。 最小単位を字句(token)と呼ぶ。例えば、
  while (abc > 10) { abc = abc - 1; }
の場合、"while", "(", "abc", ">", "10", ")", "{", "abc", "=", "abc", "-", "1", ";", "}" に分解される。

例えば、C言語では、トークンの種類は、マクロには、

に分かれる。

字句解析では、予約語か識別子の区別、演算子と記号の区別はしない考え方と 各予約語や演算子、記号に名前をつけて、異なるトークン種別とする考え方がある。

2.C言語による字句解析

例えば、次のプログラム

while (abc > 10) { abc = abc - 1; }
を字句解析して、次のように出力する字句解析器を作ろう。
c:\sys>lexer01
while (abc > 10) { abc = abc - 1; }
ID: while
SY: (
ID: abc
SY: >
NM: 10
SY: )
SY: {
ID: abc
SY: =
ID: abc
SY: -
NM: 1
SY: ;
SY: }

この機能をもつ字句解析器 lexer01.c を下に示す。 構文解析する1行のプログラム(上の例では while (abc > 10) { abc = abc - 1; } )を入力し、 Enterキーを押すと、1文字ずつ読み込み字句解析を行う。

[exer01.c]
#include <stdio.h>
#include <ctype.h>

void main() {
    int  ch;
        
    while ((ch = getchar()) != EOF) {
        if (ch <= ' ') continue;                // 空白文字 [\n\t ]
        if (isalpha(ch)) {                      // 識別子  [A-Za-z][A-Za-z0-9]*
            printf("ID: %c", ch);
            while (isalnum(ch=getchar())) putchar(ch);
            putchar('\n');
        } else if (isdigit(ch)) {               // 数値     [0-9]+
            printf("NM: %c", ch);
            while (isdigit(ch=getchar())) putchar(ch);
            if (ch == '.') {                    // 小数点    数値: [0-9]+"."[0-9]+
                putchar(ch);
                while (isdigit(ch=getchar())) putchar(ch);
            }
            putchar('\n');
        }
        if (ch > ' ') printf("SY: %c\n", ch);   // 記号   .
    }
}

3.C言語の字句規則

実際のC言語の字句規則は上で述べたものより複雑である。

数値の表記には 10進表記のほか16進表記もある。 16進表記は 0x12ab、0X12AB のように、"0x" または "0X" で始まり、後続の文字は数字か英字A〜F、a〜fである。 10進表記には 12, 10,23 などのほか 123L、23u、3.14f のように、 サフィックス L,l, U,u, F,f が付くことがあり(12ULのようにサフィックスが2文字のこともある)、 更に 6.12e-14 のような指数表記もある。

数値は、最終的には、int型やdouble型など数値データ型に変換される。 その変換は字句解析フェーズで行ってもいいし、意味解析フェーズで行ってよい。 制作演習では、意味解析フェーズで行う。 この場合、数値表記の細かいチェックは意味解析で行うほうが楽である。

C言語には、'a'、'3', '\n' のような文字定数、"abc", "学校" のような文字列が含まれる。 これらを含めた字句規則のFlexによる正規表現を以下に示す。 ただし、文字列は、エスケープ文字 '\"'に対応していないので、 文字列内に二重引用符 「"」を含んではならない。

[flex03.lex]
%{
int  yywrap(void) { return 1; }
%}
%%
[\n\t ]
"'"."'"                   printf("CH: %s\n", yytext);
"'\\"."'"                 printf("CH: %s\n", yytext);
"\""[^\"]*"\""            printf("ST: %s\n", yytext);
[_A-Za-z][_A-Za-z0-9]*    printf("ID: %s\n", yytext);
"0"[xX][0-9a-fA-F]+       printf("NM: %s\n", yytext);
[0-9.]+[lLuUfF]?          printf("NM: %s\n", yytext);
[0-9.]+[eE][+-][0-9]+     printf("NM: %s\n", yytext);
.                         printf("SY: %s\n", yytext);
%%
void main() { yylex(); }

上の字句規則をベースとしたC言語による字句解析プログラムlexer03.cを以下に示す。 このプログラムの特徴の一つは、読み込んだ文字を1文字バッファに戻すungetc関数の使用である。 lexer01.c ではungetc関数を使っていない。 字句規則が複雑になってくると、ungetc関数を使わずにプログラムを書くと、プログラムの動きがわかりにくくなる。

[lexer03.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

void error(char *msg) { fprintf(stderr, "%s\n", msg); exit(1); }
int putget(int c) { putchar(c); return getchar(); }
int iskanji(int c) { 
    return (c>=0x81 && c<=0x9F) || (c>=0xE0 && c<=0xFC); 
}

void main() {
    int  ch;

    while ((ch=getchar()) != EOF) {
        if (ch <= ' ') continue;                        //==== 空白字 [\n\t ] ====
        if (isalpha(ch) || ch=='_') {                   // 識別子 [_A-Za-z][_A-Za-z0-9]*
            printf("ID: %c", ch);
            while (isalnum(ch=getchar()) || ch=='_') putchar(ch);
            putchar('\n');
            ungetc(ch, stdin);                          // 未処理文字をバッファに戻す
        } else if (isdigit(ch)) {                       //==== 数値   [0-9.]+ ====
            printf("NM: %c", ch);
            if ((ch=getchar())=='x' || ch=='X') {       // 16進数
                putchar(ch);                            // 先頭が0というチェックは省略した
                while (isxdigit(ch=getchar())) putchar(ch);
            } else {                                    // 10進数
                while (isdigit(ch) || ch=='.') ch = putget(ch);
                while (strchr("fFlLuU",ch) != NULL) ch = putget(ch);
                if (ch == 'e' || ch == 'E') {
                    putchar(ch);    // 指数形式
                    if ((ch=getchar())=='+' || ch=='-') putchar(ch);
                    while (isdigit(ch=getchar())) putchar(ch);
                }
            }
            putchar('\n');
            ungetc(ch, stdin);
        } else if (ch == '\'') {                        //==== 文字 ====
            printf("CH: %c", ch);
            if ((ch=getchar()) == '\\') ch = putget(ch);
            putchar(ch);
            if ((ch=getchar()) != '\'') 
                error("文字定数末尾の引用符(')がない");
            printf("%c\n", ch);
        } else if (ch == '\"') {                        //==== 文字列 ====
            printf("ST: %c", ch);
            while ((ch=getchar()) != EOF && ch != '\"') {
                putchar(ch);
                if (ch=='\\' || iskanji(ch&0xff)) putchar(getchar());
            }
            if (ch != '\"') error("文字列末尾の二重引用符(\")がない");
            printf("%c\n", ch);
        } else {
            printf("SY: %c\n", ch);                     //==== 記号 ====
        }
    }
}

この字句解析器lexer03に

function(123.1+12L, 6.12e-14-0x34F + 'n', '\n', "学校", abc);
を入力して、字句解析させた結果を下に示す。
c:\sys>lexer03
function(123.1+12L, 6.12e-14-0x34F + 'n', '\n', "学校", abc);
ID: function
SY: (
NM: 123.1
SY: +
NM: 12L
SY: ,
NM: 6.12e-14
SY: -
NM: 0x34F
SY: +
CH: 'n'
SY: ,
CH: '\n'
SY: ,
ST: "学校"
SY: ,
ID: abc
SY: )
SY: ;

制作演習では、簡単のために、数値は10進数表記のみとする。

実用Cコンパイラでは、コンパイルするプログラムに文法エラーがあった場合、 プログラムの何行目にそのエラーがあったかを知らせるために、 それぞれのトークンの位置も字句解析結果に含ませる必要がある。

標準C言語コンパイラの場合、コメント文の除去は前処理で行えるが、 前処理がない場合、字句解析でコメント文の除去が必要となる。 具体的には制作演習で取り上げる。

4.エスケープ文字への対応

改行コードやタブコードなど英数字・記号1文字で表せないコードを "\n"、"\t" のように "\" と英字1文字で 表すのがエスケープ文字である。 例えば no\n のアスキーコードは順に 110,111,92,110 であるが、 C言語やJavaなどプログラミング言語で "no\n" と書くと、文字列の終端を表す0を含めて 110,111,92,110,0 ではなく、 110,111,10,0 である。すなわち \ と n がセットとなり、改行コード(アスキーコード10)とみなされる。

このようなエスケープ文字の変換は一般には字句解析で行っておくとよい。 文字列に日本語を含む場合、具体的なプログラムは日本語コード体系に依存する。 Windowsパソコンの標準である Shift-JIS コード体系でのプログラム例をしたに示す。

int  isJp(int c) { return (c>=0x81 && c<=0x9F) || (c>=0xE0 && c<=0xFC); }

char *esc(char *str) {  // \n, \r, \t, \\, \", \' のみサポート
    char c, *s = str, *d = str;
    while (*s) {
        if ((c=*s++) == '\\') {
            c = *s++;
            *d++ = c=='n' ? '\n' : c=='r' ? '\r' : c=='t' ? '\t' : c;
        } else {
            *d++ = c;
            if (isJp(c)) *d++ = *s++;   // 2バイトで表現される日本語文字
        }
    }
    *d++ = '\0';
    return str;
}

このプログラムでは元の文字列領域に変換後の文字列を上書きする。 2文字(2バイト)表現のエスケープ文字が1文字(1バイト)になるから、 変換後の文字列の長さが元の文字列の長さを超えることはない。 文字列の終端を表す 0('\0')の後ろに、元の文字列が残るが特に問題とはならない。

isJp関数は上のプログラム lexer03.c の iskanji関数と同じである。 一般には iskanji という名前が使われることが多いが、漢字だけでなく、 ひらかな、カタカナ、英数字、記号の全ての2バイトコード(一般には全角文字)が対象となるため、 初めての人に誤解を与えないために、isJpとしてみた。

5.2文字演算子

C言語などでは ==、!=、||、&& など数多くの2文字演算子が使われる。 >>= や <<= など3文字演算子も使われるが、本制作演習では扱わない。

例えば、2文字演算子が == と != だけならば、次のようにすればよい。

    token[n].type = SY;
    *p++ = ch;
    if (ch == '!' || ch == '=') {       // 2文字演算子の処理
        ch = fgetc(fin);
        if (ch == '=') *p++ = ch;       // "==" または "!="
        else ungetc(ch, fin);
    }

しかし、この方法は2文字演算子の数が増えると、プログラム行数が増え効率的ではない。 次のようにした方がよい。


#define OPERATOR2 "== != <= >= >> << && || "    // 末尾のスペース必須

   char op2[4];        // 3文字の文字列を格納する
    token[n].type = SY;
    *p++ = ch;
    sprintf(op2, "%c%c ", ch, fgetc(fin));      // 3文字目のスペース必須
    if (strstr(OPERATOR2,op2) != NULL) {
        *p++ = op2[1];		// 2文字演算子だった
    } else {
        ungetc(op2[1], fin);    // 2文字演算子でなかったので2文字目を元に戻す
    }