tiny C の構文解析

前に解説したデータ構造を使って、parserを作る。 構文解析に関連するのは、以下の2つのファイルである。

プログラムでは、ASTを作り、それを処理ルーチンに渡すということにしてあ る。

字句解析のプログラム: yylex (clex.c)

字句解析はlex.cで定義されているyylexである。 (yaccからは、yylexのインタフェースにで呼び出される) yylexからは、yaccの終端記号が返され、意味値がある場合(終端記号が値を持 つ場合)には、yylval.valにセットして返す。
int yylex()
{
    int c,n;
    char *p;
again:
    c = getChar();
    if(isspace(c)) goto again;
    switch(c){
    case '+':
    case '-':
    case '*':
    case '>':
    case '<':
    case '(':
    case ')':
    case '{':
    case '}':
    case ']':
    case '[':
    case ';':
    case ',':
    case '=':
    case EOF:
	return c;
    case '"':
	p = yytext;
	while((c = getChar()) != '"'){
	    *p++ = c;
	}
	*p = '\0';
	yylval.val = makeNum((int)strdup(yytext));
	return STRING;
    }
    if(isdigit(c)){
	n = 0;
	do {
	    n = n*10 + c - '0';
	    c = getChar();
	} while(isdigit(c));
	ungetChar(c);
	yylval.val = makeNum(n);
	return NUMBER;
    }
    if(isalpha(c)){
	p = yytext;
	do {
	    *p++ = c;
	    c = getChar();
	} while(isalpha(c));
	*p = '\0';
	ungetChar(c);
	if(strcmp(yytext,"var") == 0) 
	    return VAR;
	else if(strcmp(yytext,"if") == 0) 
	    return IF;
	else if(strcmp(yytext,"else") == 0) 
	    return ELSE;
	else if(strcmp(yytext,"return") == 0) 
	    return RETURN;
	else if(strcmp(yytext,"while") == 0) 
	    return WHILE;
	else if(strcmp(yytext,"for") == 0) 
	    return FOR;
	else if(strcmp(yytext,"println") == 0) 
	    return PRINTLN;
	else {
	    yylval.val = makeSymbol(yytext);
	    return SYMBOL;
	}
    }
    fprintf(stderr,"bad char '%c'\n",c);
    exit(1);
}

構文解析のプログラム : cparser.y

parserは、yaccで記述されている。部分部分に分けて解説する。

まずは、終端記号のtokenの定義を行う。1文字で表されるtokenはその文字自 体をつかってよい。例えば、+などの記号は、'+'と記述する。 tokenの定義は、%tokenで行う。

%token NUMBER
%token SYMBOL
%token STRING
%token VAR
%token IF
%token ELSE
%token RETURN
%token WHILE
%token FOR
%token PRINTLN
ここで、tokenとして定義された名前は、生成されたCプログラムの中で、 #defineで適当な値に定義されている。よって、最後にincludeされている clex.cの中では値として使うことができる。

次に、生成されたCプログラムの中で必要となるファイルのincludeを行う。 ここでは、stdio.hとAST.hをincludeする。

%{
#include <stdio.h>
#include "AST.h"
%}

意味値として返される値のデータ型を定義する。このプログラムでは、ASTを 意味値として返すために、ASTへのポインタを意味値の値としている。

%union {
    AST *val;
}
clex.cの中では、終端記号についての意味値はyylval.valで参照する。 このプログラムでは、一つのデータ型しかつかわないため、AST *valだけであ る。このvalは%typeでデータ型を示す名前として用いられる。

次に、tokenの優先度とtokenに対する意味値のデータ型を指定する。 意味値のデータ型の指定は%typeで行う。
%right '='
%left '<' '>'
%left '+' '-'
%left '*'

%type <val> parameter_list block local_vars symbol_list 
%type <val> statements statement expr primary_expr arg_list
%type <val> SYMBOL NUMBER STRING
なお、意味値を持たないtokenについてはデータ型を定義しない。

最後に、入力全体を表すtokenを%startで指定する。このプログラムでは、 programである。
%start program

構文の定義は%%で始める。どのような順番でもいいが、通常、top-downに 定義していく。programは空であるか(入力がなし)、もしくは外部定義の列 external_definitionsである。

%%
program: /* empty */
	| external_definitions
	;

列の定義は以下のように定義する。left recursionに定義することに注意。

external_definitions:
	  external_definition
	| external_definitions external_definition
	;

外部定義external_definitionは、関数定義、大域変数の定義、もしくは配列 の定義である。

external_definition:
	  SYMBOL parameter_list block  /* fucntion definition */
	{ defineFunction(getSymbol($1),$2,$3); }
	| VAR SYMBOL ';'
	{ declareVariable(getSymbol($2),NULL); }
	| VAR SYMBOL '=' expr ';'
        { declareVariable(getSymbol($2),$4); }
	| VAR SYMBOL '[' expr ']' ';'
	{ declareArray(getSymbol($2),$4); }
	;
この外部定義が認識された時点で、それぞれの処理する関数を呼び出す。 シンボルを引数にするために、getSymbolを使っていることに注意。 これらの関数はインタプリタ、コンパイラによって違うものをリンクする。

パラメータの並びの定義。パラメータは、シンボルの並びを'('と')'ではさん だもの。

parameter_list:
	 '(' ')'
	 { $$ = NULL; }
	| '(' symbol_list ')' 
	 { $$ = $2; }
	;

symbol_list: 
	  SYMBOL
	 { $$ = makeList1($1); }
	| symbol_list ',' SYMBOL
	 { $$ = addLast($1,$3); }
	;
シンボルの並びはシンボルを','で区切ったもの。まず、最初にmakeList1で最 初のリストを作り、それにaddLastで続くシンボルを最後に加えて生成してい る。

blockすなわち複文は、'{'ではじまり、'}'で終る。最初に局所変数の定義 local_varsがあり、文の並びが続くもの。複文を表すASTは、左に局所変数の リスト、右に文のリストをいれたものである。

block: '{' local_vars statements '}'
	{ $$ = makeAST(BLOCK_STATEMENT,$2,$3); }
	;

statements:
	  statement
	 { $$ = makeList1($1); }
	| statements statement
	 { $$ = addLast($1,$2); }
	;

local_vars: 
	  /* NULL */ { $$ = NULL; }
	| VAR symbol_list ';'
	  { $$ = $2; }
	;
local_varsは、なくてもよい。キーワードVARがある場合には、局所変数の定 義なので、シンボルの並びを読み込む。文の並びの処理の仕方は、シンボルの ならびと同じように、最初にmakeList1で最初のリストを作り、それにaddLast で続くシンボルを最後に加えてつくる。

以下が、文の定義である。それぞれの文に対応したASTを作り返す。

statement:
	 expr ';'
	 { $$ = $1; }
	| block
	 { $$ = $1; }
	| IF '(' expr ')' statement
	 { $$ = makeAST(IF_STATEMENT,$3,makeList2($5,NULL)); }
        | IF '(' expr ')' statement ELSE statement
	 { $$ = makeAST(IF_STATEMENT,$3,makeList2($5,$7)); }
	| RETURN expr ';'
	 { $$ = makeAST(RETURN_STATEMENT,$2,NULL); }
	| RETURN ';'
	 { $$ = makeAST(RETURN_STATEMENT,NULL,NULL); }
	| WHILE '(' expr ')' statement
	 { $$ = makeAST(WHILE_STATEMENT,$3,$5); }
	| FOR '(' expr ';' expr ';' expr ')' statement
	 { $$ = makeAST(FOR_STATEMENT,makeList3($3,$5,$7),$9); }
	;

以下が、式の定義である。

expr: 	 primary_expr
	| SYMBOL '=' expr
	 { $$ = makeAST(EQ_OP,$1,$3); }
	| SYMBOL '[' expr ']' '=' expr
	 { $$ = makeAST(SET_ARRAY_OP,makeList2($1,$3),$6); }
	| expr '+' expr
	 { $$ = makeAST(PLUS_OP,$1,$3); }
	| expr '-' expr
	 { $$ = makeAST(MINUS_OP,$1,$3); }
	| expr '*' expr
	 { $$ = makeAST(MUL_OP,$1,$3); }
	| expr '<' expr
	 { $$ = makeAST(LT_OP,$1,$3); }
	| expr '>' expr
	 { $$ = makeAST(GT_OP,$1,$3); }
	;

次の定義が、primary_exprである。変数や定数の他に、配列要素の参照、関数 呼び出しが定義してある。関数呼び出しでは、左に関数名、右に引数のならび のリストを持つCALL_OPのASTノードを作る。唯一のシステム関数である printlnはここで定義してある。

primary_expr:
	  SYMBOL
	| NUMBER
	| STRING
	| SYMBOL '[' expr ']'
	  { $$ = makeAST(GET_ARRAY_OP,$1,$3); }
	| SYMBOL '(' arg_list ')'
	 { $$ = makeAST(CALL_OP,$1,$3); }
	| PRINTLN  '(' arg_list ')'
	 { $$ = makeAST(PRINTLN_OP,$3,NULL); }
	;

arg_list:
	 expr
	 { $$ = makeList1($1); }
	| arg_list ',' expr
	 { $$ = addLast($1,$3); }
	;
引数のリストは、','で区切ったexprの並びになるので、順次、exprのASTのリ ストをつくる。

最後に、%%で終る。そのあとは、任意のCのプログラムが書けるので、ここに lex.cをincludeする。このようにすることのメリットは、clex.cのなかで、 tokenの名前をマクロで定義されているものとして、使うことができる点であ る。

%%
#include "clex.c"

yyerror

yaccで生成される構文解析ルーチンでは、エラーを起こした時に、つまり間違っ た構文が入力されたときに、yyerrorという関数が呼び出されるようになって いる。このプログラムでは、yyerrorは以下のように、メッセージをプリント してプログラムを停止する、簡単なものにしている。

void yyerror()
{
    printf("syntax error!\n");
    exit(1);
}
プログラムではエラーの処理については考慮されていない。 本格的なプログラムにするには、エラー処理について考慮する必要がある。

構文解析のプログラムのコンパイル

cparser.yから、yaccを使ってparserを生成する。

% yacc cparser.y
生成されたプログラムは、y.tab.cになっているので、これを適当な名前 (cparser.c)に変えて、Cコンパイラで、コンパイルする。
% mv y.tab.c cparser.c
% cc -c cparser.c
なお、clex.cはcparser.cにincludeされているので、別にコンパイルする必要 はない。