プログラミング言語は関数をどのように定義しますか?


28

プログラミング言語は、関数/メソッドをどのように定義して保存しますか?Rubyでインタープリター型プログラミング言語を作成しています。関数宣言の実装方法を見つけようとしています。

私の最初のアイデアは、宣言の内容をマップに保存することです。たとえば、次のようなことをした場合

def a() {
    callSomething();
    x += 5;
}

次に、マップにエントリを追加します。

{
    'a' => 'callSomething(); x += 5;'
}

これの問題はparse、文字列に対してメソッドを呼び出す必要があるため再帰的になり、parse遭遇したときに再度呼び出す必要がありdoSomething、最終的にスタックスペースが不足することです。

では、インタープリター言語はこれをどのように処理しますか?


ああ、これはProgrammers.SEに関する私の最初の投稿ですので、何か間違ったことをしているのか、これが話題外であるのかを教えてください。:)
ドアノブ

過去に、トークン内にすべてインラインで保存し、関数呼び出しは特定のオフセットにジャンプするだけです(アセンブリのラベルのように)。スクリプトをトークン化していますか?または毎回文字列を解析しますか?
サイモンホワイトヘッド

@SimonWhitehead文字列をトークンに分割し、各トークンを個別に解析します。
ドアノブ

3
プログラミング言語の設計と実装に慣れていない場合は、このテーマに関する文献のいくつかを確認してください。最も人気のあるものは「Dragon Book」:en.wikipedia.org/wiki/…ですが、他にも非常に優れた簡潔なテキストがあります。たとえば、Aarne Rantaによる実装プログラミング言語は、bit.ly / 15CF6gCから無料で入手できます。
evilcandybag

1
@ddyerありがとう!私はさまざまな言語のLispインタープリターを探しましたが、それは本当に役に立ちました。:)
ドアノブ

回答:


31

「解析」関数がコードを解析するだけでなく、同時に実行することを前提とするのは正しいでしょうか?そのようにしたい場合は、関数のコンテンツをマップに保存する代わりに、関数の場所を保存します。

しかし、より良い方法があります。前もって少し手間がかかりますが、複雑さが増すにつれて、より良い結果が得られます。抽象構文ツリーを使用してください。

基本的な考え方は、コードを一度だけ解析することです。次に、操作と値を表すデータ型のセットがあり、次のようにそれらのツリーを作成します。

def a() {
    callSomething();
    x += 5;
}

になる:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(これは単なる仮想ASTの構造のテキスト表現です。実際のツリーはおそらくテキスト形式ではありません。)とにかく、コードをASTに解析してから、ASTでインタープリターを直接実行するか、または、2番目の(「コード生成」)パスを使用して、ASTを何らかの出力形式に変換します。

あなたの言語の場合、おそらくあなたがすることは、関数名を関数文字列にではなく、関数名を関数ASTにマップするマップを持つことです。


わかりましたが、問題はまだあります。再帰を使用します。これを行うと、最終的にスタックスペースが不足します。
ドアノブ

3
@Doorknob:特に再帰を使用するのは何ですか?ブロック構造のプログラミング言語(ASMよりも高いレベルの最新のすべての言語)は、本質的にツリーベースであるため、本質的に再帰的です。スタックオーバーフローの発生について心配している具体的な側面は何ですか?
メイソンウィーラー

1
@Doorknob:ええ、それは機械語にコンパイルされたとしても、それはあらゆる言語の固有の特性です。(コールスタックは、この動作の明示です。)私は実際に、説明した方法で動作するスクリプトシステムへの貢献者です。chat.stackexchange.com/rooms/10470/でチャットに参加してください。効率的な解釈とスタックサイズへの影響を最小限に抑えるためのテクニックについて説明します。:)
メイソンウィーラー

2
@Doorknob:ASTの関数呼び出しは関数を名前で参照しているため、実際の関数への参照は必要ないため、ここでは再帰の問題はありません。マシンコードにコンパイルする場合、最終的には関数アドレスが必要になります。これが、ほとんどのコンパイラが複数のパスを作成する理由です。あなたが持っているしたい場合はワンパスコンパイラを、あなたは、コンパイラは、事前にアドレスを割り当てることができますので、すべての機能の「前方宣言」を必要とします。バイトコードコンパイラはこれを気にしません。ジッタは名前の検索を処理します。
アーロンノート

5
@Doorknob:それは確かに再帰的です。はい、スタックに16エントリしかない場合、解析に失敗します(((((((((((((((( x )))))))))))))))))。実際には、スタックははるかに大きくなる可能性があり、実際のコードの文法的な複雑さは非常に限られています。確かにそのコードが人間が読めるものでなければならない場合。
–MSalters

4

あなたが見たときに解析を呼び出すべきではありませんcallSomething()(私はあなたcallSomethingよりもむしろあなたが意図したと思いますdoSomething)。差aとはcallSomething、他のメソッドの呼び出しで、一方のメソッドの定義であることです。

新しい定義が表示されたら、その定義を追加できることを確認することに関連するチェックを実行する必要があります。

  • 同じ署名の関数がまだ存在していないか確認してください
  • メソッド宣言が適切なスコープで実行されていることを確認します(つまり、メソッドは他のメソッド宣言内で宣言できますか?)

これらのチェックに合格すると仮定して、マップに追加し、そのメソッドのコンテンツのチェックを開始できます。

のようなメソッド呼び出しが見つかったらcallSomething()、次のチェックを実行する必要があります。

  • callSomethingあなたのマップに存在しますか?
  • 適切に呼び出されていますか(引数の数は、見つかった署名と一致します)?
  • 引数は有効ですか(変数名が使用されている場合、宣言されていますか?このスコープでアクセスできますか?)?
  • callSomethingはあなたがいる場所から呼び出すことができますか(プライベート、パブリック、保護されていますか?)

それcallSomething()が大丈夫だとわかった場合、この時点であなたが本当にやりたいことはあなたがそれにどのようにアプローチしたいかに依存します。厳密に言えば、この時点でそのような呼び出しが問題ないことがわかったら、詳細を説明せずにメソッド名と引数のみを保存できます。プログラムを実行すると、実行時に必要な引数でメソッドを呼び出します。

さらに先に進みたい場合は、文字列だけでなく、実際のメソッドへのリンクを保存できます。これはより効率的ですが、メモリを管理する必要がある場合、混乱する可能性があります。最初は単に文字列を保持することをお勧めします。後で最適化を試みることができます。

これはすべて、プログラムを字句解析したことを前提としていることに注意してください。つまり、プログラム内のすべてのトークンを認識し、それら何であるかを知っています。それは、それらがまだ一緒に意味をなすかどうかをあなたが知っていると言うことではありません、それは構文解析段階です。トークンが何であるかまだわからない場合は、まずその情報を取得することに集中することをお勧めします。

それがお役に立てば幸いです!Programmers SEへようこそ!


2

あなたの投稿を読んで、私はあなたの質問に2つの質問に気づきました。最も重要なのは、解析方法です。多くの種類のパーサー(再帰再帰パーサーLRパーサーPackratパーサーなど)およびパーサージェネレーター(GNUバイソンANTLRなど)を使用して、(明示的または暗黙的な)文法を指定してテキストプログラムを「再帰的に」トラバースできます。

2番目の質問は、関数のストレージ形式についてです。構文指向の翻訳を行わない場合は、プログラムの中間表現を作成します。これは、抽象構文ツリーまたはカスタム中間言語であり、それをさらに処理する(コンパイル、変換、実行、書​​き込み)ことができますファイルなど)。


1

汎用的な観点から見ると、関数の定義はコード内のラベルまたはブックマークにすぎません。他のほとんどのループ、スコープ、条件演算子は似ています。抽象化の下位レベルでの基本的な「ジャンプ」または「goto」コマンドの代役です。関数呼び出しは、基本的に次の低レベルのコンピューターコマンドに要約されます。

  • すべてのパラメーターのデータと、現在の関数の次の命令へのポインターを連結して、「呼び出しスタックフレーム」と呼ばれる構造にします。
  • このフレームを呼び出しスタックにプッシュします。
  • 関数のコードの最初の行のメモリオフセットにジャンプします。

「return」ステートメントまたは同様のステートメントは、次のことを行います。

  • 返される値をレジスタにロードします。
  • 呼び出し元へのポインターをレジスターにロードします。
  • 現在のスタックフレームをポップします。
  • 呼び出し元のポインターにジャンプします。

したがって、関数は、より高いレベルの言語仕様の単なる抽象化であり、人間がより保守しやすく直感的な方法でコードを編成できるようにします。アセンブリ言語または中間言語(JIL、MSIL、ILX)にコンパイルされたとき、そして間違いなくマシンコードとしてレンダリングされたとき、そのような抽象化はほとんどなくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.