コンパイラ(スタックベースのVMなど)を記述している場合、if
ステートメントのコードは次のとおりです。
if (<some_expression>)
{
<some_instructions>
}
次の疑似アセンブリに翻訳されます:
<evaluate expression and Push result on stack>
JUMP-IF-TRUE label
<some_instructions>
:label
これはコンパイラーで簡単に実装できますが、インタープリターでこれを実装する方法がわかりません。
インタプリタとコンパイラの違いは、コンパイラは後で実行する命令を出力し、インタプリタはすぐに実行することです。
したがって、たとえばインタプリタは、命令Push
を出力する代わりに、維持しているスタック上で単にPush
を実行します。等々。
JUMP
命令(JUMP-IF-TRUE
疑似命令など)については、インタプリタにどのように実装されているのでしょうか。
より正確には、たとえばif
ステートメントを解釈するときなど、コードの一部をインタープリターで「ジャンプ」するにはどうすればよいですか?
絵は千語の価値があるので、一緒に通訳を書きましょう!もちろん、とてもシンプルなものです。これにはOCamlを使用しますが、OCamlがわからない場合は、コードをほとんど記述せずにコメントするため、これで問題ありません。
まず、プログラムとは何かを定義します。私たちは非常に単純な言語を考え、テキストを印刷し、数値を追加し、ifステートメントで数値がゼロかどうかをテストします。
type statement =
| Print of string
| If of expr * statement * statement
| Sequence of statement list
and expr =
| Constant of int
| Plus of expr * expr
上記のコードは型宣言であり、プログラムの考え方を定義しています。 OCamlで簡単に操作できる、具体的なテトラルフォームとは対照的に、プログラムの抽象的なシンボリックフォームを定義します。このタイプの定義は、プログラムが何であるかを英語で説明したものを単純に翻訳したものであり、シーケンスを追加して、より多くの操作を実行します。 (Plus
およびSequence
操作を削除すると、If
ステートメントを示す可能な限り最小の言語が得られますが、少し退屈すぎるかもしれません!)
実生活ノート実生活では、プログラムには多くの注釈が含まれています。重要なのは、プログラム要素には通常、ファイル、行、列の用語で位置が注釈が付けられていることです。これはエラーの報告に役立ちます。
私たちはすでに私たちの言語で面白いプログラムを書くことができます:
let program = Sequence([
Print("Hello, world!");
If(Plus(Constant(1),Constant(0)),
Print("One is the truth"),
Print("One is a lie"));
Print("Oh, that was a tough one!");
])
これはテキストHello, world!
Dennisを満足させ、1 + 0を計算し、結果を0と比較します–私たちの言語でのIf
の定義と同様に-結果が0と異なる場合、最初のブランチが実行されてperlishメッセージOne is the truth
が出力されます。それ以外の場合は、永続的なメッセージOne is a lie
が出力されます。このような厳しい挑戦の後、緊張したコンピューターはその気持ちを共有しますOh, that was a tough one!
。
実生活ノート実世界では、プレーンテキストを変数program
が保持するものとして抽象プログラムに変換するパーサーを作成する必要があります。 Lexとyaccを使用できます。
次に、私たちの言語のインタプリタを書きましょう。 (あなたが眠ったと感じたら、今起きてください、質問に対する実際の答えがすぐに提示されます!)
let rec interpreter = function
| Sequence(hd::tl) -> interpreter hd; interpreter(Sequence(tl))
| Sequence([]) -> ()
| Print(message) -> print_endline message
| If(condition, truebranch, falsebranch) ->
(* The answer to the question is here! *)
if (eval condition) <> 0 then
interpreter truebranch
else
interpreter falsebranch
and eval = function
| Constant(c) -> c
| Plus(a,b) -> (eval a) + (eval b)
私たちの多くはOCamlに慣れていない可能性が高いので、このコードを少しずつ見ていきましょう。
let rec interpreter = function
| Sequence(head::tail) -> interpreter head; interpreter(Sequence(tail))
(* To interpret a list of statements, we interpret the first
and then interpret the tail of the list. *)
| Sequence([]) -> ()
(* To interpret an empty list of statements, we just do nothing.
Fair enough, right? *)
| Print(message) -> print_endline message
(* To print a message, we use the Host-language printing facility. *)
| If(condition, truebranch, falsebranch) ->
(* The answer to the question is here!
We first evaluate the condition, with the eval function below
and use the Host-language if facility to take the decision
of recursively calling the interpreter on the truebranch or
the falsebranch. *)
if (eval condition) <> 0 then
interpreter truebranch
else
interpreter falsebranch
and eval = function
| Constant(c) -> c
(* Constants evaluate to themselves *)
| Plus(a,b) -> (eval a) + (eval b)
(* A Plus statement evaluates to the sum of its parts. *)
次に、プログラムを実行してみましょう。
let () = interpreter program
出力を引き起こす
Hello, world!
One is the truth
Oh, that was a tough one!
試してみたい場合は、プログラムスニペットをファイルにコピーしてくださいinterpreter.ml
とシェルプロンプトからocamlで実行します。
% ocaml imterpreter.ml
結論この小さな例のように、プログラムの中間抽象表現を使用してインタープリターを作成する場合。 If
ステートメントは、ホスト言語の条件の観点から簡単に実装できます。他の戦略も可能です。プログラムを「オンザフライ」でコンパイルして仮想マシンにフィードすることは完全に想像できます。 (一部の人々は、2つのアプローチの間に本質的に違いはないと主張するでしょう。)
言語が本当に解釈される場合(つまり、発生したとおりにステートメントを実行し、起動時にプログラム全体をデータ構造に変換しない場合)、インタープリターが条件を処理する方法は2つあります。
明示的なジャンプ。BASICのような、アセンブリからのステップアップが少ない言語では、プログラムが次に進むべき場所を明示的に言う必要があります。
10 REM 1970S-VINTAGE BASIC
20 IF A <> 5 THEN 40
30 PRINT "A IS 5"
40 PRINT "DOING THE NEXT THING"
20
行のTHEN
句は明示的なジャンプです。アセンブリを実行しているハードウェア(マシンコード)は、メモリ内のアドレスにジャンプしてこれを処理します。インタープリタされたBASICは、プログラムで宛先行番号を検索して処理します。
実際の動作は Applesoft BASICのソースコード で確認できます。ここでは、ステートメントが入力されたときにトークン化されますが、実行はまだ個別に解釈されているかのように機能します。 IF
ステートメント(アドレスD9C9
で処理)は式を評価し、trueでTHEN
句が行番号の場合、GOTO
(アドレスD9E6
)を実行します。 GOTOコード(D93E
にある)は、宛先行が大きい場合は現在の行から前方に検索し、そうでない場合はプログラムの先頭から検索します。
Block Skip。より構造化された言語は、実行が順方向のみに継続することを要求することにより、if
ステートメントの一部として直接ジャンプに制限を課します*およびtrueに達した場合に実行されるステートメントの終わりを示すものがあります。一部の言語では、ブロックマーカーを使用してこれを行います(例:BEGIN...END
または{...}
); Bourne Shellのようなその他のものには、if-statement固有の「true」ブロックの終わりを識別するためのマーカーがあります。
# Bourne Shell
if [ "$A" eq 5 ]
then
echo "A was 5"
fi
echo "Doing the next thing"
テスト結果がfalse
である場合、インタープリターはthen
ステートメントを予期して飲み込み、次にif
(else
、elsif
およびfi
以外のもの)に何らかの制御上の意味がある行が見つかるまで、実際には何も実行せずに行を読み取り続けます。 Bourne Shellの最新バージョンは、構文の正確さのためにブロック内のステートメントを解釈しようとしますが、単純な実装では、プログラムの実行方法に影響を与えずにelse
、elsif
、またはfi
に到達するまで、内容を単に無視できます。
*逆方向は、言語で提供されるループ構造または常に不快感を与えるgoto
のみを使用して許可されます。
(注:実行する方法は他にもあります。私は簡単な方法を説明しています。これは最も効率的な方法ではありません。実際の通訳者はあらゆる種類のトリックを使用して時間を節約しています)
コンパイルされたプログラムを実行するとき、プロセッサには、プログラムのどの部分を実行しているかを知らせるプログラムカウンターがあります。マシンコードのJMP命令はPCを新しい値に設定し、プロセッサーはプログラムの対応する部分の実行を開始します
インタープリターを作成するときは、プログラムカウンターに対応する変数が必要です。この変数は、ソースプログラムのどこかを指します。この変数をIPCと呼びます。
if
にアクセスして本文をスキップする必要がある場合は、ソースコードを最後まで確認する必要があります。あなたが与える例では、末尾の}
を探す必要があります。見つけたら、IPC=次の場所に設定してください。これで問題ありません。
最近のプロセッサでは、マシンコードプログラムがサブルーチンパラメータ、戻りアドレス、およびローカル変数に関する情報を格納するスタックもあります。
インタプリタでは、おそらく1つ以上のスタックまたは他のデータ構造に同様の情報を格納する必要があります。これは、マシンコードスタックから独立しています。
ループは複雑です。ループを開始するには、ループに関する情報を逆方向に検索する別のデータ構造に保持する必要があります。
このすべてを簡単な方法で行うとslowになります。同じif
が再び発生した場合は、コードをもう一度検索する必要があります。
実際の通訳者は多くの時間節約のトリックを追加します。インタプリタははるかに高速になりますが、はるかに複雑になります。
その価値のあるインタプリタは、ソースコードを直接「実行」しません。むしろ、それは Abstract Syntax Tree (AST)のような、ソースコードの他の内部表現を実行します。
プログラムがロードされると、パーサーはソースコードをすばやく通過して、ソースを対応する内部形式に変換します。これは非常に迅速に行うことができます。ソースがASTに入ると、特定のアドレスを直接指すジャンプ命令がすでに含まれています。これにより、ジャンプのターゲットラベルが見つかるまで、実行中にソースを解析する必要がなくなります。