ソースコードをプログラム上の最小単位に分割することを字句解析(Lexical Analysis)と言う。
最小単位を字句(token)と呼ぶ。例えば、
while (abc > 10) { abc = abc - 1; }
の場合、"while", "(", "abc", ">", "10", ")", "{", "abc", "=", "abc", "-", "1", ";", "}"
に分解される。
例えば、C言語では、トークンの種類は、マクロには、
字句解析では、予約語か識別子の区別、演算子と記号の区別はしない考え方と 各予約語や演算子、記号に名前をつけて、異なるトークン種別とする考え方がある。
字句解析自体はさほど難しくないため、C言語などで字句解析器(Lexer)を作成することは 比較的に容易である。しかし、特にボトムアップ構文解析法で構文解析器(Parser)を直接作成するのは簡単ではない。
このため、BNFに似た記法から構文解析器を生成するツールとして、 Yacc(Yet Another Compiler Compiler、ヤック)や GNU の Bison がよく知られている。また、 字句解析器のほうも、BNFに似た規則定義を与えれば字句解析器を自動生成してくれる便利なツールとして、 Lex(lexical analyzer)やFlex(fast lexical analyzer generator)などのレキシカルアナライザジェネレータがある。
ここでは、 Flex(first lexical analyzer generator) によるプログラムソースコードの字句解析について述べる。 下に、英数字からなる文字列と記号だけを区別するプログラムを示す。 %%から%%で囲まれた字句規則は正規表現で記述されている。 [A-Za-z0-9]+ は、AからZ、aからz、および0から9が1個以上を表す。 0個以上のときは、"+" を "*" に変える。0個か1個のときは "?" とする。 2行目([\r\n\t ])は空白文字、即ち、'\r', '\n', '\t', ' ' があったときは何もしないことを表す。 3行目はその他の文字、すなわち、この場合は記号文字が現れたときの処理を表す。 先頭文字「.」は全ての1文字にマッチする正規表現である。
[flex01.lex]%{ int yywrap(void) { return 1; } %} %% [A-Za-z0-9]+ printf("ID: %s\n", yytext); [\r\n\t ] . printf("SY: %s\n", yytext); %% void main() { yylex(); }
flexでC言語プログラムlex.yy.cを生成し、TCCでそれをコンパイルし、flex01.exe を生成する。 その後、flex01.exe を起動し、while (abc > 10) { abc = abc - 1; } をコピペすると、次の結果を得る。
c:\chap05\flex_bison>flex flex01.lex c:\chap05\flex_bison>tcc lex.yy.c -o flex01.exe c:\chap05\flex_bison>flex01.exe while (abc > 10) { abc = abc - 1; } ID: while SY: ( ID: abc SY: > ID: 10 SY: ) SY: { ID: abc SY: = ID: abc SY: - ID: 1 SY: ; SY: }
%{ int yywrap(void) { return 1; } %} %% [\n\t ] [A-Za-z][A-Za-z0-9]* printf("ID: %s\n", yytext); [0-9]+ printf("NM: %s\n", yytext); . printf("SY: %s\n", yytext); %% void main() { yylex(); }
c:\chap05\flex_bison>flex flex02.lex c:\chap05\flex_bison>tcc lex.yy.c -o flex02.exe c:\chap05\flex_bison>flex02.exe 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: }
%{ int yywrap(void) { return 1; } %} %% [\n\t ] [A-Za-z][A-Za-z0-9]* printf("ID: %s\n", yytext); [0-9]+ printf("NM: %s\n", yytext); [0-9]+"."[0-9]+ printf("NM: %s\n", yytext); . printf("SY: %s\n", yytext); %% void main() { yylex(); }
c:\chap05\flex_bison>flex flex02.lex c:\chap05\flex_bison>tcc lex.yy.c -o flex02.exe c:\chap05\flex_bison>flex02.exe while (abc > 10.5) { abc = abc - 1; } ID: while SY: ( ID: abc SY: > NM: 10.5 SY: ) SY: { ID: abc SY: = ID: abc SY: - NM: 1 SY: ; SY: }
ここでは、数値の表記を大きくは 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:\flex_bison>flex03 123.1+12L 6.12e-14-0x34F + 'n' '\n' ("学校") abc NM: 123.1 SY: + NM: 12L NM: 6.12e-14 SY: - NM: 0x34F SY: + CH: 'n' CH: '\n' SY: ( ST: "学校" SY: ) ID: abc
flex03.lex の字句規則は文字列にエスケープ文字や日本語文字が含むとき、正しく動作しないことがある。
実際のコンパイラでは、文法エラーを検出した場合、エラーのある場所をユーザに知らせる必要がある。 即ち、ファイル名や行番号(および行の先頭からの位置)を知らせる。 Flexの場合も追加は可能と思われるが、C言語プログラム部分が大きくなり、 シンプルさが徐々に薄れる。