Most programming languages draw a strict boundary between two phases: compilation, when the compiler translates source code into executable code, and execution, when the computer runs that code. Alusus breaks this boundary with the feature of compile-time execution: the ability to run code during the compilation process itself, and to use the results of that execution to generate new code that is automatically injected into the program. This unique feature unlocks capabilities typically found in dynamic languages, but within a low-level, ahead-of-time compiled language like Alusus. Let's first take a look at this feature before exploring examples of the significant benefits it offers.
The tool Alusus provides for this purpose is the preprocess statement: preprocess . Any code inside this statement is executed at compile time, not when the containing element runs. For example, if you write a preprocess statement inside a function body, it will execute when the compiler begins compiling that function, and only then does the function itself get compiled. This allows us to dynamically alter the function's contents in ways that go far beyond what macros in other languages can offer. The same applies to classes: you can place a preprocess statement inside a class body and it will execute when the compiler begins compiling that class.
To understand the difference between compile time and run time, let's start with an example that prints messages at both points. When the Alusus compiler processes a function or class, it follows these steps:
Let's look at a simple example that illustrates this:
import "Srl/Console.alusus";
import "Spp.alusus";
use Srl;
func test {
Console.print("[ run time ] function body\n");
preprocess {
Console.print("[ compile time ] preprocess inside function\n");
}
}
Console.print("[ run time ] before calling the function\n");
test();
Console.print("[ run time ] after calling the function\n");
Output:
[ run time ] before calling the function [ compile time ] preprocess inside function [ run time ] function body [ run time ] after calling the function
The first thing you need to know to understand what's happening here is that Alusus treats root-level statements differently from the rest of the program: it compiles and executes each statement before moving to the next, as long as that statement is a command rather than a definition. That's why the first thing you see in the output above is the line that precedes the call to test . At that point, the compiler hasn't started compiling the function yet, because it doesn't compile elements until they're needed. When it reaches the line that calls the function, it begins compiling it — but before doing so, it executes the preprocess statements. Then it compiles the function and calls it. This is why the compile-time message is printed before the run-time message, even though it appears after it in the source code.
Printing messages at compile time was purely for illustration. The real benefit lies in generating code that is injected into the program at compile time. The Spp library provides tools for working with the Abstract Syntax Tree (AST), including insertAst , which adds new nodes to the tree during compilation.
insertAsttakes two arguments: - The first: an AST template containing placeholder identifiers surrounded by the code to be generated. - The second: a map linking each placeholder to the AST node it should be replaced with.
Let's see this in action: a class with a fixed list of field names, where we want to generate an Int member variable for each name at compile time.
import "Srl/Console.alusus";
import "Srl/String.alusus";
import "Srl/Array.alusus";
import "Srl/Map.alusus";
import "Core/Data.alusus";
import "Spp.alusus";
use Srl;
class Statistics {
preprocess {
// A fixed list of field names
def fields: Array[String];
fields.add(String("sales"));
fields.add(String("purchases"));
fields.add(String("profits"));
def i: Int;
for i = 0, i < fields.getLength(), ++i {
// Generate an Int member variable for each name
Spp.astMgr.insertAst(
ast { def name: Int },
Map[String, ref[Core.Basic.TiObject]]()
.set(String("name"), Core.Data.Ast.Identifier(fields(i).buf))
);
}
}
};
// Result: as if the class was manually written as:
// class Statistics {
// def sales: Int;
// def purchases: Int;
// def profits: Int;
// };
func start {
def s: Statistics;
s.sales = 150;
s.purchases = 80;
s.profits = 70;
Console.print("Sales: %d\n", s.sales);
Console.print("Purchases: %d\n", s.purchases);
Console.print("Profits: %d\n", s.profits);
}
start();
Output:
Sales: 150 Purchases: 80 Profits: 70
In this example, the Statistics class contains no member variables explicitly written in the source code. The compiler is the one that adds them via the preprocess statement before it finishes compiling the class. From the rest of the program's perspective, the result is identical to writing the variables manually.
The previous example established the principle, but let's look at a more practical scenario. In this example, we'll build a class dedicated to reading a settings file, ensuring that access to configuration values goes through a class that guarantees type safety — without needing to manually declare any properties.
The settings file (settings.txt ) uses env-style format as shown below.
db_server=localhost port=5432 username=alusus password=alusus123
The goal is for the compiler to automatically produce a class containing:
- db_server: String - port: Int - username: String - password: String
without any of these definitions being written manually in the source code. We also want the data-loading method to be generated automatically.
The class contains two preprocess statements:
The first preprocess (directly inside the class body): reads the file and generates the member variables. It infers the type of each field from its value — if the value consists solely of digits, the type is Int ; otherwise it is String .
The second preprocess (inside the load method): reads the file again during compilation of the load method, and generates an if statement for each known field that compares the key name read at run time and assigns the value to the appropriate variable with the correct type conversion.
The result: the load code looks at run time as if it had been written manually like this:
// Generated code inside load(): if parts(0).trim() == "db_server" this.db_server = parts(1).trim(); if parts(0).trim() == "port" this.port = String.parseInt(parts(1).trim().buf); if parts(0).trim() == "username" this.username = parts(1).trim(); if parts(0).trim() == "password" this.password = parts(1).trim();
import "Srl/Console.alusus";
import "Srl/String.alusus";
import "Srl/Array.alusus";
import "Srl/Map.alusus";
import "Srl/Fs.alusus";
import "Core/Data.alusus";
import "Spp.alusus";
use Srl;
// Helper function shared between both preprocess blocks to check if a string represents an integer
func isNumber(value: String): Bool {
if value.getLength() == 0 return 0;
def i: Int;
for i = 0, i < value.getLength(), ++i {
if value(i) < '0' return 0;
if value(i) > '9' return 0;
}
return 1;
}
// The "Settings" class contains member variables and a loading operation generated at compile time
// by reading a file in "name=value" format
class Settings [fileName: string] {
// preprocess generates a member variable for each field in the file,
// inferring the type from the field's value
preprocess {
def content: String = Fs.readFile(fileName);
def lines: Array[String] = content.split("\n");
def i: Int;
for i = 0, i < lines.getLength(), ++i {
def line: String = lines(i).trim();
if line == "" continue;
def parts: Array[String] = line.split("=");
if parts.getLength() < 2 continue;
def fieldName: String = parts(0).trim();
def fieldValue: String = parts(1).trim();
def fieldType: String = String().{ if isNumber(fieldValue) this = "Int" else this = "String" };
// Insert member variable definition with the inferred type
Spp.astMgr.insertAst(
ast { def name: type },
Map[String, ref[Core.Basic.TiObject]]()
.set(String("name"), Core.Data.Ast.Identifier(fieldName.buf))
.set(String("type"), Core.Data.Ast.Identifier(fieldType.buf))
);
}
}
// This operation loads field values from the file at runtime.
// The preprocess inside it reads the file at compile time and generates an "if"
// statement for each known field: compares the name and assigns the value with the correct type.
handler this.load() {
def content: String = Fs.readFile(fileName);
def lines: Array[String] = content.split("\n");
def i: Int;
for i = 0, i < lines.getLength(), ++i {
def line: String = lines(i).trim();
if line == "" continue;
def parts: Array[String] = line.split("=");
if parts.getLength() < 2 continue;
// preprocess reads the file at compile time and for each known field generates:
// if parts(0).trim() == "fieldName" this.fieldName =
preprocess {
def contentT: String = Fs.readFile(fileName);
def linesT: Array[String] = contentT.split("\n");
def j: Int;
for j = 0, j < linesT.getLength(), ++j {
def lineT: String = linesT(j).trim();
if lineT == "" continue;
def partsT: Array[String] = lineT.split("=");
if partsT.getLength() < 2 continue;
def fieldName: String = partsT(0).trim();
def fieldValue: String = partsT(1).trim();
if isNumber(fieldValue) {
// Int field: generate assignment with string-to-integer conversion
Spp.astMgr.insertAst(
ast { if parts(0).trim() == literal this.field = String.parseInt(parts(1).trim().buf) },
Map[String, ref[Core.Basic.TiObject]]()
.set(String("literal"), Core.Data.Ast.StringLiteral(fieldName.buf))
.set(String("field"), Core.Data.Ast.Identifier(fieldName.buf))
);
} else {
// String field: generate direct assignment
Spp.astMgr.insertAst(
ast { if parts(0).trim() == literal this.field = parts(1).trim() },
Map[String, ref[Core.Basic.TiObject]]()
.set(String("literal"), Core.Data.Ast.StringLiteral(fieldName.buf))
.set(String("field"), Core.Data.Ast.Identifier(fieldName.buf))
);
}
}
}
}
}
};
func start {
def settings: Settings["settings.txt"];
settings.load();
Console.print("db_server: %s\n", settings.db_server.buf);
Console.print("port: %d\n", settings.port);
Console.print("username: %s\n", settings.username.buf);
Console.print("password: %s\n", settings.password.buf);
}
start();
Output:
db_server: localhost port: 5432 username: alusus password: alusus123
Automatic type inference: The value of port is 5432 , consisting entirely of digits, so an Int variable is generated for it. The remaining fields contain non-digit characters, so they are generated as String . There is no type-inference logic in the run-time code at all — it all happens at compile time.
The template parameter `[fileName]`: Note that Settings is a template, meaning the compiler creates a distinct instantiation of the class for each different argument (settings file). Each instantiation carries the structure of its own file, generated during the compilation of that instantiation.
Reading the file twice: The file is read twice at compile time: once to generate the member variables, and once inside the load preprocess to generate the assignment statements. Both reads happen at compile time, before the program is ever run.
Compile-time execution enables writing programs that write part of themselves, adapting to internal or external sources — configuration files, schemas, databases — while retaining full type safety and the performance of compiled code. This is precisely what allows a library like Rows to provide ORM capabilities and translate Alusus code into SQL at compile time, delivering higher performance and safety at run time while keeping the source code clean, readable, and easy to write. This feature doesn't just enable dynamic code generation — it does so in a structured way that makes it easy to reason about where generated code will be inserted. The Spp library also allows reading the program's own AST, not just external sources, which is exactly what Rows does: it generates ORM code based on the modifiers the user attaches to their classes. The flexibility that Alusus provides for interacting with the compiler, paired with the preprocess statement, opens vast possibilities for building libraries that combine strong typing, high performance, and clear, expressive source code — something that mainstream languages simply do not offer.