calc5 — 变量绑定与增强错误提示
1. 概述
本次迭代在 calc4 的 REPL 基础上增加两大功能:
功能变化:
| 变量引用 | 不支持 | x = 5 + 3 → x * 2 → 16 |
| 错误提示 | col 3: illegal character | 3 + @ 5 + ^ 标记 |
| 内部命令 | exit | exit + vars |
| 新模块 | — | sym.h / sym.c 符号表 |
2. 变更清单
| sym.h | 新建 | 变量符号表 API(sym_set / sym_get / sym_print / sym_clear) |
| sym.c | 新建 | 线性查找实现,最多 64 个变量 |
| parse.h | 编辑 | 新增 TOKEN_IDENTIFIER;Token 增加 name[32] 字段 |
| parse.c | 编辑 | 识别字母/下划线开头的标识符 |
| eval.h | 编辑 | 新增 eval_set_input(const char *) |
| eval.c | 编辑 | parse_factor 支持变量查找;增强错误上下文;eval_set_input 实现 |
| main.c | 编辑 | 变量赋值检测(is_assignment);vars 命令;eval_set_input 设置 |
| Makefile | 编辑 | SRC 加入 sym.c |
3. 变量绑定
3.1 符号表模块(sym.h / sym.c)
独立的符号表模块,使用线性查找数组存储变量名 → 整数值的映射。
#define SYM_MAX 64
void sym_set(const char *name, int val); // 设置变量(覆盖旧值)
int sym_get(const char *name, int *val); // 获取变量值,返回 0 成功 / 1 未定义
void sym_print(void); // 打印所有变量
void sym_clear(void); // 清空所有变量
内部数据结构:
typedef struct {
char name[32];
int value;
} SymEntry;
static SymEntry sym_table[SYM_MAX];
static int sym_count;
3.2 tokenizer 扩展(parse.h / parse.c)
TokenType 新增:
TOKEN_IDENTIFIER, /**< 标识符(变量名) */
Token 结构体扩展:
typedef struct {
TokenType type;
int value;
int col;
char name[32]; // 标识符名称(仅 TOKEN_IDENTIFIER 有效)
} Token;
parse_next 标识符识别(在数字分支之后、负号分支之前):
if (isalpha((unsigned char)*input_pos) || *input_pos == '_') {
int i = 0;
while (i < (int)sizeof(tok.name) – 1 &&
(isalnum((unsigned char)*input_pos) || *input_pos == '_')) {
tok.name[i++] = *input_pos++;
column++;
}
tok.name[i] = '\\0';
tok.type = TOKEN_IDENTIFIER;
tok.value = 0;
update_expect(tok.type);
return tok;
}
3.3 求值器扩展(eval.c)
在 parse_factor 中新增 TOKEN_IDENTIFIER 分支:
if (t.type == TOKEN_IDENTIFIER) {
parse_consume();
int val;
if (sym_get(t.name, &val) != 0) {
fprintf(stderr, " error: undefined variable '%s'\\n", t.name);
set_error();
return 0;
}
return val;
}
3.4 REPL 赋值检测(main.c)
is_assignment 函数判断输入是否为 identifier = expr 格式:
跳过空白 → 读取变量名(字母/下划线开头) → 跳过空白 → 检查 '=' → 跳过空白
赋值流程:
输入 "x = 5 + 3"
→ is_assignment() 识别出 var_name="x", rhs="5 + 3"
→ eval_expr("5 + 3") 求值 → 8
→ sym_set("x", 8)
→ 输出 " result: 8"
3.5 vars 命令
REPL 中新增 vars 命令,调用 sym_print() 打印所有变量:
calc> vars
x = 8
y = 16
4. 更好的错误提示
4.1 设计目标
| 非法字符 @ | col 3: illegal character | 显示 ^ 标记 + 定位 |
| 未定义变量 | — | error: undefined variable 'x' |
| 不匹配括号 | 无提示 | error: expected ')' |
| 意外结束 | — | error: unexpected end of expression |
| 多余 token | col 5: … | 显示 ^ 标记 + 定位 |
4.2 eval_set_input API
void eval_set_input(const char *input);
在 REPL 中,每次求值前调用 eval_set_input(buf) 存储当前输入字符串引用。eval.c 内部通过 error_input 静态指针持有。
4.3 print_error_context 函数
static void print_error_context(int col)
{
if (!error_input) return;
fprintf(stderr, " %s\\n", error_input);
fprintf(stderr, " ");
for (int i = 1; i < col; i++)
fputc(' ', stderr);
fprintf(stderr, "^\\n");
}
在 parse_factor 中遇到 TOKEN_ERROR 或意外 token 时调用:
if (t.type == TOKEN_ERROR) {
print_error_context(t.col);
fprintf(stderr, " error: unexpected character at position %d\\n", t.col);
set_error();
return 0;
}
4.4 错误场景对照
calc> 3 + @ 5
3 + @ 5
^
error: unexpected character at position 5
calc> x + y
error: undefined variable 'x'
calc> (3 + 5
error: unexpected end of expression
4.5 错误后步骤打印不再执行
修复了 calc4 中的一个问题:当 parse_term() 或 parse_unary() 返回后 eval_error 已被设置,外层函数不应继续打印步骤和计算结果。
在 parse_expr 和 parse_term 中,每个二元运算的右操作数求值后立即检查 eval_error:
int right = parse_term();
if (eval_error) break; // ← 新增,跳过步骤打印
int result = left + right;
if (print_steps)
printf(" %d + %d = %d\\n", left, right, result);
5. 目录结构(更新后)
calc/
├── Makefile # 构建脚本
├── parse.h # tokenizer 头文件(含 TOKEN_IDENTIFIER)
├── parse.c # tokenizer 实现(含标识符识别)
├── eval.h # 求值器头文件(含 eval_set_input)
├── eval.c # 递归下降求值器(含变量查找、错误上下文)
├── sym.h # 变量符号表头文件(新增)
├── sym.c # 变量符号表实现(新增)
├── main.c # 入口程序(含 is_assignment、vars 命令)
├── test.expr # 测试用例文件(15 个用例)
├── doc/
│ ├── calc1.md # tokenizer 基础
│ ├── calc2.md # 取模、负号区分、测试套件
│ ├── calc3.md # 表达式求值器
│ ├── calc4.md # 交互式 REPL
│ └── calc5.md # 本次构建:变量绑定 + 增强错误提示
└── build/
└── calc
6. 测试验证
6.1 回归测试
$ 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*2 → 13
PASS [ 9] (3+5)*2 → 16
PASS [10] 10/2+3 → 8
PASS [11] 10%3*2 → 2
PASS [12] 3+@5 → error
PASS [13] <empty> → error
PASS [14] x=5 → error
PASS [15] y=10 → error
============================================
15 passed, 0 failed, 15 total
============================================
6.2 REPL 交互效果
$ ./build/calc
calc> x = 5 + 3
5 + 3 = 8
result: 8
calc> y = x * 2
8 * 2 = 16
result: 16
calc> x + y
8 + 16 = 24
result: 24
calc> vars
x = 8
y = 16
calc> 3 + @ 5
3 + @ 5
^
error: unexpected character at position 5
calc> z + 1
error: undefined variable 'z'
calc> exit


