「電卓プログラムの作成」では、構文解析のときに式の計算をいっしょに行っていましたが、今後の拡張を考えて、今回は簡単な「構文木」を組み立てるように修正します。また、メモリ領域の取得には Boehm GC を使いましょう。不要になったメモリを自動的に回収してくれるので、メモリ管理はとても簡単になります。
最初に、構文木 (式) を表すデータ型 Expr を定義します。数式は二分木で表すことができるので、Expr は二分木と同じような構造になります。
リスト : 構文木の定義
// タグ
enum {Num, Add2, Sub2, Mul2, Div2, Add1, Sub1};
// 構文木
typedef struct expr {
int tag;
union {
double num; // 数値
struct { // 構文木
struct expr *left;
struct expr *right;
};
};
} Expr;
構造体 Expr のメンバ変数 tag がデータの種別を表すタグです。Num が数値、Add2 (+), Sub2 (-), Mul2 (*), Div2 (/) が二項演算子、Add1 (+), Sub1 (-) が単項演算子です。単項演算子は式を 1 つメンバ変数 left に、二項演算子は式を 2 つ left と right に格納します。left が左辺の式、right が右辺の式を表します。union は無名の共用体になっていることに注意してください。
簡単な例を示しましょう。ここでは、Expr を (op, left, right) で表すことにします。
1 + 2 - 3 => (Sub2, (Add2, 1, 2), 3) 1 + 2 * 3 => (Add2, 1, (Mul2, 2, 3)) 1 + -2 * 3 => (Add2, 1, (Mul2, (Sub1, 2, NULL), 3))
1 + 2 * 3 は * の優先順位が高いので、(Mul2, ...) が (Add2, ...) の子になります。
次は構文木を生成する関数を作ります。
リスト ; 構文木の生成
// 数値の生成
Expr *make_number(double n)
{
Expr *e = GC_MALLOC_ATOMIC(sizeof(Expr));
e->tag = Num;
e->num = n;
return e;
}
// 単項演算子の生成
Expr *make_op1(int op, Expr *e1)
{
Expr *e = GC_MALLOC(sizeof(Expr));
e->tag = op;
e->left = e1;
e->right = NULL;
return e;
}
// 二項演算子の生成
Expr *make_op2(int op, Expr *e1, Expr *e2)
{
Expr *e = GC_MALLOC(sizeof(Expr));
e->tag = op;
e->left = e1;
e->right = e2;
return e;
}
make_number は数値を格納する節を生成します。メンバ変数にポインタが含まれていないので、メモリの取得は GC_MALLOC_ATOMIC で行います。make_op1 は単項演算子を表す節を生成します。メンバ変数 left に引数 e1 を、right には NULL をセットします。make_op2 が二項演算子を表す節を生成します。メンバ変数 left に引数 e1 を、right に引数 e2 をセットします。どちらの関数もメンバ変数にポインタがあるので、メモリ領域の取得は GC_MALLOC で行います。
次は構文解析のプログラムを修正します。
リスト : 構文解析
// プロトタイプ宣言
Expr *expression(void);
Expr *term(void);
Expr *factor(void);
// 式
Expr *expression(void)
{
Expr *e = term();
while (true) {
switch (token) {
case Add:
get_token();
e = make_op2(Add2, e, term());
break;
case Sub:
get_token();
e = make_op2(Sub2, e, term());
break;
default:
return e;
}
}
}
// 項
Expr *term(void)
{
Expr *e = factor();
while (true) {
switch (token) {
case Mul:
get_token();
e = make_op2(Mul2, e, factor());
break;
case Div:
get_token();
e = make_op2(Div2, e, factor());
break;
default:
return e;
}
}
}
// 因子
Expr *factor(void)
{
switch (token) {
case Lpar:
get_token();
Expr *e = expression();
if (token == Rpar)
get_token();
else
error("')' expected");
return e;
case Number:
get_token();
return make_number(value);
case Add:
get_token();
return make_op1(Add1, factor());
case Sub:
get_token();
return make_op1(Sub1, factor());
default:
error("unexpected token");
}
}
関数 expression, term, factor の返り値は、データ型が Expr * になります。expression の場合、トークンが Add, Sub であれば、式 e と term の返り値を make_op2 に渡して、構文木を組み立てます。term も同様に、トークンが Mul, Div であれば構文木を組み立てます。
factor の場合、トークンが Lpar であれば expression を再帰呼び出しして、変数 e にセットします。そして、右カッコで閉じられていることを確認してから式 e を返します。トークンが Number の場合は、外部変数 value の値を make_number に渡して、数値を格納した節を生成して返します。Add と Sub の場合は make_op1 で単項演算子を表す節を生成して返します。
次は構文木を評価して式の値を求める関数 eval を作りましょう。次のリストを見てください。
リスト : 式の計算
double eval(Expr *e)
{
switch (e->tag) {
case Num: return e->num;
case Add1: return eval(e->left);
case Sub1: return -eval(e->left);
case Add2: return eval(e->left) + eval(e->right);
case Sub2: return eval(e->left) - eval(e->right);
case Mul2: return eval(e->left) * eval(e->right);
case Div2: return eval(e->left) / eval(e->right);
default:
fprintf(stderr, "invalid operator %d\n", e->tag);
exit(1);
}
}
eval は再帰呼び出しを使うと簡単です。引数 e のタグが Num であれば、e->num を返します。タグが Add1 であれば eval(e->left) の値を返します。Sub1 であれば -eval(left) の値を返します。なお、単項演算子 + は、構文解析の段階で削除することもできます。二項演算子の場合、式 e->left と e->right を eval で評価してから、タグに対応した計算を行うだけです。
最後に式を入力して評価する処理を修正します。次のリストを見てください。
リスト : 式の入力と評価
void toplevel(void)
{
Expr *e = expression();
if (token == Semic) {
printf("=> %.16g\nCalc> ", eval(e));
fflush(stdout);
} else {
error("invalid token");
}
}
関数 toplevel は expression を実行して入力された数式を解析します。そのあと、式がセミコロン (Semic) で終了していることを確認します。次に、expression の返り値 e を eval で評価して、入力された数式を計算し、その結果を表示します。
それでは簡単な実行例を示します。
Calc> 1 + 2 + 3 + 4; => 10 Calc> (1 + 2) * (3 + 4); => 21 Calc> 123456789 * 123456789; => 1.524157875019052e+16 Calc> 1.11111111 * 1.11111111; => 1.234567898765432 Calc> 3 / 2; => 1.5 Calc> 1.23456789 / 1.1111111; => 1.111111112111111
今回はここまでです。次回は変数と組込み関数の機能を追加します。
//
// calc3.c : 電卓プログラム (構文木を組み立てる)
//
// Copyright (C) 2015-2013 Makoto Hiroi
//
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#include <setjmp.h>
#include <gc.h>
// タグ
enum {Num, Add2, Sub2, Mul2, Div2, Add1, Sub1};
// 構文木
typedef struct expr {
int tag;
union {
double num; // 数値
struct { // 構文木
struct expr *left;
struct expr *right;
};
};
} Expr;
// 数値の生成
Expr *make_number(double n)
{
Expr *e = GC_MALLOC_ATOMIC(sizeof(Expr));
e->tag = Num;
e->num = n;
return e;
}
// 単項演算子の生成
Expr *make_op1(int op, Expr *e1)
{
Expr *e = GC_MALLOC(sizeof(Expr));
e->tag = op;
e->left = e1;
e->right = NULL;
return e;
}
// 二項演算子の生成
Expr *make_op2(int op, Expr *e1, Expr *e2)
{
Expr *e = GC_MALLOC(sizeof(Expr));
e->tag = op;
e->left = e1;
e->right = e2;
return e;
}
// トークン
enum {Eof, Number, Add, Sub, Mul, Div, Lpar, Rpar, Semic, Others};
// 外部変数
int ch; // 記号
int token; // トークン
double value; // 数値
jmp_buf err; // エラー脱出用
// トークン名
char *token_name[] = {
"EOF",
"Number",
"+",
"-",
"*",
"/",
"(",
")",
";",
"Others",
};
// 記号の先読み
void nextch(void)
{
ch = getchar();
}
// 先読み記号の取得
int getch(void)
{
return ch;
}
// エラー
_Noreturn
void error(char *mes)
{
fprintf(stderr, "%s, %s\n", mes, token_name[token]);
longjmp(err, 1);
}
#define SIZE 1024
// 整数部をバッファに格納する
int get_fixnum(char *buff, int i)
{
while (isdigit(getch())) {
buff[i++] = getch();
nextch();
}
return i;
}
// 数値を求める
double get_number(void)
{
char buff[SIZE + 1];
char *err;
int i = get_fixnum(buff, 0);
if (getch() == '.') {
buff[i++] = getch();
nextch();
i = get_fixnum(buff,i);
}
if (getch() == 'e' || getch() == 'E') {
buff[i++] = getch();
nextch();
if (getch() == '+' || getch() == '-') {
buff[i++] = getch();
nextch();
}
i = get_fixnum(buff, i);
}
buff[i] = '\0';
double value = strtod(buff, &err);
if (*err != '\0')
error("get_number: not Number\n");
return value;
}
// トークンの切り分け
void get_token(void)
{
// 空白文字の読み飛ばし
while (isspace(getch())) nextch();
if (isdigit(getch())) {
token = Number;
value = get_number();
} else {
switch(getch()){
case '+':
token = Add;
nextch();
break;
case '-':
token = Sub;
nextch();
break;
case '*':
token = Mul;
nextch();
break;
case '/':
token = Div;
nextch();
break;
case '(':
token = Lpar;
nextch();
break;
case ')':
token = Rpar;
nextch();
break;
case ';':
token = Semic;
nextch();
break;
case EOF:
token = Eof;
break;
default:
token = Others;
}
}
}
// 構文解析
Expr *expression(void);
Expr *term(void);
Expr *factor(void);
Expr *expression(void)
{
Expr *e = term();
while (true) {
switch (token) {
case Add:
get_token();
e = make_op2(Add2, e, term());
break;
case Sub:
get_token();
e = make_op2(Sub2, e, term());
break;
default:
return e;
}
}
}
Expr *term(void)
{
Expr *e = factor();
while (true) {
switch (token) {
case Mul:
get_token();
e = make_op2(Mul2, e, factor());
break;
case Div:
get_token();
e = make_op2(Div2, e, factor());
break;
default:
return e;
}
}
}
Expr *factor(void)
{
switch (token) {
case Lpar:
get_token();
Expr *e = expression();
if (token == Rpar)
get_token();
else
error("')' expected");
return e;
case Number:
get_token();
return make_number(value);
case Add:
get_token();
return make_op1(Add1, factor());
case Sub:
get_token();
return make_op1(Sub1, factor());
default:
error("unexpected token");
}
}
// 数式の評価
double eval(Expr *e)
{
switch (e->tag) {
case Num: return e->num;
case Add1: return eval(e->left);
case Sub1: return -eval(e->left);
case Add2: return eval(e->left) + eval(e->right);
case Sub2: return eval(e->left) - eval(e->right);
case Mul2: return eval(e->left) * eval(e->right);
case Div2: return eval(e->left) / eval(e->right);
default:
fprintf(stderr, "invalid operator %d\n", e->tag);
exit(1);
}
}
void toplevel(void)
{
Expr *e = expression();
if (token == Semic) {
printf("=> %.16g\nCalc> ", eval(e));
fflush(stdout);
} else {
error("invalid token");
}
}
int main(void)
{
printf("Calc> ");
fflush(stdout);
nextch();
while (true) {
if (!setjmp(err)) {
get_token();
if (token == Eof) break;
toplevel();
} else {
// 入力のクリア
while (getch() != '\n') nextch();
printf("Calc> ");
fflush(stdout);
}
}
printf("bye\n");
return 0;
}