【问题标题】:Any suggestions about how to implement a BASIC language parser/interpreter?关于如何实现 BASIC 语言解析器/解释器的任何建议?
【发布时间】:2012-10-20 14:00:57
【问题描述】:

我一直在尝试实现一个 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 编写的程序很容易变得一团糟。这就是为什么我需要一些建议,阅读一些书或其他任何东西来让我对这个主题保持清醒。你有什么建议?

【问题讨论】:

  • 你知道Coursera吗?他们免费提供大学课程。编译器类是为自学而提供的,它包含了构建词法分析器、解析器以及随后出现的所有内容所需了解的所有内容。 class.coursera.org/compilers-2012-selfservice/class/index
  • 数百 种关于解析的书籍和论文。你怎么可能找不到呢?从简单的东西开始,比如 LL 递归下降解析器,对于 Basic 来说应该绰绰有余。
  • 看看我的项目 MY-BASIC github.com/paladin-t/my_basic 怎么样,这似乎符合你的问题。

标签: parsing interpreter basic


【解决方案1】:

您选择了一个很棒的项目 - 编写口译员会很有趣!

但首先,我们所说的解释器是什么意思?有不同类型的口译员。

有纯解释器,您只需在找到每个语言元素时对其进行解释。这些是最容易编写的,也是最慢的。

更进一步,将每个语言元素转换为某种内部形式,然后对其进行解释。还是很容易写的。

下一步,将实际解析语言,生成语法树,然后解释它。这有点难写,但是一旦你做了几次,它就会变得很容易。

一旦您有了语法树,您就可以相当轻松地为自定义堆栈虚拟机生成代码。一个更难的项目是为现有的虚拟机生成代码,例如 JVM 或 CLR。​​

在编程中,就像大多数工程工作一样,仔细规划会大有帮助,尤其是对于复杂的项目。

所以第一步是决定你想写哪种类型的解释器。如果您还没有阅读任何编译器书籍(例如,我总是推荐 Niklaus Wirth 的“编译器构造”作为对该主题的最佳介绍之一,现在可以在网络上以 PDF 形式免费获得),我会推荐你和纯粹的解释器一起去。

但是您仍然需要做一些额外的计划。您需要严格定义要解释的内容。 EBNF 非常适合这一点。关于 EBNF 的异类介绍,请阅读 http://www.semware.com/html/compiler.html 的 Simple Compiler 的前三部分,它是在高中水平编写的,应该很容易消化。是的,我先在我的孩子身上试过 :-)

一旦定义了要解释的内容,就可以编写解释器了。

抽象地说,你的简单解释器将分为扫描器(技术上是词法分析器)、解析器和评估器。在简单的纯插值器情况下,解析器和求值器将结合在一起。

扫描仪易于编写,易于测试,因此我们不会在它们上花费任何时间。有关制作简单扫描仪的信息,请参阅上述链接。

让(例如)定义你的 goto 语句:

gotostmt -> 'goto' integer

integer -> [0-9]+

这告诉我们,当我们看到令牌“goto”(由扫描仪提供)时,唯一可以跟随的就是一个整数。整数只是一个数字字符串。

在伪代码中,我们可以这样处理:

(token - 是当前的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();
        }
    }
}

希望这足以让您入门。玩得开心!

【讨论】:

    【解决方案2】:

    说这话我肯定会挨打……但是……:

    首先,我实际上正在开发一个独立的库(作为一种爱好),它由以下组件组成:

    • 一个标记器,从源文本构建线性(平面列表)标记,并遵循与文本相同的序列(从文本流创建的词法)。
    • 手动解析器(语法分析;伪编译器)
    • 没有“伪代码”或“虚拟 CPU/机器”。
    • 指令(例如'return'、'if'、'for'、'while'...然后是算术表达式)由基本的c++-struct/class 表示并且是对象本身。基础对象,我将其命名为atom,有一个名为“eval”的虚拟方法,以及其他常见成员,即“执行/分支”本身。因此,无论我有一个“if”语句,其可能的分支(单个语句或语句/指令块)作为真或假条件,都会从基本虚拟 atom::eval() ... 调用,依此类推对于 atom 的所有内容。
    • 即使是变量等“对象”也是“原子”。 'eval()' 将简单地从 atom 本身持有的 variant 容器返回其值(指针,指的是持有 'atom' 的 'local' 变体实例(实例变体本身)或指向另一个变体持有的在给定的“块/堆栈”中创建的原子。因此“原子”是“就地”指令/对象。

    截至目前,作为一个例子,下面这段没有真正意义的“代码”可以正常工作:

    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 构建的: 原子'A':

        r0
         |
         A
        / \
       r1  r2
    

    关于 c++ 运行时和“脚本”库之间的“全双工”机制,我制作了 class_adaptoradaptor&lt;&gt;

    例如:

    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...)) { ... }
    

    第二:我知道那里有很多工具和库,例如 lua、boost::bind、QML、JSON 等...但是在我的情况下,我需要创建自己的 [编辑] 'independant' [/edit] 用于“实时脚本”的库。我害怕我的“解释器”会占用大量内存,但令我惊讶的是它没有使用 QML、jscript 甚至 lua 大:-)

    谢谢你:-)

    【讨论】:

      【解决方案3】:

      不要费心手动破解解析器。使用解析器生成器。 lex + yacc 是经典的词法分析器/解析器生成器组合,但谷歌搜索会发现很多其他的。

      【讨论】:

      • 手工编写词法分析器确实毫无意义。但是手工编写解析器是非常可行的,有时比弯曲语法以适应解析器生成器使用的解析算法(在 yacc 的情况下为 LALR)的限制要简单得多。我可以证明一个用于现实世界编程语言(在我的例子中是 Pascal 方言)的手写 pratt 解析器就足够了,而且非常简单。
      • @delnan,感谢您分享您的经验——您甚至可能想将其作为单独的答案发布。我也写过几次解释器/编译器,特别记得有一次我使用的 LALR 解析器生成器给我带来了很多痛苦。现在我尽可能使用 Packrat 解析器生成器;它们的效率相对较高,而且您可以以非常直接的方式编写语法。我很难想象手写的解析器代码会更简单。
      • 如果没有词法分析器,手动编写解析器很容易。递归下降解析器非常容易编写(甚至易于阅读),即使在 C 这么低级别的语言中也是如此。而且总是可以在 C++ 甚至 C 中使用某种形式的解析组合器(如在 Parsec 中)。Packrat解析器在没有外部生成器的情况下也相对容易编写(尽管手动编写没有多大意义)。
      • 鉴于问题未指定平台或项目约束,这不是一个理想的答案。是的,大多数时候需要鼓励更多的人使用解析器生成器,但是如果你正在做一些事情,比如为 DOS 项目实现一个可嵌入的 BASIC,那么以可用的解析器生成器没有的方式压缩字节可能非常重要被设计来做。例如,这就是我正在为安装程序创建者使用 Open Watcom C/C++ for real-mode DOS 所做的事情,并且不能允许将实际内容从软盘中挤出。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-24
      • 2017-08-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多