【问题标题】:REPL for interpreter using Flex/Bison使用 Flex/Bison 的解释器 REPL
【发布时间】:2011-10-01 23:35:29
【问题描述】:

我为类似 C 的语言编写了一个解释器,使用 Flex 和 Bison 作为扫描器/解析器。执行完整的程序文件时它工作正常。

现在我正在尝试在解释器中实现一个 REPL 以供交互使用。我希望它像 Ruby 或 ML 中的命令行解释器一样工作:

  1. 显示提示
  2. 接受一个或多个语句就行了
  3. 如果表达式不完整
    1. 显示继续提示
    2. 允许用户继续输入行
  4. 当行以完整的表达式结尾时
    1. 回显最后一个表达式的计算结果
    2. 显示主提示

我的语法以 top_level 产生式开头,它代表语言中的单个语句。词法分析器在标准输入上配置为交互模式。我在完整文件和 REPL 模式下都使用相同的扫描器和语法,因为这两个接口没有语义差异。

我的主要评估循环的结构是这样的。

while (!interpreter.done) {
    if (interpreter.repl)
        printf(prompt);
    int status = yyparse(interpreter);
    if (status) {
        if (interpreter.error)
            report_error(interpreter);
    }
    else {
        if (interpreter.repl)
            puts(interpreter.result);
    }
}            

除了提示和回显逻辑外,这很好用。如果用户在一行中输入多个语句,则此循环会打印出多余的提示和表达式。如果表达式在多行上继续,则此代码不会打印出继续提示。出现这些问题是因为提示/回显逻辑的粒度是语法中的top_level 语句,而行读逻辑在词法分析器中很深。

重构评估循环以处理 REPL 提示和回显的最佳方法是什么?那就是:

  • 如何每行显示一个提示
  • 如何在正确的时间显示继续提示
  • 如何判断一个完整的表达式何时是一行的最后一个表达式

(我宁愿不更改扫描仪语言来传递换行符,因为这会严重改变语法。修改YY_INPUT 并在 Bison 语法中添加一些操作就可以了。另外,我正在使用Xcode 附带的库存 Flex 2.5.35 和 Bison 2.3。)

【问题讨论】:

    标签: c bison lex read-eval-print-loop flex-lexer


    【解决方案1】:

    在查看了 Python 和 SML/NJ 等语言如何处理它们的 REPL 之后,我得到了一个很好的解释器。我没有将提示/回显逻辑放在最外层的解析器驱动程序循环中,而是将其放在最内层的词法分析器输入例程中。解析器和词法分析器中的操作设置控制输入例程提示的标志。

    我使用的是可重入扫描器,因此yyextra 包含在解释器层之间传递的状态。大致是这样的:

    typedef struct Interpreter {
        char* ps1; // prompt to start statement
        char* ps2; // prompt to continue statement
        char* echo; // result of last statement to display
        BOOL eof; // set by the EOF action in the parser
        char* error; // set by the error action in the parser
        BOOL completeLine // managed by yyread
        BOOL atStart; // true before scanner sees printable chars on line
        // ... and various other fields needed by the interpreter
    } Interpreter;
    

    词法分析器输入例程:

    size_t yyread(FILE* file, char* buf, size_t max, Interpreter* interpreter)
    {
        // Interactive input is signaled by yyin==NULL.
        if (file == NULL) {
            if (interpreter->completeLine) {
                if (interpreter->atStart && interpreter->echo != NULL) {
                    fputs(interpreter->echo, stdout);
                    fputs("\n", stdout);
                    free(interpreter->echo);
                    interpreter->echo = NULL;
                }
                fputs(interpreter->atStart ? interpreter->ps1 : interpreter->ps2, stdout);
                fflush(stdout);
            }
    
            char ibuf[max+1]; // fgets needs an extra byte for \0
            size_t len = 0;
            if (fgets(ibuf, max+1, stdin)) {
                len = strlen(ibuf);
                memcpy(buf, ibuf, len);
                // Show the prompt next time if we've read a full line.
                interpreter->completeLine = (ibuf[len-1] == '\n');
            }
            else if (ferror(stdin)) {
                // TODO: propagate error value
            }
            return len;
        }
        else { // not interactive
            size_t len = fread(buf, 1, max, file);
            if (len == 0 && ferror(file)) {
                // TODO: propagate error value
            }
            return len;
        }
    }
    

    顶级解释器循环变为:

    while (!interpreter->eof) {
        interpreter->atStart = YES;
        int status = yyparse(interpreter);
        if (status) {
            if (interpreter->error)
                report_error(interpreter);
        }
        else {
            exec_statement(interpreter);
            if (interactive)
                interpreter->echo = result_string(interpreter);
        }
    }
    

    Flex 文件获得这些新定义:

    %option extra-type="Interpreter*"
    
    #define YY_INPUT(buf, result, max_size) result = yyread(yyin, buf, max_size, yyextra)
    
    #define YY_USER_ACTION  if (!isspace(*yytext)) { yyextra->atStart = NO; }
    

    YY_USER_ACTION 处理语言语法中的标记和输入行之间的复杂相互作用。我的语言就像 C 和 ML 一样,需要一个特殊字符 (';') 来结束语句。在输入流中,该字符后面可以跟一个换行符来表示行尾,也可以后面跟一个新语句的一部分。如果自最后一个语句结束后扫描的唯一字符是换行符或其他空格,则输入例程需要显示主提示;否则它应该显示继续提示。

    【讨论】:

    • 感谢您的精彩解释!
    【解决方案2】:

    我也在做这样一个解释器,我还没有到做 REPL 的地步,所以我的讨论可能有些模糊。

    如果在一行上给出一系列语句,只打印最后一个表达式的结果,是否可以接受?因为您可以像这样重构您的顶级语法规则:

    top_level = 顶级声明 |声明;

    你的 top_level 的输出可以是一个链接的语句列表,interpreter.result 将是这个列表尾部的评估。

    【讨论】:

    • 在我开始为 REPL 重构之前,我的语法大致就是这样。递归模式的挑战在于它是贪婪的:解析器将不断请求前瞻令牌(即来自扫描仪的新输入行),直到其中一个分支失败或到达 EOF。
    • 右递归怎么样:top_level = statement top_level |声明
    猜你喜欢
    • 2018-08-13
    • 1970-01-01
    • 2012-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多