情報科学(10-11) 言語処理系と仮想機械 目次 • 数式を読み込んで式木を生成する • 式木からその式の値を計算する,仮想機械 の機械語プログラムを生成する • 仮想機械を作り,生成された機械語プログラ ムを実行する Ruby Rubyで書くと, while true do p eval(gets) end ...それはともかく 復習 木構造(tree) 数式の構造は木構造になじむ 式木と呼ぶ ÷ - 3 + × 5 1 × 数式を扱おうとすると,因数分解や展開でどんど ん数式の形が変わる 冪 2 このようにあらかじめデータの大きさも形も予測 できない場合,ポインタを使って自由に大きさを 変えられるとうれしい ここで対象とする数式 数値: 自然数(0を含む) 演算: 四則演算(+, -, *, /) 演算の順序 カッコ内の演算を優先: (1+2)*3では + を * より先に 計算 *, / を +, - より優先: 1+2*3は,1+(2*3)と解釈 左を右より優先: 1-2-3は(1-2)-3と解釈 数式から式木への変換の流れ 文字列: "1-2*3" 字句解析 トークン列: 1, -, 2, *, 3 構文解析 式木: 1 2 * 3 字句解析 • 字句解析: 入力の文字列を解析して,数値や 識別子などのトークンに変換すること • • • "123" や "MyName" のように複数文字をまとめる 「123」は数値,「MyName」は識別子というように種 別を判定する トークンは有限(状態)オートマトンを使って切り 出す 復習 有限オートマトン • 状態遷移による計算は,計算が状態空間を移 動してゆくものと考えられる 状態空間 f'(4,4) →f'(3,12) → f'(2,24) →f'(1,24) → 24 • 有限オートマトンに対応 → 数理的なモデル 自然数を受理する有限オートマトン 0,1,2,3,4,5,6,7,8,9 0,1,2,3,4,5,6,7,8,9 初期状態 受理状態 入力が"123+567"の場合,1を読むだけでも受理状態になるが, 通常は進めなくなるまでどんどん(greedy)に進める Ruby # 文字列$lineの先頭文字を覗く # 次回も同じ文字が見える def peekChar() $line[0..0] end # $lineの先頭文字を使う # 次回は次の文字が見える def getChar() ch = peekChar $line[0..0] = "" return ch end 準備: 次の文字を見る 先頭の文字 $line[0]は文字コードになって しまうので注意 先頭の文字を除く Ruby =~で一致するかどうか計算する. # 自然数の読み取り def getNumber /[0-9]/は0から9の間の文字,つまり数字 value = getChar.to_i while peekChar =~ /[0-9]/ value = 10*value+(getChar.to_i) end return value end to_iで文字列から数値に変換 0,1,2,3,4,5,6,7,8,9 0,1,2,3,4,5,6,7,8,9 初期状態 受理状態 Ruby 字句解析 def nextToken if peekChar =~/ [0-9]/ $type = :NUMBER; $value = getNumber else $type = :SYMBOL; $value = {"+"=>:+, "-"=>:-, "*"=>:*, "/" =>:/, "("=>:lpar, ")"=>:rpar}[getChar] end end {“+”=>:+,...} はハッシュ.f["+"]=:+ となる関数 f getChar の値が "+" なら {…}[getChar] の値は :+ となる $type: トークンの種別 自然数なら :NUMBER 記号なら :SYMBOL $value: トークンが自然数ならその値,記号なら記号を表すシン ボルを保持する 構文解析 • 構文解析: トークンがどのように組み合わさ っていくかを解析 • 組み合わさり方は文法で定義する 復習 再帰的データ構造 ::= は「定義」の意味 | (縦棒)は「または」と読む • リスト構造 ::= セル | nil | 数や文字列などの基本データ型 • セル ::= リスト構造とリスト構造の対 • 木構造 ::= 変数名 | 数 | 木構造と演算子と木構造の3組 このような記法はBNF(Backus-Naur Form) と呼ばれ,文法 の記述によく使われる BNF • トークン(終端記号): 字句要素 例: 0, 1 • 非終端記号: 例: <B> (非終端記号が何を意味するかは以下の生成 規則で決まる) • 生成規則: 非終端記号 ::= トークンと非終端記号の列 左辺から右辺を導出できることを示す 左辺が等しいものは右辺を「|」で区切ってまとめる 例: <B> ::= 0 <B> | 1 <B> | ε • 開始記号: 非終端記号の一つ εは空列を表す 例: <B> 数式(式)のBNF • 優先順位を表すために以下の非終端記号を 導入する <E>: 式(expression) 例 8-4/(7-5) <T>: 項(term) 例 4/(7-5) <F>: 因子(factor) 例8 例 (7-5) 式のBNF 簡単のため 変数はない <E> ::= <E> + <T> | <E> - <T> | <T> <T> ::= <T> * <F> | <T> / <F> | <F> <F> ::= NUMBER | ( <E> ) トークンは赤で示した NUMBER は数値のトークン • 開始記号から生成規則を使って導出を繰り返すこ とで木ができる.この木を解析木という. • 解析木の葉を左から右に順に並べたトークン列は, この文法で受理されるという. • 受理されるトークン列を全て集めたものを,この文 法が定義する言語という. 開始記号 8+4/(7-5)は式か? <E> ::= <E> + <T> で導出 <E> <E> + <T> <T> <T> <F> <F> / <F> ( <E> ) NUMBER:8 NUMBER:4 <E> 8+4/(7-5)は受理される つまり,8+4/(7-5)は式 <T> ::= <T> / <F> で導出 - <T> <T> <F> <F> NUMBER:5 NUMBER:7 1++2は式か? <E> <E> + <T> ? <T> <F> NUMBER:1 + が2つ並ぶような解析木は作れないので,1++2 は受理されない つまり,1++2 は(ここで考えている)式ではない. 文法の階層(チョムスキー階層) 政治的発言も しています. Noam Chomsky ここでα,β,γは終端記号ある いは非終端記号の列を意味 する • タイプ-0文法: – チューリングマシンが受理する言語 指定できる 範囲が広 い(強い) • タイプ-1文法: 文脈依存文法 – 生成規則: αAβ ::= αγβの形式 • タイプ-2文法: 文脈自由文法 今やっている のはこれ – 生成規則: A ::= γの形式 • タイプ-3文法: 正規表現 – 有限オートマトンが受理する言語 指定できる 範囲が狭い (弱い) FAQ Q. 正規表現より文脈自由文法の方が強力なのであ れば,字句解析を構文解析にまとめてしまえるので はないか? A. 原理的には可能だが,文脈自由文法は強力なだ けに解析が難しいので,有限オートマトンで容易に解 析できる部分を先にやってしまった方が楽なので分 けている.(米国西海岸に行くときにも,最初はバスや電車,飛 び道具は成田に行ってから) 解析木の作り方 開始記号 <E> <E> <E> + + <T> <T> いくら導出しても,入力トークン列の先頭と比較するとこ ろがでてこない 開始記号から下向きに式の解析木を作ろうとする と堂々巡りになって止まらない (悪い再帰) 左再帰の問題 左再帰: ある非終端記号<A>から導出(生成規則 の左辺を右辺に置き換えること)を繰り返した時に, <A>γ のように,最も左に同じ非終端記号Aが生 じることがあるなら,文法は左再帰であるという この堂々巡りは,対象(この例では式)の性質ではな く,文法(の書き方)の性質 左再帰の除去 文法を書き換えることで,言語を変えずに,左再 帰を除去できることがある <E> ::= <T> <E’> <E’> ::= + <T> <E’> | - <T> <E’> | ε <T> ::= <F> <T’> <T’> ::= * <F> <T’> | / <F> <T’> | ε <F> ::= NUMBER | ( <E> ) εは空列 <E’> ::= + <T> <E’> | - <T> <E’> | ε <T’> ::= * <F> <T’> | / <F> <T’> | ε <F> ::= NUMBER | ( <E> ) 一部の再掲 この文法では,一つの非終端記号に対して,複数の生成 規則がある場合,トークンを一つ先読みすれば どの生 成規則を使うべきか分かる 再帰下降構文解析: 開始記号から出発して,先読み したトークンにより決まる生成規則を使って解析木を 作る方法 以下では,解析木の一部を消しながら式木を作る 以下では,解析木の一部を消しながら式木を作る 1-2-3の解析木 <E> <T> 前のページでこのように書いたが, 部分木の作り方は少し違う.「-」などの四則演算は 左結合なので左から先にノードを作る. <E’> 1-2-3の式木 <F> <T'> - <T> <E'> - 3 1 ε <F> <T'> 2 ε - <T> <E'> 1 - 2 <F> <T'> 3 ε ε 逆になってる Ruby 復習 再帰的データ構造定義の例 # 中置記法の数式に合わせて Expr = Struct.new(:left, :operator, :right) このExprを使って式木を表現する # 1-2-3から得られるデータ構造 Expr.new(Expr.new(1, :-, 2), :-, 3)) Ruby <E> ::= <T> <E’> <E’> ::= + <T> <E’> | - <T> <E’> | ε $level = { :+ => 1, :- => 1, :* => 2, :/ => 2, :lpar => 3, :rpar => 0 } e や t はメソッド def e :+ と :- は優先度 1 node = t while $type==:SYMBOL and $level[$value]==1 op = $value; nextToken node = Expr.new(node,op,t) end return node end Ruby <T> ::= <F> <T’> <T’> ::= * <F> <T’> | / <F> <T’> | ε $level = { :+ => 1, :- => 1, :* => 2, :/ => 2, :lpar => 3, :rpar => 0 } def t :* と :/ は優先度 2 node = f while $type==:SYMBOL and $level[$value] == 2 op = $value; nextToken node = Expr.new(node,op,f) end return node end e の定義と,$levelが2,e が t, t が fに ずれただけで形は同じ Ruby <F> ::= NUMBER | ( <E> ) $level = { :+ => 1, :- => 1, :* => 2, :/ => 2, :lpar => 3, :rpar => 0 } def f :lpar は優先度 3 if $type==:NUMBER then value = $value; nextToken; return value elsif $type==:SYMBOL and $level[$value]==3 nextToken; node = e; nextToken return node end end Ruby 式木の生成 node 8 $line=gets nextToken node=e として 8+4/(7-5) を入力すると 右の式木ができる. + 4 / 7 - 5 Ruby e 呼び出し t f $type: :NUMBER $value: 8 $line: +4/(7-5) Ruby e t f def f if $type==:NUMBER then value = $value; nextToken; return value … $type: :NUMBER $value: 8 $line: +4/(7-5) Ruby e t node 前のスライドで ここが終わった 8 $level[:+] は1 なので 満たさない def t node = f while $type==:SYMBOL and $level[$value] == 2 … $type: :SYMBOL $value: :+ $line: 4/(7-5) Ruby 前のスライドで これが終わった e node 8 $level[:+] は1 なので 満たす def e node = t while $type==:SYMBOL and $level[$value]==1 … $type: :SYMBOL $value: :+ $line: 4/(7-5) Ruby e node op=:+ t 8 f op = $value; nextToken node = Expr.new(node,op,t) end … $type: :NUMBER $value: 4 $line: /(7-5) ここで t が 呼び出される Ruby e node op=:+ 8 t node op=:/ $level[:lpar] は優先度 3 def f 4 f … elsif $type==:SYMBOL and $level[$value]==3 nextToken; node = e; nextToken ... $type: :SYMBOL $value: :lpar $line: 7-5) Ruby e node op=:+ t node op=:/ 8 4 e f ここでeが 呼び出される def f … $type: :NUMBER nextToken; node = e; nextToken $value: 7 $line: -5) Ruby e node op=:+ t node op=:/ 8 4 7 e node $type: :SYMBOL $value: :$line: 5) f $level[:-] は1 なので 繰り返しを実行 def e node = t while $type==:SYMBOL and $level[$value]==1 op = $value; nextToken node = Expr.new(node,op,t) end Ruby e node op=:+ t node op=:/ 8 4 e node $type: :SYMBOL $value: :rpar $line: f ちょっと飛ばして e中の node = Expr.new(node,op,t) の実行結果 7 - 5 Ruby e node op=:+ 8 t node t 中の node = Expr.new(node,op,f) の実行結果 $type: $value: $line: 空 4 / 7 - 5 Ruby e node 8 e 中の node = Expr.new(node,op,t) の実行結果 $type: $value: $line: + 4 / 7 - 5 Ruby 式木の表示 (中置記法) def pr(node) if node.kind_of?(Fixnum) then print node else print "(" pr(node.left); print node.operator; pr(node.right) print ")" end end pr(node)を実行すると,式木nodeを辿って数式を表示 Ruby 式木の表示(後置記法) def pr(node) if node.kind_of?(Fixnum) then print node else pr(node.left); print " " 空白を出力 pr(node.right); print " " print node.operator end end 8+4/(7-5) は後置記法では,8 4 7 5 - / + と表記される (カッコが不要) Ruby 式木から式の値の計算 proc{|a,b| a+b}はf(a,b)=a+b なる関数 f def calc(node) if node.kind_of?(Fixnum) {…}[:+]の値がf(a,b)=a+bなる return node 関数fとなるハッシュ else return { node.operatorが :+ :+ =>proc{|a,b| a+b}, ならf(a,b)=a+b となる :- =>proc{|a,b| a-b}, 関数 f となる :* =>proc{|a,b| a*b}, :/ =>proc{|a,b| a/b}} [node.operator]. call(calc(node.left), calc(node.right)) end end node.operator が :+ なら, calc(node.left)+calc(node.right) を計算 コード生成 • 式木から式を計算する機械語を生成する • ターゲットとする計算機: ED21 http://lecture.ecc.u-tokyo.ac.jp/ johzu/joho/ed21/ 復習 高水準言語 コンピュータが直接実行できるのは • 高水準言語 機械語なので,高水準言語は – 人間が理解しやすい形でプログラム 機械語に変換してから実行する. を読み書きできる言語 変換するプログラムを – “コンパイラ”を使うことで機械語に変 コンパイラという 換する – 高水準言語の例: C言語,C++言語 • コンパイラ(compiler) Rubyはちょっと 違うので後で… – プログラム言語で書かれたプログラ ム(ソース)を機械語(オブジェクト)に 変換(翻訳)するソフトウェア ここでは以下のコンパイラを作ってみる • 高水準言語:式木 • 機械語:計算機シミュレータED21 「計算機シミュレータED21」 http://lecture.ecc.u-tokyo.ac.jp/johzu/joho/ed21/ load A ACにA番地の内容を読みこむ loadi D ACに数値Dを直接セットする store A ACの内容をA番地に保存する add A, sub A, mul A ACの内容にA番地の内容を加算/減 算/乗算する. div A 同じく除算する.下位16ビットが商,上 位16ビットが余りとなる. shiftr D, shiftl D ACの内容をDビット右/左にシフトする jA A番地にジャンプする jz A ACが0の時,A番地にジャンプする stop 0 プログラムを停止する ED21の機械語 1+2を計算する機械語プログラム 実行結果 アドレス 0 1 2 3 loadi 1 add 3 stop 0 2 式木からその値を計算する機械語の生成 各頂点(葉)の値を計算した結果をACに残す機械語を生成するに は, 1. 右の部分式を計算した結果をACに残す機械語を生成する. 2. ACの内容をメモリmに一時的に保存する機械語を生成する. (左の部分式を計算しないといけないので,ACを空ける.) 3. 左の部分式を計算した結果をACに残す機械語を生成する. (その間メモリmの値を変更しないように,m+1以降のメモリを 作業用に使う.) 4. その頂点の演算をACとメモリmに対して行う機械語を生成す る. 式木 機械語 LOADI 5 5 式木と生成される機械語 式木 機械語 LOADI 5 STORE 20 LOADI 7 SUB 20 作業用のメモリの 番地.機械語と 重ならない所に. 7 - 式木と生成される機械語 5 式木 20番地の内容は使ったので, 次に別の用途で使ってもよい 4 7 / - 式木と生成される機械語 5 機械語 LOADI 5 STORE 20 LOADI 7 SUB 20 STORE 20 LOADI 4 DIV 20 SHIFTL 16 SHIFTR 16 余りを消す 式木 8 + 4 7 / - 式木と生成される機械語 5 機械語 LOADI 5 STORE 20 LOADI 7 SUB 20 STORE 20 LOADI 4 DIV 20 SHIFTL 16 SHIFTR 16 STORE 20 LOADI 8 ADD 20 Ruby 式木nodeを計算する機械語を出力 作業用のメモリはbase以降を使用 右の部分式の機械語を生成 def code(node, base) 右の部分式を計算した値をbaseに保存 if node.kind_of?(Fixnum) print "LOADI ",node,"¥n" 左の部分式をbase+1以降を使って else 計算する機械語を生成 code(node.right, base) print "STORE ",base,"¥n" code(node.left, base+1) print ({ :+ => "ADD", :- => "SUB", :* => "MUL", :/ => "DIV" } [node.operator], DIVの場合は余りを消すために " ", base, SHIFTL, SHIFTR生成 { :+ => "¥n", :- => "¥n", :* => "¥n", :/ => "¥nSHIFTL 16¥nSHIFTR 16¥n" }[node.operator]) end end node.operator が :+ ならADD命令を表示 コード生成 8+4/(7-5) (8+(4/(7-5))) LOADI 5 STORE 20 LOADI 7 SUB 20 STORE 20 LOADI 4 DIV 20 SHIFTL 16 SHIFTR 16 STORE 20 LOADI 8 ADD 20 入力した式 式木を中置記法で表示したもの ①貼付 け ②実行 ③実行結果 仮想機械 • 仮想機械: ソフトウェアにより仮想的に実現したコ ンピュータ – インタープリタ:高級言語を機械語とする仮想機械 – シミュレータ,エミュレータ:現実にある(かもしれない) マシンを模倣する仮想機械 • 例 – ED21(ED21の機械語を実行するシミュレータ) – Java VM(Virtual Machine=仮想機械) – ruby(Rubyのプログラムを実行するインタープリタ) これから作る仮想機械の機械語ED21' 0xは16進数の指定 機械語 loadi D store A add A sub A mul A div A 命令 0x1000+D(下位8ビット,2の補数) 0x1800+A(下位8ビット,非負,以下同じ) 0x3000+A 0x3800+A 0x4000+A 0x4800+A(ED21と異なりACには商のみを置く ことにする) stop 0 0 Ruby 式木nodeを計算するED21'の機械語をcodep から生成.作業用のメモリはbase以降を使用. 生成した機械語の次のアドレスを返す 負の場合の配慮は省略 def codeBin(node,codep,base) if node.kind_of?(Fixnum) $mem[codep] = 0x1000+node #loadi else codep = codeBin(node.right,codep,base) $mem[codep] = 0x1800+base #store codep = codeBin(node.left,codep+1,base+1) $mem[codep] = {:+ => 0x3000, :- => 0x3800, :* => 0x4000, :/ => 0x4800}[node.operator]+ base end return codep+1 end node.operator が :+ なら 0x3000 を返す コード生成 ED21'の仮想機械のインタプリタ 1. 仮想機械の状態を表すPC,AC,レジスタ,メイン メモリ(配列$mem)を用意する 2. $mem[PC]を読み出して,命令,オペランドに分 解する 3. 命令に応じた処理を行う • 停止命令の場合はここで実行終了 4. ジャンプ命令以外の場合は,PCの値を増やす 5. 2から繰り返す Ruby 16進数FFとandを取って下8ビットを取り出す. これがオペランド def sim 上位24ビットが命令の指定 pc = ac = 0 while $mem[pc] != 0 case A when B B' when C C' else D oprand = $mem[pc] & 0xff は case $mem[pc] & 0xffffff00 if A==B when 0x1000 # loadi then B' ac = oprand; pc += 1 elsif A==C then C' else D when 0x1800 # store $mem[oprand] = ac; pc += 1 の意味(場合分け) else ac = { 0x3000 => proc {|a,b| a+b}, 0x3800 => proc {|a,b| a-b}, 0x4000 => proc {|a,b| a*b}, 0x4800 => proc {|a,b| a/b} } [ $mem[pc] & 0xffffff00].call(ac,$mem[oprand]) pc += 1 end end return ac 命令が0x3000なら 仮想機械のインタプリタ end f(a,b)=a+bなる関数fを返す もう一歩先へ • 文献: Aho, Appel, ... • ツール: lex, yacc, bison, SableCC, JavaCC • BNF rules for Java, ...
© Copyright 2024 ExpyDoc