2

私は BASIC 言語インタープリターを (C/C++ で) 実装しようとしてきましたが、言語構造を解析するプロセスを説明する本や (完全な) 記事を見つけられませんでした。一部のコマンドはかなり複雑で解析が難しく、特に IF-THEN-ELSE や FOR-STEP-NEXT などの条件文やループは、変数と定数、式全体、コード、その他すべてを混在させることができるためです。次に例を示します。

10 IF X = Y + Z THEN GOTO 20 ELSE GOSUB P
20 FOR A = 10 TO B STEP -C : PRINT C$ : PRINT WHATEVER
30 NEXT A

そのようなものを解析して機能させることができるのは悪夢のようです。さらに悪いことに、BASIC で作成されたプログラムは、簡単に混乱してしまう可能性があります。だからこそ、この主題について自分の心を明確にするために、何かアドバイスをしたり、本を読んだり、何かを読んだりする必要があります。何を提案できますか?

4

3 に答える 3

7

あなたは素晴らしいプロジェクトを選びました - インタプリタを書くのはとても楽しいものです!

しかし、最初に、通訳者とは何を意味するのでしょうか? 通訳者には様々なタイプがいます。

純粋なインタープリターがあり、各言語要素を見つけたときに単純に解釈します。これらは最も書きやすく、最も遅いです。

ステップアップとして、各言語要素をある種の内部形式に変換し、それを解釈します。相変わらず書きやすい。

次のステップは、実際に言語を解析し、構文ツリーを生成してから解釈することです。これを書くのは少し難しいですが、何度かやるとかなり簡単になります。

構文ツリーがあれば、カスタム スタック仮想マシン用のコードをかなり簡単に生成できます。JVM や CLR などの既存の仮想マシン用のコードを生成するのは、はるかに困難なプロジェクトです。

プログラミングでは、ほとんどのエンジニアリングの取り組みと同様に、特に複雑なプロジェクトでは、慎重な計画が大いに役立ちます。

したがって、最初のステップは、作成するインタープリターのタイプを決定することです。多数のコンパイラに関する本を読んだことがない場合 (たとえば、Niklaus Wirth の「Compiler Construction」は、このテーマの最良の入門書の 1 つとして常にお勧めします。現在、Web 上で PDF 形式で無料で入手できます)。純粋な通訳者と一緒に行くこと。

ただし、追加の計画を立てる必要があります。何を解釈しようとしているのかを厳密に定義する必要があります。EBNF はこれに最適です。初心者向けの EBNF の紹介については、http://www.semware.com/html/compiler.htmlで Simple Compiler の最初の 3 つの部分を読んでください。これは高校レベルで書かれており、簡単に理解できるはずです。はい、最初に子供たちに試してみました:-)

何を解釈したいかを定義したら、インタプリタを書く準備が整います。

抽象的に言えば、単純なインタープリターは、スキャナー (技術的には字句解析器)、パーサー、および評価器に分割されます。単純な純粋なインターポレーターの場合、パーサーとエバリュエーターが組み合わされます。

スキャナーは書きやすく、テストも簡単なので、ここでは時間を割きません。単純なスキャナーの作成に関する情報については、前述のリンクを参照してください。

(たとえば)gotoステートメントを定義しましょう:

gotostmt -> 'goto' integer

integer -> [0-9]+

これは、トークン「goto」(スキャナーによって配信されたもの)を見ると、それに続くことができる唯一のものは整数であることを示しています。そして、整数は単なる数字の文字列です。

疑似コードでは、これを次のように処理します。

(token - 現在のトークンであり、スキャナーを介して返された現在の要素です)

loop
    if token == "goto"
        goto_stmt()
    elseif token == "gosub"
        gosub_stmt()
    elseif token == .....
endloop

proc goto_stmt()
    expect("goto")          -- redundant, but used to skip over goto
    if is_numeric(token)
--now, somehow set the instruction pointer at the requested line
    else
        error("expecting a line number, found '%s'\n", token)
    end
end

proc expect(s)
    if s == token
        getsym()
        return true
    end

    error("Expecting '%s', found: '%s'\n", curr_token, s)
end

それがどれほど簡単か分かりますか?実際、単純なインタープリターで理解するのが難しいのは、式の処理だけです。これらを処理するための適切なレシピは次のとおりです。 http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm前述の参考文献と組み合わせると、BASIC で遭遇するような式を処理するのに十分なはずです。 .

わかりました、具体例の時間です。これは、Tiny BASIC の拡張バージョンを処理する、より大きな「純粋なインタープリター」からのものです (しかし、Tiny Star Trek を実行するのに十分な大きさです :-) )

/*------------------------------------------------------------------------
  Simple example, pure interpreter, only supports 'goto'
 ------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <setjmp.h>
#include <ctype.h>

enum {False=0, True=1, Max_Lines=300, Max_Len=130};

char *text[Max_Lines+1];    /* array of program lines */
int textp;                  /* used by scanner - ptr in current line */
char tok[Max_Len+1];        /* the current token */
int cur_line;               /* the current line number */
int ch;                     /* current character */
int num;                    /* populated if token is an integer */
jmp_buf restart;

int error(const char *fmt, ...) {
    va_list ap;
    char buf[200];

    va_start(ap, fmt);
    vsprintf(buf, fmt, ap);
    va_end(ap);
    printf("%s\n", buf);
    longjmp(restart, 1);
    return 0;
}

int is_eol(void) {
    return ch == '\0' || ch == '\n';
}

void get_ch(void) {
    ch = text[cur_line][textp];
    if (!is_eol())
        textp++;
}

void getsym(void) {
    char *cp = tok;

    while (ch <= ' ') {
        if (is_eol()) {
            *cp = '\0';
            return;
        }
        get_ch();
    }
    if (isalpha(ch)) {
        for (; !is_eol() && isalpha(ch); get_ch()) {
            *cp++ = (char)ch;
        }
        *cp = '\0';
    } else if (isdigit(ch)) {
        for (; !is_eol() && isdigit(ch); get_ch()) {
            *cp++ = (char)ch;
        }
        *cp = '\0';
        num = atoi(tok);
    } else
          error("What? '%c'", ch);
}

void init_getsym(const int n) {
    cur_line = n;
    textp = 0;
    ch = ' ';
    getsym();
}

void skip_to_eol(void) {
    tok[0] = '\0';
    while (!is_eol())
        get_ch();
}

int accept(const char s[]) {
    if (strcmp(tok, s) == 0) {
        getsym();
        return True;
    }
    return False;
}

int expect(const char s[]) {
    return accept(s) ? True : error("Expecting '%s', found: %s", s, tok);
}

int valid_line_num(void) {
    if (num > 0 && num <= Max_Lines)
        return True;
    return error("Line number must be between 1 and %d", Max_Lines);
}

void goto_line(void) {
    if (valid_line_num())
        init_getsym(num);
}

void goto_stmt(void) {
    if (isdigit(tok[0]))
        goto_line();
    else
        error("Expecting line number, found: '%s'", tok);
}

void do_cmd(void) {
    for (;;) {
        while (tok[0] == '\0') {
            if (cur_line == 0 || cur_line >= Max_Lines)
                return;
            init_getsym(cur_line + 1);
        }
        if (accept("bye")) {
            printf("That's all folks!\n");
            exit(0);
        } else if (accept("run")) {
            init_getsym(1);
        } else if (accept("goto")) {
            goto_stmt();
        } else {
            error("Unknown token '%s' at line %d", tok, cur_line); return;
        }
    }
}

int main() {
    int i;

    for (i = 0; i <= Max_Lines; i++) {
        text[i] = calloc(sizeof(char), (Max_Len + 1));
    }

    setjmp(restart);
    for (;;) {
        printf("> ");
        while (fgets(text[0], Max_Len, stdin) == NULL)
            ;

        if (text[0][0] != '\0') {
            init_getsym(0);
            if (isdigit(tok[0])) {
                if (valid_line_num())
                    strcpy(text[num], &text[0][textp]);
            } else
                do_cmd();
        }
    }
}

うまくいけば、それはあなたが始めるのに十分であることを願っています. 楽しむ!

于 2013-09-29T14:33:27.103 に答える
3

こんなこと言ったら確実に殴られる…でも…

まず、私は実際に (趣味として) スタンドアロン ライブラリに取り組んでおり、次のもので構成されています。

  • ソーステキストからトークンの線形 (フラットリスト) を構築し、テキストと同じシーケンス (テキストフローから作成された語彙) に従うトークナイザー。
  • 手によるパーサー (構文解析; 疑似コンパイラー)
  • 「疑似コード」も「仮想 CPU/マシン」もありません。
  • 命令 (「return」、「if」、「for」、「while」、算術式など) は、基本 C++ 構造体/クラスによって表され、オブジェクト自体です。私が名前を付けたベースオブジェクトには、atom他の共通メンバーの中でも「eval」と呼ばれる仮想メソッドがあり、それ自体が「実行/ブランチ」でもあります。したがって、真または偽の条件として可能な分岐 (単一のステートメントまたはステートメント/命令のブロック) を含む「if」ステートメントを持っていても、それはベース仮想 atom::eval() から呼び出されます ... などであるすべてのものに対してatom
  • 変数などの「オブジェクト」も「アトム」です。'eval()' はvariant、アトム自体が保持するコンテナからその値を返すだけです (ポインター、'アトム' を保持する「ローカル」バリアント インスタンス (インスタンス バリアント自体) を参照するか、アトムが保持する別のバリ​​アントを参照します)。特定の「ブロック/スタック」で作成されたので、「アトム」は「インプレース」命令/オブジェクトです。

現時点では、例として、以下のようなあまり意味のない「コード」のチャンクが機能します。

r = 5!; // 5! : (factorial of 5 )
Response = 1 + 4 - 6 * --r * ((3+5)*(3-4) * 78);
if (Response != 1){ /* '<>' also is not equal op. */
 return r^3;
} 
else{
 return 0;
}

式 (算術) は に組み込まれていbinary tree expressionます。

A = b+c; =>
    =
   / \
  A   +
     / \
    b   c

したがって、上記のような式の「命令」/ステートメントはツリー エントリ アトムであり、上記の場合は「=」(バイナリ) 演算子です。

ツリーは atom::r0,r1,r2 : atom 'A' で構築されます:

    r0
     |
     A
    / \
   r1  r2

C++ ランタイムと「スクリプト」ライブラリ間の「全二重」メカニズムに関して、私は次のように作成class_adaptorしましたadaptor<>

元。:

template<typename R, typename ...Args> adaptor_t<T,R, Args...>& import_method(const lstring& mname, R (T::*prop)(Args...)) { ... }

template<typename R, typename ...Args> adaptor_t<T,R, Args...>& import_property(const lstring& mname, R (T::*prop)(Args...)) { ... }

2 番目: lua、boost::bind<*>、QML、JSON などのツールやライブラリがたくさんあることは知っていますが、私の状況では、独自の [編集] '独立した' [/編集] 「ライブ スクリプト」用の lib。「インタープリター」が大量の RAM を消費するのではないかと心配していましたが、QML、jscript、さらには lua を使用するほど大きくないことに驚いています :-)

ありがとうございました :-)

于 2015-02-11T14:55:26.540 に答える
1

パーサーを手作業でハッキングする必要はありません。パーサージェネレーターを使用します。lex+yaccは古典的なレクサー/パーサー ジェネレーターの組み合わせですが、Google で検索すると他にもたくさん見つかります。

于 2012-10-20T14:04:15.390 に答える