Compile-Time Execution in Alusus Programming Language

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 Preprocess Statement

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.


Execution Sequence

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:

  1. The compiler inspects the contents of the function or class.
  2. It immediately executes any preprocess statements, at compile time.
  3. It compiles the remaining code into executable code that runs when the program is launched.

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.


Generating Code at Compile Time

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.

insertAst

takes 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.


A Practical Example: A Dynamic Settings Class

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.

How the `Settings` Class Works

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();

 

Full Source Code

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

 

Notes on the Example

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.


Conclusion

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.