顯示具有 bison 標籤的文章。 顯示所有文章
顯示具有 bison 標籤的文章。 顯示所有文章

2022年8月5日 星期五

union and std::string

這個其實是我在用 bison 和 c++ 時遇到的問題, 我不想用 char*, std::string *, 想直接用 std::string, 參考 u.cpp L7, 所以才碰到這個問題。

bison 的解法是改用輸出 c++ 版本的 parser, 紀錄在「yacc/bison 系列 (3) bison 與 c++

u.cpp
 1 #include <cstdio>
 2 #include <string>
 3 using namespace std;
 4 
 5 union YYSTYPE
 6 {
 7   string id;
 8   int num;
 9   #if 0
10   YYSTYPE(){};
11   ~YYSTYPE(){};
12   YYSTYPE operator=(const YYSTYPE&){}
13   #endif
14 };
15 
16 YYSTYPE yylval;
17 
18 int main(int argc, char *argv[])
19 {
20 
21   return 0;
22 }


list 1. error message
g++ -std=c++17 u.cpp

1 u.cpp:16:9: error: use of deleted function ‘YYSTYPE::YYSTYPE()’
2    16 | YYSTYPE yylval;
3       |         ^~~~~~
4 u.cpp:5:7: note: ‘YYSTYPE::YYSTYPE()’ is implicitly deleted because the default definition would be ill-formed:
5     5 | union YYSTYPE


這個情境在 c++ primer the 5th 中文版「19.6 union:節省空間的類別」有提到, 有點複雜, 摘錄其部份內容。

具有類別型別的成員的 union (p848)

在早期版本的C++ 底下,union 不能有成員是定義了自己的建構器或拷貝控制成員的類別型 別。在新標準之下,這項限制鬆綁了。然而,具有的成員定義了它們自己的建構器或拷貝控 制成員的 union 使用起來會比成員是內建型別的 union 還要複雜。

當一個union 具有內建型別的成員,我們可以使用一般的指定來改變那個 union 所存放的值。 而成員不是簡單類別型別的 union 就不是這樣了,當我們將 union 的值切換至或切換自類別型別的一個成員,我們就必須建構或摧毀那個成員:當我們將 union 切換至類別型別的一個成員,我們就必須執行那個成員的型別的一個建構器;當我們切換自那個成員,就必須執行它的解構器。

當一個 union 有內建型別的成員,編譯器會合成逐個成員(memberwise)版的預設建構器 或拷貝控制成員,但對成員是定義有自己的預設建構器或一或多個拷貝控制成員的 union 來 我,就不是如此了。如果一個union 的成員的型別定義了這些成員其中之一,那麼編譯器就會把union 對應的成員合成為 deleted(§13.1.6)。

舉例來說, string 類別定義了所有的五個拷貝控制成員,以及預設建構器,如果一個 union 含有一個 string, 而且並沒有定義自己的預設建構器或其中一個拷貝控制成員, 那麼編譯器就會合成那個缺少的成員為 deleted, 如果一個類別有一個 union 成員具有一個 deleted 的拷貝控制成員,那麼該類別本身對應的拷貝控制運算也會是 deleted 的。

2022年4月1日 星期五

yacc/bison 系列 (3) - c parser

看人挑擔不吃力, 自己挑擔壓斷肩。
馬上就要挑戰 c parser 嗎? 是也不是, 最主要只處理合法的 c 語言輸入, 並沒有要輸出組合語言、AST 或是語意分析之類的動作, 這樣就簡單很多。

麻煩的是 c yacc 語法規則要怎麼寫, 我自己是寫不出來的, The c programming language 附錄 A 有一個這樣的 yacc 語法, 稍加修改就可以用了。但我不會修改, 也不想輸入那麼多字, 在網路上找到了範例。至於為什麼 The c programming language 附錄 A 會有 yacc 文法, 因為 yacc 也是那時候開發出來的, Dennis MacAlistair Ritchie 沒理由不用的, 不只 c 用了, 連 c++ 也有用 yacc, 畢竟是同時期的貝爾實驗室同事。

https://www.lysator.liu.se/c/ANSI-C-grammar-y.html, 只有這個當然不行, 還得有搭配的 lexer, 在這邊 https://www.lysator.liu.se/c/ANSI-C-grammar-l.html

我本來想找 usenet/net.sources/ansi.c.grammar.Z, 但是找了 ftp.uu.net mirror 站台都找不到, 只好自己拼湊了。只要補上相關的 code 就可以了,

完成這個不難, 最難的部份人家都寫好了, 我只是補上可以編譯的 code 而已, 測試了一些很複雜的宣告, 例如:

list 1. pthread_create
1 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

A, 有錯耶, 奇怪, 失敗了嗎? 後來發現是 pthread_attr_t, 這些都不是內建型別, 需要先用 typedef 宣告之後才能用, 然後我就發現 typedef 也不能用。思考一下之後就發現問題了, 之後稍加改 code, typedef 可以用了, 證實是我想的那樣。不過不用管 typedef, 先把這些非內建型別換成 int 就好, 這就通過 c 語法檢查了。

之前的 simple_compiler 我有一點沒有做好, 型別紀錄, 就是符號表 (symbol table), 每個符號有其屬性, 這邊我沒有特別處理, 舉例來說 str="xyz";, "xyz" type 是 const char*, str type 也要是 const char *, 所以做 assign (=) 是一個合法的 statement, 如果沒有紀錄 type, 就無法判定是不是正確的 assign。另外還有 scope 的問題, str 這時候是在合法的 scope 嗎?
{
  {
    const char *str;
  }
  str = "abc";
}
像這樣就不合法了。

型別的另外一個難點是怎麼用程式碼紀錄這些型別, 我想不到一個好的辦法, 畢竟型別的組合是無限多種。

list 1 在處理 int xyz;, 開啟 debug mode, 可以觀察到僅僅是這麼簡單的宣告, 就執行了好多次的文法規則。

list 1 d.txt
 1 descent@debian64:hoc$ ./c -d 1
 2 enable bison debug mode
 3 Starting parse
 4 Entering state 0
 5 Reading a token: int
 6 Next token is token INT ()
 7 Shifting token INT ()
 8 Entering state 9
 9 Reducing stack by rule 95 (line 243):
10    $1 = token INT ()
11 int
12 cc INT
13 -> $$ = nterm type_specifier ()
14 Stack now 0
15 Entering state 24
16 Reading a token: 
17 xyz
18 Next token is token IDENTIFIER ()
19 Reducing stack by rule 79 (line 208):
20    $1 = nterm type_specifier ()
21 -> $$ = nterm declaration_specifiers ()
22 Stack now 0
23 Entering state 22
24 Next token is token IDENTIFIER ()
25 Shifting token IDENTIFIER ()
26 Entering state 32
27 Reducing stack by rule 132 (line 327):
28    $1 = token IDENTIFIER ()
29 xyz
30 dd IDENTIFIER: xyz, type_data.storage_class_: (null), type_data.type_specifier_: 291
31 -> $$ = nterm direct_declarator ()
32 Stack now 0 22
33 Entering state 39
34 Reading a token: 
35 ;
36 Next token is token ';' ()
37 Reducing stack by rule 131 (line 323):
38    $1 = nterm direct_declarator ()
39 -> $$ = nterm declarator ()
40 Stack now 0 22
41 Entering state 38
42 Next token is token ';' ()
43 Reducing stack by rule 85 (line 223):
44    $1 = nterm declarator ()
45 -> $$ = nterm init_declarator ()
46 Stack now 0 22
47 Entering state 37
48 Reducing stack by rule 83 (line 218):
49    $1 = nterm init_declarator ()
50 -> $$ = nterm init_declarator_list ()
51 Stack now 0 22
52 Entering state 36
53 Next token is token ';' ()
54 Shifting token ';' ()
55 Entering state 55
56 Reducing stack by rule 76 (line 202):
57    $1 = nterm declaration_specifiers ()
58    $2 = nterm init_declarator_list ()
59    $3 = token ';' ()
60 -> $$ = nterm declaration ()
61 Stack now 0
62 Entering state 21
63 Reading a token: ;

source code:
  • https://bitbucket.org/dsung/hoc/src/master/c.l
  • https://bitbucket.org/dsung/hoc/src/master/c.y

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月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

2022年1月20日 星期四

yacc/bison 系列 (1) - 四則運算, 靠文法規則來處理優先順序

行是知之始,知是行之成。


hoc_arithmetic.y
 1 %{
 2 #include <stdio.h>
 3 #include <ctype.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #define YYSTYPE double
 7 #define YYDEBUG 1
 8 
 9 int yylex();
10 int yyerror(const char *s);
11 %}
12 
13 %token NUMBER
14 %%
15 list:    {printf("\taaempty\n");}
16      | list '\n' {printf("list \\n\n");}
17      | list expr '\n' { printf("%.8g\n", $2); }
18 
19 expr: term
20     | expr '+' term {$$ = $1 + $3;}
21     | expr '-' term {$$ = $1 - $3;}
22 
23 term: primary_expr
24     | term '*' primary_expr {$$ = $1 * $3;}
25     | term '/' primary_expr {$$ = $1 / $3;}
26 
27 primary_expr: NUMBER
28        | '(' expr ')'
29 
30 %%
31 char *progname;
32 int lineno = 1;
33 
34 
35 int yylex()
36 {
37   int c;
38   while ((c=getchar()) == ' ' || c == '\t')
39     ;
40   
41   if (c == EOF)
42     return 0;
43   if (c == '.' || isdigit(c) )
44   {
45     ungetc(c, stdin);
46     scanf("%lf", &yylval);
47     return NUMBER;
48   }
49 
50   if (c == '\n')
51     ++lineno;
52   return c;  
53 }
54 
55 int warning(const char *s, const char *t)
56 {
57   fprintf(stderr, "%s: %s", progname, s);
58   if (t)
59     fprintf(stderr, " %s", t);
60 
61   fprintf(stderr, " near line %d\n", lineno);
62 }
63 
64 int yyerror(const char *s)
65 {
66   return warning(s, 0); 
67 }
68 
69 int main(int argc, char *argv[])
70 {
71   int opt;
72   progname = argv[0];
73 
74   while ((opt = getopt(argc, argv, "d:h?")) != -1)
75   {
76     switch (opt)
77     {
78       case 'd':
79       {
80         yydebug = strtol(optarg, 0, 10);
81         if (yydebug == 1)
82           printf("enable bison debug mode\n");
83         break;
84       }
85     }
86   }
87 
88   //yydebug = 1;
89   yyparse();
90 }      


前一篇」提到使用的四則運算是依靠 bison 的運算符定義來達成優先順序, 這篇介紹使用文法定義來處理運算符號優先權, 可以看出文法規則 hoc_arithmetic.y (L15 ~ 28) 比之前複雜不少, 雖然只是簡單的四則運算, 但這文法規則還是蠻燒腦了, 我花了一些功夫才弄懂, 不過若是要自己寫出這些規則, 我沒有這個本事。

藉助 bison 這樣的神兵利器, 只要把文法規則重新編寫之後, 再次執行 bison, 就有了一樣的四則運算功能。

本篇沒打算說明文法規則, 如果你沒有修過編譯器課程, 這些文法規則可能會難倒你 (其實也難倒我), 文法規則可以參考「自己动手写编译器」。

2021年12月24日 星期五

yacc/bison 系列 (0) - 四則運算, 使用 bison 自訂運算符號的功能處理優先權

寫在前面: 文中會混用 lex/flex, yacc/bison, 指的是一樣的東西。
棄捐勿複道, 努力加餐飯


之前手工打造四則運算, 這次想挑戰 yacc/bison 來完成, 一樣嘗試很久, 卻沒有什麼進展。bison 是很古老的 parser generator, 現在有新的 parser generator, 不過我還是鍾愛它, 一直想把他學好, 看看這次能不能成功。

網路上或是編譯器書籍提到的教學, 看過之後老實說還是覺得很難, 一直都沒有能掌握 yacc/bison。我從「自制编程语言」開始學習 lex/yacc, 但還是沒從這邊掌握到學習的方法。

fig 1. 自制编程语言, 20140818 購於台南若水堂 356nt

另外還有「flex与bison (中文版)」, 但是我沒有買到這本, 手邊有的是 lex & yacc 英文版, 讀過幾次, 英文不算難懂, 但可能因為英文的關係, 也沒能從這本學好。

不好學的原因可能有幾點: 不熟文法規則, 這種文法蠻燒腦的, 如果沒有參考別人寫好的規則, 要全靠自己把四則運算的規則寫出來並不容易, 我光是看別人寫的就花了不少力氣才搞懂。另外也試著參考別人的 ebnf 寫了個 if 判斷式, 花了不少功夫才勉強可用, 不過真的可以成功分析 if 判斷式。

不同的教學文用了不同的文法, 導致學習上帶了來混亂。一般這篇教學文看不懂, 我們會找其他篇, 但由於舉例的文法規則不同, 很可能導致我們看的教學文沒有「累積」的效果, 永遠都是處在學習的最開始階段。

直到最近看了「UNIX 编程环境」第八章, Kernighan 介紹了 yacc 打造的一個語言, 讓我找到新的學習方向。

fig 2. UNIX 编程环境

Kernighan 寶刀未老, 這本書的內容很不錯, 強力推薦。原文書名是 The UNIX Programming Environment, 和另外一本大作 Advanced Programming in the UNIX Environment [W. Richard Stevens] 名字很像, 不要搞錯, 學 unix programming, 這兩本都讀一讀大有裨益。

大部分的書籍和網路文章都會把 lex, yacc 一起介紹, 2 個分開都很難學習了, 把他們兜在一起, 更是添增了學習難度。「UNIX 编程环境」8.1.3 給的例子是初學都會提到的四則運算, 但是沒有使用 lex, 只使用 yacc, 加上幾個簡單的文法, 看起來很簡單, 但我花了不少時間才搞懂其文法規則。

因為使用了 yacc 的運算符號定義能力, 所以文法才可以寫得那麼簡單。「自制编程语言」沒有運用這功能, 靠文法規則定義出運算符號的優先權, 自然文法複雜了不只一點。我個人比較喜歡用「自制编程语言」單純靠文法規定的方式, 而不是用 parser generator 的特異功能。

hoc.y
 1 %{
 2 #include <stdio.h>
 3 #include <ctype.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #define YYSTYPE double
 7 #define YYDEBUG 1
 8 
 9 int yylex();
10 int yyerror(const char *s);
11 %}
12 
13 %token NUMBER
14 %left '+' '-'
15 %left '*' '/'
16 %%
17 list:    {printf("\taaempty\n");}
18      | list '\n' {printf("list \\n\n");}
19      | list expr '\n' { printf("%.8g\n", $2); }
20 
21 expr: NUMBER {$$ = $1;}
22        | expr '+' expr {$$ = $1 + $3;}
23        | expr '-' expr {$$ = $1 - $3;}
24        | expr '*' expr {$$ = $1 * $3;}
25        | expr '/' expr {$$ = $1 / $3;}
26        | '(' expr ')'
27 
28 %%
29 char *progname;
30 int lineno = 1;
31 
32 
33 int yylex()
34 {
35   int c;
36   while ((c=getchar()) == ' ' || c == '\t')
37     ;
38   
39   if (c == EOF)
40     return 0;
41   if (c == '.' || isdigit(c) )
42   {
43     ungetc(c, stdin);
44     scanf("%lf", &yylval);
45     return NUMBER;
46   }
47 
48   if (c == '\n')
49     ++lineno;
50   return c;  
51 }
52 
53 int warning(const char *s, const char *t)
54 {
55   fprintf(stderr, "%s: %s", progname, s);
56   if (t)
57     fprintf(stderr, " %s", t);
58 
59   fprintf(stderr, " near line %d\n", lineno);
60 }
61 
62 int yyerror(const char *s)
63 {
64   return warning(s, 0); 
65 }
66 
67 int main(int argc, char *argv[])
68 {
69   int opt;
70   progname = argv[0];
71 
72   while ((opt = getopt(argc, argv, "d:h?")) != -1)
73   {
74     switch (opt)
75     {
76       case 'd':
77       {
78         yydebug = strtol(optarg, 0, 10);
79         if (yydebug == 1)
80           printf("enable bison debug mode\n");
81         break;
82       }
83     }
84   }
85 
86   //yydebug = 1;
87   yyparse();
88 }      

hoc.y L33 自己寫了 yylex, 如果用 lex, lex 就會產生一個 yylex(), 這是給 bison 產生的 parser (yyparser) 呼叫的。從這邊可以清楚的看到 yylex 要怎麼寫, return 什麼, 可以知道和 yyparser 的關係。

另外 L86 把 yydebug = 1 會啟動 yacc debug mode, 這時候可以看到 yyparser 是怎麼做 shift, reduce 的動作, 我就是使用這樣的方式才知道 L17 的規則是怎麼被觸發的。我加了一個 option 來開關 debug, 使用 ./hoc -d 1 可以打開 yydebug。

談 L17 的規則前先來看 L18 的規則是幹麻用的。當你一直按下 enter 時, 觸發的就是這條規則, 所以不管按下多少次 enter (\n) parser 都會正確, 不會離開。

yyparser 如果遇到不符合文法規則的輸入值, 預設行為會離開程式。

L17 是指 list 可以是空的, 這個讓我困惑很久, 因為我們打字不可能打一個空的東西進去阿, 你不管打什麼, 一定有一個對應的輸入, 怎麼會有空集合, 這邊讓我百思不解這行文法的作用。打開 debug 之後我才知道, 這個只會用到一次, yyparser 一開始就會做一次 reduce, 就會 match 這個規則。

list 1.
 1 descent@debian-vm:hoc$ ./hoc1  -d 1
 2 enable bison debug mode
 3 Starting parse
 4 Entering state 0
 5 Stack now 0
 6 Reducing stack by rule 1 (line 17):
 7       aaempty
 8 -> $$ = nterm list ()
 9 Entering state 1
10 Stack now 0 1

從 list 1 L8 可以看到, 什麼都沒輸入, 就會做 reduce 然後對應到 hoc.y L17 的規則, 所以目前處在 list 的狀態, 之後在輸入 enter (\n), 就會 match list \n 這個規則, 又被 reduce 到 list, 再次輸入 enter (\n), 又 match list \n, 再次 reduce 到 list, 這便是可以一直按下 enter 也能符合文法規則的原因。

其他 hoc.y 的四則運算規則和運算符定義就沒那麼特別了。hoc.y L14, 15 定義運算符號是左結合, 以及 */ 比 +- 優先 (寫在下面的優先權越高)。

ref: