独自の言語でコンパイラを書く


204

直感的には、言語用のコンパイラFoo自体をFooで作成することはできないようです。より具体的には、言語の最初のコンパイラFooはFooで作成できませんが、後続のコンパイラはで作成できますFoo

しかし、これは本当ですか?最初のコンパイラーが「それ自体」で書かれた言語について読んだことについて、漠然とした記憶があります。これは可能ですか?可能な場合、どのようにですか?



これは非常に古い質問ですが、Javaで言語Fooのインタプリタを書いたと言います。次に、fooという言語を使用して、独自のインタープリターを作成しました。FooはまだJREを必要としますか?
ジョージザビエル

回答:


231

これは「ブートストラップ」と呼ばれます。最初に、他の言語(通常はJavaまたはC)で使用する言語用のコンパイラー(またはインタープリター)をビルドする必要があります。それが終わったら、コンパイラの新しいバージョンをFoo言語で書くことができます。最初のブートストラップコンパイラーを使用してコンパイラーをコンパイルし、次にこのコンパイル済みコンパイラーを使用して他のすべてのもの(それ自体の将来のバージョンを含む)をコンパイルします。

ほとんどの言語は実際にこの方法で作成されます。これは、言語デザイナーが作成している言語を使用することを好むためと、非自明なコンパイラーが言語がいかに「完全」であるかの有用なベンチマークとして役立つことが多いためです。

この例はScalaです。最初のコンパイラは、Martin Oderskyが実験的な言語であるPizzaで作成したものです。バージョン2.0以降、コンパイラはScalaで完全に書き直されました。その時点から、新しいPizzaコンパイラーは将来の反復のために自分自身をコンパイルするために使用できるため、古いPizzaコンパイラーは完全に破棄される可能性があります。


多分愚かな質問:コンパイラをマイクロプロセッサの別のアーキテクチャに移植したい場合、ブートストラップはそのアーキテクチャで動作しているコンパイラから再開する必要があります。これは正解?これが正しい場合は、コンパイラを他のアーキテクチャに移植するのに役立つ可能性があるため、最初のコンパイラを保持する方がよいことを意味します(特にCのような「ユニバーサル言語」で書かれている場合)?
piertoni

2
@piertoniでは、通常、コンパイラのバックエンドを新しいマイクロプロセッサにリターゲットする方が簡単です。
bstpierre

たとえば、LLVMをバックエンドとして使用します

76

ソフトウェアエンジニアリングラジオのポッドキャストを聞いたときのことを思い出します。ディックガブリエルは、LISPで必要最低限​​のバージョンを紙に書いて手作業で機械コードに組み立て、元のLISPインタープリターのブートストラップについて話しました。それ以降、残りのLISP機能はLISPで作成され、LISPで解釈されました。


すべてが、多くのハンズオンを備えたジェネシストランジスタからブートストラップされます

47

以前の答えに好奇心を追加します。

Linux From Scratchマニュアルからの引用は、ソースからGCCコンパイラの構築を開始する段階にあります。(Linux From Scratchは、ターゲットシステムのバイナリをすべてコンパイルする必要があるという点で、ディストリビューションのインストールとは根本的に異なるLinuxのインストール方法です。)

make bootstrap

「ブートストラップ」ターゲットはGCCをコンパイルするだけでなく、数回コンパイルします。最初のラウンドでコンパイルされたプログラムを使用して、2回目にコンパイルされ、次に3回目にコンパイルされます。次に、これらの2番目と3番目のコンパイルを比較して、問題なく再現できることを確認します。これは、正しくコンパイルされたことも意味します。

「ブートストラップ」ターゲットの使用は、ターゲットシステムのツールチェーンをビルドするために使用するコンパイラが、ターゲットコンパイラのバージョンとまったく同じではない可能性があるという事実によって動機付けられています。そのように進めると、ターゲットシステムで、自分自身をコンパイルできるコンパイラを確実に取得できます。


12
「ターゲットシステムの実際にすべてのバイナリをコンパイルする必要があります」それでも、ソースはそれ自体をコンパイルできないため、どこかから取得したgccバイナリから開始する必要があります。連続する各gccの再コンパイルに使用された各gccバイナリの系統をさかのぼって追跡したとしたら、K&Rの元のCコンパイラまでさかのぼることができるでしょうか。
robru

43

C用の最初のコンパイラーを作成するときは、それを他の言語で作成します。これで、たとえばアセンブラーにC用のコンパイラーができました。最終的には、文字列、特にエスケープシーケンスを解析する必要がある場所に移動します。\n10進コード10(および\r13など)の文字に変換するコードを記述します。

そのコンパイラの準備ができたら、Cでの再実装を開始します。このプロセスは「ブートストラップ」と呼ばれます。

文字列解析コードは次のようになります。

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

これがコンパイルされると、「\ n」を理解するバイナリができます。つまり、ソースコードを変更できます。

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

では、「\ n」が13のコードであるという情報はどこにあるのでしょうか。バイナリにあります!それはDNAのようなものです。このバイナリを使用してCソースコードをコンパイルすると、この情報が継承されます。コンパイラがそれ自体をコンパイルする場合、この知識を子孫に渡します。この時点から、コンパイラーが何をするかをソースだけから確認する方法はありません。

一部のプログラムのソースでウイルスを隠したい場合は、次のようにして行うことができます。コンパイラのソースを取得し、関数をコンパイルする関数を見つけて、次の関数で置き換えます。

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

興味深い部分はAとBです。AはcompileFunctionウイルスを含めるためのソースコードであり、おそらく何らかの方法で暗号化されているため、結果のバイナリを検索しても明らかではありません。これにより、それ自体でコンパイルするようにコンパイルすると、ウイルス注入コードが保持されます。

Bは、ウイルスで置き換えたい関数でも同じです。たとえば、おそらくLinuxカーネルからのものであるソースファイル「login.c」の関数「login」である可能性があります。通常のパスワードに加えて、rootアカウントのパスワード「joshua」を受け入れるバージョンに置き換えることができます。

それをコンパイルしてバイナリとして拡散した場合、ソースを調べてウイルスを見つける方法はありません。

アイデアの元のソース:https : //web.archive.org/web/20070714062657/http : //www.acm.org/classics/sep95/


1
ウイルスに感染したコンパイラを書くことについて、後半のポイントは何ですか?:)
mhvelplund

3
@mhvelplundブートストラップがあなたを殺すことができるという知識を広めるだけです。
アーロンディグラ2018年

19

開始ソースコードをコンパイルするための手段がないため、コンパイラー自体を作成することはできません。これを解決するには2つの方法があります。

最も好ましくないのは次のとおりです。言語の最小限のセット用にアセンブラー(yuck)で最小限のコンパイラーを作成し、そのコンパイラーを使用して言語の追加機能を実装します。すべての言語機能を備えたコンパイラができるまで、自分の道を築き上げます。通常、他に選択の余地がない場合にのみ行われる痛みを伴うプロセス。

推奨されるアプローチは、クロスコンパイラーを使用することです。別のマシン上の既存のコンパイラのバックエンドを変更して、ターゲットマシンで実行される出力を作成します。次に、ターゲットマシンで動作する完全なコンパイラを用意します。交換可能なプラグイン可能なバックエンドを備えた既存のコンパイラーがたくさんあるので、これに最も人気のあるのはC言語です。

あまり知られていない事実は、GNU C ++コンパイラーにCサブセットのみを使用する実装があることです。その理由は、通常、新しいターゲットマシン用のCコンパイラを見つけて、それから完全なGNU C ++コンパイラをビルドできるからです。これで、ターゲットマシンにC ++コンパイラーがインストールされることになりました。


14

一般に、最初に動作するコンパイラーの(プリミティブの場合)有効なカットを用意する必要があります。それから、セルフホスティングにすることを考え始めることができます。これは実際、一部の言語では重要なマイルストーンと見なされています。

私が「mono」から覚えていることから、おそらく彼らはそれを機能させるためにいくつかのことをリフレクションに追加する必要があるでしょう。monoチームはいくつかのことが単にでは不可能であることを指摘し続けReflection.Emitます。もちろん、MSチームはそれらが間違っていることを証明するかもしれません。

これにはいくつかの本当の利点があります:初心者にとってはかなり良い単体テストです!また、心配する言語は1つだけです(つまり、C#の専門家がC ++をあまり知らない可能性がありますが、今ではC#コンパイラを修正できます)。しかし、私はここで働くことにプロのプライドはそれほど多くないのではないかと思っています。彼らは単にそれをセルフホスティングにしたいのです

コンパイラではありませんが、最近セルフホスティングシステムで作業しています。コードジェネレーターはコードジェネレーターを生成するために使用されます...スキーマが変更された場合は、それ自体で単純に実行します。新しいバージョンです。バグがある場合は、以前のバージョンに戻って再試行します。とても便利で、メンテナンスもとても簡単です。


アップデート1

私はPDCでAndersのこのビデオを見たところですが彼は(約1時間後)、サービスとしてのコンパイラーについて、もっと有効な理由をいくつか明らかにしています。参考までに。


4

ここにダンプがあります(実際には検索するのが難しいトピックです):

これはPyPyRubiniusのアイデアでもあります:

(これはForthにも当てはまると思いますが、Forthについては何も知りません。)


おそらくSmalltalk関連の記事への最初のリンクは、現在有用で即時の情報がないページを指しています。
nbro 2017

1

GNAT、GNU Adaコンパイラーは、Adaコンパイラーを完全にビルドする必要があります。GNATバイナリがすぐに利用できないプラットフォームに移植する場合、これは骨の折れる作業です。


1
理由がわかりませんか?(すべての新しいプラットフォームの場合のように)2回以上ブートストラップする必要があるルールはありません。現在のものとクロスコンパイルすることもできます。
マルコファンデフォールト2012年

1

実際、ほとんどのコンパイラは、上記の理由により、コンパイルする言語で記述されています。

最初のブートストラップコンパイラは通常、C、C ++、またはアセンブリで記述されています。


1

MonoプロジェクトのC#コンパイラは、長い間「自己ホスト型」でした。つまり、C#自体で記述されているということです。

私が知っているのは、コンパイラが純粋なCコードとして開始されたことですが、ECMAの「基本」機能が実装されると、C#でコンパイラを書き直し始めました。

コンパイラーを同じ言語で作成することの利点は知りませんが、少なくとも言語自体が提供できる機能を使用する必要があると確信しています(たとえば、Cはオブジェクト指向プログラミングをサポートしていません)。 。

詳細については、こちらをご覧ください


1

私はSLIC(コンパイラーを実装するための言語システム)自体を書きました。次に、それを手作業でコンパイルしてアセンブリにします。SLICは5つのサブ言語の単一のコンパイラであったため、SLICには多くの機能があります。

  • 構文パーサープログラミング言語PPL
  • GENERATOR LISP 2ベースのツリークロールPSEUDOコード生成言語
  • ISOインシーケンス、PSEUDOコード、最適化言語
  • アセンブリコード生成言語のようなPSEUDOマクロ。
  • MACHOPアセンブリ機械命令定義言語。

SLICはCWIC(Compiler for Writing and Implementing Compilers)に触発されました。ほとんどのコンパイラ開発パッケージとは異なり、SLICおよびCWICは、特殊なドメイン固有の言語を使用したコード生成に対処しました。SLICは、CWICのコード生成を拡張し、ISO、PSEUDO、MACHOPサブ言語を追加して、ターゲットマシンの仕様をツリークロールジェネレーター言語から分離します。

LISP 2ツリーとリスト

LISP 2ベースのジェネレーター言語の動的メモリ管理システムは重要なコンポーネントです。リストは角括弧で囲まれた言語で表現され、そのコンポーネントはコンマで区切られています。つまり、3つの要素[a、b、c]リストです。

木:

     ADD
    /   \
  MPY     3
 /   \
5     x

最初のエントリがノードオブジェクトであるリストで表されます。

[ADD,[MPY,5,x],3]

ツリーは通常、ブランチの前にノードが分離して表示されます。

ADD[MPY[5,x],3]

LISP 2ベースのジェネレーター関数による解析解除

ジェネレータ関数は、(unparse)=> action>ペアの名前付きセットです...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

解析されていない式は、ツリーパターンやオブジェクトタイプに一致するテストであり、それらを分解し、それらの部分をローカル変数に割り当てて、その手続き型アクションで処理します。種類の異なる引数をとるオーバーロードされた関数のようなものです。()=> ...を除いて、テストはコード化された順序で試行されます。対応するアクションの実行に成功した最初のunparse。解析されていない式は、テストを分解しています。ADD [x、y]は、ローカル変数xおよびyにブランチを割り当てる2つのブランチのADDツリーと一致します。アクションは、単純な式でも、.BEGIN ... .END境界コードブロックでもかまいません。今日はcスタイル{...}ブロックを使用します。ツリーマッチング、[]、非解析ルールは、返された結果をアクションに渡すジェネレーターを呼び出す場合があります。

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

具体的には、上記のexpr_gen unparseは2つのブランチのADDツリーに一致します。テストパターン内では、ツリーブランチに配置された単一の引数ジェネレータがそのブランチで呼び出されます。ただし、引数リストは、返されたオブジェクトに割り当てられたローカル変数です。unparseの上では、2つのブランチがADDツリーの逆アセンブリであることを指定し、各ブランチをexpr_genに再帰的に押します。左側の分岐はローカル変数xに配置されます。同様に、右のブランチは、戻りオブジェクトyとともにexpr_genに渡されました。上記は、数値式エバリュエーターの一部である可能性があります。上記のノード文字列の代わりに、ベクターと呼ばれるショートカット機能がありました。ノードのベクターは、対応するアクションのベクターで使用できます。

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

上記のより完全な式エバリュエーターは、expr_genの左のブランチからの戻りをxに割り当て、右のブランチをyに割り当てます。xとyに対して実行された対応するアクションベクトルが返されます。最後のunparse => actionペアは、数値およびシンボルオブジェクトに一致します。

シンボルとシンボル属性

シンボルは名前付きの属性を持つことができます。val:(x)は、xに含まれるシンボルオブジェクトのval属性にアクセスします。一般化されたシンボルテーブルスタックはSLICの一部です。SYMBOLテーブルは、関数にローカルシンボルを提供してプッシュおよびポップできます。新しく作成されたシンボルは、上部のシンボルテーブルにカタログ化されています。シンボルルックアップでは、最初にスタックを後方に向かって、最上位のテーブルからシンボルテーブルスタックを検索します。

マシンに依存しないコードを生成する

SLICのジェネレーター言語はPSEUDO命令オブジェクトを生成し、セクションコードリストに追加します。.FLUSHを指定すると、PSEUDOコードリストが実行され、リストから各PSEUDO命令が削除されて呼び出されます。実行後、PSEUDOオブジェクトのメモリが解放されます。PSEUDOとGENERATORアクションの手続き本体は、出力を除いて基本的に同じ言語です。PSEUDOは、マシンに依存しないコードの逐次化を提供するアセンブリマクロとして機能することを目的としています。これらは、ツリークロールジェネレーター言語から特定のターゲットマシンを分離します。PSEUDOはMACHOP関数を呼び出してマシンコードを出力します。MACHOPは、アセンブリ疑似op(dc、定数の定義など)と機械命令、またはベクトル化されたエントリを使用した同様の形式の命令のファミリを定義するために使用されます。それらは単に、パラメータを、命令を構成する一連のビットフィールドに変換します。MACHOP呼び出しは、アセンブリのように見え、アセンブリがコンパイルリストに表示されるときにフィールドの印刷フォーマットを提供することを目的としています。例のコードでは、簡単に追加できるが元の言語にはなかったcスタイルのコメントを使用しています。MACHOPは、ビットアドレス可能なメモリにコードを生成します。SLICリンカーはコンパイラーの出力を処理します。ベクター化されたエントリを使用したDEC-10ユーザーモード命令のMACHOP:MACHOPは、ビットアドレス可能なメモリにコードを生成します。SLICリンカーはコンパイラーの出力を処理します。ベクター化されたエントリを使用したDEC-10ユーザーモード命令のMACHOP:MACHOPは、ビットアドレス可能なメモリにコードを生成します。SLICリンカーはコンパイラーの出力を処理します。ベクター化されたエントリを使用したDEC-10ユーザーモード命令のMACHOP:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

.MORG 36、O(18):$ / 36; ロケーションを36ビット境界に揃えて、18ビットのロケーション$ / 36ワードアドレスを8進数で出力します。9ビットopcd、4ビットレジスタ、間接ビット、および4ビットインデックスレジスタを組み合わせて、1つの18ビットフィールドのように出力します。18ビットアドレス/ 36または即値が出力され、8進数で出力されます。MOVEIの例は、r1 = 1およびr2 = 2で出力します。

400020 201082 000005            MOVEI r1,5(r2)

コンパイラアセンブリオプションを使用すると、生成されたアセンブリコードがコンパイルリストに表示されます。

リンクする

SLICリンカーは、リンクとシンボル解決を処理するライブラリとして提供されます。ターゲット固有の出力ロードファイルのフォーマットは、ターゲットマシン用に記述し、リンカーライブラリライブラリとリンクする必要があります。

ジェネレーター言語は、ツリーをファイルに書き込み、それらを読み取ることができるため、マルチパスコンパイラーを実装できます。

コード生成とオリジンの短い要約

SLICが真のコンパイラコンパイラであることを確実にするために、最初にコード生成について説明しました。SLICは、1960年代後半にSystems Development Corporationで開発されたCWIC(Compiler for Writing and Implementing Compilers)に触発されました。CWICには、GENERATOR言語から数値バイトコードを生成するSYNTAXおよびGENERATOR言語しかありませんでした。バイトコードは、名前付きセクションに関連付けられ、.FLUSHステートメントによって書き出されたメモリバッファーに配置または植え付け(CWICのドキュメントで使用されている用語)。CWICに関するACM論文は、ACMアーカイブから入手できます。

主要なプログラミング言語の実装に成功

1970年代後半、SLICはCOBOLクロスコンパイラの作成に使用されました。約3か月で1人のプログラマーがほぼ完成しました。必要に応じてプログラマーと少し作業をしました。別のプログラマーが、ターゲットTI-990ミニコンピューター用のランタイムライブラリとMACHOPを作成しました。そのCOBOLコンパイラーは、アセンブリーで作成されたDEC-10ネイティブCOBOLコンパイラーよりも1秒あたり実質的に多くの行をコンパイルしました。

コンパイラについての詳細は、通常、

コンパイラを最初から作成する際の大きな部分は、ランタイムライブラリです。シンボルテーブルが必要です。入力と出力が必要です。動的メモリ管理など。コンパイラのランタイムライブラリを作成してからコンパイラを作成する方が簡単です。しかし、SLICでは、そのランタイムライブラリはSLICで開発されたすべてのコンパイラに共通です。2つのランタイムライブラリがあることに注意してください。言語の(COBOLなどの)ターゲットマシン用。もう1つは、コンパイラコンパイラランタイムライブラリです。

これらはパーサージェネレーターではなかったと思います。これで、バックエンドについて少し理解したところで、パーサープログラミング言語について説明できます。

パーサープログラミング言語

パーサーは、単純な方程式の形式で記述された式を使用して記述されています。

<name> <formula type operator> <expression> ;

最下位レベルの言語要素は文字です。トークンは、言語の文字のサブセットから形成されます。文字クラスは、これらの文字サブセットに名前を付けて定義するために使用されます。文字クラス定義演算子は、コロン(:)文字です。クラスのメンバーである文字は、定義の右側にコーディングされます。印刷可能な文字は、プライム文字列 'で囲まれています。非印刷文字および特殊文字は、序数で表すことができます。クラスメンバーは代替によって区切られます| オペレーター。クラス式はセミコロンで終わります。文字クラスには、以前に定義されたクラスが含まれる場合があります。

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

skip_class 0b00000001は事前に定義されていますが、skip_classを定義しているため、行き過ぎかもしれません。

要約すると、文字クラスは代替のリストであり、文字定数、文字の序数、または以前に定義された文字クラスのみにすることができます。文字クラスを実装したとき:クラス式にはクラスビットマスクが割り当てられています。(上記のコメントに示されています)文字リテラルまたは序数を持つクラス式があると、クラスビットが割り当てられます。マスクは、含まれているクラスのクラスマスクと割り当てられたビット(存在する場合)を組み合わせて作成されます。クラステーブルは、文字クラスから作成されます。キャラクターの序数によって索引付けされたエントリーには、キャラクターのクラスメンバーシップを示すビットが含まれています。クラスのテストはインラインで行われます。キャラクターの序数がeaxのIA-86コード例は、クラステストを示しています。

test    byte ptr [eax+_classmap],dgt

次が続く:

jne      <success>

または

je       <failure>

IA-86命令は今日より広く知られていると思うので、IA-86命令のコード例が使用されています。クラスマスクに評価されるクラス名は、序数(eax)の文字でインデックス付けされたクラステーブルと非破壊的にANDされます。ゼロ以外の結果は、クラスのメンバーシップを示しています。(文字を含むal(EAXの下位8ビット)を除いて、EAXはゼロ化されます)。

これらの古いコンパイラでは、トークンは少し異なっていました。キーワードはトークンとして説明されていません。それらは単に、パーサー言語で引用符で囲まれた文字列定数と一致しました。引用文字列は通常保持されません。修飾子を使用できます。+は文字列を一致させます。(つまり、+ '-'は、-文字と一致し、成功したときに文字を保持します)、操作(つまり、 'E')は、文字列をトークンに挿入します。空白は、最初の一致が見つかるまで先頭のSKIP_CLASS文字をスキップするトークン式によって処理されます。明示的なskip_class文字の一致はスキップを停止し、トークンがskip_class文字で始まることを許可することに注意してください。文字列トークンの式は、一重引用符のquiddd文字または二重引用符で囲まれた文字列に一致する、主要なskip_class文字をスキップします。興味深いのは、引用符で囲まれた文字列内の「」文字の照合です。

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

最初の選択肢は、一重引用符で囲まれた文字と一致します。正しい選択肢は、2つの "文字を組み合わせて1つの"文字を表す二重引用符文字を含むことができる二重引用符で囲まれた文字列に一致します。この式は、独自の定義で使用される文字列を定義します。内側の右選択肢 '"' $(-" "" ".ANY |" "" "" "、" "" ") '"'は、二重引用符で囲まれた文字列と一致します。引用符付きの単一文字を使用して、二重引用符付きの文字に一致させることができます。たとえば、引用符以外の任意の文字と一致する内側の左側の選択肢では:

-"""" .ANY

否定的な先読み-"" ""は、成功した場合( "文字に一致しない場合"に.ANY文字に一致する場合に使用されます( "-" ""であるため "文字"にはできません)がその可能性を排除します)。正しい代替案は、「-」を引き受け、「」文字に一致し、失敗した場合の正しい代替案です。

"""""",""""

は、2つの "文字をそれらを1つのダブル" using、 "" ""に一致させて、単一の "文字を挿入します。閉じた文字列の引用文字に失敗した両方の内部代替が一致し、MAKSTR []が呼び出されて文字列オブジェクトが作成されます。$シーケンス、シーケンスが一致するときにループ、演算子はシーケンスの照合に使用されます。トークン式は、先頭のスキップクラス文字をスキップします(スペース内)。最初の一致が行われると、skip_classスキップは無効になります。[]を使用して、他の言語でプログラムされた関数を呼び出すことができます。MAKSTR []、MAKBIN []、MAKOCT []、MAKHEX []、MAKFLOAT []、およびMAKINT []は、一致するトークン文字列を型付きオブジェクトに変換するライブラリ関数として提供されています。以下の数式は、かなり複雑なトークン認識を示しています。

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

上記の数値トークン式は、整数と浮動小数点数を認識します。-代替案は常に成功しています。数値オブジェクトは計算に使用できます。式が成功すると、トークンオブジェクトが解析スタックにプッシュされます。(+ 'E' | 'e'、 'E')の指数リードは興味深いです。MAKEFLOAT []には常に大文字のEが必要です。ただし、小文字の「e」を「、」を使用して置き換えることができます。

文字クラスとトークン式の一貫性に気づいたかもしれません。解析式は、バックトラックの代替とツリー構築演算子を追加することを続けています。バックトラッキングと非バックトラッキングの代替演算子は、式レベル内で混在させることはできません。(a | b \ c)非バックトラックを混在させることはできません| \バックトラックの代わり。(a \ b \ c)、(a | b | c)、((a | b)\ c)は有効です。\バックトラッキング代替手段は、左側の代替手段を試行する前に解析状態を保存し、失敗すると、正しい代替手段を試行する前に解析状態を復元します。一連の代替案では、最初に成功した代替案がグループを満たします。さらなる代替案は試みられません。因数分解とグループ化は、継続的な前進解析を提供します。バックトラックの代替は、左の代替を試行する前に、解析の保存状態を作成します。解析が部分的に一致して失敗する可能性がある場合は、バックトラッキングが必要です。

(a b | c d)\ e

上記でaが失敗した場合、代わりのcdが試行されます。その後cが失敗を返すと、代替のバックトラックが試行されます。aが成功し、bが失敗した場合、解析はバックトラックされ、eが試行されます。同様に、失敗したcが成功し、bが失敗した場合、解析はバックトラックされ、代替eが実行されます。バックトラックは数式内に限定されません。いずれかの解析式がいつでも部分的に一致して失敗した場合、解析は最上位のバックトラックにリセットされ、その代替案が採用されます。バックトラックが作成されたことを意味するコードが出力された場合、コンパイルエラーが発生する可能性があります。コンパイルを開始する前にバックトラックが設定されます。失敗を返すか、それをバックトラックすると、コンパイラの失敗になります。バックトラックがスタックされます。ネガティブおよびポジティブを使用できますか?解析を進めずにテストするために、オペレーターを先読み/先読みします。文字列テストであることは、入力状態を保存してリセットするだけで十分です。先読みは、失敗する前に部分的に一致する解析式です。先読みはバックトラッキングを使用して実装されます。

パーサー言語はLLパーサーでもLRパーサーでもありません。しかし、ツリー構築をプログラムする再帰的なまともなパーサーを書くためのプログラミング言語:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

一般的に使用される解析の例は、算術式です。

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

ループを使用するExpおよびTermは、左手系ツリーを作成します。右再帰を使用する因数分解は、右手系ツリーを作成します。

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

以下は、ccスタイルのコメントを含むSLICの更新バージョンであるccコンパイラのビットです。関数のタイプ(文法、トークン、文字クラス、ジェネレーター、PSEUDO、またはMACHOPは、IDに続く初期構文によって決定されます。これらのトップダウンパーサーを使用すると、プログラムを定義する式から始めます。

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

//ツリーの作成時にidがどのように因数分解され、後で結合されるかに注意してください。

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

注目すべきは、パーサー言語がコメントとエラー回復をどのように処理するかです。

私は質問に答えたと思います。SLICの後継の大部分を書いたので、cc言語自体がここにあります。まだコンパイラはありません。しかし、私はそれをアセンブリコード、裸のasm cまたはc ++関数に手動でコンパイルできます。


0

はい、その言語のコンパイラをその言語で作成できます。いいえ、その言語でブートストラップするための最初のコンパイラーは必要ありません。

ブートストラップに必要なのは、言語の実装です。それはコンパイラかインタプリタのどちらかです。

歴史的に、言語は通常、インタプリタ言語またはコンパイルされた言語と考えられていました。インタープリターは前者のためだけに書かれ、コンパイラーは後者のためだけに書かれました。そのため、通常、コンパイラが言語用に作成される場合、最初のコンパイラは他の言語で作成されてブートストラップされ、オプションで、対象の言語用にコンパイラが再作成されます。しかし、代わりに別の言語でインタプリタを書くことはオプションです。

これは理論的なものではありません。私はたまたまこれを自分でやっています。私は自分で開発したサーモンという言語用のコンパイラーに取り組んでいます。私は最初にCでSalmonコンパイラを作成し、今はSalmonでコンパイラを作成しているので、他の言語で書かれたSalmon用のコンパイラがなくてもSalmonコンパイラを動作させることができます。


-1

たぶん、あなたはBNFを説明するBNFを書くことができます。


4
確かに可能ですが(それほど難しいことでもありません)、その唯一の実用的なアプリケーションはパーサージェネレーターです。
Daniel Spiewak、2008年

実際、私はその方法を使用してLIMEパーサージェネレーターを作成しました。メタ文法の制限された、簡略化された、表形式の表現は、単純な再帰下降パーサーを通過します。次に、LIMEは文法の言語のパーサーを生成し、そのパーサーを使用して、誰かが実際にパーサーの生成に興味を持っている文法を読み取ります。これは私が書いたばかりのことを書く方法を知る必要がないことを意味します。まるで魔法のようです。
Ian

BNFはそれ自体を説明できないため、実際にはできません。非終端記号が引用されていないyaccで使用されるようなバリアントが必要です。
ローンの侯爵

1
<>を認識できないため、bnfを使用してbnfを定義することはできません。EBNFは、言語の定数文字列トークンを引用することで修正しました。
GK
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.