Skip to content
📖 Auto 编程语言(主书)本书目录

编译时与元编程 ​

在整本书中,我们编写的代码都是在_运行时_执行的——即程序运行的时候。但 Auto 还 赋予你在_编译时_运行代码的能力:在编译器将源代码转换为可执行文件的过程中。这被称 为 comptime("compile time"的缩写),它是 Auto 最强大的特性之一。

Comptime 实现了三件事:_属性_用于修改函数和类型的行为,_宏_用于生成代码,以及 _编译时求值_用于在程序启动之前计算值。这些共同构成了 Auto 的元编程系统——编写 能编写代码的代码的艺术。

为什么 Comptime 很重要 ​

考虑一个简单的问题:在 Auto 中如何运行测试?在 Rust 中,你使用 #[test] 属性将函数标记为测试,cargo test 会发现并运行所有这样的函数。在 Python 中, 你继承 unittest.TestCase。在 C 中,测试需要外部框架(如 Unity 或 Check)。 在每种情况下,都需要超越普通函数调用的东西。

这个"东西"就是元编程。编译器需要额外信息才能知道 test_greeting 不是一 个普通函数——它是一个需要被测试工具收集和执行的测试。属性提供了这些信息。

类似地,如何编写一个在 Linux 和 Windows 上行为不同的函数?你需要编译器选择 一个版本并丢弃另一个。这就是条件编译,它要求编译器在编译时评估条件。

那么如何预计算 fibonacci(10),使程序不必在每次运行时浪费时间去重新计算呢? 你需要编译器在编译期间执行 fibonacci 函数,并将结果直接嵌入二进制文件中。 这就是编译时求值。

所有这些场景都有一个共同的主题:它们要求编译器在程序运行_之前_就完成工作。 Auto 的 comptime 系统提供了一种统一、简洁的方式来表达所有这些需求。

属性 ​

_属性_是你附加到函数、类型或变量上的元数据。它告诉编译器以特殊方式处理该项。 属性使用 #[] 语法编写,放在它们所修改的项的上方。

#[test] 属性 ​

最常见的属性是 #[test],它将函数标记为单元测试。automan test 命令会自动 发现所有带有 #[test] 注解的函数并运行它们。

使用 `#[test]` 属性标记单元测试

让我们逐步分析发生了什么:

  1. #[test] -- 这个属性告诉 Auto 编译器 test_greeting 是一个测试函数。 编译器将它注册到一个特殊的测试表中。当你运行 automan test 时,测试工具 遍历这个表并调用每个已注册的函数。

  2. assert!(greeting.contains("Hello")) -- 这个断言检查字符串 "Hello" 是否出现在 greeting 变量中。如果断言失败,测试工具会报告失败,并附带 文件名和行号。assert 后面的 ! 是 Auto 调用类似宏的断言的方式——它解包 布尔值,如果为 false 则触发 panic。

  3. fn main() -- main 函数与测试是分开的。当你运行 automan test 时, 测试工具运行的是 test_greeting,而不是 main。当你运行 automan run 时, 程序正常执行 main。

#[test] 属性是元编程最简单的例子之一:它不改变函数本身的代码,但它改变了 编译器如何处理该函数。这个函数仍然是普通的 Auto 函数——它只是多了一个作为测试 的额外"角色"。

在 Rust 中,#[test] 是编译器识别的内置属性。在 Python 中,测试被组织成 继承自 unittest.TestCase 的类。在 C 中,测试通常需要带有自己宏系统的单独框架。 在 TypeScript 中,断言使用内置的 assert 模块。

其他内置属性 ​

Auto 还提供了其他几个内置属性:

  • #[derive(Debug, Eq)] -- 自动为类型生成 Debug 和 Eq spec 实现 (在第 8 章中介绍过)。
  • #[inline] -- 提示编译器应该在每个调用点内联函数以提高性能。
  • #[deprecated] -- 将函数或类型标记为已弃用,使用时会产生警告。

你还可以定义自己的自定义属性用于特定领域,例如用于 Web 服务器端点的 #[route] 或用于 CLI 子命令的 #[command]。自定义属性在编译时由 comptime 函数处理。

宏 ​

_宏_是一段生成其他代码的代码。属性注解已有的代码,而宏主动创建新的代码。Auto 提供了 #[macro] 属性来使用常规函数语法定义宏。

定义宏 ​

使用 `#[macro]` 定义简单的 `log` 宏

以下是使它成为宏而非普通函数的原因:

  1. #[macro] -- 这个属性告诉编译器 log 是一个宏。当编译器遇到类似 log("Starting up") 的调用时,它不会生成函数调用。相反,它在调用点_展开_ 宏,将 log("Starting up") 替换为 print("[LOG] " + "Starting up")。

  2. 没有函数调用开销 -- 因为宏是内联展开的,所以在运行时没有函数调用的成本。 生成的代码就像你直接写了 print("[LOG] Starting up") 一样。

  3. 常规 Auto 语法 -- 与 Rust 的 macro_rules! 使用特殊的模式匹配语法不同, Auto 宏使用普通的函数语法。宏的主体是正常的 Auto 代码。这使得宏更容易阅读、 编写和调试。

在 Rust 中,macro_rules! 系统使用 ($msg:expr) 模式和 => 转换,看起来 与常规 Rust 代码完全不同。过程宏更加复杂,需要单独的 crate 和 syn/quote 库。Auto 的方法更简单:宏只是一个带有 #[macro] 标记的函数。

在 C 中,宏使用 #define 定义并使用 C 预处理器。它们是没有类型检查的文本替换—— 这是常见的 bug 来源。相比之下,Auto 的宏经过了类型检查,因为它们使用常规的 Auto 语法。

在 Python 和 TypeScript 中,宏不是语言的一部分。等价物只是一个普通的函数调用。 Auto 的宏展开发生在编译时,所以即使在这些目标语言中,log 函数也会在可能的情况 下生成为内联展开。

何时使用宏 ​

在以下情况使用宏:

  • 代码是重复的模式,手动输入会很繁琐
  • 你需要零开销的抽象(没有函数调用成本)
  • 你想强制执行一种约定(如日志格式)

不要过度使用宏。如果普通函数同样有效,请使用函数。宏使代码更难调试,因为展开 在源代码中不可见。尽可能优先使用普通函数和泛型而不是宏。

条件编译 ​

_条件编译_让你可以根据在编译时评估的条件来包含或排除代码。最常见的用途是编写 平台特定代码——为 Linux、macOS 和 Windows 提供不同的实现。

#[cfg(target)] 属性 ​

#[cfg(...)] 属性根据编译时条件有条件地包含其下方的项。如果条件为真,该项被 包含在编译后的程序中。如果为假,该项被完全丢弃。

使用 `#[cfg(target)]` 编写平台特定路径

关键观察:

  1. platform_path 的两个定义 -- 第一个用 #[cfg(target = "linux")] 注解, 意味着它只在目标平台是 Linux 时编译。第二个用 #[cfg(target = "windows")] 注解,意味着它只在 Windows 上编译。编译器选择恰好一个版本并丢弃另一个。

  2. 被丢弃的代码不存在 -- 在 Linux 上,Windows 版本的 platform_path 从未被 编译。它不会出现在二进制文件中。未使用版本没有任何运行时成本。

  3. 必须恰好有一个版本匹配 -- 如果你为 macOS 编译且两个条件都不匹配,编译器 会报错,因为 platform_path 未定义。你必须为你打算支持的每个目标提供定义。

在 Rust 中,#[cfg(target = "linux")] 是一个检查目标三元组的内置属性。在 C 中, #if defined(__linux__) 是一个检查编译器定义宏的预处理器指令。在 Python 中, 检查通过 sys.platform 在运行时发生。在 TypeScript 中,os.platform() 也是 运行时检查。只有在 Rust、C 和 Auto 中,检查才是真正的编译时——未使用的代码从未 被编译。

平台检测 ​

Auto 提供了几个内置条件用于在编译时检测目标平台。这些超越了简单的 target 匹配,为你提供细粒度的控制。

target_os 及相关条件 ​

使用 `target_os` 检测操作系统

这个例子展示了 target_os 如何提供比通用的 target 条件更具体的检测:

  1. target_os -- 匹配目标三元组中的操作系统部分。常见值包括 "linux"、 "macos"、"windows"、"freebsd" 和 "android"。

  2. 多个 #[cfg] 块 -- 你可以根据需要拥有任意多个条件版本。编译器选择 条件与当前目标匹配的那个,并丢弃其余的。

  3. 简短的返回语法 -- 当函数体是单个表达式时,Auto 允许省略 return 关键字。fn os_name() { "Linux" } 等同于 fn os_name() { return "Linux" }。

Auto 还支持以下编译时条件:

条件示例匹配时机
target_ostarget_os = "linux"操作系统为 Linux
target_archtarget_arch = "x86_64"CPU 架构为 x86-64
target_familytarget_family = "unix"目标为 Unix 系列 OS
target_envtarget_env = "gnu"目标使用 GNU C 库
debug_assertionsdebug_assertions以调试模式编译

在 C 中,平台检测使用 __linux__、__APPLE__ 和 _WIN32 等预处理器宏, 这些宏由编译器定义。在 Python 中,sys.platform 在运行时返回 "linux"、 "darwin" 或 "win32"。在 TypeScript 中,os.platform() 执行类似的运行时 检查。只有 Auto、Rust 和 C 可以在编译时消除未使用的分支。

编译时求值 ​

最强大的 comptime 特性是_编译时求值_:在编译期间运行任意 Auto 代码并将结果 嵌入二进制文件的能力。这让你可以在程序启动之前预计算值、验证输入和生成代码。

#[comptime] 属性 ​

变量上的 #[comptime] 属性告诉编译器在编译时评估该变量的初始化器。结果作为 常量存储在二进制文件中。在运行时,值已经被计算好了——不需要任何工作。

使用 `#[comptime]` 在编译时预计算 Fibonacci(10)

让我们逐步分析关键概念:

  1. fn fib(n int) int -- 这是一个计算第 n 个 Fibonacci 数的普通递归函数。 在运行时,调用 fib(10) 会涉及许多递归调用(朴素 Fibonacci 算法具有指数 时间复杂度)。但有了 #[comptime],这个成本在编译时支付,而不是在运行时。

  2. #[comptime] -- 这个属性告诉编译器:"在编译期间评估这个变量的初始化器。" 编译器运行 fib(10),得到结果 55,并直接将 55 存储在二进制文件中。 递归函数在运行时从未被调用。

  3. var FIB_10 = fib(10) -- 尽管使用了 var 关键字,这个变量实际上是 一个常量。#[comptime] 属性确保它的值只被计算一次(在编译时)并且永远不会 改变。你可以在任何需要常量的地方使用它:作为数组大小、匹配分支或默认值。

  4. 零运行时成本 -- 当程序运行时,FIB_10 只是数字 55。没有函数调用, 没有递归,没有计算。编译器已经完成了所有工作。

在 Rust 中,等价物是一个求值内联函数的 const 块。Rust 编译器在编译时评估 const 表达式。在 C 中,预处理器 #define 创建文本替换——值 55 由转译器 硬编码,因为 Auto 编译器已经计算了它。在 Python 和 TypeScript 中,计算在模块 加载时发生(对于解释型语言来说,这已经足够接近编译时了)。

什么可以 Comptime? ​

Auto 可以在编译时评估:

  • 算术表达式 -- comptime var SIZE = 1024 * 4
  • 函数调用 -- comptime var RESULT = fib(10)(如上所示)
  • 字符串操作 -- comptime var GREETING = "Hello".to_upper()
  • 类型检查 -- comptime var SIZE_OF_INT = sizeof(int)
  • 代码生成 -- Comptime 函数可以生成新的函数、类型和 spec 实现

关键限制是 comptime 代码不能执行 I/O——它不能读取文件、发起网络请求或打印到 控制台。Comptime 代码只能访问其输入和语言的纯计算设施。这确保了编译的确定性: 相同的源代码总是产生相同的二进制文件。

总结 ​

本章介绍了 Auto 的编译时元编程系统:

特性语法用途
测试属性#[test]将函数标记为单元测试
宏定义#[macro]定义代码生成函数
条件编译#[cfg(target = "...")]仅在特定目标下包含代码
平台检测#[cfg(target_os = "...")]细粒度操作系统检测
编译时求值#[comptime]在编译期间计算值

关键要点:

  1. 属性是给编译器的元数据 -- #[test]、#[macro]、#[cfg] 和 #[comptime] 告诉编译器如何处理其下方的项。它们不改变项的代码;它们改变 编译器处理它的方式。

  2. 宏在编译时生成代码 -- 带有 #[macro] 标记的函数在每个调用点内联展开。 这消除了函数调用开销,但使调试更困难。谨慎使用宏,只有在收益超过复杂性时 才使用。

  3. 条件编译消除死代码 -- #[cfg] 让你编写平台特定代码而无需运行时检查。 未使用的分支从未被编译,也不会出现在二进制文件中。

  4. Comptime 求值预计算值 -- #[comptime] 在编译期间运行代码并将结果嵌入 为常量。这对于在编译时有已知输入的昂贵计算非常有用。

  5. 相同的概念存在于各种语言中 -- Rust 有 #[test]、macro_rules!、 #[cfg] 和 const。C 有 #define、#if 和预处理器宏。Python 使用 unittest 和运行时检查。TypeScript 使用 assert 模块和 os.platform()。 Auto 提供了一个在所有目标上工作的统一系统。

Comptime 是你编写的语言和机器运行的语言之间的桥梁。通过将工作从运行时转移到 编译时,你可以编写更快、更小、更正确的程序——让编译器为你完成繁重的工作。

基于 MIT 许可发布。