calc4 --- 交互式 REPL 模式
1. 概述
本次迭代在 calc3 求值器基础上增加交互式 REPL 模式(Read-Eval-Print Loop),提供实时运算体验,并显示运算步骤。
运行模式对比:
| 模式 | 启动方式 | 输出 | 用途 |
|---|---|---|---|
| 批处理 | ./calc "表达式" |
仅结果数字 | 脚本调用、自动化测试 |
| 交互式 | ./calc(无参数) |
运算步骤 + 结果 | 手动输入探索 |
交互示例:
$ ./build/calc
calc> (90-18)/3+15
90 - 18 = 72
72 / 3 = 24
24 + 15 = 39
result: 39
calc> 3+5*2
5 * 2 = 10
3 + 10 = 13
result: 13
calc> exit
2. 变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
eval.h |
编辑 | 新增 eval_set_print_steps(int enable) 声明 |
eval.c |
编辑 | 新增 print_steps 标志;parse_expr 和 parse_term 中每步运算插入步骤打印;新增 eval_set_print_steps 实现 |
main.c |
编辑 | 新增 repl() 函数;main() 按参数有无分流批处理/交互模式 |
| 其余文件 | 无需改动 | parse.h/parse.c/Makefile/test.expr 不变 |
3. 交互模式设计
3.1 main.c --- REPL 循环
c
static int repl(void)
{
char buf[MAX_INPUT];
eval_set_print_steps(1);
while (1) {
printf("calc> ");
fflush(stdout);
if (!fgets(buf, sizeof(buf), stdin))
break;
/* 去掉末尾换行符 */
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n')
buf[len - 1] = '\0';
if (strcmp(buf, "exit") == 0)
break;
if (*buf == '\0')
continue;
int result;
if (eval_expr(buf, &result) != 0)
continue;
printf(" result: %d\n", result);
}
return 0;
}
核心逻辑:
- 打印
calc>提示符(fflush确保立即显示) fgets读取一行用户输入- 去掉末尾换行符
- 判断
exit→ 退出循环 - 判断空行 → 跳过
- 调用
eval_expr求值 → 输出结果 - 循环往复,直到 EOF 或
exit
3.2 main() 分流
c
int main(int argc, char *argv[])
{
if (argc > 1) {
/* 批处理模式:从命令行参数读取 */
return parse_and_eval(argv[1]);
}
/* 交互模式 */
return repl();
}
- 有参数 → 批处理(
eval_set_print_steps(0),不输出步骤) - 无参数 → 交互式 REPL(
eval_set_print_steps(1),输出步骤)
4. 运算步骤打印
4.1 eval_set_print_steps API
c
void eval_set_print_steps(int enable);
在 eval.c 中通过 print_steps 静态变量控制,eval_expr 每次调用时会重置 eval_error 但保留 print_steps 标志。
4.2 步骤打印位置
在 parse_expr 和 parse_term 的每个二元运算完成后插入打印:
c
// parse_expr 中 ------ 处理 + -
int result = left + right;
if (print_steps)
printf(" %d + %d = %d\n", left, right, result);
left = result;
// parse_term 中 ------ 处理 * / %
int result = left * right;
if (print_steps)
printf(" %d * %d = %d\n", left, right, result);
left = result;
4.3 优先级自动体现
利用递归下降的天然求值顺序------内层语法层先求值,外层后求值------步骤打印顺序自动反映运算符优先级:
| 输入 | 步骤输出顺序 | 说明 |
|---|---|---|
3+5*2 |
5 * 2 = 10 → 3 + 10 = 13 |
* 行先于 + 行 |
(90-18)/3+15 |
90 - 18 = 72 → 72 / 3 = 24 → 24 + 15 = 39 |
括号内先求值 |
无需额外排序逻辑,打印顺序等于求值顺序。
5. 目录结构(更新后)
calc/
├── Makefile # 构建脚本
├── parse.h # tokenizer 头文件
├── parse.c # tokenizer 实现
├── eval.h # 求值器头文件(含 eval_set_print_steps)
├── eval.c # 递归下降求值器实现(含步骤打印)
├── main.c # 入口程序(批处理 + REPL 双模式)
├── test.expr # 测试用例文件
├── doc/
│ ├── calc1.md # 第一次构建:tokenizer 基础
│ ├── calc2.md # 第二次构建:取模、负号区分、测试套件
│ ├── calc3.md # 第三次构建:表达式求值器
│ └── calc4.md # 本次构建:交互式 REPL
└── build/
└── calc
6. 测试验证
6.1 回归测试
批处理模式不受影响,make test 9/9 全部 PASS:
bash
$ make test
============================================
calc --- 测试套件
============================================
PASS [ 1] (90-18)/3+15 → 39
PASS [ 2] 10%3 → 1
PASS [ 3] -5+3 → -2
PASS [ 4] 3+-5 → -2
PASS [ 5] (-3) → -3
PASS [ 6] (-8+20)%-3 → 0
PASS [ 7] 3 + 5 → 8
PASS [ 8] 3+@5 → error
PASS [ 9] <empty> → error
============================================
9 passed, 0 failed, 9 total
============================================
6.2 交互模式验证
$ ./build/calc
calc> (90-18)/3+15
90 - 18 = 72
72 / 3 = 24
24 + 15 = 39
result: 39
calc> 3+5*2
5 * 2 = 10
3 + 10 = 13
result: 13
calc> 10%3
10 % 3 = 1
result: 1
calc> --5
result: 5
calc> exit
6.3 管道输入验证
bash
$ echo "(90-18)/3+15" | ./build/calc
calc> 90 - 18 = 72
72 / 3 = 24
24 + 15 = 39
result: 39
7. 设计要点
- 双模式共存 :
main()通过argc简单分流,两种模式互不干扰 - 步骤打印嵌入求值过程 :在递归下降的每个二元运算点插入
printf,无需额外遍历 - 打印顺序自动反映优先级 :内层
term先于外层expr求值,*/%的步骤行自然出现在+-之前 - 静默求值不受影响 :
eval_set_print_steps(0)时print_steps为假,不产生任何额外输出,批处理和测试路径完全透明