本章导读
编码规范不是审美偏好——它是团队协作的接口契约。xlang 编译器自身就是一个 130 个源码文件、695 个测试用例、843 条代码生成规则的中型工程,它的开发纪律(RC0 守门脚本、禁止回退扫描、诊断基线)本身就是本章最好的案例。本章的规范建议分两层:硬规则(违反会导致编译失败或运行时错误)和软约定(违反不会报错但会降低可维护性)。硬规则用编译器行为证明,软约定用编译器自身的代码风格示范。
掌握 .xlp 工程文件的格式与目录布局约定;理解 xlang 命名约定(函数、类、变量、常量、文件);掌握 import/source import 的模块拆分策略与依赖方向原则;理解错误处理的最佳实践(try/catch 粒度、assert 防御、错误传播);掌握测试体系(正例运行、负例诊断断言、分类套件);学会用 xl check --json 集成 CI;理解 xl build 的构建分发工作流与资源嵌入;掌握 C 混编的工程组织(extern 声明文件、native-backed class 封装、ABI 头文件生成);了解文档注释语法;建立性能感知编码的检查清单。
42.1 工程文件与目录布局
单文件脚本用 xl run hello.xl 即可运行;一旦项目超过一个文件,就需要工程文件 .xlp(xlang project):
{
"format": "xlang-project-1",
"kind": "exe",
"entry": "src/main.xl",
"cSources": ["src/native.c"],
"resources": ["app.res.json"]
}
字段规则:
| 字段 | 必填 | 说明 |
|---|---|---|
| format | 是 | 固定 "xlang-project-1",标识工程文件格式版本 |
| kind | 是 | "exe"(可执行程序)或 "dll"(动态库) |
| entry | 是 | 入口 .xl 文件,相对 .xlp 所在目录 |
| cSources | 否 | 参与编译的 C 源文件列表(.c/.cc/.cpp/.cxx) |
| resources | 否 | Windows 资源配置文件(.json/.xson),用于版本信息、图标、manifest |
推荐目录布局(参照 xlang 编译器自身的工程结构):
myproject/ ├── app.xlp # 工程入口 ├── src/ │ ├── main.xl # 程序入口 │ ├── core/ # 核心逻辑模块 │ │ ├── parser.xl │ │ └── engine.xl │ ├── net/ # 网络模块 │ │ └── http.xl │ └── native.c # C 混编源文件 ├── test/ │ ├── cases/ # 测试用例 .xl │ └── run_tests.bat # 测试入口脚本 ├── docs/ # 设计文档 └── build.bat # 构建脚本
所有相对路径都相对 .xlp 所在目录。xl run app.xlp 或 xl build app.xlp -o app.exe 即可运行或构建整个工程。
42.2 命名约定
xlang 编译器不强制命名风格(不会因命名报错),但一致的命名是团队协作的基础。以下是 xlang 标准库和编译器自身使用的约定:
| 实体 | 风格 | 示例 |
|---|---|---|
| 函数 / 方法 | camelCase 或 snake_case(标准库偏 snake_case) | println, math.sqrt, string.split |
| class / union / type | PascalCase | HttpClient, JsonNode, FileHandle |
| 局部变量 / 参数 | camelCase 或 snake_case | itemCount, user_name |
| 常量(文件作用域不可变) | UPPER_SNAKE 或 PascalCase | MAX_RETRY, DefaultTimeout |
| 文件名 | snake_case 或 kebab-case | http_client.xl, json-parser.xl |
| 包名 / 模块名 | 小写单词或 snake_case | math, os, http, json_util |
| extern C 符号 | 保持 C 侧原始命名 | xrtMalloc, file_open |
核心原则:同一个项目内保持一致比选择哪种风格更重要。标准库使用 snake_case(println、to_string、array.append),你的项目可以选 camelCase,但不要混用。
避免的命名:以 __xl 或 xl6_ 开头的标识符是编译器内部保留前缀;以 xrt 开头的是 XRT 运行时保留前缀。用户代码不应使用这些前缀。
42.3 模块拆分与 import
第 35 章介绍了 import 和 source import 的语法;本节讲拆分的策略:
拆分原则:
一,单一职责。一个 .xl 文件做一件事。http_client.xl 只管 HTTP 请求,不混入 JSON 解析逻辑。当文件超过 300 行时,考虑拆分。
二,依赖方向单向。上层模块 import 下层模块,不反向。core/ 不 import net/,net/ 可以 import core/。循环依赖在 xlang 中会导致编译错误(import 展开阶段检测)。
三,公开接口最小化。只 export 需要被其他模块使用的函数和类型。内部辅助函数不加 export——它们不会进入 ABI 元数据,也不会被外部 import 看到。
// src/core/types.xl —— 基础类型定义 class Request string method string path dict<string> headers end // src/net/http.xl —— 网络层,依赖 core import "../core/types" export func send(Request req) as string // ... end // src/main.xl —— 入口,组装所有模块 import "core/types" import "net/http" func main(args: array<string>) // ... end
source import vs import:source import 是编译期文本合并(类似 C 的 #include),适合紧密耦合的内部模块;import 是语义级引用(带 ABI 边界),适合松耦合的公共接口。不确定时优先用 import。
42.4 错误处理模式
第 10 章讲了 try/catch/throw 的语法;本节讲什么时候用哪种:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 不变量被破坏(逻辑错误) | assert 条件 | 第一现场报警,不传播脏状态 |
| 可恢复的外部错误(文件不存在、网络超时) | try/catch 具体异常类型 | 调用方可以决定重试、降级或报告 |
| 不可恢复的致命错误 | throw 自定义 Error 或让 fatal 发生 | 不要 catch 后吞掉——让程序快速失败 |
| 函数可能失败但调用方只关心成功/失败 | 返回 bool + out 参数 | 避免为简单的成功/失败创建异常对象 |
try/catch 粒度原则:try 块只包裹可能抛出异常的最小代码段。不要把整个函数体塞进一个 try——那样 catch 无法区分是哪一步失败了。
// 模式 1:assert 守护不变量 func transfer(byref int from, byref int to, int amount) assert amount > 0 assert from >= amount from = from - amount to = to + amount end // 模式 2:try/catch 处理外部错误 func loadConfig(path: string) as string try return file.read(path) catch e: IOError println(f"配置读取失败: {e.message}") return "{}" // 降级为默认配置 end end // 模式 3:bool + out 表示简单成功/失败 func parseInt(text: string, out int value) as bool // ... end
42.5 测试体系
xlang 编译器自身拥有 695 个测试用例,分为正例和负例两大类。你的项目也应建立测试纪律:
正例测试(运行并验证输出):
import "../../src/core/math_ext" func main() assert clamp(15, 0, 10) == 10 assert clamp(-5, 0, 10) == 0 assert clamp(7, 0, 10) == 7 println("PASS: test_math") end
负例测试(验证编译器报错):对于库的公开 API,应验证非法用法确实被编译器拒绝。xlang 编译器的做法是:运行 xl check bad_code.xl,断言退出码非零,并检查诊断文本包含预期关键词。
测试组织建议:
test/ ├── cases/ │ ├── test_math.xl # 按模块命名 │ ├── test_http.xl │ └── test_json.xl ├── negatives/ │ ├── bad_type.xl # 应编译失败的用例 │ └── bad_import.xl └── run_tests.bat # 一键运行全部
测试脚本模板:
@echo off
setlocal
set XL=..\release\xl.exe
set PASS=0
set FAIL=0
for %%f in (cases\test_*.xl) do (
%XL% run %%f >nul 2>&1
if !errorlevel! equ 0 (set /a PASS+=1) else (
echo FAIL: %%f
set /a FAIL+=1
)
)
echo Passed: %PASS% Failed: %FAIL%
if %FAIL% gtr 0 exit /b 1
42.6 CI 集成与守门
xl check 是为 CI 设计的快速验证命令——不生成代码,只跑词法、语法、语义检查:
$ xl check src/main.xl # 成功:无输出,退出码 0 # 失败:输出诊断,退出码 1 $ xl check src/main.xl --json # JSON 诊断输出,适合 CI 解析: [{"file":"src/main.xl","line":5,"col":10,"phase":"sem","message":"..."}]
CI 管线推荐步骤:
| 步骤 | 命令 | 目的 |
|---|---|---|
| ① 静态检查 | xl check src/**/*.xl --json | 快速发现语法/语义错误 |
| ② 运行测试 | xl run test/cases/test_*.xl | 验证运行时行为 |
| ③ 构建产物 | xl build app.xlp -o app.exe | 验证完整构建链路 |
| ④ 负例验证 | xl check negatives/*.xl(期望失败) | 确保非法代码被拒绝 |
守门脚本思想:xlang 编译器的 RC0 守门(rc0_guard.bat)包含必需文件检查、禁止回退扫描、构建验证、测试全量运行。你的项目可以简化为:check + test + build 三步,任何一步失败就阻止合并。
42.7 构建与分发
xlang 提供两种执行模式(第 37 章):
| 模式 | 命令 | 适用场景 | 产物 |
|---|---|---|---|
| 脚本运行 | xl run app.xl | 开发调试、嵌入式脚本 | 无持久产物(内存执行) |
| 原生构建 | xl build app.xlp -o app.exe | 发布分发 | 独立 .exe 或 .dll |
构建工作流建议:
一,开发期用 xl run 快速迭代——毫秒级编译,即时反馈。
二,发布时用 xl build 生成独立可执行文件——不依赖 xl.exe 运行。
三,Windows 发布可通过 --res app.res.json 嵌入版本信息、图标和 manifest:
{
"version": "1.0.0",
"description": "My xlang application",
"icon": "app.ico"
}
四,构建 DLL 时 .xlp 的 kind 设为 "dll",配合 --emit-header 生成给 C 使用的头文件:
$ xl build lib.xlp -o mylib.dll --emit-header mylib.h
42.8 C 混编工程实践
第 32–34 章讲了 extern/export/native-backed class 的语法;本节讲工程组织:
一,extern 声明集中管理。把所有 C 函数的 extern 声明放在一个专门的绑定文件里(如 bindings/crt.xl),不散落在业务代码中。好处:C 库升级时只改一个文件。
// XRT 函数的 xlang 声明——集中管理 extern "xrtNow" func xrtNow() as int extern "xrtRandomInt" func xrtRandomInt(min: int, max: int) as int extern "xrtFileReadText" func xrtFileReadText(path: string) as string
二,native-backed class 封装资源。不要让用户直接操作 point 句柄——用 native-backed class 把 C 资源包装成有生命周期的对象(第 34 章)。destroy 映射确保资源释放不依赖用户记忆。
三,ABI 头文件自动生成。export 的函数和 class 可以通过 xl cgen --emit-header 自动生成 C 头文件——不要手写。生成的头文件包含正确的类型映射和调用约定。
四,C 源文件纳入 .xlp。混编的 .c 文件通过 cSources 字段参与构建,不需要单独的 Makefile 或编译脚本。
42.9 文档注释
xlang 支持文档注释语法,用于为公开 API 生成说明:
/// 计算两个整数的最大公约数。 /// /// 参数: /// a - 第一个整数(必须 > 0) /// b - 第二个整数(必须 > 0) /// /// 返回:a 和 b 的最大公约数 /// /// 示例: /// assert gcd(12, 8) == 4 export func gcd(a: int, b: int) as int while b != 0 int t = b b = a % b a = t end return a end
注释原则(参照 xlang 编译器自身的代码风格):
一,注释说明为什么,代码说明做什么。不要写 // 加 1 这种重复代码的注释。
二,公开 API 必须有文档注释;内部辅助函数用简短行注释即可。
三,多写空行分隔功能段落——xlang 编译器的源码风格是"函数前用注释说明功能,函数内部按步骤分段"。
42.10 性能感知编码清单
第 41 章讲了内存模型的机制;本节把它浓缩为编码时的检查清单:
| 检查项 | 做法 | 避免 |
|---|---|---|
| 变量类型 | 知道类型就声明类型(int, float, string) | 用 var 代替具体类型 |
| 容器选择 | 同质数据用 auto 推断为静态泛型 | 把同质数据存入 var 容器 |
| 字符串拼接 | 多次拼接用 f-string 一次成形 | 循环里 result = result + item |
| 对象复制 | 需要独立副本时显式 copy/clone | 误以为赋值或 byval 会复制对象 |
| 虚方法 | 确定不被覆盖的方法标 final | 所有方法都标 virtual |
| 闭包 | 热路径用普通函数代替无捕获闭包 | 在紧密循环里创建闭包 |
| 并发共享 | shared 容器并发写加 mutex | 假设 shared = 线程安全 |
| 资源释放 | 用 native-backed class 管理 C 资源 | 手动记住释放 point 句柄 |
性能调优工作流:先用 xl run 确认正确性 → 用 time 模块定位热点 → 用 xl cgen --emit 查看生成代码 → 确认是算法问题还是装箱问题 → 针对性优化 → 再次验证正确性。不要在没有 profiling 数据的情况下猜测瓶颈。
深入细节
xlang 编译器的 RC0 守门脚本维护一份"禁止回退清单":src 中不得重新出现直接 CRT malloc/free(必须走 XRT)、不得直接 printf(必须走 xl6Console)、不得直接 fopen(必须走 XRT 文件 API)。每次提交前自动扫描,违反即阻止合并。这种"只收紧不放松"的纪律保证了代码质量单调递增。你的项目可以建立类似的禁止清单:比如"不得在业务代码中直接 print 调试信息""不得 catch 后空处理"。
xlang 编译器把诊断文本视为稳定接口:负例测试不仅断言退出码非零,还断言诊断文本包含预期关键词。这意味着编译器升级不能随意改变报错措辞——下游工具(IDE、CI 脚本)可能依赖诊断格式。你的库如果抛出自定义异常,异常消息也应视为公开接口的一部分,升级时保持向后兼容。
.xlp 的 entry 指定入口文件,但入口文件通过 import 拉入的其他 .xl 文件会自动参与编译——不需要在 .xlp 里列出所有源文件。cSources 只列 C 文件。这与 C 项目的 Makefile 需要列出所有 .c 文件不同:xlang 的依赖发现是语义级的(import 链),不是文件级的。
⚠️ 常见错误
超过 500 行的单文件难以导航、难以复用、难以并行开发。当文件开始"什么都有"时,就是拆分的信号。按职责拆分:类型定义、核心逻辑、I/O、入口各一个文件。
try ... catch e ... end 里什么都不做,等于把错误藏起来。至少打印日志或重新 throw。如果确实可以忽略,写注释说明为什么可以忽略。
只验证"正确输入产生正确输出"是不够的。边界值(空数组、零、null)、非法输入(类型错误、越界)和并发场景都应覆盖。xlang 编译器的 695 个测试里,负例占了相当比例。
每个用到 C 函数的文件都写一遍 extern 声明,C 库升级时要改 N 个文件。集中到一个绑定文件,其他文件 import 它。
本章小结
| 主题 | 核心要点 |
|---|---|
| 工程文件 | .xlp 定义 kind/entry/cSources/resources,相对路径基于 .xlp 目录 |
| 命名 | 项目内一致即可;避免 __xl/xl6_/xrt 保留前缀 |
| 模块 | 单一职责、依赖单向、接口最小化;优先 import,紧耦合用 source import |
| 错误处理 | assert 守不变量、try/catch 处理外部错误、bool+out 表示简单成败 |
| 测试 | 正例验证行为、负例验证拒绝、分类套件组织、一键运行脚本 |
| CI | xl check --json 快速守门 + 测试运行 + 构建验证 |
| 构建 | 开发用 run、发布用 build、DLL 用 --emit-header、资源用 --res |
| C 混编 | extern 集中管理、native-backed class 封装资源、ABI 头文件自动生成 |
| 文档 | /// 文档注释用于公开 API;注释说"为什么"不说"做什么" |
| 性能 | 声明具体类型、同质用静态容器、f-string 代替循环拼接、final 降级虚派发 |
复习题
- .xlp 文件的 kind 字段有哪些合法值?entry 字段必须指向什么类型的文件?
- 解释"依赖方向单向"原则。如果 core/ 和 net/ 互相 import 会发生什么?
- assert 和 try/catch 的使用场景有什么不同?为什么不应该用 try/catch 代替 assert?
- xl check --json 的输出格式是什么?它适合集成到什么场景?
- 负例测试验证什么?为什么只检查退出码非零是不够的?
- xl build 和 xl run 的产物和适用场景有什么不同?
- 为什么 extern 声明应该集中管理而不是散落在业务代码中?
- 文档注释(///)和普通注释(//)的区别是什么?哪些实体应该有文档注释?
- 列出性能感知编码清单中你最容易犯的三个错误,并说明正确做法。
- xlang 编译器的"禁止回退"纪律是什么?你的项目可以建立哪些类似的禁止规则?
编程练习
创建一个 .xlp 工程,包含:src/main.xl(入口)、src/core/math_ext.xl(至少 3 个数学函数)、src/core/string_ext.xl(至少 2 个字符串工具)、test/cases/test_math.xl 和 test/cases/test_string.xl。验证 xl run app.xlp 能运行,xl check 通过,测试全部 PASS。
参考思路:main.xl import 两个模块并调用;测试文件也 import 对应模块并用 assert 验证。
为你的练习 1 工程编写一个 ci.bat(或 ci.sh),依次执行:① xl check 所有 .xl 文件;② xl run 所有测试用例;③ xl build 生成 exe。任何一步失败则退出码非零并打印失败步骤。再故意引入一个类型错误,验证 CI 脚本能正确拦截。
参考思路:用 for 循环遍历文件,检查 errorlevel,累计失败计数。
创建一个 .xlp 工程,包含一个 native.c(实现一个 C 函数 int add_c(int a, int b))和一个 main.xl(通过 extern 声明调用 add_c)。验证 xl run 和 xl build 都能正确执行。然后添加 --emit-header 生成头文件,检查生成的 .h 内容。
参考思路:cSources 字段加入 native.c;main.xl 里 extern "add_c" func addC(a: int, b: int) as int。