使用Flex / Bison进行口译的REPL

我为类似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); } } 

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

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

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

(我宁愿不改变扫描仪语言来传递换行标记,因为这会严重改变语法。修改YY_INPUT并在Bison语法中添加一些操作YY_INPUT了。另外,我使用的是Flex 2.5.35版本与Xcode一起发布的Bison 2.3。)

在查看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,因为需要一个特殊字符(’;’)来结束语句。 在输入流中,该字符后面可以跟一个换行符来表示行尾,或者后面跟着属于新语句的字符。 如果自上一个语句结束后扫描的唯一字符是换行符或其他空格,则输入例程需要显示主提示符; 否则它应该显示继续提示。

我也在为这样的翻译工作,我还没有达到制作REPL的程度,所以我的讨论可能有些模糊。

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

top_level = top_level声明| 声明;

然后top_level的输出可以是一个链接的语句列表,而interpreter.result则是对该列表尾部的评估。