我为类 C 语言编写了一个解释器,使用 Flex 和 Bison 作为扫描器/解析器。它在执行完整程序文件时工作正常。
现在我正在尝试在解释器中实现一个 REPL 以供交互使用。我希望它像 Ruby 或 ML 中的命令行解释器一样工作:
- 显示提示
- 在线接受一个或多个声明
- 如果表达式不完整
- 显示继续提示
- 允许用户继续输入行
- 当行以一个完整的表达式结束时
- 回显最后一个表达式的计算结果
- 显示主要提示
我的语法以 top_level
产生式开始,它代表语言中的单个语句。词法分析器在 stdin 上配置为交互模式。我在全文件模式和 REPL 模式下使用相同的扫描器和语法,因为这两个接口(interface)没有语义差异。
我的主要评估循环结构如下。
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);
}
}
除了提示和回显逻辑外,这工作正常。如果用户在一行中输入多个语句,这个循环会打印出多余的提示和表达式。如果表达式在多行中继续,则此代码不会打印出继续提示。出现这些问题是因为prompt/echo逻辑的粒度在语法中是top_level
语句,而line-reading逻辑在词法分析器中很深。
重构评估循环以处理 REPL 提示和回显的最佳方法是什么?即:
- 如何每行显示一个提示
- 如何在正确的时间显示继续提示
- 我如何判断一个完整的表达式何时是一行中的最后一个
(我宁愿不更改扫描器语言来传递换行符,因为这会严重改变语法。修改 YY_INPUT
并向 Bison 语法添加一些操作就可以了。此外,我使用的是 Xcode 附带的 Flex 2.5.35 和 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,因为需要一个特殊字符 (';') 来结束语句。在输入流中,该字符可以后跟换行符以表示行结束,也可以后跟作为新语句一部分的字符。如果自上次语句结束后扫描的唯一字符是换行符或其他空格,则输入例程需要显示主提示符;否则它应该显示继续提示。
关于c - 使用 Flex/Bison 的解释器的 REPL,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6636808/