# 第 15 章：诊断与实用惯例

诊断分四级：`print`（信息）、`warn`（警告）、`err`（终止）、`assert`（不变式检查）。源码组织也一样：命名清晰、函数小巧、层次明确。

## 诊断格式

错误消息带源文件位置、严重级别和说明：

```text
source.asm:12:5: error: header size must be four
```

所有阶段共用同一错误报告模型：解析、Meta 求值、指令编码、布局、重定位、收尾处理。

先看第一个错。后面的可能是连带出来的。

## print 和 warn

`print` 输出信息，`warn` 输出非致命警告：

```asm
// 输出当前位置，帮助用户了解本次汇编采用的映像起点。
print("image origin", here());
// 输出仍然有效，但使用默认对齐值时给出明确提醒。
warn("using default alignment", 16);
// 确认继续汇编所需的配置条件成立。
assert(true, "configuration must be valid");
// 诊断信息不会改变输出内容，这里实际写出一个字节。
emit.u8(0x7d);
```

汇编照常成功，输出：

```text
7d
```

消息后的参数值也会打出：

```text
note: image origin 0
warning: using default alignment 16
```

有意义的汇编时信息用 `print`，不是调试印迹。配置值得注意但输出有效时用 `warn`。条件会导致非法文件或不支持的程序时用 `err`。

## err 终止汇编

`err` 打出致命错误并终止：

```text
if target.bits != 64 {
    err("this source requires a 64-bit target", target.bits);
}
```

参数跟在消息后面。把导致失败的值传进去，方便使用者纠正。

消息应说明违反的要求：

```text
err("section alignment must be a nonzero power of two", alignment);
```

不要只说 `invalid value`，要说哪个属性不合法。

## assert 编码不变式

`assert` 检查源码中应始终成立的条件：

```asm
// 定义由两个 16 位字段组成的紧凑文件头布局。
packed struct Header {
    kind: u16
    size: u16
}

// 文件头大小发生变化时立即停止汇编。
assert(sizeof(Header) == 4, "Header must remain four bytes");

// 写出文件头及其后紧接的三个字节数据。
emit.struct(Header {
    kind: 1,
    size: 3,
});
emit.bytes(b"ABC");
```

通过时不产生输出。输出：

```text
01 00 03 00 41 42 43
```

条件不成立时停止汇编，消息就是错误文本。

源码处理阶段已知的事实直接用 `assert`：
- 类型大小和字段偏移
- 选项组合
- 列表和 map 内容
- 目标需求
- 常量范围
- 声明值之间的关系

需等映像稳定才能确认的事实放在 `defer` 的 `assert` 里：
- 最终文件和逻辑大小
- 已解析的偏移和地址
- 回填的文件头字段
- 校验和
- 生成表的内容
- 指令重定位产生的字节

断言和输入应在同一阶段，免依赖临时布局值。

## 目标需求就近检查

目标检查放在依赖它的代码旁边：

```asm
// 明确选择 64 位 x86 指令编码。
x86.use64();

// 不满足位数要求时，在汇编期间直接拒绝该目标平台。
if target.bits != 64 {
    err("this routine requires x86-64");
}

entry:
    // 例程只为 x86-64 生成清零和返回指令。
    xor eax, eax
    ret
```

输出：

```text
31 c0 c3
```

目标条件在汇编时求值，不生成运行时检查。

显式指定模式后，读者一眼就知道跑在什么 ISA 上，不需要猜是 x86-16、x86-32、x86-64、RISC-V 32 还是 RISC-V 64。

## 常量代替魔数

格式、协议、布局相关的数值取名字：

```text
const header_size: u32 = 16
const page_alignment: u64 = 0x1000
const record_kind_code: u16 = 3
```

算法内嵌的数值操作数可以保持局部。偏移、标志、结构大小、格式值、对齐策略这些应该命名。

封闭集合中的选择用命名常量。多个字段组成一个记录用 struct。动态计划用 map 或 list。

## 命名体现值类型

二进制写入器同时涉及文件偏移、虚拟地址、逻辑距离等多种值。命名体现含义：

```text
header_foa
text_rva
entry_address
payload_file_size
payload_logical_size
```

`offset` 可能指文件偏移、区域相对偏移、虚拟地址或结构字段偏移时，不要用这种泛称。

函数参数也一样。接受 `logical_address` 和 `file_offset` 的函数，比接受 `start` 和 `position` 的容易用对。

## 用标签查询替代硬编码

- 逻辑距离 → 标签相减
- 结构体布局 → `sizeof`、`offset_of`
- 当前物理位置 → `file_offset()`
- 稳定区域事实 → `defer` 里查
- 表计数和顺序 → 格式描述符列表决定
- 生成索引 → 符号和重定位声明决定

外部格式规范要求精确值时才硬编码。即使硬编码，也要命名并断言周围布局。

## defer 和 late_layout 只在必要时用

普通源文件按普通顺序写出。

只有主源码后必须追加的字节才用 `late_layout`。只有检查和回填稳定映像才用 `defer`。

不要把普通写出挪到后期阶段，仅仅因为不想组织源码。一个明确的文件头 + 数据区 + 尾部 + 收尾处理序列，比一张延迟副作用网络好理解。

每个 `defer` 聚焦相关字段。回填文件头并验证的块，比到处改不相关区域的块容易审查。

## 按职责分模块

项目布局按功能切分：
- 共享常量和数据类型
- 可重用的写出过程
- 目标相关指令例程
- 格式或容器构造
- 数据输入
- 顶层源文件（选配置、拼最终映像）

只需初始化一次的模块用 `import`。确实需要重复文本求值时才用 `include`。

公共函数参数要显式。能传 plan、list、map、label、option 过去的地方，不要依赖隐藏状态。

## 标准格式用库

标准可执行文件和目标文件从 `format/format.inc` 开始：

```asm
// 导入用于构造标准文件格式的常规接口。
import("format/format.inc");
```

格式库管描述符计数、表顺序、索引、偏移和格式不变式。

仅当需要新格式库、或普通库不支持某种布局时，才用底层 include。不要因为底层辅助函数暴露了更多数字就换层——那些数字通常是格式库该管的。

项目专用格式用第 13 章的 flat 自定义路由，不需要塞进操作系统格式。

## 在输出边界检查

在 XIRASM 把输出交给外部系统的边界上验证：
- 写自定义文件前断言最终结构
- 显式声明可执行文件的权限和重定位策略
- 写出前验证导入数据
- 尽早拒绝不支持的目标组合
- 入口点和导出符号保持命名
- 每个公共 include 保留一个可运行的小例子

验证描述约定，不重复实现。断言表的计数和声明一致是有用的。断言每一步内部算术则难维护。

## 文档分工

本文档（语言指南）管：
- 编译时源文件和 ISA 文本如何协作
- 值、控制流、函数、集合、token 的行为
- 标签、数据、区域、虚拟输出、收尾处理如何组织文件
- flat 输出和格式库的选择

API Reference 管：
- 精确的函数签名
- 接受的值类型
- 常量和选项标志
- 行为和错误约束

[《格式教程》](../format-tutorial.md)管：
- 完整的 PE、COFF、ELF 程序
- 可执行文件、DLL、目标文件、PIE、共享库
- 导入、导出、符号、资源、重定位
- 多 section 和多 segment
- 普通格式接口的参数、调用顺序和常见错误

高级格式构造指南负责需要手动控制文件头、表项、区域和重定位的场景。四份文档彼此独立，各自保持可读。

## 最终检查清单

源文件完成前确认：

- [ ] 目标和指令模式显式指定
- [ ] 常量和值类型有描述性名字
- [ ] 标签替代硬编码地址
- [ ] 外部依赖的结构体大小已断言
- [ ] import 和 include 用对了模型
- [ ] 预留的尾部和物理间隙是故意的
- [ ] late_layout 只创建真正晚出的字节
- [ ] 收尾处理修改已有存储并验证稳定事实
- [ ] 标准格式用格式库，除非有高级需求
- [ ] 致命条件给出可操作的消息
- [ ] 最终输出有可复现的汇编或执行验证

[返回目录](../language.md)
