4.4 语义错误的定位与修复


4.4 语义错误的定位与修复

本节摘要:语义错误指类型冲突、未声明引用、重复声明、作用域违规——文法拦不住、语义规则拦下的毛病。本节给三大类错误各配检测与报错实现,分析语义错误为什么比语法错误难恢复(树已建、名字已登记、错误会传染),并给出"登记错误条目、继续检查"的实用策略。

阅读完本节,你应当能够:

  1. 实现未声明、重复声明、类型冲突三类错误的检测代码
  2. 解释语义恢复难的根源:错误状态会写进符号表并传染
  3. 设计错误条目机制防止重复报错
  4. 区分"可降级错误"与"致命错误"的处理策略

三大类错误现场

给一段集齐三类病灶的代码:

源程序(带病灶): 1 float price, discount; 2 int qty; 3 4 totoal = price * qty - discount; ← 病灶一:totoal 未声明(拼写错) 5 float price; ← 病灶二:同层重复声明 6 price = "hello"; ← 病灶三:字符串赋给浮点 三类错误的检测点: 一:类型检查对 ID 节点调 lookup 返回空 → 未声明 二:declare 时发现同层已有同名 → 重复声明 三:ASSIGN 节点合成:左 float 右 string → 转换表无此行 → 类型冲突

检测代码基于 4.2 节的符号表与 4.1 节的合成规则:

def check_assign(st, name_node, value_type): entry = st.lookup(name_node.lexeme) if entry is None: # 病灶一 st.declare_error_entry(name_node.lexeme) # 登记错误条目 return Err(f"第 {name_node.line} 行:{name_node.lexeme} 未声明") if not compatible(entry["type"], value_type): return Err(f"第 {name_node.line} 行:无法将 {value_type} " f"赋给 {entry['type']} 变量 {name_node.lexeme}") return Ok() def declare_checked(st, name, kind, typ, line): top = st.stack[-1] if name in top.names: # 病灶二 return Err(f"第 {line} 行:{name} 在本层重复声明," f"首次声明为 {top.names[name]['type']}") st.declare(name, kind, typ) return Ok()

注意病灶一的处理细节:查不到就登记一个错误条目。不登记的话,totoal 后面再被赋值、被引用,每个引用点都报一次"未声明",一条拼写错刷出十几条报错。错误条目的类型标记为"未知"并设置与一切相容,保证检查能继续走完。

语义恢复为什么比语法难

语法错误的边界清晰:错在单元流的某个位置,丢弃一段继续。语义错误的麻烦在状态已写入且会传染:totoal 未声明,赋值节点的类型无法合成,父节点合成跟着失败——一个底层错误沿树向上变成一串错误。三种应对策略按激进度排:

停在上报。检出即报,该子树标错,父节点见到错误标记直接跳过合成。保守可靠,代价是漏报同树其他独立问题。

降级续查。错误节点赋默认类型(如 int),继续合成。能暴露更多独立错误,但默认值可能触发新的假错误——用 int 默认遇到除法,可能误报"除零倾向"类警告。风险换覆盖。

错误条目加相容默认(上面的实现)。符号表登记错误条目、类型设为万能相容。这是工程主流,等价于"这个位置的问题已记录,请系统假装它没问题继续巡检"。

三种策略对病灶一的后续影响: 停在上报: 第 4 行报错后,本行不再报其他错。若行内还有独立错 → 漏报 降级续查: totoal 默认 int,total 右侧 float 赋 int 又触发一条警告 → 可能假错 错误条目法: totoal 记为错误条目、万能相容,本行其他检查照常 → 独立错不漏

图 一个错误沿树传染与三道防火墙

图 一个错误沿树传染与三道防火墙

报错措辞:语义错误最该下功夫的地方

语义错误的报错最接近"给程序员的诊断"。同一种类型冲突,三档措辞:

不及格:"type error" 及格: 第 6 行:类型不匹配 优秀: 第 6 行:不能把字符串值赋给浮点变量 price 提示:若想把字面文本转成数字,请用解析函数; 若 price 本应是字符串,请检查第 1 行的声明

优秀档做了两件事:把冲突双方的类型写出来(用户不用猜),给出修复方向的猜测(引用声明位置)。现代编译器的"建议修复"功能(拼写建议:totoal 是否想写 total)走得更远——编辑距离算法在符号表已有名字里找近似候选,命中即附在报错后。这类功能的原料就是符号表本身,成本低、体验收益大。

严重度分级

不是所有语义错误都值得同等对待。实用分级:错误(未声明、类型冲突——无法继续生成正确代码)、警告(可疑的隐式转换、未使用的变量——能编过但埋雷)、提示(风格建议)。分级让用户先修错再看警告,也让大型项目的告警治理(把历史警告清零)成为可能。编译器选项控制警告开关与视警告为错误,属于工程必须品。

⚠️ 常见坑:降级续查的默认类型选择不当,制造大片假警告。默认 int 遇浮点上下文报转换警告、默认浮点遇位运算直接报错——两种都会污染真错误。错误条目的"万能相容"是更安全的默认。

💡 关键直觉:语义分析是编译器里最像"理解程序"的阶段,报错质量的上限最高——它有符号表全部知识可用。投资报错措辞与修复建议,用户体感回报远超优化算法十倍复杂度的投入。

本节要点回顾

  • 三大类:未声明(查表落空)、重复声明(登记撞名)、类型冲突(合成无规则行)
  • 传染性:错误沿树向上传播,比语法错误的局部性恶劣
  • 三道防火墙:错误条目万能相容、标错跳过、降级默认,按语言严厉度选用
  • 报错措辞:写出双方类型、引用声明位置、给修复猜测
  • 严重度分级:错误/警告/提示三档,支持告警治理流程

语义检查收官,主线语句已是有类型的四条三地址码。下一章进入中端腹地:优化器将合并、改写、重排这些指令,让同样的语义跑得更快。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U