Skip to content
📖 The Auto Programming LanguageBook contents

Comptime & Metaprogramming ​

Throughout this book we have written code that runs at runtime -- when your program executes. But Auto also gives you the ability to run code at compile time: while the compiler is turning your source into an executable. This is called comptime (short for "compile time"), and it is one of Auto's most powerful features.

Comptime enables three things: attributes that modify how functions and types behave, macros that generate code, and compile-time evaluation that computes values before the program even starts. Together, these form Auto's metaprogramming system -- the art of writing code that writes code.

Why Comptime Matters ​

Consider a simple question: how do you run tests in Auto? In Rust, you use the #[test] attribute to mark a function as a test, and cargo test discovers and runs all such functions. In Python, you subclass unittest.TestCase. In C, testing requires an external framework like Unity or Check. In every case, something beyond ordinary function calls is needed.

That "something" is metaprogramming. The compiler needs extra information to know that test_greeting is not a regular function -- it is a test to be collected and executed by a test harness. Attributes provide that information.

Similarly, how do you write a function that works differently on Linux versus Windows? You need the compiler to choose one version and discard the other. That is conditional compilation, and it requires the compiler to evaluate conditions at compile time.

And how do you precompute fibonacci(10) so that the program does not waste time recalculating it on every run? You need the compiler to execute the fibonacci function during compilation and embed the result directly in the binary. That is compile-time evaluation.

All of these scenarios share a common theme: they require the compiler to do work before the program runs. Auto's comptime system provides a unified, clean way to express all of them.

Attributes ​

An attribute is metadata you attach to a function, type, or variable. It tells the compiler to treat that item specially. Attributes are written with the #[] syntax and placed above the item they modify.

The #[test] Attribute ​

The most common attribute is #[test], which marks a function as a unit test. The automan test command discovers all functions annotated with #[test] and runs them automatically.

Using the `#[test]` attribute to mark a unit test

Let us break down what is happening:

  1. #[test] -- This attribute tells the Auto compiler that test_greeting is a test function. The compiler registers it in a special test table. When you run automan test, the test harness iterates over this table and calls each registered function.

  2. assert!(greeting.contains("Hello")) -- This assertion checks that the string "Hello" appears within the greeting variable. If the assertion fails, the test harness reports a failure with the file name and line number. The ! after assert is Auto's way of calling a macro-like assertion -- it unwraps the boolean and panics if it is false.

  3. fn main() -- The main function is separate from the test. When you run automan test, the test harness runs test_greeting, not main. When you run automan run, the program executes main normally.

The #[test] attribute is one of the simplest examples of metaprogramming: it does not change the code of the function itself, but it changes how the compiler handles that function. The function is still a normal Auto function -- it just gets an extra "role" as a test.

In Rust, #[test] is a built-in attribute recognized by the compiler. In Python, tests are organized into classes that inherit from unittest.TestCase. In C, testing typically requires a separate framework with its own macro system. In TypeScript, assertions use the built-in assert module.

Other Built-In Attributes ​

Auto provides several other built-in attributes:

  • #[derive(Debug, Eq)] -- Automatically generates Debug and Eq spec implementations for a type (covered in Chapter 8).
  • #[inline] -- Hints to the compiler that a function should be inlined at each call site for performance.
  • #[deprecated] -- Marks a function or type as deprecated, producing a warning when it is used.

You can also define your own custom attributes for domain-specific purposes, such as #[route] for web server endpoints or #[command] for CLI subcommands. Custom attributes are processed at compile time by comptime functions.

Macros ​

A macro is a piece of code that generates other code. While attributes annotate existing code, macros actively create new code. Auto provides the #[macro] attribute to define macros using regular function syntax.

Defining a Macro ​

Defining a simple `log` macro with `#[macro]`

Here is what makes this a macro rather than a regular function:

  1. #[macro] -- This attribute tells the compiler that log is a macro. When the compiler encounters a call like log("Starting up"), it does not generate a function call. Instead, it expands the macro inline at the call site, replacing log("Starting up") with print("[LOG] " + "Starting up").

  2. No function call overhead -- Because the macro is expanded inline, there is no function call at runtime. The generated code is as if you had written print("[LOG] Starting up") directly.

  3. Regular Auto syntax -- Unlike Rust's macro_rules! which uses a special pattern-matching syntax, Auto macros use ordinary function syntax. The body of the macro is normal Auto code. This makes macros easier to read, write, and debug.

In Rust, the macro_rules! system uses ($msg:expr) patterns and => transitions that look nothing like regular Rust code. Procedural macros are even more complex, requiring separate crates and the syn/quote libraries. Auto's approach is simpler: a macro is just a function marked with #[macro].

In C, macros are defined with #define and use the C preprocessor. They are textual substitutions with no type checking -- a common source of bugs. Auto's macros, by contrast, are type-checked because they use regular Auto syntax.

In Python and TypeScript, macros are not part of the language. The equivalent is just a regular function call. Auto's macro expansion happens at compile time, so even in these targets, the log function is generated as an inline expansion when possible.

When to Use Macros ​

Use macros when:

  • The code is a repeated pattern that would be tedious to type manually
  • You need zero-overhead abstraction (no function call cost)
  • You want to enforce a convention (like logging format)

Do not overuse macros. If a regular function works just as well, use the function. Macros make code harder to debug because the expansion is not visible in the source. Prefer regular functions and generics over macros when possible.

Conditional Compilation ​

Conditional compilation lets you include or exclude code based on conditions evaluated at compile time. The most common use is writing platform-specific code -- different implementations for Linux, macOS, and Windows.

The #[cfg(target)] Attribute ​

The #[cfg(...)] attribute conditionally includes the item below it based on a compile-time condition. If the condition is true, the item is included in the compiled program. If false, the item is discarded entirely.

Using `#[cfg(target)]` for platform-specific paths

Key observations:

  1. Two definitions of platform_path -- The first is annotated with #[cfg(target = "linux")], meaning it is only compiled when the target platform is Linux. The second is annotated with #[cfg(target = "windows")], meaning it is only compiled for Windows. The compiler selects exactly one version and discards the other.

  2. The discarded code does not exist -- On Linux, the Windows version of platform_path is never compiled. It does not appear in the binary. There is no runtime cost for the unused version.

  3. Exactly one version must match -- If you compile for macOS and neither condition matches, the compiler will error because platform_path is undefined. You must provide a definition for every target you intend to support.

In Rust, #[cfg(target = "linux")] is a built-in attribute that checks the target triple. In C, #if defined(__linux__) is a preprocessor directive that checks for compiler-defined macros. In Python, the check happens at runtime via sys.platform. In TypeScript, os.platform() is also a runtime check. Only in Rust, C, and Auto is the check truly compile-time -- the unused code is never compiled.

Platform Detection ​

Auto provides several built-in conditions for detecting the target platform at compile time. These go beyond simple target matching and give you fine-grained control.

Detecting the operating system with `target_os`

This example shows how target_os provides more specific detection than the general target condition:

  1. target_os -- Matches the operating system part of the target triple. Common values are "linux", "macos", "windows", "freebsd", and "android".

  2. Multiple #[cfg] blocks -- You can have as many conditional versions as you need. The compiler selects the one whose condition matches the current target and discards the rest.

  3. Short return syntax -- When a function body is a single expression, Auto allows omitting the return keyword. fn os_name() { "Linux" } is equivalent to fn os_name() { return "Linux" }.

Auto also supports these compile-time conditions:

ConditionExampleMatches when
target_ostarget_os = "linux"OS is Linux
target_archtarget_arch = "x86_64"CPU architecture is x86-64
target_familytarget_family = "unix"Target is a Unix family OS
target_envtarget_env = "gnu"Target uses the GNU C library
debug_assertionsdebug_assertionsCompiled in debug mode

In C, platform detection uses preprocessor macros like __linux__, __APPLE__, and _WIN32, which are defined by the compiler. In Python, sys.platform returns "linux", "darwin", or "win32" at runtime. In TypeScript, os.platform() performs a similar runtime check. Only Auto, Rust, and C can eliminate the unused branches at compile time.

Compile-Time Evaluation ​

The most powerful comptime feature is compile-time evaluation: the ability to run arbitrary Auto code during compilation and embed the result in the binary. This lets you precompute values, validate inputs, and generate code -- all before the program starts.

The #[comptime] Attribute ​

The #[comptime] attribute on a variable tells the compiler to evaluate the variable's initializer at compile time. The result is stored as a constant in the binary. At runtime, the value is already computed -- no work is needed.

Precomputing Fibonacci(10) at compile time with `#[comptime]`

Let us walk through the key ideas:

  1. fn fib(n int) int -- This is a regular recursive function that computes the nth Fibonacci number. At runtime, calling fib(10) would involve many recursive calls (the naive Fibonacci algorithm has exponential time complexity). But with #[comptime], this cost is paid at compile time, not at runtime.

  2. #[comptime] -- This attribute tells the compiler: "Evaluate the initializer of this variable during compilation." The compiler runs fib(10), gets the result 55, and stores 55 directly in the binary. The recursive function is never called at runtime.

  3. var FIB_10 = fib(10) -- Despite the var keyword, this variable is effectively a constant. The #[comptime] attribute ensures its value is computed once (at compile time) and never changes. You can use it anywhere a constant is expected: as an array size, a match arm, or a default value.

  4. Zero runtime cost -- When the program runs, FIB_10 is just the number 55. There is no function call, no recursion, no computation. The compiler has already done all the work.

In Rust, the equivalent is a const block that evaluates an inline function. The Rust compiler evaluates const expressions at compile time. In C, the preprocessor #define creates a text substitution -- the value 55 is hard-coded by the transpiler because the Auto compiler already computed it. In Python and TypeScript, the computation happens at module load time (which is close enough to compile time for interpreted languages).

What Can Be Comptime? ​

Auto can evaluate at compile time:

  • Arithmetic expressions -- comptime var SIZE = 1024 * 4
  • Function calls -- comptime var RESULT = fib(10) (as shown above)
  • String operations -- comptime var GREETING = "Hello".to_upper()
  • Type checks -- comptime var SIZE_OF_INT = sizeof(int)
  • Code generation -- Comptime functions can generate new functions, types, and spec implementations

The key limitation is that comptime code cannot perform I/O -- it cannot read files, make network requests, or print to the console. Comptime code has access only to its inputs and the language's pure computation facilities. This ensures that compilation is deterministic: the same source code always produces the same binary.

Summary ​

This chapter covered Auto's compile-time metaprogramming system:

FeatureSyntaxPurpose
Test attribute#[test]Mark a function as a unit test
Macro definition#[macro]Define a code-generating function
Conditional compilation#[cfg(target = "...")]Include code only for specific targets
Platform detection#[cfg(target_os = "...")]Fine-grained OS detection
Compile-time evaluation#[comptime]Compute values during compilation

Key takeaways:

  1. Attributes are metadata for the compiler -- #[test], #[macro], #[cfg], and #[comptime] tell the compiler how to treat the item below them. They do not change the item's code; they change how the compiler processes it.

  2. Macros generate code at compile time -- A function marked with #[macro] is expanded inline at each call site. This eliminates function call overhead but makes debugging harder. Use macros sparingly, only when the benefit outweighs the complexity.

  3. Conditional compilation eliminates dead code -- #[cfg] lets you write platform-specific code without runtime checks. The unused branches are never compiled and do not appear in the binary.

  4. Comptime evaluation precomputes values -- #[comptime] runs code during compilation and embeds the result as a constant. This is useful for expensive computations that have known inputs at compile time.

  5. The same concepts exist across languages -- Rust has #[test], macro_rules!, #[cfg], and const. C has #define, #if, and preprocessor macros. Python uses unittest and runtime checks. TypeScript uses the assert module and os.platform(). Auto provides a unified system that works across all targets.

Comptime is the bridge between the language you write and the language the machine runs. By moving work from runtime to compile time, you can write programs that are faster, smaller, and more correct -- with the compiler doing the heavy lifting for you.

Released under the MIT License.