Transcript 資料10-11

情報科学(10-11)
言語処理系と仮想機械
目次
• 数式を読み込んで式木を生成する
• 式木からその式の値を計算する,仮想機械の
機械語プログラムを生成する
• 仮想機械を作り,生成された機械語プログラ
ムを実行する
Ruby
Rubyで書くと,
while true do p eval(gets) end
...それはともかく
復習
木構造(tree)
数式の構造は木構造になじむ
式木と呼ぶ
÷
-
3
+
×
5
冪
2
1
×
数式を扱おうとすると,因数分解や展開でどんど
ん数式の形が変わる
このようにあらかじめデータの大きさも形も予測で
きない場合,ポインタを使って自由に大きさを変え
られるとうれしい
ここで対象とする数式
数値: 自然数(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
value = getChar.to_i
/[0-9]/は0から9の間の文字,つまり数字
while peekChar =~ /[0-9]/
value = 10*value+(getChar.to_i) end
return value
to_iで文字列から数値に変換
end
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>
<T>
8+4/(7-5)は受理される
つまり,8+4/(7-5)は式
<T> ::= <T> / <F>
で導出
<F>
NUMBER:7
-
<T>
<F>
NUMBER:5
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'>
<F> <T'>
3
ε
ε
1 - 2
逆になってる
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 はメソッド
:+ と :- は優先度 1
def e
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 }
:* と :/ は優先度 2
def t
node = f
while $type==:SYMBOL and $level[$value] == 2
op = $value; nextToken
node = Expr.new(node,op,f) end
return node
e の定義と,$levelが2,e が t, t が fにず
end
れただけで形は同じ
Ruby
<F> ::= NUMBER | ( <E> )
$level = { :+ => 1, :- => 1, :* => 2, :/ => 2, :lpar => 3, :rpar => 0 }
:lpar は優先度 3
def f
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)"
e
node
op=:+
Ruby
t
8
f
op = $value; nextToken
node = Expr.new(node,op,t) end
…
$type: :NUMBER
$value: 4
$line: "/(7-5)"
ここで t が
呼び出される
e
node
op=:+
Ruby
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)"
e
node
op=:+
Ruby
t
node
op=:/
8
4
e
$type: :NUMBER
$value: 7
$line: "-5)"
f
ここでeが
呼び出される
def f
…
nextToken; node = e; nextToken
e
node
op=:+
Ruby
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
e
node
op=:+
Ruby
t
node
op=:/
8
4
e
node
$type: :SYMBOL
$value: :rpar
$line: ""
f
ちょっと飛ばして
e中の
node = Expr.new(node,op,t)
の実行結果
7
-
5
e
node
op=:+
Ruby
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)
return node
else
{…}[:+]の値がf(a,b)=a+bなる
return {
関数fとなるハッシュ
:+ =>proc{|a,b| a+b},
node.operatorが :+
:- =>proc{|a,b| a-b},
ならf(a,b)=a+b となる
:* =>proc{|a,b| a*b},
関数 f となる
:/ =>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)
if node.kind_of?(Fixnum)
右の部分式を計算した値をbaseに保存
print "LOADI ",node,"¥n"
else
左の部分式をbase+1以降を使って
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
命令が0x3000なら
return ac
仮想機械のインタプリタ
f(a,b)=a+bなる関数fを返す
end
もう一歩先へ
• 文献: Aho, Appel, ...
• ツール: lex, yacc, bison, SableCC,
JavaCC
• BNF rules for Java, ...