使用Flex/Bison进行口译的REPL

Jay*_*ske 10 c lex bison read-eval-print-loop flex-lexer

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

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

  1. 显示提示
  2. 接受该行的一个或多个陈述
  3. 如果表达式不完整
    1. 显示继续提示
    2. 允许用户继续输入行
  4. 当该行以完整表达结束时
    1. 回显评估最后一个表达式的结果
    2. 显示主要提示

我的语法从一个top_level生产开始,它代表了该语言中的一个语句.词法分析器配置为stdin上的交互模式.我在全文件和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);
    }
}            
Run Code Online (Sandbox Code Playgroud)

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

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

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

(我宁愿不改变扫描仪语言来传递换行标记,因为这会严重改变语法.修改YY_INPUT和添加一些动作到Bison语法会很好.而且,我使用的是Flex 2.5.35和与Xcode一起发货的Bison 2.3.)

Jay*_*ske 7

在查看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;
Run Code Online (Sandbox Code Playgroud)

词法分析器输入例程:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

顶级解释器循环变为:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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; }
Run Code Online (Sandbox Code Playgroud)

YY_USER_ACTION处理在语言语法令牌和输入线之间的棘手相互作用.我的语言就像C和ML,因为需要一个特殊字符(';')来结束语句.在输入流中,该字符后面可以跟一个换行符来表示行尾,或者后面可以跟着属于新语句的字符.如果自上一个语句结束后扫描的唯一字符是换行符或其他空格,则输入例程需要显示主提示符; 否则它应该显示继续提示.