catch 住异常之后,半份机器码怎么办?

错误被捕获之后,已产生的状态与输出仍需明确处理。

Posted by Bruce Lee on 2026-06-14

系列目录 · 资源与调度 · Read in English

“异常已经被 catch 了,编译器不会崩。”

“那输出目录里为什么有一半文件?”

“因为写文件发生在另一条路径。”

代码生成的失败,很少只发生在一个漂亮的 try 块里。形状检查可能失败,描述符字段可能无法编码,依赖计数可能不合法,直到最后导出二进制,才发现某条指令装不进目标格式。把异常接住只是第一步;下一步是决定失败以后,调用者看到什么,系统留下什么。

错误处理首先是一个层间协议

被阅读的改动把算子生成接口改为显式返回成功或失败,并可选填写错误文本。在支持异常的低层库内部,用统一包装捕获标准异常和未知异常;编译器侧检查状态,再把错误附加到具体操作位置。

这解决了一个实际的架构缝隙:低层库可能使用异常,而上层编译器采用显式失败传播,甚至以关闭异常的方式构建。如果每个算子调用方自己写 try/catch,规则容易遗漏,构建选项也难以统一。

提交为库侧显式启用异常,并增加一个关闭异常编译的边界测试程序。它测试失败是否能够通过返回值跨过接口,而不是要求上层处理抛出的异常。

一个简化接口可以表达为:

1
2
3
4
result = generate_operation(config, diagnostic)
if result is failure:
report_at_current_operation(diagnostic)
stop_this_compilation

“stop”在这里很重要。返回失败并不自动允许在同一个生成器里继续生成下一条操作。

两个失败时刻,必须分别覆盖

有些错误发生在生成前:输入输出形状不匹配、切片轴不合法、参数列表长度错误。另一些错误发生在延迟编码阶段:指令对象已经建立,但字段范围直到导出二进制时才被检查。

因此不能只检查算子生成返回值。已核实的修改还覆盖依赖生成、等待、释放、终结指令、汇编转储、增量转储和二进制导出。调试信息构造从直接返回对象改为可失败结果,避免“为了输出调试文件,反而让未处理异常逃出去”。

一个测试特意设置这样的情形:算子生成成功,随后二进制编码失败,而且调用方原有输出向量应保持不变。它证明的是序列化输出参数的失败契约,并不证明生成器内部所有状态已经回滚。

状态接口为什么要清空旧诊断

假设同一段调用代码复用一个字符串。上一次失败留下“参数越界”,下一次成功却没有清空它,日志系统可能看到一个成功结果加一条旧错误。问题虽然不会改变机器码,却足以让调试者追错方向。

阅读到的统一包装在调用前清空错误文本,成功时保证没有旧消息;失败时填写具体异常信息。没有提供诊断指针时,仍然返回失败。也就是说,文本是附加信息,不能成为判断失败的唯一渠道。

使用 nodiscard 一类接口标记,能帮助发现被忽略的状态,但它仍依赖编译器警告策略与调用习惯。若迁移后某个调用点丢掉返回值,异常虽然不再向上冒泡,错误也可能更安静地被吞掉。

不要把异常转换叫作事务

一个真正的事务通常承诺:成功提交全部变化;失败恢复到可定义的先前状态。代码生成器的变化包括指令列表、伪指令列表、标签编号、增量读取游标、依赖组计数、临时资源和内存分配摘要。

而这里核实到的包装,主要完成“捕获异常并返回状态”。它没有建立所有这些对象的统一快照,也没有在失败后逆向撤销所有动作。因此更准确的失败策略是:失败后终止并废弃这次生成上下文,除非某个接口单独承诺可恢复。

寄存器与 RAII中的 RAII 能归还局部资源,但无法自动删除已经追加的指令。指令编码的边界中把参数验证移到追加伪指令之前,则是对特定接口加强“失败前不改变输出”的保证。局部保证很有价值,不能被拼成未经证明的全局事务。

为什么导出前先编码一遍

相关写出逻辑把完整二进制编码提前到创建和写出后续产物之前,调试汇编也先序列化到内存,再进入文件写出流程。这样可以把“指令根本不能编码”的失败挡在更早位置,减少留下半份输出的机会。

不过,这也不等于文件系统事务。编码成功以后,磁盘空间不足、权限错误或进程中断,仍可能留下部分文件。若产品要求输出目录要么完整、要么不存在,还需要临时目录、完整性校验与原子发布之类机制。

提前编码也有成本:内存中暂存完整输出,可能增加峰值。是否值得,需要比较产物规模、重复编码次数和失败时的诊断收益,而不是把所有预处理都当作纯改进。

辅助函数也要说同一种错误语言

迁移不只替换顶层 try/catch。切片描述符构造中的轴归一化、数组读取、形状检查等辅助函数,改为显式可失败结果。一个尤其值得注意的变化是:拿不到必要内存信息时,不再返回看似正常的默认描述符,而是明确失败。

默认对象有时是最危险的错误传播方式。字段全零很像合法初始值,后续步骤可能继续分配、编码,最终在很远的位置报出误导性问题。可失败类型使“没有构造出描述符”与“构造了一个全零描述符”成为两种状态。

伴随测试还调整了部分形状变换枚举用例。这类改动不应被包装成新的运行时能力;更合理的解释是让测试调用与当前支持的编码集合一致,并保留非法枚举的负例。

返回失败以后,调用者最容易犯的第二个错误

迁移到状态接口后,一种常见反模式是记录诊断,然后继续执行后续导出:日志看起来很友好,产物却来自不完整上下文。另一种是复用同一个上下文重试不同配置,却没有恢复已经变化的依赖计数和输出游标。

因此接口文档至少应给失败后的对象状态一个名字:可继续使用、仅允许销毁,或者允许某种受限重试。这个约定比“返回成功或失败”多了一句,却决定调用者是否能写出可靠控制流。

增量转储尤其需要注意。若汇编读取成功并推进游标,二进制读取随后失败,第二次重试可能只得到空汇编。解决方式可以是先分别生成临时结果,全部成功再提交游标,也可以是明确一次失败后废弃上下文。两条路线都能成立,不能把前者的强保证偷偷加在后者实现上。

还有一个评审问题:诊断应该由哪一层补充上下文?低层最清楚哪个字段不合法,上层最清楚它属于哪个操作和源位置。把两者拼起来通常比低层直接打印全局错误更有用,也便于测试精确检查失败层次。不要让每层重复打印同一条消息,最后把一个根因伪装成十个独立错误。

回归矩阵与故障注入计划

失败层 例子 需要保证
参数校验 形状、轴、列表长度错误 返回失败且诊断定位到操作
内核生成 无法表示的立即数 异常被限定在库边界内
延迟编码 不可编码的数据类型或字段 导出显式失败,旧输出不被覆盖
依赖管理 空对象、非法计数 不把失败当作正常调度
增量转储 前几类输出成功,后一类失败 不宣称整段可重试,明确废弃策略
文件写出 编码成功后写入失败 区分生成失败与产物发布失败

提交已经覆盖多种边界与正反用例;表中全流程故障注入是建议扩展,没有实际执行。

实验可以在每个阶段注入一次可控失败,记录返回状态、诊断归属、内存输出、游标和落盘文件。先定义“失败后废弃上下文”的基本保证,再决定哪些接口值得提供更强恢复能力。避免一上来实现昂贵的全量回滚,却没有调用者真正需要重试。

错误处理成熟的标志,不只是程序没有崩溃,而是每一层都能清楚回答:这次失败停在哪里,留下了什么,以及接下来允许做什么。


返回系列目录 · 上一篇 · 下一篇


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !