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> exit2. 变更清单
| 文件 | 操作 | 说明 |
|---|---|---|
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 循环
staticintrepl(void){charbuf[MAX_INPUT];eval_set_print_steps(1);while(1){printf("calc> ");fflush(stdout);if(!fgets(buf,sizeof(buf),stdin))break;/* 去掉末尾换行符 */size_tlen=strlen(buf);if(len>0&&buf[len-1]=='\n')buf[len-1]='\0';if(strcmp(buf,"exit")==0)break;if(*buf=='\0')continue;intresult;if(eval_expr(buf,&result)!=0)continue;printf(" result: %d\n",result);}return0;}核心逻辑:
- 打印
calc>提示符(fflush确保立即显示) fgets读取一行用户输入- 去掉末尾换行符
- 判断
exit→ 退出循环 - 判断空行 → 跳过
- 调用
eval_expr求值 → 输出结果 - 循环往复,直到 EOF 或
exit
3.2 main() 分流
intmain(intargc,char*argv[]){if(argc>1){/* 批处理模式:从命令行参数读取 */returnparse_and_eval(argv[1]);}/* 交互模式 */returnrepl();}- 有参数 → 批处理(
eval_set_print_steps(0),不输出步骤) - 无参数 → 交互式 REPL(
eval_set_print_steps(1),输出步骤)
4. 运算步骤打印
4.1 eval_set_print_steps API
voideval_set_print_steps(intenable);在eval.c中通过print_steps静态变量控制,eval_expr每次调用时会重置eval_error但保留print_steps标志。
4.2 步骤打印位置
在parse_expr和parse_term的每个二元运算完成后插入打印:
// parse_expr 中 —— 处理 + -intresult=left+right;if(print_steps)printf(" %d + %d = %d\n",left,right,result);left=result;// parse_term 中 —— 处理 * / %intresult=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/ └── calc6. 测试验证
6.1 回归测试
批处理模式不受影响,make test9/9 全部 PASS:
$maketest============================================calc — 测试套件============================================PASS[1](90-18)/3+15 →39PASS[2]10%3 →1PASS[3]-5+3 →-2PASS[4]3+-5 →-2PASS[5](-3)→-3PASS[6](-8+20)%-3 →0PASS[7]3+5→8PASS[8]3+@5 → error PASS[9]<empty>→ error============================================9passed,0failed,9total============================================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> exit6.3 管道输入验证
$echo"(90-18)/3+15"|./build/calc calc>90-18=7272/3=2424+15=39result:397. 设计要点
- 双模式共存:
main()通过argc简单分流,两种模式互不干扰 - 步骤打印嵌入求值过程:在递归下降的每个二元运算点插入
printf,无需额外遍历 - 打印顺序自动反映优先级:内层
term先于外层expr求值,*/%的步骤行自然出现在+-之前 - 静默求值不受影响:
eval_set_print_steps(0)时print_steps为假,不产生任何额外输出,批处理和测试路径完全透明