490 likes | 868 Views
計算機科学実験及演習 3 ( ソフトウェア ). 中澤 篤志 大本 義正 馬谷 誠二. 1. 概要. 実験内容. Tiny C コンパイラ の作成 yacc (bison) と lex (flex) を用いて作成 ターゲット言語は IA のアセンブリ言語 一人一つ作成 上位互換であれば言語を拡張してもよい 教科書・資料と異なる設計をしてもよい. この実験の目的・意義. プログラミングスキルの向上 これまで学んだプログラミング技術の応用 ある程度大きいプログラムの作成 字句解析 (lex ファイル ) : 50 行
E N D
計算機科学実験及演習3(ソフトウェア) 中澤 篤志 大本義正 馬谷 誠二
実験内容 • Tiny Cコンパイラの作成 • yacc (bison) と lex (flex) を用いて作成 • ターゲット言語はIAのアセンブリ言語 • 一人一つ作成 • 上位互換であれば言語を拡張してもよい • 教科書・資料と異なる設計をしてもよい
この実験の目的・意義 • プログラミングスキルの向上 • これまで学んだプログラミング技術の応用 • ある程度大きいプログラムの作成 • 字句解析 (lexファイル):50行 • 構文解析・構文木生成(yaccファイル):400行 • 意味解析・識別子操作:200行 • コード生成:550行 • コンパイラ作成を通してプログラムに対する理解を深める • yacc/lexをマスターすることは重要でない • 解らないことはTA・教員にどんどん訊く
Tiny C • C言語のサブセット • 型はint型のみ • 制御構造は最低限のもの(if, while)だけ • 追加してもかまわない(for, do-while, ...) int fact(int n) { if (n == 1) return 1; else return n * fact(n-1); }
Tiny Cプログラムのコンパイル ソースプログラム ソースファイル(*.tc) TinyCコンパイラ アセンブリ プログラム 他の オブジェクトファイル アセンブリファイル(*.asm) アセンブラ (nasm) オブジェクト プログラム オブジェクトファイル(*.o) リンカ (ld, gcc) ライブラリファイル 実行形式 プログラム
コンパイル例 • fact.tc • main.c (このファイルはgccでコンパイル) • gccと同一の stack layout / calling convention int fact(int n) { if (n == 1) return 1; else return n * fact(n-1); } #include <stdio.h> main() { printf("%d\n", fact(10)); }
コンパイル例(続き) % tcc fact.tc ; fact.asm を生成 % nasm –f elf fact.asm ; fact.o を生成 % gcc –o fact main.c fact.o % ./fact 3628800 ソースファイル(*.c) ソースプログラム TinyCコンパイラ (tcc) アセンブリファイル(*.asm) アセンブリ プログラム アセンブラ (nasm) 他のオブジェクトファイル オブジェクトファイル(*.o) オブジェクト プログラム リンカ (ld, gcc) ライブラリファイル 実行形式 プログラム
Tiny Cコンパイラの出力(テキストファイル) fact.asm: GLOBAL fact fact push ebp mov ebp, esp cmp dword [8+ebp], 1 jne L2 mov eax, 1 jmp L1 L2 mov eax, [8+ebp] sub eax, 1 push eax call fact add esp, 4 imul eax, [8+ebp] L1 pop ebp ret
実験の進め方 • 実験資料の課題1~8を順に解く • 課題1, 2はレポートに書く必要なし • 課題5は選択自由 • 課題7, 8が本実験の主要部分 • 費やす時間もこの部分が大きい • 資料のコンパイラは教科書に沿った設計 • 教科書もよく読むこと • 各課題の解答となるソースは残しておく • 上書きすると元に戻せない
成績評価 • レポート3回 • 中間レポート2回 (課題3〜6) • 最終レポート (課題7, 8) • 課題毎にソースを残し,パス名をレポートに明記 • プログラムの解説 • 感想・要望 • 報告会(7/18(金),7/11(金)はお休み) • サンプルプログラムのコンパイルと実行 • 成績 • コンパイラの完成度 • レポートの出来(分かりやすい説明) メールでPDFを提出
その他の注意 • 出席を重視する • 止むを得ない場合はできるだけ事前に連絡する • 積極的にTA・教員に訊く • 予習をしっかりと • 予め資料を読み, 理解し, 方針を立てておく • 演習時間は「実装・デバッグの時間」 • webページ • http://www.fos.kuis.kyoto-u.ac.jp/~umatani/le3b/ • 教員・TA宛メールアドレス • le3soft@fos.kuis.kyoto-u.ac.jp
lex (flex) • 字句解析部 (Cのプログラム)を作成 • lexへの入力:lexファイル • 字句構造を正規表現で定義 • lexの出力:Cファイル • 関数yylex()を定義 • 生成される字句解析プログラムの本体 • 入力文字列から字句要素を切り出す “a = b+c;”→“ a”, “=”, “b”, “+”, “c”, “;” • 字句要素はトークンとして構文解析部に渡される
定義 セクション ルール セクション Cコード セクション lexファイルの例 Cのコード.字句解析プログラムの先頭にそのまま挿入される %option noyywrap %{ #include "calc.tab.h" %} digit [0-9] %% {digit}+ { yylval = atoi(yytext); return INTEGER; } "-"|"+"|\n return *yytext; [ \t] ; /* スペース,タブは無視 */ .yyerror("Error: invalid character"); %% 数字を表すマクロ digitの定義 cf. #define
ルールセクションの基本構造 • pattern1 action1 pattern2 action2 … • pattern: 字句構造の正規表現 • action: 字句要素を切り出したときに実行されるコード • Cで記述 • 普通はトークンをyaccに渡すコードを記述 • 字句要素:切り出した文字列 • トークン:字句要素を構文解析に適した形に変換したデータ • lexはルールから関数yylex()を生成 • 入力からpatternに一致する最長の文字列を探す • 見つかれば,対応するactionを実行
トークンの値 切り出した字句要素 数字からなる1文字以上の文字列 字句解析の中断 & トークンの種類を返す - or + or 改行 ルールセクション例 • yaccにおけるトークン • トークンの種類:識別子,整数定数…yylex()の返値 • トークンの値:識別子の名前,整数値…大域変数yylvalの値 [0-9]+ { yylval = atoi(yytext); return INTEGER; } “-”|“+”|”\n” return *yytext; [ \t] ;/* スペース,タブは無視 */ .yyerror("Error: invalid character");
lexが出力するCファイルの内容 #include "calc.tab.h" … int yylex() { … yylval = atoi(yytext); return INTEGER; … } /* Cコードセクションのコード */ …
yacc (bison) • 構文解析部(Cプログラム)を生成 • yaccへの入力:yaccファイル • BNFに似た記法で生成規則を記述 • yaccの出力:Cファイルとヘッダファイル • 関数yyparse()の定義 • 生成される構文解析プログラムの本体 • yylexを用いてトークンを取得し,構文をチェック • 抽象構文木を生成
定義 セクション ルール セクション Cコード セクション yaccファイルの例 %token INTEGER %% program: /* empty */ | program expr '\n' { printf("%d\n", $2); } ; expr: INTEGER { $$ = $1; } | expr '+' INTEGER { $$ = $1 + $3; } | expr '-' INTEGER { $$ = $1 - $3; } ; %% int yyerror(char *s) { … } main() { yyparse(); }
定義セクション • 終端記号INTEGERの定義 • 自動的に適当な整数値が割り振られる • yaccが出力するヘッダファイルで定義 • ASCIIコードは避けられる • ASCII文字はそのまま終端記号になる • ’a’’b’’=’’<’’\n’などなど %token INTEGER
定義セクション(続き) • %{ … %} • Cコードを記述可能 • yaccの出力ファイルの先頭にそのまま貼られる • %union • yylval,終端記号,非終端記号の型として複数の型を許す • %union宣言しなければ,int型 • 型を指定する方法 • yylval: yylval.(%unionのメンバ名) • 非終端記号: %type <(%unionのメンバ名)> (記号) • 終端記号: %token <(%unionのメンバ名)> (記号)
yaccファイルの例 %token INTEGER %% program: /* empty */ | program expr '\n' { printf("%d\n", $2); } ; expr: INTEGER { $$ = $1; } | expr '+' INTEGER { $$ = $1 + $3; } | expr '-' INTEGER { $$ = $1 - $3; } ; %% int yyerror(char *s) { … } main() { yyparse(); } ルール セクション
ルールセクション nonterminal: rule1 action1 | rule2 action2 … ; • nonterminal: BNFの左辺に相当 • rule: BNFの右辺に相当,構文規則を規定 • action: nonterminalを還元するときに実行されるCコード • 構文木生成コード • インタプリタのコードなど • yaccはルールから関数yyparse()を生成 • LALR(1)構文解析 • トークンを一つ得るために yylex() を一回呼出す • yylex()の返値 (=トークンの種類) • yylvalの値 (=トークンの値)
終端記号 終端記号 ルールセクション:rule • トークンの種類 = 終端記号 ε program: /* empty */ | program expr '\n' { printf("%d\n", $2); } ; expr: INTEGER { $$ = $1; } | expr '+' INTEGER { $$ = $1 + $3; } | expr '-' INTEGER { $$ = $1 - $3; } ; 非終端記号
lexファイル %{ #include "calc.tab.h" %} %% {digit}+ { yylval = atoi(yytext); return INTEGER; } "-"|"+"|\n return *yytext; yaccファイル %token INTEGER %% program: /* empty */ | program expr '\n' { printf("%d\n", $2); } ; expr: INTEGER { $$ = $1; } | expr '+'INTEGER { $$ = $1 + $3; } | expr '-'INTEGER { $$ = $1 - $3; } ;
ルールセクション:action • 終端記号・非終端記号は値を持つ • 終端記号の値 = トークンの値 • $n (n=1,2,3…) でアクセス可能 program: /* empty */ | program expr '\n' { printf("%d\n", $2); } ; expr: INTEGER { $$ = $1; } | expr '+' INTEGER { $$ = $1 + $3; } | expr '-' INTEGER { $$ = $1 - $3; } ; 還元されたexprの値 INTEGERの値 = yylvalの値 exprの値
3 7('-') 7 $3 4 $2 $1 値スタックと$$,$n • yaccが持つ値スタックの要素へのアクセス expr: INTEGER { $$ = $1; } | expr '+' INTEGER { $$ = $1 + $3; } | expr '-' INTEGER { $$ = $1 - $3; } ; トークンの値 トークンの種類 入力:7 - 3 yylval: yylex()の返値: '-' INTEGER INTEGER 7 3 ・7 - 3 yylvalの値 をpush INTEGER ・- 3 シフト expr ・–3還元 pop してpush expr '–' ・3シフト expr '–' INTEGER ・ シフト expr ・還元 値スタック
yaccが出力するCファイル • Cコードセクションのコードは別ファイルに記述しても構わない #define INTEGER 257 … yyparse() { … } int yyerror(char *s) { … } main() { yyparse(); }
makeのススメ • 実行ファイル作成手順 • bisonにより *.tab.c と *.tab.h を生成 • flexにより lex.yy.c • gccにより*.tab.c, lex.yy.c 他必要なソースをコンパイル・リンク yaccファイルを変更したら,flexを実行しなければならない hoge.tab.c bison lexファイル (hoge.l) yaccファイル (hoge.y) flex lex.yy.c hoge.tab.h
Makefileの例 YACC=bison YFLAGS= -d LEX=flex LFLAGS= OBJS = bar.tab.o lex.yy.o all: a.out bar.tab.c: bar.y $(YACC) $(YFLAGS) bar.y lex.yy.c: foo.l bar.tab.h $(LEX) $(LFLAGS) foo.l a.out: $(OBJS) $(CC) –o $@ $(OBJS) • OBJSのリストの並び順によっては以下も必要 bar.tab.h: bar.y $(YACC) $(YFLAGS) bar.y タブ文字
+ + e - * a b c d 構文木 • 抽象構文木 • Tiny Cプログラムの別の表現 • 優先順位,結合性,elseの結びつきが容易に読み取れる • 元のTiny Cプログラムの場合,(複雑な)生成規則と(複雑な)構文解析が必要 a * b + (c – d) + e
構文木の型 • 木のノードの型 nd • tree型 = ndへのポインタ型 (nd *) typedef union nd { struct { int op; } n; struct tp tp; struct tk tk; struct c c;} ノードの種類を示す (tp, tk, cに共通のメンバ) タプル=子要素を持つノード 識別子ノード 整数定数ノード
構造体のメモリ割当て struct st #include <stdlib.h> typedef struct st {… } *st_p; • st_p p; /* ポインタ変数のメモリ割り当て */ p->??? = … /* 不可 */ • st_p p = (st_p)malloc(sizeof(*p)); p->??? = … /* OK */ • foo() { st_p p = (st_p)malloc(sizeof(*p)); /* free()するまで有効 */ … /* ただし,pはfoo()内でのみ有効 */ } p
共用体 struct st1 { … }; struct st2 { … }; union un { struct st1 s1; struct st2 s2;}; • union un *u = (union un *) malloc(sizeof(union un)); u->st1.??? = …; • sizeof(union un):s1 または s2を格納するのに十分なサイズ • uは st2として再利用可
共用体(続き) • struct st1 *s = (struct st1 *) malloc(sizeof(struct st1)); union un *u = (union un *)s; u->st1.??? = …; • sizeof(struct st1) ≧ sizeof(struct st2) ? • uはstruct st2型として再利用できるとは限らない • どちらの方法が望ましいか? • 用途・要求による • メモリの使用量 vs. メモリの管理効率 • mallocの実装法
yaccファイルでの%union指定 • %union { int i; char *str; tree n; } • 型treeの宣言を持つヘッダファイルを用意してyaccファイルの先頭でインクルード • %token <str> Identifier%token <i> Constant%token IF ELSE WHILE… • yylval.i = atoi(yytext);yylval.str = strdup(yytext); • %type <n> program … トークンIdentifierの値は 文字列(char *)型 非終端記号programの値は tree型
構文木の表示 • 括弧で括った表示 • 木構造の形を忠実に反映した表示が可能 • a + b + c • (+ (+ a b) c) • if (e1) if (e2) s1 else s2 = if (e1) { if (e2) s1 else s2 } • (if e1 (if e2 s1 s2))
+ + e - * a b c d 構文木の表示(続き) • preorderの木の走査 • 葉でないノードの表示 • ‘(’を表示 • 節の値を表示 • 子の値を再帰的に表示 • ‘)’を表示 • 課題6(構文木の表示) • 表示の仕様は自由 • インデントなどはしなくても構わない
構文木の表示(続き) • 構文の要素毎に表示ルーチンを用意 • print_statement …文の表示 • print_expression …式の表示などなど • 演算子の結合の優先順位 • 構文解析時に構文木に反映しているはずなので,表示側で考慮する必要はない
opt の扱い1 a: bopt c d (b c d またはc d にマッチ) ↓ a: b_opt c d ; b_opt: /* empty */ { $$ = NULL; } | b ; • yaccのエラー「empty rule for typed nonterminal, and no action」が出る場合 • /* empty */ に対するアクションを明示的に書く
opt の扱い2 a: bopt c d ↓ a: c d | b c d ; a: bopt c d x: a | y ↓ a_aux: c d ; x: a_aux | b a_aux | y ;
opt の扱いまとめ • どの方法が優れているかは一概には言えない(状況によって異なる) • 可読性・記述性 • アクションの可読性・記述性 • conflictの問題 • conflictが生じたら他の方法を試す • bisonの-vオプションにより生成される*.outputファイルにより, conflictの原因を探る
ミニ補講 • 課題が難しくて困っている • プログラミングスキル不足を感じている人達を対象にTAが実施 • 週に1回, 1時間程度 • 詳細は後日改めてお知らせします
C言語以外で演習するには • 演習資料の読み替えは自己責任 • C++ • flex, bisonがそのまま使えます • Scheme, Common Lisp • 課題6の結果を渡せばOK • Java • JFlex, Cupというツールがあります • サンプル入力を用意してあるので参考に • JRuby, Jython, Scala, Clojure, ... • その他: Ruby, OCaml, Haskell, …
グループ分け • 実装言語の選択 • 座席 • 明日の朝に固定 入口