コンピュータプログラミングにおいて、ワンパスコンパイラとは、各コンパイル単位の各部分を1 回だけ通過し、各部分を最終的なマシンコードに即座に変換するコンパイラです。これは、ソースコードとマシンコードの間のステップでプログラムを 1 つ以上の中間表現に変換し、各連続パスでコンパイル単位全体を再処理するマルチパスコンパイラとは対照的です。
これはコンパイラの論理的な機能を指し、ソース ファイルを実際に 1 回だけ読み取るという意味ではありません。たとえば、ソース ファイルは一時ストレージに 1 回読み込まれますが、その後そのコピーは何度もスキャンされる可能性があります。IBM 1130 Fortranコンパイラはソースをメモリに保存し、多くのパスを使用しました。対照的に、ディスク ストレージ ユニットのないシステム上のアセンブラでは、ソースのカード デッキをカード リーダー/パンチに 2 回提示する必要がありました。
プロパティ
ワンパスコンパイラはマルチパスコンパイラよりも小さくて高速です。[1]
ワンパス コンパイラは、利用可能な情報の範囲が限られているため、マルチパス コンパイラほど効率的なプログラムを生成できません。多くの効果的なコンパイラ最適化では、基本ブロック、ループ (特にネストされたループ)、サブルーチン、またはモジュール全体に対して複数のパスが必要です。プログラム全体に対してパスが必要なものもあります。一部のプログラミング言語は、設計上、1 回のパスではコンパイルできません。たとえば、PL/I では、データ宣言をプログラム内の任意の場所に配置できますが、具体的には、まだ宣言されていない項目への参照の後に配置することができるため、プログラム全体がスキャンされるまでコードは生成されません。言語定義には、コンパイルされるソース コードを生成するプリプロセッサ ステートメントも含まれており、複数のパスが確実です。対照的に、多くのプログラミング言語は、ワンパス コンパイラでコンパイルされるように特別に設計されており、ワンパス コンパイルを可能にする特別な構造が含まれています。
困難
基本的な問題は前方参照です。ソース ファイルのある時点でのシンボルの正しい解釈は、ソース ファイル内のさらに先の他のシンボルの有無に依存する可能性があり、それらのシンボルに遭遇するまで、現在のシンボルの正しいコードを生成することはできません。これはコンテキスト依存性の問題であり、範囲は隣接するシンボルから任意の大量のソース テキストまでどこにでも及ぶ可能性があります。
ローカルコンテキスト
たとえば、記号 < が「より大きい」ではなく「より小さい」比較として認識されるとします。文字コードの制限により、グリフ ≤ は標準エンコードで使用できない可能性があるため、複合表現「<=」が許可されます。このコンテキストは、次の記号によって決定されますが、「<」がいつ出現するかは不明です。同様に、記号「=」は、複合記号の一部である場合のように、常に「=」を意味するわけではありません。他の複合記号には、特殊文字「<」が使用できない場合に備えて「.lt.」が含まれる場合があります。グリフ ¬ (「not」) の文字コードが使用できない場合のもう 1 つの可能性は、「¬=」または「not equal」の「<>」です。一部のシステムでは、さらにバリエーションとして、¬ の代わりに ~ または ! を使用します。1 つの方法は、「<」の後にスキャンを進め、「=」に遭遇したらバックトラックすることです。これはもちろん、その部分のテキストを 2 回通過することを意味し、これは避ける必要があります。さらに言えば、ソース ファイルは、カード リーダーなど、戻って再度読み取る操作をサポートしていないデバイスから取得される可能性があります。後で元に戻す必要がある可能性のある早期の決定を行う代わりに、字句解析器は、量子重ね合わせの概念のように複数の解釈を維持し、後で決定的なシンボルを観察したときにのみ特定の選択にまとめることができます。特に、COBOL コンパイラは、10 進定数に現れるピリオドとステートメントの末尾に現れるピリオドを区別するために 1 つのパスを割り当てます。このようなスキームは、シングル パス コンパイラでは利用できません。
項目の名前も同様です。1 文字の名前に制限する言語はほとんどないため、1 文字の名前としての文字 "x" は、"text" などの名前内の文字 "x" とはまったく異なります。この場合、コンテキストはすぐ隣の文字を超えて拡張されます。 連続するソース ストリームの項目を言語のトークンに分割するのは、字句解析器の役割です。単語だけではありません。"<" と "<=" もトークンです。名前は通常、文字で始まり、文字と数字、および場合によっては "_" などのいくつかの追加記号が続きます。数値を指定するために許可されている構文は驚くほど複雑で、たとえば +3.14159E+0 は有効です。トークン間に任意の数のスペース文字を許可するのが一般的であり、Fortran は見かけ上のトークン内でもスペースを許可 (および無視) する点で珍しいため、"GO TO" と "GOTO" は "<=" と "< =" と同じになります。ただし、一部のシステムでは、特定のトークンを区切るためにスペースが必要な場合があり、Python などの他のシステムでは、Begin ... Endまたは同様のマーカーで示される可能性のあるプログラム ブロックの範囲を示すために先頭のスペースを使用します。
表現内の文脈
算術式を使用できる言語は、通常、優先順位規則のある挿入記法の構文に従います。つまり、式のトークンがソース テキストから引き出されるため、式の評価用のコード生成はスムーズに進みません。たとえば、式 x + y*(u - v) は、x が y に追加されないため、load x, add y と同等にはなりません。算術にスタック スキームが使用される場合、コードは Load x で始まる場合がありますが、後続の + トークンに対応するコードは続きません。代わりに、(u - v) のコードが生成され、その後に y による乗算が続き、その後に初めて x が追加されます。算術式のパーサーは、解析中にソースに沿って前後に移動せず、優先順位規則によって駆動される遅延操作のローカル スタックを使用します。算術式を逆ポーランド記法または同様の表記で表現することを要求することで、このダンスを回避できます。上記の例では、uv - y * x + のようなもので、厳密に左から右にスキャンされます。
最適化コンパイラは算術式の形式を分析し、繰り返しを識別して削除したり、その他の潜在的な改善を行ったりすることがあります。
a*sin(x) + b*sin(x)
いくつかの言語では算術式の中で代入ができるので、プログラマーは次のように書くこともできる。
a*(t:=sin(x)) + b*t
しかし、そうするために必要な労力は別として、結果として得られる文の形式は乱雑になり、コード化されている数式と簡単に比較できなくなります。間違いが起こりやすくなります。代わりに、コンパイラは式全体の形式 (通常はツリー構造を使用) を表し、その構造を分析および変更してから、改善された形式のコード (おそらく など) を出力できます(a + b)*sin(x)。連続する代入文のブロックに明らかな拡張があります。これは、ソース テキストを 2 度目に通過するものではありません。
中程度の文脈
字句解析器は入力ストリームをトークンのストリームに分割し (コメントは破棄します)、言語の構文に従ったこれらのトークンの解釈はコンテキストに依存する場合があります。Fortran 疑似コードで次のステートメントを検討してください。
if (式) =など。 if (式) label1、label2、label3 if (式) x = .true. if (式) then
1 つ目は、何らかの算術式 ( etc )の値を「if」と呼ばれる 1 次元配列の要素に割り当てることです。Fortran は予約語を含まないという点で珍しいため、トークン「write」は、必ずしも write ステートメントが進行中であることを意味するわけではありません。その他のステートメントは、確かに if ステートメントです。2 つ目は算術 if で、式の結果の符号を調べ、それが負、ゼロ、または正であるかどうかに基づいてラベル 1、2、または 3 にジャンプします。3 つ目は論理 if で、その式の結果がブール値 (Fortran の用語では「論理的」) である必要があります。これがtrue の場合、次のステートメント部分が実行されます (ここでは、 x にtrue を割り当てています。「true」は予約語ではないため、ピリオドで区切ることで特別な意味が示されます)。4 つ目は、if ... then ... else ... end ifシーケンスの開始で、これも式がブール値の結果を生成する ことを必要とします。
したがって、字句解析器から出てくるトークン「if」の正しい解釈は、式がスキャンされ、閉じ括弧の後に等号、数字(label1のテキスト:Fortran はラベルとして整数のみを使用しますが、ラベルは ASSIGN ステートメントで指定できるため、スキャンは 2 つのコンマを見つける必要があります)、代入ステートメント(または write ステートメントなど)の開始、または「then」(他のテキストが続くことはできません)のいずれかが現れるまで行うことはできません。したがって、式は任意であるため、コンテキストは任意の量のソース テキストにまたがります。ただし、4 つのケースすべてで、コンパイラはスキャンが進むにつれて式を評価するためのコードを生成できます。したがって、字句解析は、許容される構文の不規則性のために、識別したばかりのトークンの意味を常に判断できるとは限りません。そのため、バックトラックを回避するには、構文解析で可能な状態の重ね合わせを維持する必要があります。
構文解析が重なり合った状態の霧の中を漂っていると、エラーが発生した場合 (つまり、有効な構文フレームに当てはまらないトークンが見つかった場合)、役立つメッセージを生成することが困難になることがあります。たとえば、B6700 Algol コンパイラは、ソース行のリストと問題の場所を示すマーカー (多くの場合、セミコロンをマーク) とともに、「セミコロンが必要です」などのエラー メッセージが表示されることで有名でした。セミコロンがない場合、指示どおりに実際にセミコロンが配置されていると、再コンパイル時に「予期しないセミコロン」というメッセージが表示される可能性があります。多くの場合、コンパイラからの最初のエラー メッセージだけに注意を払う価値があります。後続のメッセージがうまくいかないためです。ソース ファイルにエラーがある場合、現在の解釈をキャンセルして次のステートメントの開始時にスキャンを再開することは困難であり、後続のメッセージは役に立ちません。もちろん、それ以上のコード生成は中止されます。
この問題は、予約語の使用によって軽減できます。たとえば、「if」、「then」、「else」は常に if 文の一部であり、変数名にはなり得ませんが、驚くほど多くの有用な単語が使用できなくなる可能性があります。別のアプローチは「ストロッピング」です。これは、予約語を、たとえばピリオドなどの特殊文字の間、または Algol の一部のバージョンのようにアポストロフィの間に配置することによってマーク付けするものです。これは、'if'と がif別のトークンであることを意味します。後者は通常の名前ですが、すべてのアポストロフィを指定するのはすぐに面倒になります。多くの言語では、スペースで十分な情報が得られますが、これは複雑になる場合があります。多くの場合、スペース (またはタブなど) だけでなく、文字や数字以外の文字が、トークンの可能性のあるテキストを終了します。上記の例では、 if 文の式は括弧で囲む必要があります。「(」は「if」の識別を確実に終了し、同様に「)」は「then」の識別を可能にします。さらに、複合 if 文の他の部分は、新しい行に記述する必要があります: "else" と "end if" (または "endif") と "else if"。対照的に、Algol などでは括弧は必要なく、if 文のすべての部分を 1 行に記述できます。Pascal では、if a or b then などは有効ですが、aとb が式である場合は、括弧で囲む必要があります。
コンパイラによって生成されるソース ファイル リストは、識別される予約語を下線付き、太字、または斜体で表示することで読みやすくすることができますが、「Algol は斜体のピリオドと通常のピリオドを区別する唯一の言語です」という批判もあります。実際、これは冗談ではありません。Fortran では、 などの do ステートメントの開始は、 という変数への値 1.15 の割り当て (DO 12 I = 1,15スペースは無関係であることに注意してください) とコンマとピリオドの違いによってのみ区別され、印刷されたリストのグリフは整形式ではない可能性があります。
DO 12 I = 1.15DO12I
言語の設計に細心の注意を払うことで、表現の明快さと簡潔さが促進され、動作が簡単に理解できる信頼性の高いコンパイラを作成できます。しかし、不適切な選択はよくあります。たとえば、Matlab は、A' のようにアポストロフィを使用して行列の転置を表しますが、これは例外なく数学的な使用法に厳密に従っています。これは良いことですが、テキスト文字列の区切り文字については、Matlab は二重引用符記号がどのような目的でも提供する機会を無視し、これにもアポストロフィを使用します。Octave はテキスト文字列に二重引用符を使用しますが、Matlab ステートメントも受け入れるように努めているため、問題は別のシステムにまで及びます。
プリプロセッサ拡張
この段階では、プリプロセッサオプションが実行されます。これは、入力ソースをコンパイラが適切に処理する前に実行されるため、このように呼ばれます。これらは、アセンブラシステムの「マクロ展開」オプションを反映していますが、より洗練された構文が期待されます。最も一般的な配置は、
条件 が成立したらこのソース、そうでなけれ ば他のソース
多くの場合、プリプロセッサソース文を「通常の」ソース文と区別するために何らかの取り決めが行われます。たとえば、pl/iの%記号や#などで始まる文などです。もう1つの簡単なオプションは、
これを定義する=それ
しかし、注意が必要です。
SumXY = (x + y)を定義します。 合計:=3*合計XY;
括弧がなければ、結果は sum:=3*x + y となるため、同様に、置換テキストの範囲と結果のテキストがどのようにスキャンされるかを決定する際に注意が必要です。
#define three = 3; #define point = .; #define one = 1; x:=3.1;
ここで、defineステートメントはセミコロンで終了していますが、セミコロン自体は置換の一部ではありません。x:=threepointone;これは別の名前であるため、呼び出しはできませんが、three point one呼び出しは可能であり3 . 1、後続のスキャンでは、それを単一のトークンとして認識できる場合と認識できない場合があります。
一部のシステムでは、コンパイルされるソース テキストを出力するプリプロセッサ プロシージャの定義が許可されており、そのようなソースでさらにプリプロセッサ項目を定義できる場合もあります。このようなオプションを巧みに使用すると、定数に説明的な名前を付けたり、難解な詳細を簡単なニーモニックに置き換えたり、新しいステートメント形式を作成したり、実際のプロシージャを考案するのではなく、一般的なプロシージャの特定の使用法 (ソートなど) 用のインライン コードを生成することができます。パラメーターとパラメーター タイプが急増すると、必要な組み合わせの数は指数関数的に増加します。
さらに、同じプリプロセッサ構文を複数の異なる言語、さらには人物名、ニックネーム、飼い犬の名前などを使用したストーリー テンプレートからストーリーを生成する自然言語に使用することもできます。そのため、ソース ファイルを受け入れ、プリプロセッサ アクションを実行し、次の段階であるコンパイルに備えて結果を出力するプリプロセッサ プログラムを考案したくなるかもしれません。しかし、これは明らかにソースを少なくとも 1 回余分に通過することになるため、このようなソリューションはシングル パス コンパイラでは利用できません。したがって、実際の入力ソース ファイルの進行は断続的に進む可能性がありますが、それでも一方向です。
長期的な文脈
コンパイラによるコード生成も前方参照の問題に直面します。最も直接的なのは、Go to labelのような場合です。この場合、宛先ラベルはソース ファイル内で未知の距離先にあり、そのため、そのラベルの位置に到達するためのジャンプ命令は、まだ生成されていないコード上の未知の距離を経由します。一部の言語設計は、おそらく「GOTO は有害であると見なされている」の影響を受けて、GOTO ステートメントを持っていませんが、プログラム内に暗黙の GOTO に相当するものが多数存在するため、この問題は回避されません。次の例を考えてみましょう。
条件 が成立したらコード は真、 それ以外は コードは偽、 fi
前述のように、条件を評価するコードはすぐに生成できます。ただし、 thenトークンに遭遇すると、宛先アドレスがcode falseステートメントのコード開始である JumpFalse 操作コードを配置する必要があります。同様に、elseトークンに遭遇すると、 code trueステートメントの完了したばかりのコードの後に、if ステートメントの末尾に続くコード (ここではfiトークンでマークされています) を宛先とする GOTO スタイルのジャンプ操作を続ける必要があります。これらの宛先は、まだスキャンされていないソースに対して任意の量のコードが生成された後でのみ知ることができます。caseステートメントなど、ソースの一部が任意の量のソースにまたがるステートメントでも、同様の問題が発生します。
再帰下降コンパイラは、if 文などの各タイプの文に対してプロシージャをアクティブ化し、適切なプロシージャを呼び出して、その文のコードtrue部分とコード false部分の文のコードを生成します。同様に、他の文についても構文に従ってコードを生成します。ローカル ストレージでは、不完全な JumpFalse 操作のアドレス フィールドの位置を追跡し、thenトークンに遭遇すると、現在わかっているアドレスを配置し、同様に、コード trueコードの後にジャンプするために必要なfiトークンに遭遇すると、ジャンプします。GoTo 文は、ジャンプするコードがその文の形式内にないという点で異なります。そのため、最終的にラベルに遭遇したときに使用される「フィックスアップ」の補助テーブルにエントリが必要です。この概念は拡張できます。すべての未知の宛先へのジャンプは、ジャンプ テーブル (宛先に遭遇したときに後でアドレスが埋められます) のエントリを介して行うことができますが、このテーブルに必要なサイズはコンパイルが終了するまでわかりません。
この問題の解決方法の 1 つは、コンパイラがアセンブラ ソース (ジャンプなどの宛先としてコンパイラが生成したラベルを含む) を出力し、アセンブラが実際のアドレスを決定することです。ただし、これにはソース ファイル (のバージョン) をさらに通過する必要があることは明らかであるため、シングル パス コンパイラでは許可されません。
残念な決断
上記の説明では、特定のフィールドを後で修正できるように残してコードが生成される可能性があるという概念を採用しましたが、そのようなコード シーケンスのサイズは一定であるという暗黙の仮定がありました。これは当てはまらない可能性があります。多くのコンピューターには、異なる量のストレージを占有する操作、特に相対アドレス指定が用意されています。相対アドレス指定では、宛先が -128 または +127 のアドレス指定ステップ内にある場合は 8 ビットのアドレス フィールドを使用できますが、それ以外の場合は到達するのにはるかに大きなアドレス フィールドが必要になります。したがって、コードが期待どおりに短いアドレス フィールドで生成された場合、後で戻って長いフィールドを使用するようにコードを調整する必要が生じる可能性があり、その結果、変更後の場所を参照する以前のコードも調整する必要があります。同様に、変更をまたいで後から参照する場合は、既知のアドレスを参照していたものであっても、修正する必要があります。また、修正情報自体も正しく修正する必要があります。一方、近いかどうかが確実でないすべてのケースで長いアドレスを使用できますが、結果として得られるコードはもはや理想的ではありません。
ワンパスシーケンシャル入力、不規則シーケンス出力
すでに述べたように、単一のステートメント内での最適化にはいくつかの可能性があります。複数のステートメントにわたる最適化には、コードが発行される前に分析および操作できる何らかのデータ構造にステートメントの内容を保持する必要があります。このような場合、仮のコードを生成することは、修正を考慮しても障害になります。制限的に、これはコンパイラがプログラム全体を内部形式で表すデータ構造を生成することを意味しますが、藁にもすがる思いで、ソース ファイルの最初から最後までの 2 回目のパスは実際には存在しないと主張することもできます。おそらく、コンパイラを宣伝する PR ドキュメントで。
したがって、コンパイラは、ソースの各部分が読み取られるたびに即座にコードを生成することはできず、単一の連続したシーケンスでコードを生成することもできません。出力は引き続き順番に書き込むことができますが、そのセクションの出力が、そのセクションのすべての保留中の修正が行われるまで延期される場合に限られます。
使用前の申告
さまざまな式のコードを生成する場合、コンパイラはオペランドの性質を認識している必要があります。たとえば、 A:=B; のようなステートメントは、 A と B が整数か浮動小数点変数か (およびサイズ: 単精度、倍精度、または 4 倍精度)、あるいは複素数、配列、文字列、プログラマ定義の型などによって、かなり異なるコードを生成する可能性があります。 この場合、簡単な方法は適切な数の記憶域ワードを転送することですが、文字列の場合、受信側が送信側よりも小さい可能性があり、いずれにしても文字列の一部しか使用されない可能性があるため、この方法は不適切である可能性があります。たとえば、1,000 文字分のスペースがあるのに、現在は 10 文字しか含まれていません。 次に、COBOL や pl/i によって提供される、より複雑な構造があります。A:=B by name;この場合、 A と B は集約 (または構造) であり、A には、たとえば部分A.x、および がありA.y、 B には、、、およびの順序でA.other部分 があります。 「名前で」機能は、 と同等であることを意味しますが、は A に対応するものがないため、また、 B に対応するものがないため、それらは関与しません。
B.yB.cB.xA.y:=B.y; A.x:=B.x;B.cA.other
これらすべては、アイテムが使用される前に宣言されるという要件によって処理できます。一部の言語では明示的な宣言は必要なく、新しい名前に初めて遭遇したときに暗黙の宣言が生成されます。Fortran コンパイラが、最初の文字が I、J、...、N のいずれかである、これまで知られていなかった名前に遭遇した場合、変数は整数になり、そうでない場合は浮動小数点変数になります。したがって、名前はDO12I浮動小数点変数になります。これは便利ですが、名前を誤って入力した経験が何度かあるため、ほとんどのプログラマーはコンパイラ オプション「implicit none」を使用するべきだと同意しています。
他のシステムでは、最初に遭遇した性質に基づいて、文字列や配列などの型を決定します。インタープリタ型言語は特に柔軟で、実行時に決定が下されます。
条件が 満たされる場合、 pi:="3.14"、そうでない場合はpi:=3.14 fi ; piを印刷します。
このような言語のコンパイラーが存在する場合、変数 pi を表す複雑なエンティティーを作成し、その現在の型が何であるかを示す情報と、そのような型を表す関連ストレージを含める必要があります。これは確かに柔軟性がありますが、A が 100 次行列で、その要素のいずれかが突然異なる型になる可能性がある Ax = b を解くような集中的な計算には役立たない可能性があります。
手順と機能
使用前の宣言は、同様に、プロシージャと関数で満たすべき簡単な要件であり、これはプロシージャ内のプロシージャのネストにも適用されます。ALGOL、Pascal、PL/I など多くの言語と同様に、MATLAB と (1995 年以降) Fortran では、関数 (またはプロシージャ) に別の関数 (またはプロシージャ) の定義を含めることができます。この定義は、関数内だけで表示されますが、これらのシステムでは、関数 (またはプロシージャ) は、プロシージャの終了 後に定義する必要があります。
しかし、再帰が許可されると、問題が発生します。お互いを呼び出す 2 つのプロシージャは、使用前に両方を宣言することはできません。ソース ファイルでは、どちらか一方が最初になければなりません。未知の変数に遭遇したときのように、その遭遇から、コンパイラが未知のプロシージャの呼び出しに適したコードを生成できることが十分に推測できる場合、これは問題ではありません。もちろん、プロシージャの定義に遭遇したときに、戻って宛先の正しいアドレスを埋めるための「修正」装置が配置されています。たとえば、これはパラメータのないプロシージャの場合です。関数呼び出しから返される結果は、呼び出しから識別可能な型である可能性がありますが、これは常に正しいとは限りません。関数は浮動小数点の結果を返すかもしれませんが、その値は整数に割り当てられている可能性があります。
Pascal では、"事前宣言" を要求することでこの問題を解決しています。最初にプロシージャまたは関数の宣言の 1 つを指定する必要がありますが、プロシージャまたは関数の本体の代わりに、キーワードforwardを指定します。次に、他のプロシージャまたは関数を宣言し、その本体を定義します。ある時点で、"forward" プロシージャまたは関数は、関数の本体とともに再宣言されます。
パラメータ付きのプロシージャ (または関数) の呼び出しでは、パラメータの型は既知 (使用前に宣言されている) ですが、プロシージャ呼び出しでの使用方法は不明な場合があります。たとえば、Fortran ではすべてのパラメータを参照 (つまりアドレス) で渡すため、コードの生成にすぐに問題が発生することはありません (実際のアドレスは後で修正されます)。ただし、Pascal やその他の言語では、プログラマの選択によりさまざまな方法でパラメータを渡す (参照、値、または「名前」 ) ことができます。これはプロシージャの定義でのみ示され、定義に遭遇するまでは不明です。特に Pascal の場合、パラメータの指定で接頭辞「Var」は参照で受け取られる必要があることを意味し、接頭辞がない場合は値で受け取られることを意味します。最初のケースでは、コンパイラはパラメータのアドレスを渡すコードを生成する必要があり、2 番目のケースでは、通常はスタックを介して値のコピーを渡す別のコードを生成する必要があります。いつものように、これを処理するために「修正」メカニズムを呼び出すこともできますが、非常に面倒です。マルチパス コンパイラは、もちろん、前後に往復しながら必要な情報をすべて照合できますが、シングルパス コンパイラはできません。スキャンが進む間、必要なエンティティが見つかるまでコード生成を一時停止できます (その結果は内部ストレージに保持されます)。コード生成段階はすぐに追いつくため、ソースの 2 回目のパスとは見なされない可能性があります。コード生成段階はしばらく停止しているだけです。ただし、これは複雑になります。代わりに、特別な構造が導入され、プロシージャのパラメータ使用の定義が、後の完全な定義の「前」に宣言されるため、コンパイラは必要に応じて使用前にそれを認識できます。
First Fortran (1957) 以降、プログラムの各部分を個別にコンパイルすることが可能になり、プロシージャと関数のライブラリの作成がサポートされるようになりました。コンパイルされるソース ファイル内のプロシージャは、外部のコレクションから関数を呼び出すため、未知の関数によって返される結果の型を認識している必要があります。これは、結果を見つけるために適切な場所を検索するコードを生成するためだけです。もともと、整数と浮動小数点変数しかなかったときは、暗黙の宣言のルールに従って選択できましたが、サイズと型が急増したため、呼び出しプロシージャは関数の型宣言を必要とします。これは特別なことではなく、プロシージャ内で宣言された変数と同じ形式です。
満たすべき要件は、シングル パス コンパイルの現在の時点で、エンティティに関する情報が必要であり、後でアドレス修正を行う場合に、そのエンティティの正しいコードを今すぐ生成できることです。必要な情報がソース ファイルで後で見つかるか、または別途コンパイルされたコード ファイルで見つかるかに関係なく、情報はここで何らかのプロトコルによって提供されます。
プロシージャ (または関数) のすべての呼び出しが相互の互換性と定義についてチェックされるかどうかは別の問題です。Algol のようなインスピレーションから派生した言語では、このチェックは通常厳格ですが、他のシステムでは無関心な場合があります。プロシージャにオプションのパラメータを持たせることができるシステムは別として、パラメータの数と型の間違いは通常、プログラムのクラッシュを引き起こします。完全なプログラムの一部を個別にコンパイルし、後で「リンク」できるようにするシステムでは、パラメータと結果の型と数が正しいかどうかもチェックする必要があります。間違いはさらに起こりやすいのですが、多くの場合は行われません。一部の言語 (Algol など) には、「アップグレード」または「ワイドニング」または「プロモーション」という正式な概念があり、これにより、倍精度パラメータを期待するプロシージャが、単精度変数として呼び出されることがあります。この場合、コンパイラは、単精度変数を一時的な倍精度変数に格納し、実際のパラメータになるコードを生成します。ただし、これにより、パラメータの受け渡しメカニズムがコピーイン、コピーアウトに変更され、動作に微妙な違いが生じる可能性があります。プロシージャが倍精度パラメータやその他のサイズの変化を期待しているときに単精度変数のアドレスを受け取った場合の結果は、はるかに微妙ではありません。プロシージャ内でパラメータの値が読み取られる場合、指定されたパラメータよりも多くのストレージが読み取られ、結果の値が改善される可能性は低くなります。さらに悪いのは、プロシージャがパラメータの値を変更する場合です。何かが確実に破損します。これらの見落としを見つけて修正するには、多くの忍耐が必要です。
パスカルの例
このような構造の例として、Pascalの前方宣言があります。 Pascal では、プロシージャは使用前に宣言または完全に定義されている必要があります。 これは、ワンパス コンパイラの型チェックに役立ちます。どこにも宣言されていないプロシージャを呼び出すと、明らかなエラーになります。 前方宣言は、使用前に宣言するルールにかかわらず、 相互再帰プロシージャが直接呼び出すのに役立ちます。
関数odd ( n : integer ) : boolean ; begin if n = 0 then odd := false else if n < 0 then odd := even ( n + 1 ) { コンパイラ エラー: 'even' が定義されていません } else odd := even ( n - 1 ) end ;
関数even ( n :整数) :ブール値; begin n = 0の場合even := true else n < 0の場合even : = odd ( n + 1 ) else even := odd ( n - 1 ) end ;
関数 の前に関数 の前方宣言を追加することで、ワンパス コンパイラーに、プログラムの後半で の定義があることが伝えられます。
evenoddeven
関数even ( n :整数) :ブール値;前方;
関数odd ( n :整数) :ブール値; { その他 }
関数本体の実際の宣言が行われるとき、パラメータは省略されるか、元の前方宣言と完全に同一である必要があります。そうでない場合は、エラーがフラグ付けされます。
プリプロセッサの再帰
複雑なデータ集合を宣言する場合、関数 Odd と Even の使用が考えられます。データ集合 X のストレージ サイズが奇数バイトの場合、Odd(ByteSize(X)) のテストの制御下で 1 バイトの項目を追加して偶数にすることができます。上記の Odd と Even の同等の宣言を考えると、パラメータの使用法はプリプロセッサに既知であり、参照と値のいずれかを選択する機会はおそらくないため、「前方」宣言はおそらく必要ありません。ただし、呼び出しの結果が既知である必要があるため、実際の定義が終わるまでソース コード内でこれらの関数を呼び出すことはできません (定義外)。もちろん、プリプロセッサがソース ファイルの複数のパスに関与した場合は別です。
前方宣言は有害とみなされる
大規模なプログラム内のプロシージャの宣言と使用、およびルーチン ライブラリの使用 (特に変更中のもの) の間で一貫性を維持しようとしたことがある人なら、現在のコンパイルで呼び出されるが定義されていないプロシージャに対するforward宣言または同様の追加宣言の使用に苦労したことがあるでしょう。特に異なるソース ファイル間で、大きく離れた場所間の同期を維持するには、注意が必要です。予約語を使用する宣言は簡単に見つけることができますが、役立つ宣言が通常の宣言と区別されていない場合、作業は面倒になります。ワンパス コンパイルの目標を放棄するだけでこの負担がなくなる場合、より迅速なコンパイルの利点は不十分に思えるかもしれません。
参照
参考文献
- ^ 「シングルパス、ツーパス、マルチパスコンパイラ」。GeeksforGeeks 。 2019年3月13日。 2023年5月15日閲覧。
