Most of what your program does happens at runtime. But some computations only need to happen once—when you build. Etch’s comptime feature lets you execute code during compilation, turning runtime work into compile-time constants.
Imagine you need to compute factorial(10) in your program. You could calculate it every time the program runs, or you could calculate it once during compilation and embed the result (3,628,800) as a constant. That’s what comptime does.
The benefits:
Everything in a comptime block runs during compilation. The results become constants in your program, with zero runtime overhead.
The simplest use of comptime is wrapping an expression to evaluate it during compilation:
fn factorial(n: int) -> int {
if n <= 1 { return 1; }
return n * factorial(n - 1);
}
let value: int = comptime(factorial(10));
print(value); // Prints 3628800, computed at compile time
The compiler executes factorial(10) during the build, replaces comptime(factorial(10)) with the constant result, and emits code that just prints the number. No recursion at runtime.
Any pure function—functions that compute a result from their arguments without side effects—can run at compile time:
let sq: int = comptime(square(8)); // 64
let sum: int = comptime(add(10, 20)); // 30
let combo: int = comptime(square(3) + add(5, 2)); // 16
Note: Simple arithmetic like 5 + 3 * 2 is automatically constant-folded by the compiler, so you don’t need comptime for literal expressions. Use it when calling functions.
For multiple statements that should run at compile time, use a comptime { } block:
fn main() {
comptime {
print("Building...");
print("Version 1.0");
}
print("Running!"); // Runtime
}
During compilation, you’ll see:
Building...
Version 1.0
When you run the program, you’ll see:
Running!
Variables in comptime blocks exist only during compilation. They’re not part of the final program:
comptime {
let build_config = 42;
print(build_config); // Prints during build
}
// build_config doesn't exist at runtime
You can use conditionals and loops in comptime blocks to make build-time decisions:
comptime {
let mode = 1;
if mode == 1 {
print("Debug build");
} else {
print("Release build");
}
}
This lets you generate different code based on compile-time conditions.
One of comptime’s most practical features is embedding file contents directly into your binary. No more shipping configuration files, templates, or assets separately—they become part of the executable.
Use readFile() in a comptime expression:
let config: string = comptime(readFile("config.txt"));
print(config); // Content is in the binary
The file is read during compilation and its contents become a string constant in your program. At runtime, there’s no file I/O—the data is already there.
Single executable deployment - Ship one file instead of an executable plus data files
Guaranteed availability - The file content can’t go missing or be corrupted
Faster startup - No file system access needed; data is already in memory
Simpler distribution - Users don’t need to maintain file structures
Configuration files:
let defaults: string = comptime(readFile("defaults.json"));
HTML templates:
let page: string = comptime(readFile("template.html"));
Shader code:
let shader: string = comptime(readFile("shader.glsl"));
The most powerful comptime feature is inject(), which lets you dynamically create variables that exist at runtime, based on compile-time logic.
Create runtime variables from compile-time code:
comptime {
inject("version", "string", "1.0.0");
inject("build_num", "int", 42);
}
// Now these variables exist at runtime
print(version); // "1.0.0"
print(build_num); // 42
The inject(name, type, value) function takes:
Combine file reading with injection to generate configuration from external files:
comptime {
let mode = readFile(".build_mode");
if mode == "debug" {
inject("logging_enabled", "int", 1);
inject("optimization", "int", 0);
} else {
inject("logging_enabled", "int", 0);
inject("optimization", "int", 3);
}
}
// Use the injected variables
if logging_enabled == 1 {
initLogging();
}
This pattern lets you generate different code for debug vs release builds without preprocessor macros or build system complexity.
When you need constant data, compute it once at compile time:
let factorial_10: int = comptime(factorial(10));
let factorials = [
comptime(factorial(0)),
comptime(factorial(5)),
comptime(factorial(10))
];
Perfect for mathematical constants, hash tables, or any data that doesn’t change.
Embed version info from external sources:
comptime {
inject("version", "string", readFile("VERSION"));
inject("commit", "string", readFile(".git/HEAD"));
}
print("v" + version + " (" + commit + ")");
Enable or disable features at build time:
comptime {
let enable_experimental = readFile(".features") == "exp";
inject("experimental", "int", if enable_experimental { 1 } else { 0 });
}
if experimental == 1 {
runExperimentalFeature();
}
The disabled code path might not even be compiled into the binary, depending on optimization.
Embed all your assets into a single executable:
let logo: string = comptime(readFile("assets/logo.txt"));
let help: string = comptime(readFile("assets/help.txt"));
let license: string = comptime(readFile("LICENSE"));
Ship one file instead of a directory tree.
If a value never changes, compute it once at build time:
// ✅ Computed once during compilation
let pi_squared: float = comptime(square(3.14159));
// ❌ Computed every time the program runs
let pi_squared: float = square(3.14159);
Embedding large files increases binary size. Only embed assets you actually use:
// ✅ Embed essential config
let config: string = comptime(readFile("config.ini"));
// ❌ Don't embed huge data files unnecessarily
let huge_db: string = comptime(readFile("10GB_database.sql")); // Bad idea!
Since inject() creates variables without explicit declarations, document them:
comptime {
// Injects: version (string), build_num (int)
inject("version", "string", readFile("VERSION"));
inject("build_num", "int", 42);
}
Side effects in comptime blocks happen during compilation, not at runtime:
comptime {
print("Building..."); // Prints when you compile, not when you run
}
What comptime can’t do:
Compile-time evaluation shifts work from runtime to build time:
Use comptime whenever you can compute something once instead of repeatedly.
Next: Explore Operator Overloading to customize operators for your types.