B演習(言語処理系演習)第4回 田浦 構文解析器 def fib ( n ): if n < 2: return 1 else: return … 構文解析器 字句列(token stream) 構文木 “fib” def n if < n … 2 全体像 データ型定義 • 各構文の種類(変数, 演算式, def, for, etc.)ごとに, その種類の構文木の定義 (typedef struct …) • 式すべて, 文すべてを表す構文木の定義 コンストラクタ • 各種類の構文木を作る関数 構文解析器本体 • 各構文の種類ごとに, 字句解析器から字句の列 を受け取り, その構文木を作る関数 構文木の定義(文の例) typedef struct stmt_while { /* while文 */ … } stmt_while, * stmt_while_t; typedef struct stmt_for { /* for文 */ … } stmt_for, * stmt_for_t; typedef struct stmt_fundef { /* def文 */ … } stmt_fundef, * stmt_fundef_t; … typedef struct stmt { /* 文全体 */ stmt_kind_t k; /* 種類 */ union { … } } stmt, * stmt_t; while文の例 typedef struct stmt_while { /* while文 */ expr_t e; /* 条件部分 */ stmt_vec_t body; /* 繰り返し本体 */ } stmt_while, * stmt_while_t このように, 個々の構文の種類に対し, その構 成要素(while文であれば条件式と本体)を フィールドに持つような構造体を作る 文法定義を眺めれば, 個々の構造体がどの ようなフィールドを持たねばならないかがわか る コンストラクタ 個々の構文の種類ごとに, その種類の構文 木を作るような関数を作る stmt_t mk_stmt_while(expr_t e, stmt_t s, …) { … } stmt_t mk_stmt_for(char * x, expr_t e, stmt_t s, …) { …} • 注: …のところには, ソースコード上の位置情報な ど, 必要な情報を適宜追加することだろう 構文解析器の作成 以上の「準備」ができていると仮定して, 実際 の構文解析(字句の列から構文木を作る)処理 について考える 文法定義の読み方(1) 単純な連結 例 (while文): while_stmt ::= "while" expression ":" suite 読み方: • while_stmt (とみなされる字句の列)とは, • • • • “while” expression “:” suite (というただひとつの字句からなる列) (とみなされる字句の列), (というただひとつの字句からなる列) (とみなされる字句の列) をこの順に並べたものである 文法定義の読み方(2) 複数選択肢 例 (文全体) statement ::= expression_stmt NEWLINE | assignment_stmt NEWLINE | … | while_stmt | for_stmt | funcdef statement (とみなされる字句の列)とは, • assignment_stmt とみなされる字句の列とNEWLINEとい うただひとつの字句からなる列をこの順で連結したもの, または, • …, または • funcdefとみなされる字句の列, のいずれかである 構文解析器の仕組み概要(1) LL(1)文法用再帰下向き構文解析法 文法記号Aに対し,関数 parse_A(tokenizer_t t, …) を定義 • 渡されたtから生成されるいくつかの字句列が A(とみなされる字句列)であれば,そこまで字句 列を読み込んで,Aを表す構造体を作って返す • さもなければエラー(行番号など)を表示して終了 (exit) 構文解析器の仕組み概要(2) parse_A(t, …)開始時 t … tの現在の字句 A? (tok_cur_token_XXX(t)) parse_A(t, …)終了時 t … tの現在の字句 (tok_cur_token_XXX(t)) 各記号ごとの構文解析器の導出 文法規則から(機械的に)導く 基本アイデア A -> B C D に対し, parse_A(tokenizer_t t) { parse_B(t); parse_C(t); parse_D(t); } 注: 構文木を組み立てる部分は省略 複数の選択肢がある場合 例 : A -> B C D | E F G 現在の字句によってどちらをとるべきかを選択する parse_A(tokenizer_t t) { if (現在のtokenがB C Dの先頭になりうる) { parse_B(t); parse_C(t); parse_D(t); … } else if (E F Gの先頭になりうる) { parse_E(t); parse_F(t); parse_G(t); … } else parse_err(t); } 構文解析器の課題 プログラムを読み込み,構文木を作って,またそれ をprintする main() { tree = parse_program(…); print_program(tree, …); } (空白やコメント以外は)まったく同じプログラムが出 力されるはず 出力された文字列をもう一度同じプログラムに入れ てみる.今度はまったく同一の文字列が出てくるは ず ./parser a.py > b.py ./parser b.py > c.py diff b.py c.py 注: • a.py と b.py はコメントや空白を除き同一 • b.py と c.py は同一の文字列 自然なソースコードの構成 syntree.h : 構文木のtypedefおよびコンストラ クタの宣言 syntree.c : 多数のコンストラクタ(mk_A)の定 義 parser.h : トップレベルの構文解析器 (parse_file_input)の宣言 parser.c : 多数の構文解析器(parse_A)の定義 (syntree.hを#include) いくつかのヘルプ 原理がわかっていても実際にたくさんの構造 体定義や関数を書くのは大変なので少しテン プレートを提供する • syntree.h • almost_empty_parser.c • parser.c において, 少数の解析木だけを説明用に残し, あとはほとんど空にしたもの 使うと作業の道筋がわかりやすくなるとは思 うが, これの「解読」に明けくれないように注意 (基本アイデアは説明したとおり) プリント修正(HP上のPDFは修正 済み) 資料(3) 字句解析器 p20. 4.4節 最後の行 • cs_cur_lineを実現するために… cs_cur_line_stringを実現するために… p21. 4.5節 tokenizer の定義内 • char_buf_t str_buf; /* 現在の行の先頭の空白の数 */ char_buf_t str_buf; /* 現在の行(先頭から現在の文 字まで)を保持 */ • int_vec_t indents; int_stack_t indents; ここまでを前半の課題とする 締め切り … 詳しくはHPにアナウンスする(まだしてない) • subversionに”parser”という名前のモジュールを登録 • ソース&Makefile • checkoutして “make” で“parser”という名前のプログラム ができるようにしておく • ./parser プログラムファイル名 でプログラムの構文解析 を行い,その構文木をまたプログラムに戻して表示 「できました」報告は別途メール • 班番号,checkoutすべきモジュール名を報告 C言語雑学 汎用コンテナ コンテナ(入れ物) • リスト,ハッシュ表,探索木,etc. 頻出(多くのプログラミング言語で,ライブラリ として提供,または組み込み) • C++ STL • Javaクラスライブラリ • Python 辞書,リスト,タプル コンテナの何が問題か コンテナ実装のロジックは,入れるものが何 であってもほとんど同じ にもかかわらず,intの入れ物と,char *の入 れ物を別々に定義しなくてはならない 例 : リスト of T (Tを入れるリスト) • • • • typedef struct T_list { … } * T_list_t; T_list_t mk_T_list(); void list_append(T_list_t l, T x); T list_get(T_list_t l, int i) 例 : リスト 整数のリスト • int_list_t mk_int_list(); • void int_list_append(int_list_t l, int x); • int int_list_get(int_list_t l, int i); char * のリスト • string_list_t mk_string_list(); • void string_list_append(string_list_t l, char * x); • char * string_list_get(string_list_t l, int i); 選択肢? [その1]: あきらめて似たコードを何度も書く [その2]: int_listだけを定義する.そして思い 切って(笑), int_listにchar *を入れてみる • int_list_t l = mk_int_list(); int_list_append(l, “hello”); char * x = int_list_get(l, 0); 実は,ある場合には[その2]が機能する そもそも[その2]が(一般には)まずい根本的な の原因は,int型の領域(変数, 配列/構造体の 要素)にchar *の値を代入するところにある • 例: int_list_append(l, “hello”); しかし実際には,char *もintも32 bit整数であ るので,実はこの代入によって情報が失われ ているわけではない • char * p = “hello”; int x = p; char * y = x; /* y == p */ C言語における危険だが合法な代入 C言語ではこのような,「機械語上では可能」 な代入を(自己責任で)合法としている コンパイラは警告を出すがエラーとはしない • char * x = 1000; /* ポインタ := 整数 */ • int a[10]; char * p = a; /* char* := int* */ • f(int x) { … } f(“hello”); void* はgenericポインタとよばれ,「不明な 型」へのポインタを保持するのに慣習的に用 いられる つまり,以下は合法 int_list_t l = mk_int_list(); int_list_append(l, “hello”); char * x = int_list_get(l, 0); キャストを用いて黙らせることも可能 int_list_t l = mk_int_list(); int_list_append(l, (int)“hello”); char* x =(char*)int_list_get(l, 0); しかし, • 無様 • 間違いを犯す元(一般に,リストに何(int? char*?)が入っ ているのかを区別する手段がない ベターな慣習 “汎用版”を一度だけ定義 • 中身はvoid*としておくのが良い • generic_list_t mk_generic_list(); generic_list_append(generic_list_t l, void * x); generic_list_get(generic_list_t l, int i); それらを用いて個々の型に対するコンテナを 定義 例 int_list の定義 typedef struct int_list * int_list_t; int_list_t mk_int_list() { return (int_list_t)mk_generic_list(); } int int_list_append(int_list_t l, int x) { generic_list_append((generic_list_t)l, (void *)x); } int int_list_get(int_list_t l, int i) { return (int)generic_list_get((generic_list_t)l, i); } 注: 要するにどんな代入も合法か? もちろん否 • ポインタ := 浮動小数点数 • ポインタ := 構造体 • 整数 := 構造体 • 注: 「構造体へのポインタ」ではなく • 構造体A := 構造体B
© Copyright 2024 ExpyDoc