2022年2月19日 星期六

yacc/bison 系列 (3) bison 與 c++

千丈之堤, 以螻蟻之穴潰
yacc/bison 系列 (2) - 輸出 AST, if statement」展示了 bison 和 c++ 的用法, 雖然可以用, 但不算是正式的用法, bison 有「支援真正的 c++」用法, 輸出的 parser 是 c++ 版本, 還跟上 c++20 的標準, 對於我這個 c++ 愛好者來說, 這樣很棒。

但是我不會用 ...

好不容易花了很大的力氣才有點會用 bison, 突然要改用 c++ 版本, 又要突破一些障礙才行, 感覺又要重學。我真的應該為了使用 c++ 而去學習嗎?

而且網路上的文章很少這樣用, 用 bison, c++ keyword 找到的文章, 大部份找到和 c++ 的搭配都是我之前的那種用法; 另外的就是 bison 文件裡頭的 c++ 說明 - 10.1.1 A Simple C++ Example

還有範例: https://github.com/akimd/bison/tree/master/examples/c%2B%2B

bison 文件除了 c++ 還有 d, java 的說明。

其中的 calc++ 範例從 bison 弄出可以編譯的版本有點麻煩, 我直接把 calc++ 這個範例放在 bitbucket。

另外找到這篇: Flex and Bison in C++

flex 也有個輸出 c++ lexer 的版本, 組合下來的情況有點亂, 都不知道怎麼相互搭配了。另外還有一個 bisoncpp, 讓情況更複雜了。

以 calc++ 來說明 flex/bison 怎麼搭配使用。bison 輸出的是 c++ code parser, flex 輸出的是 c++ code, 但不是 class 版本的 yylex(), 然後使用的 yylex() prototype 是 yy::parser::symbol_type yylex(driver& drv), 看傳回值的部份, 不是原本的 int, 所以這邊用了

driver.hh
26 // Give Flex the prototype of yylex we want ...
27 # define YY_DECL \
28   yy::parser::symbol_type yylex (driver& drv)
29 // ... and declare it for the parser's sake.
30 YY_DECL;


這樣會就使用 yy::parser::symbol_type yylex() 而不是 int yylex(), 那一定要用 yy::parser::symbol_type yylex(), 不能用 int yylex() 嗎? 看起來是不行, 如果可以 return token::NUMBER 也許還可以, 不過 list 1 定義的 enum 是被放在 class private, 所以無法直接存取, 還是得透過 make_XXXX 來使用這些 token enum, 就算可以好了, 也沒有 yylval 來把 yylex 的 token 傳給 bison。補充的 hoc_cpp_1.yy 勉強可以這樣用。

list 1. hoc_cpp.cpp
 695     /// Token kinds.
 696     struct token
 697     {
 698       enum token_kind_type
 699       {
 700         YYEMPTY = -2,
 701     END_OF_FILE = 0,               // END_OF_FILE
 702     YYerror = 256,                 // error
 703     YYUNDEF = 257,                 // "invalid token"
 704     NUMBER = 258,                  // NUMBER
 705     ASSIGN = 259,                  // ":="
 706     MINUS = 260,                   // "-"
 707     PLUS = 261,                    // "+"
 708     STAR = 262,                    // "*"
 709     SLASH = 263,                   // "/"
 710     LPAREN = 264,                  // "("
 711     RPAREN = 265,                  // ")"
 712     NEWLINE = 266                  // "\n"
 713       };


目前我遇到的困境是, 使用 bison 輸出 c++ parser 的版本, 不知道怎麼和 flex 輸出的 lexer 搭配。原本的 c parser 是搭配 int yylex(), 但是 c++ parser 是搭配 parser::symbol_type yylex(), 我目前還不知道怎麼用 flex 輸出 parser::symbol_type yylex()。

不過沒關係, 先來搞定 bison 輸出 c++ parser 的用法。為什麼要這麼麻煩呢? 因為我想要用 std::string, 但是原本的 c parser union 在使用 std::string 時, 會有問題, bison 會輸出類似 u.cpp 的 union, 用 c++ 編譯會有問題, 需要自己補上相關的 ctor 才行, 而要讓 bison 輸出 c parser 編譯可以過, 還要 copy ctor。

u.cpp
 2 #include <cstdio>
 3 #include <string>
 4 using namespace std;
 5
 6 union YYSTYPE
 7 {
 8   string id;
 9   int num;
10   #if 0
11   YYSTYPE(){};
12   ~YYSTYPE(){};
13   YYSTYPE operator=(const YYSTYPE&){}
14   #endif
15 };
16
17 YYSTYPE yylval;
18
19 int main(int argc, char *argv[])
20 {
21
22   return 0;
23 }
24
25 g++ -g -std=c++17 -Wall a1.cpp -o a1
26 a1.cpp:16:9: error: use of deleted function ‘YYSTYPE::YYSTYPE()’
27    16 | YYSTYPE yylval;

前言說完了, 該進入正題, 來把最一開始的四則運算改寫為 c++ 版本的 bison 語法。

hoc_cpp.yy
  1 %require "3.2"
  2 %debug
  3 %language "c++"
  4 %define api.token.constructor
  5 %define api.value.type variant
  6 %define api.location.file none
  7 %define parse.assert
  8 %locations
  9 
 10 %code requires // *.hh
 11 {
 12 #include <string>
 13 #include <vector>
 14 typedef std::vector<std::string> strings_type;
 15 }
 16 
 17 %code // *.cc
 18 {
 19 #include <iostream>
 20 #include <sstream>
 21 
 22   namespace yy
 23   {
 24     // Prototype of the yylex function providing subsequent tokens.
 25     static parser::symbol_type yylex ();
 26 
 27     // Print a vector of strings.
 28     std::ostream&
 29     operator<< (std::ostream& o, const strings_type& ss)
 30     {
 31       o << '{';
 32       const char *sep = "";
 33       for (strings_type::const_iterator i = ss.begin (), end = ss.end ();
 34            i != end; ++i)
 35         {
 36           o << sep << *i;
 37           sep = ", ";
 38         }
 39       return o << '}';
 40     }
 41   }
 42 
 43   // Convert to string.
 44   template <typename T>
 45     std::string
 46     to_string (const T& t)
 47   {
 48     std::ostringstream o;
 49     o << t;
 50     return o.str ();
 51   }
 52 }
 53 
 54 %token <int> NUMBER;
 55 %token <char> CHAR;
 56 %token END_OF_FILE 0;
 57 %token
 58   ASSIGN  ":="
 59   PLUS    "+"
 60   MINUS   "-"
 61   MUL     "*"
 62   DIV     "/"
 63   LPAREN  "("
 64   RPAREN  ")"
 65   NEWLINE  "\n"
 66 ;
 67 
 68 %type <int> list;
 69 %type <int> expr;
 70 
 71 %left "+" "-"
 72 %left "*" "/"
 73 
 74 %%
 75 
 76 list:    {printf("\taaempty\n");}
 77      | list "\n" {printf("list \\n\n");}
 78      | list expr "\n" { printf("%d\n", $2); }
 79 
 80 expr: NUMBER {$$ = $1; printf("xx num %d\n", $1);}
 81        | expr "+" expr {$$ = $1 + $3;}
 82        | expr "-" expr {$$ = $1 - $3;}
 83        | expr "*" expr {$$ = $1 * $3;}
 84        | expr "/" expr {$$ = $1 / $3;}
 85        | "(" expr ")"
 86        {
 87          $$ = $2;
 88        }
 89 
 90 
 91 %%
 92 
 93 char *progname;
 94 int lineno = 1;
 95 
 96 namespace yy
 97 {
 98   // Use nullptr with pre-C++11.
 99 #if !defined __cplusplus || __cplusplus < 201103L
100 # define NULLPTR 0
101 #else
102 # define NULLPTR nullptr
103 #endif
104 
105   // The yylex function providing subsequent tokens:
106   // TEXT         "I have three numbers for you."
107   // NUMBER       1
108   // NUMBER       2
109   // NUMBER       3
110   // TEXT         "And that's all!"
111   // END_OF_FILE
112 
113   static
114   parser::symbol_type
115   yylex ()
116   {
117     int c;
118     int input_val;
119     static int count = 0;
120     const int stage = count;
121     ++count;
122     parser::location_type loc (NULLPTR, stage + 1, stage + 1);
123 
124     while ((c=getchar()) == ' ' || c == '\t')
125       ;
126 
127     if (c == EOF)
128       return parser::make_END_OF_FILE (loc);
129     if (c == '.' || isdigit(c) )
130     {
131       ungetc(c, stdin);
132       //scanf("%lf", &input_val);
133       scanf("%d", &input_val);
134       //val = 5;
135       return parser::make_NUMBER (input_val, loc);
136     }
137 
138     switch (c)
139     {
140       case '+':
141       {
142         return parser::make_PLUS(loc);
143         break;
144       }
145       case '-':
146       {
147         return parser::make_MINUS(loc);
148         break;
149       }
150       case '*':
151       {
152         return parser::make_MUL(loc);
153         break;
154       }
155       case '/':
156       {
157         return parser::make_DIV(loc);
158         break;
159       }
160       case '(':
161       {
162         return parser::make_LPAREN(loc);
163         break;
164       }
165       case ')':
166       {
167         return parser::make_RPAREN(loc);
168         break;
169       }
170     }
171 
172     if (c == '\n')
173     {
174       ++lineno;
175       return parser::make_NEWLINE(loc);
176     }
177     //return c;
178     //return parser::make_CHAR(c, loc);
179     char str[2] = {0};
180     str[0] = c;
181     throw yy::parser::syntax_error (loc, "invalid character: " + std::string(str));
182   }
183 
184   // Mandatory error function
185   void parser::error (const parser::location_type& loc, const std::string& msg)
186   {
187     std::cerr << loc << ": " << msg << '\n';
188   }
189 }
190 
191 int main ()
192 {
193   yy::parser p;
194   p.set_debug_level (!!getenv ("YYDEBUG"));
195   return p.parse ();
196 }
197 
198 // Local Variables:
199 // mode: C++
200 // End:


hoc_cpp.yy L1 ~ 52 從 https://github.com/akimd/bison/blob/master/examples/c%2B%2B/variant.yy 這邊照抄, 其他部份也是從這個檔案改寫而來。

最主要是 parser::symbol_type yylex (); 的改寫, 本來 return NUMBER 這樣的 macro 改為 return parser::make_NUMBER (input_val, loc), 另外也要定義 hoc_cpp.yy L55 ~ L65 的 token, 這樣才能用 parser::make_END_OF_FILE(), parser::make_NEWLINE(), parser::make_PLUS(), parser::make_MINUS() 這些 member function。

來看看 make_NUMBER (int v, location_type l) ref: list 2, 怎麼那麼巧, 第一個參數是 int, 那就是 hoc_cpp.yy L54 定義的 54 %token <int> NUMBER;, 如果是寫成 %token <std::string> NUMBER;, 那 make_NUMBER(std::string v, location_type l) 就會長這樣。

list 2. hoc_cpp.cpp
1071 #if 201103L <= YY_CPLUSPLUS
1072       static
1073       symbol_type
1074       make_NUMBER (int v, location_type l)
1075       {
1076         return symbol_type (token::NUMBER, std::move (v), std::move (l));
1077       }
1078 #else
1079       static
1080       symbol_type
1081       make_NUMBER (const int& v, const location_type& l)
1082       {
1083         return symbol_type (token::NUMBER, v, l);
1084       }
1085 #endif


比較麻煩的是本來可以 return getch 的 c, 我不知道要怎麼產生一個類似 make_CHAR 的 member function, 所以用 parser::make_NUMBER 代替, 另外要處理 parser::make_PLUS(), parser::make_MINUS() 也比原本 return c 麻煩不少。

L58, L59 MINUS, PLUS 似乎要用 "+", 用 '+' 就會有奇怪的錯誤, 這個經過測試有點複雜, 某些組合是可以用 '+', 但是要改規則寫法, 就不多提了。

再來 main call parse() 也不一樣, 變成 member function 了。

以下是編譯指令:

g++ -g -std=c++17 -Wall -c hoc_cpp.cpp
g++ -g -std=c++17 -Wall hoc_cpp.o -o hoc_cpp

這樣就完成一個 c++ 版本的 bison parser。

另外補充一個寫法 hoc_cpp_1.yy, 沒有使用 %define api.token.constructor, 影響到什麼呢? yylex 的 function prototype, hoc_cpp_1.yy L24 那樣, 而 yylex return 也不同, 改成 hoc_cpp_1.yy L133, L134, 使用了 emplace(), 相當奇怪的用法。「10.1.7 C++ Scanner Interface」提到了這個, 有興趣的朋友自己看, 就不說明了。

hoc_cpp_1.yy
  1 %language "c++"
  2 %require "3.2"
  3 %debug
  4 %define api.value.type variant
  5 %define parse.assert
  6 %locations
  7 
  8 %code requires // *.hh
  9 {
 10 #include <string>
 11 #include <vector>
 12 typedef std::vector<std::string> strings_type;
 13 
 14 #include "hoc_cpp_1.tab.hh"
 15 }
 16 
 17 %code // *.cc
 18 {
 19 #include <iostream>
 20 #include <sstream>
 21 
 22   namespace yy
 23   {
 24     int yylex (yy::parser::value_type *yylval, yy::parser::location_type *yylloc);
 25 
 26     // Print a vector of strings.
 27     std::ostream&
 28     operator<< (std::ostream& o, const strings_type& ss)
 29     {
 30       o << '{';
 31       const char *sep = "";
 32       for (strings_type::const_iterator i = ss.begin (), end = ss.end ();
 33            i != end; ++i)
 34         {
 35           o << sep << *i;
 36           sep = ", ";
 37         }
 38       return o << '}';
 39     }
 40   }
 41 
 42   // Convert to string.
 43   template <typename T>
 44     std::string
 45     to_string (const T& t)
 46   {
 47     std::ostringstream o;
 48     o << t;
 49     return o.str ();
 50   }
 51 }
 52 
 53 %token <int> NUMBER;
 54 %token END_OF_FILE 0;
 55 %token
 56   ASSIGN  ":="
 57   MINUS   "-"
 58   PLUS    "+"
 59   STAR    "*"
 60   SLASH   "/"
 61   LPAREN  "("
 62   RPAREN  ")"
 63   NEWLINE  "\n"
 64 ;
 65 
 66 %type <int> list;
 67 %type <int> expr;
 68 
 69 %left "+" "-"
 70 %left "*" "/"
 71 
 72 %%
 73 
 74 list:    {printf("\taaempty\n");}
 75      | list "\n" {printf("list \\n\n");}
 76      | list expr "\n" { printf("%d\n", $2); }
 77 
 78 expr: NUMBER {$$ = $1; printf("xx num %d\n", $1);}
 79        | expr "+" expr {$$ = $1 + $3;}
 80        | expr "-" expr {$$ = $1 - $3;}
 81        | expr "*" expr {$$ = $1 * $3;}
 82        | expr "/" expr {$$ = $1 / $3;}
 83        | '(' expr ')'
 84 
 85 
 86 %%
 87 
 88 char *progname;
 89 int lineno = 1;
 90 
 91 namespace yy
 92 {
 93   // Use nullptr with pre-C++11.
 94 #if !defined __cplusplus || __cplusplus < 201103L
 95 # define NULLPTR 0
 96 #else
 97 # define NULLPTR nullptr
 98 #endif
 99 
100   // The yylex function providing subsequent tokens:
101   // TEXT         "I have three numbers for you."
102   // NUMBER       1
103   // NUMBER       2
104   // NUMBER       3
105   // TEXT         "And that's all!"
106   // END_OF_FILE
107 
108   int yylex (yy::parser::value_type *yylval, yy::parser::location_type *yylloc)
109   {
110     int c;
111     int input_val;
112     static int count = 0;
113     const int stage = count;
114     ++count;
115     //parser::location_type loc (NULLPTR, stage + 1, stage + 1);
116 
117     while ((c=getchar()) == ' ' || c == '\t')
118       ;
119 
120     if (c == EOF)
121     {
122       ;//return parser::make_END_OF_FILE (loc);
123       return yy::parser::token::END_OF_FILE;
124     }
125     if (c == '.' || isdigit(c) )
126     {
127       ungetc(c, stdin);
128       //scanf("%lf", &input_val);
129       scanf("%d", &input_val);
130       //scanf("%d", yyla->value);
131       //val = 5;
132       ;//return parser::make_NUMBER (input_val, loc);
133       yylval->emplace<int>() = input_val;
134       return yy::parser::token::NUMBER;
135     }
136 
137     switch (c)
138     {
139       case '+':
140       {
141         ;//return parser::make_PLUS(loc);
142         return yy::parser::token::PLUS;
143         break;
144       }
145       case '-':
146       {
147         ;//return parser::make_MINUS(loc);
148         return yy::parser::token::MINUS;
149         break;
150       }
151     }
152 
153     if (c == '\n')
154     {
155       ++lineno;
156       ;//return parser::make_NEWLINE(loc);
157       return yy::parser::token::NEWLINE;
158     }
159       yylval->emplace<int>() = c;
160       //return yy::parser::token::NUMBER;
161     return c;
162     //return parser::make_NUMBER (c, loc);
163 
164   #if 0
165     static int count = 0;
166     const int stage = count;
167     ++count;
168     parser::location_type loc (NULLPTR, stage + 1, stage + 1);
169     switch (stage)
170       {
171       case 0:
172         return parser::make_TEXT ("I have three numbers for you.", loc);
173       case 1:
174       case 2:
175       case 3:
176         return parser::make_NUMBER (stage, loc);
177       case 4:
178         return parser::make_TEXT ("And that's all!", loc);
179       default:
180         return parser::make_END_OF_FILE (loc);
181       }
182   #endif 
183   }
184 
185   // Mandatory error function
186   void parser::error (const parser::location_type& loc, const std::string& msg)
187   {
188     std::cerr << loc << ": " << msg << '\n';
189   }
190 }
191 
192 int main ()
193 {
194   yy::parser p;
195   p.set_debug_level (!!getenv ("YYDEBUG"));
196   return p.parse ();
197 }
198 
199 // Local Variables:
200 // mode: C++
201 // End:


編譯指令:

bison -d hoc_cpp_1.yy
g++ hoc_cpp_1.tab.cc -o hoc_cpp_1

另外 flex 不是 gnu 套件的一部份, 有點驚訝, 不知道為什麼?

f
1 lftp ftp.gnu.org:/gnu/flex> cat flex.README
2 Flex is a free implementation of the well-known Lex program for lexical
3 analysis. Since it is not (and never was) a GNU package, we don't
4 distribute it here. Please see http://flex.sourceforge.net for the
5 latest release and information.


ref:

2022年2月5日 星期六

3 分平價娃衣 (1)

繼前一次的 3 分平價娃衣之後, 又購買了類似的平價娃衣。

梦童话娃娃3分娃衣bjd衣服梦童话娃衣洛丽塔娃衣古装娃衣60cm娃娃

颜色分类: 喜鹊衣服[适合3分女娃] 高度: 适合3分女娃



這次選購的是中國風, 不知道是哪個朝代的, 但也不像是漢服。看起來還不錯, 圖片 model 是 dddy L胸, 穿起來合適。

fig 1. 2021-03-26 訂購, 179 rmb。


除了衣服本身另外還有頭飾、項鍊等配件, 項鍊蠻特別的, 不是一般有個鍊子的項鍊, 是一個圓形的環狀結構, 搭配 2 顆金色球體和一塊金牌, 蠻搭這套服裝。



售價 179 rmb, 在材質上當然不會太好, 不過我通常不在意材質的精細, 比較在意造型, 畢竟不是人穿的, 好看就可以, 材質本身一般般我也可以接受。衣服部份沒有扣子拉鍊, 都是用類似魔鬼氈的黏貼方式, 這我也不在意, 穿的起來就好, 但是魔鬼氈會黏到一些衣服上, 移除時會有脫絲的問題, 所以穿的時候我都是把衣服附上的布先貼在魔鬼氈上, 避免去黏到其他的衣服, 整個穿好才把那塊布拿下來, 貼好開口處。



隨著娃衣愈來愈多, 目前已經減少購買的娃衣, 身體已經不夠用了, 堆著一堆娃衣也實在有點浪費, 克制自己的購買慾望還蠻難的, 儘量把舊娃衣拿出來穿了。

>

60厘米娃娃衣服多丽丝凯蒂新款宫廷风bjd公主洋娃娃服装女孩玩具 [交易快照]

颜色分类多洛莉丝衣服(只是衣服)高度60厘米娃娃衣服



158rmb

2022年2月1日 星期二

yacc/bison 系列 (2) - 輸出 AST, if statement

今天是 2022/2/1 是農曆 2022 大年初一, 照例發篇文章來慶祝一下。

黑暗越是深邃, 顯現出來的光芒就越是璀璨耀眼
可以 eval if statement 之後, 我想要印出 if AST, 這個比 eval if statement 還要難。本來應該先寫 eval if statement, 但是我剛做完 if AST, 印象深刻, 先發此文。

找了 ref 列出的參考資料, 看過之後還是覺得難, 程式太完整, 不容易看, 在我自己的經驗中, 教學文必須要簡單, 怎麼樣才能簡單, 拆解, 所以我這篇只做 「if statement」加上「四則運算」, 當然如果只做「四則運算」的 ast 會更簡單, 但 ... 就讓我偷懶一下。

然後範例程式也簡單, 一個檔案就包含全部, 再來是編譯指令也要提供, 這樣看的人才能完整測試。

範例程式雖然簡單, 但不代表這個主題很簡單, bison 我挑戰好幾次了, 直到這次才有一點小進展, 我相信 bison 帶來的回報是可觀的, 所以我願意花時間在上面。

搭配參考資料的範例加上我自己一步一步分析之後, 終於搞出一個版本, 很簡單, 只針對 if statement, 本來更簡單應該要先嘗試四則運算, 但這次我懶得從這裡開始, 跳個一小步, 從「if statement」加上「四則運算」開始。

所謂的一步一步分析是什麼麼意思? 就是修改 bison 檔案之後, 觀察輸出的 .cpp 是長什麼樣, 會有哪些資料結構, 文法規則的程式碼怎麼執行, 推敲出這些執行順序。

我有自信這個範例是最小集合, 只有一個 .y 檔案, 也很容易理解, 程式也沒用上什麼難懂的技巧, 使用 c++ 來搭配 bison, bison 原本是和 c 搭配使用, 但 c++ 憑藉了與 c 的相容性, 輕鬆搭上 bison 列車, 也順便展示 bison 和 c++ 的搭配。c++ 也來到了 c++20 的標準, 還沒能搭上 c++20 的列車, 目前學的 c++ 技能還算夠用。fig 1 是我辛苦後的成果, 當然比之前手工寫法輕鬆很多。

parser 是什麼意思呢? 就是根據輸入的字串分析之後產生對應的動作, 以 if statement 來說:

if (6-3) {2-(1+5);}

分析之後要做什麼動作?

得到 -4 嗎? 不一定哦, 像我是想得到一個 ast, 我並不想 eval 這個敘述句。另外也可能輸出某個機器的組合語言來完成這個 if statement, 這是 bison 另外一個難的地方, bison 幫你處理文法了, 你要怎麼寫程式達到你要的目的呢? 只是要求值 -4 簡單很多。

fig 1. 使用 bison 輸出 AST


hoc_if_ast.y
  1 %{
  2 #include <stdio.h>
  3 #include <ctype.h>
  4 #include <stdlib.h>
  5 #include <unistd.h>
  6 
  7 #include <string>
  8 #include <vector>
  9 #include <iostream>
 10 
 11 using namespace std;
 12 
 13 //#define YYSTYPE double
 14 #define YYDEBUG 1
 15 
 16 
 17 enum NodeType 
 18 {
 19   IF_TYPE,
 20   PLUS,
 21   MINUS,
 22   MUL,
 23   DIV
 24 };
 25 
 26 //#define YYSTYPE AstNode
 27 
 28 class AstNode
 29 {
 30   public:
 31     AstNode(const string str):str_(str), val_(0)
 32     {
 33     }
 34     string str_;
 35     void set_val(const double val)
 36     {
 37       val_ = val;
 38     }
 39     int add_child(AstNode *n)
 40     {
 41       children_.push_back(n);
 42       return 0;
 43     }
 44     double val_;
 45     vector<AstNode *> children_;
 46   private:
 47     NodeType node_type_;
 48 };
 49 
 50 AstNode root{"root"};
 51 
 52 static int cnt;
 53 
 54 int yylex();
 55 int yyerror(const char *s);
 56 %}
 57 
 58 
 59 %union
 60 {
 61   AstNode *node_;
 62   double num_;
 63 }
 64 
 65 %token <num_> NUMBER
 66 %token IF
 67 %left '+' '-'
 68 %left '*' '/'
 69 
 70 %type <node_> expr selection_statement
 71 
 72 %%
 73 
 74 
 75 selection_statement: {}
 76                    | selection_statement IF '(' expr ')' '{' expr ';' '}' '\n'
 77                    {
 78                      // if (6-3) {2-(1+5);}
 79                      AstNode *n = new AstNode{"if"};
 80                      n->add_child($4);
 81                      n->add_child($7);
 82                      cout << "$4->str_: " << $4->str_ << endl;
 83                      cout << "$7->str_: " << $7->str_ << endl;
 84 
 85                      $$ = n;
 86                      root.add_child(n);
 87                      
 88                      printf("new if/then node, cnt: %d\n", cnt);
 89                      ++cnt;
 90                    }
 91 
 92 expr: NUMBER 
 93       {
 94         AstNode *n = new AstNode{"num"};
 95         n->set_val($1);
 96         $$ = n;
 97 
 98       }
 99        | expr '+' expr 
100          {
101            AstNode *n = new AstNode{"plus"};
102            n->add_child($1);
103            n->add_child($3);
104            cout << "$1.val: " << $1->val_ << endl;
105            cout << "$3.val: " << $3->val_ << endl;
106            $$ = n;
107            ++cnt;
108          }
109        | expr '-' expr 
110          {
111            AstNode *n = new AstNode{"minus"};
112            n->add_child($1);
113            n->add_child($3);
114            cout << "$1.val: " << $1->val_ << endl;
115            cout << "$3.val: " << $3->val_ << endl;
116            $$ = n;
117            ++cnt;
118          }
119        | '(' expr ')'
120        {
121         $$ = $2;
122        }
123 
124 %%
125 char *progname;
126 int lineno = 1;
127 
128 
129 int yylex()
130 {
131   int c;
132   while ((c=getchar()) == ' ' || c == '\t')
133     ;
134   
135   if (c == EOF)
136     return 0;
137   if (c == '.' || isdigit(c) )
138   {
139     ungetc(c, stdin);
140     scanf("%lf", &yylval.num);
141     return NUMBER;
142   }
143 
144   if (c == 'i')
145   {
146     int ch;
147 
148     ch = getchar();
149     if (ch == 'f')
150       return IF;
151     else
152     {
153       ungetc(ch, stdin);
154     }
155   }
156 
157   if (c == '\n')
158     ++lineno;
159   return c;  
160 }
161 
162 int warning(const char *s, const char *t)
163 {
164   fprintf(stderr, "%s: %s", progname, s);
165   if (t)
166     fprintf(stderr, " %s", t);
167 
168   fprintf(stderr, " near line %d\n", lineno);
169   return 0;
170 }
171 
172 int yyerror(const char *s)
173 {
174   return warning(s, 0); 
175 }
176 
177 void print_node(const AstNode *n)
178 {
179   cout << "n: ("  << n->str_ << ", "<< n->val_ << ")" << endl;
180   for (auto i: n->children_)
181   {
182     print_node(i);
183   }
184 }
185 
186 void print_tree(const AstNode *n)
187 {
188   if (n->children_.size() == 0) // leaf node
189   {
190     cout << "("  << n->str_ << ", "<< n->val_;
191     cout << ")";
192   }
193   else
194   {
195     cout << "("  << n->str_ << ", "<< n->val_;
196     for (auto i: n->children_)
197     {
198       print_tree(i);
199     }
200     cout << ")";
201   }
202 
203 }
204 
205 
206 int main(int argc, char *argv[])
207 {
208   int opt;
209   progname = argv[0];
210 
211   while ((opt = getopt(argc, argv, "d:h?")) != -1)
212   {
213     switch (opt)
214     {
215       case 'd':
216       {
217         yydebug = strtol(optarg, 0, 10);
218         if (yydebug == 1)
219           printf("enable bison debug mode\n");
220         break;
221       }
222     }
223   }
224 
226   yyparse();
227   cout << "\\tree";
228   print_tree(&root);
229   cout << endl;
230 }      


list 1 編譯指令
bison -y hoc_if_ast.y -o hoc_if_ast.cpp
g++ -g -std=c++17 -Wall hoc_if_ast.cpp -o hoc_if_ast

執行 hoc_if_ast 輸入 if (6-3) {2-(1+5);} 就可以看到結果, 如果要輸出 fig 1 的樹狀圖, 需要 tree 這個工具, 之前也介紹過了。

hoc_if_ast.y L28 定義一個 class AstNode, 用來處理 ast node, 之前做過, 弄個簡化版, 這個要怎麼用呢? 在規則內指定給 $$, 問題來了, $$ 要怎麼指定成 AstNode type, 目的想做 $$ = new AstNode 這樣。

hoc_if_ast.y L59 ~ 63, L70 就是在做這件事, 把 node_ type 也就是 AstNode 指定給 expr, selection_statement 這些規則, 之前一直搞不懂 %type 的意思, 原來是這樣用。

NUMBER 這個希望 $1 是 double type, 得用 hoc_if_ast.y L65 token 語法來定義。

 65 %token <num_> NUMBER
 59 %union
 60 {
 61   AstNode *node_;
 62   double num_;
 63 }
 70 %type <node_> expr selection_statement

對照 bison 寫法轉出來的 c++ code。

 92 expr: NUMBER
 93       {
 94         AstNode *n = new AstNode{"num"};
 95         n->set_val($1);
 96         $$ = n;
 98       }


轉出的 c++ code。

1202   case 4: /* expr: NUMBER  */
1203 #line 93 "hoc_if_ast.y"
1204       {
1205         AstNode *n = new AstNode{"num"};
1206         n->set_val((yyvsp[0].num_));
1207         (yyval.node_) = n;
1209       }


有點感覺了吧, 來看看 yyvsp, yyval type 是什麼? YYSTYPE, 這個 YYSTYPE 是什麼? hoc_if_ast.y L59 ~ L63 union 定義出來的資料結構。
 181 /* Value type.  */
 182 #if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLARED
 183 union YYSTYPE
 184 {
 185 #line 60 "hoc_if_ast.y"
 186
 187   AstNode *node_;
 188   double num_;
 189
 190 #line 191 "hoc_if_ast.cpp"
 192 };
再來看看 if (6-3) {2-(1+5);} bison 是怎麼處理的, 參考 list 2。

list 2. if (6-3) {2-(1+5);} 處理順序
1 if (6-3) {1+5;}
2 6
3 3
4 -
5 1
6 5
7 +
8 if minus plus

bison 會依序處理 6, 3, -, 1, 5, +, if, 所以想法是這樣, 在處理 6 的地方, 產生一個 6 的 AstNode, 在 3 的地方產生一個 AstNode, 在 - 的地方產生一個 - AstNode 並把 6, 3 這 2 個 AstNode 加入到 - AstNode。這個就是四則運算的 AST。 加入 if statement 之後, 麻煩的是怎麼把這個 - AstNode 接到 if AstNode 的子 node。我是看了參考資料的程式碼才知道要這麼寫的, 但我想不透為什麼是這樣。

不過也不用想的太複雜, 反正 $$ 就是規則的傳回值, 我在 expr '+' expr 回傳 + node, expr '-' expr 回傳 - node, selection_statement IF '(' expr ')' '{' expr ';' '}' '\n' 也照樣做就好, $4 就是 - node, $7 就是 + node, 不用管 bison 是怎麼做的, 然後把 - node 放在第一個子 node, then 的部份放在第二個子 node, 如果有 else 的話就放在第三個子 node, 這樣就搞定了。

使用 gdb debug bison 產生的 .c 或是 .cpp 時, 吃到的 source file 是 .y, 有時候你可能不想這樣, 想要對應的是 .c 或是 .cpp 檔案, 這時候可以在 bison 使用 -l, bison 就不會產生 #line, 這樣 gdb 的 source file 就會是 .c 檔。

另外, 使用 --graph 就可以輸出 .dot 檔案, 搭配 dot 這個工具, 就可以輸出一個 svg 圖, 展示 bison grammer 是如何工作的。使用以下指令就可以輸出對應的圖檔。

dot -Tpng hoc_if_ast.dot -o hoc_if_ast.png
dot -Tsvg hoc_if_ast.dot -o hoc_if_ast.svg


最後來談談這個 if 文法規則, 這是我胡亂自己湊出來的, 不是正規的文法, 不過由於符合我的需求, 就這麼用了。參考連結 3 提供了 if 文法規則, 可以參考看看。

ref:
  1. How to use C++ with Bison, V 1.0.2
  2. How to create an abstract syntax tree while parsing an input stream.
  3. if 陳述式 (C)
  4. 5 The Bison Parser Algorithm
  5. 8 Debugging Your Parser