compilation


Compile Time

Compile time is the phase when source code is translated into executable code by a compiler. During this stage, syntax errors, type errors, and other issues that can be detected without running the program are identified and reported.

Note that compile time is the time before compilation is complete.

Run Time

Run time is the phase when a program is executed after it has been successfully compiled. During this stage, the program performs its intended tasks, and errors related to logic, input, or system resources may occur.

Compile-Time Directives

A compile-time directive is an instruction to the compiler. In Jai, many directives begin with #, such as #if, #import, #load, #run, and #scope_file. These are handled while the program is being compiled, before the finished executable runs.

Compile-time directives are used for conditional compilation, code generation, module loading, visibility control, introspection, and other work that should happen as part of building the program rather than as part of running it.

#if

#if is a compile-time conditional. Its condition must be known at compile time. The compiler keeps the selected branch and discards the other branches before normal runtime execution.

Use #if when the shape of the program should change depending on a compile-time constant, such as a build option, a target platform, or whether an optional feature is enabled. This is different from a normal if, which chooses a branch while the program is running.

DEBUG :: true;

main :: () {
    #if DEBUG {
        print("debug checks are enabled\n");
    } else {
        // This branch is not included when DEBUG is true.
    }
}

#ifx

#ifx is a compile-time conditional that tests whether an expression or declaration is valid in the current compile-time context. It is useful for generic code, where a type or value may or may not support a particular operation.

The successful branch can use the expression that was tested. The fallback branch can provide an alternate implementation for types that do not have the required field, member, overload, or operation.

print_count_if_present :: (value: $T) {
    #ifx value.count {
        print("count is %\n", value.count);
    } else {
        print("value has no count field\n");
    }
}

#run

#run executes a procedure while the program is compiling. The returned value can be used as a compile-time constant, and the procedure can perform build-time work such as generating data, validating assumptions, or constructing declarations used by the rest of the program.

Code executed by #run runs in the compiler's build-time environment, not in the final program. Use it for work that should happen once during compilation rather than every time the executable starts.

make_lookup_table :: () -> [4] int {
    return .[1, 4, 9, 16];
}

SQUARES :: #run make_lookup_table();

Scope

A scope is a region of a program within which a set of identifiers (such as variable names, procedure names, or type names) are visible and accessible. An identifier declared inside a scope is not visible outside of it. Scopes can be nested: code in an inner scope can see identifiers from enclosing outer scopes, but code in an outer scope cannot see identifiers declared in an inner scope.

Top Level Scope

The top level scope (also called the global scope) is the outermost scope of your program. An identifier created at the top level, outside of any { ... } block and lives in this scope and is visible to all code within your application.

File Scope

Given a Jai file, we say that its scope is the collection of identifiers created in the file. A file scope sits underneath the top level scope, meaning code in a file can also see identifiers from the top level scope.

Redeclaration

A redeclaration error occurs when the same identifier is declared more than once in the same scope. The duplicate declaration is ambiguous, so the compiler reports a redeclaration error and points back to the first declaration.

This can happen directly in one file, or indirectly when #load inserts the same declarations into a scope more than once.

If a scope already contains number :: 1;, another declaration of number in that same scope produces Redeclaration of 'number'.

number :: 1;
number :: 2;  // Error: Redeclaration of 'number'.

#scope_file

#scope_file is a compile time directive that causes all identifiers declared after it to be private to the current file. These identifiers are placed into the file scope and are not visible to any other file, not via #load, not via #import, and not to other files within the same module. This is the most restrictive visibility level.

#scope_export

#scope_export is a compile time directive that restores the default visibility for identifiers declared after it. These identifiers are exported, they are visible to other files and to code that imports or loads the file. This is the least restrictive visibility level. If no scope directive is specified, #scope_export is the default.

#scope_module

#scope_module is a compile time directive that causes all identifiers declared after it to be visible across all files within the current module, but not to code that imports the module. In a regular application (not inside an imported module), #scope_module behaves the same as #scope_export, because the application itself is the module.

Module

We say that a Jai file is a module when it is written in a way so that all identifiers are in its file scope. A module can also be a folder of files, where the entry point is a file called module.jai. Identifiers within a module have three possible visibility levels, controlled by #scope_export, #scope_module, and #scope_file.

Module Search Directories

Module search directories are the directories the compiler searches when resolving a #import. If the compiler cannot find the requested module, its diagnostic reports that it was unable to find the module in any of the module search directories, then lists each searched path.

For a module named a, the compiler looks for module files such as a.jai and module folders such as a/module.jai inside those directories. The searched directories can include a modules directory near the target program and module directories provided by the Jai installation.

Modules are meant to be brought into scope using "#import", as explained below.

#import

#import "Module_Name"; is a compile time directive that searches the module search directories for the file module_name.jai or module_name/module.jai. Once found, the file is treated as a module. The module's #scope_export identifiers are made visible to the file scope that the #import exists in, but they are not added to it, they remain in the module's own scope. Identifiers behind #scope_module or #scope_file are not visible to the importer.

The syntax Module_Name :: #import "Module_Name"; binds the module's scope to the identifier Module_Name. The module's exported identifiers are then accessed through it, e.g. Module_Name.some_identifier.

For example, this project layout can import the module a, because a.jai exists inside the local modules directory:

.
|-- main.jai
`-- modules
    `-- a.jai

The corresponding main.jai can import a like this:

#import "a";

main :: () {
}

A common pitfall is writing #import "a.jai"; when trying to import the file a.jai. That does not import the file directly. Instead, it asks the compiler to find a module named a.jai, which means the compiler will search for module files and folders based on that full module name.

The extension is omitted because a module may be represented by a single file, such as a.jai, or by a directory containing a module.jai file, such as a/module.jai. In both cases, the module name is a, so the import writes the module name rather than the underlying filename.

Import Resolver

The import resolver is the part of the Jai compiler that finds all #import directives, locates the corresponding modules, and makes their identifiers available to the importing code. It also handles module deduplication, which ensures that if multiple files import the same module, the module is only compiled once and the same identifiers are shared, rather than being redefined each time.

#load

#load "file.jai"; is a compile time directive that inserts the contents of the specified file into the current file scope. Only identifiers under #scope_export in the loaded file are brought into the loading file's scope. Identifiers behind #scope_file remain private to the loaded file. Unlike #import, the loaded file does not get its own isolated scope, it shares the same scope as the file that loaded it and can see (and contribute to) identifiers in the top level scope.

Using the same layout as above, main.jai can load the file modules/a.jai directly:

.
|-- main.jai
`-- modules
    `-- a.jai
#load "modules/a.jai";

main :: () {
}

A common mistake is writing #load "a.jai"; in this layout. That does not search the module search directories, and it does not search inside modules by module name. #load expects a valid file path, relative to the file that contains the #load directive. Since a.jai is inside modules, the path must include that directory.


Compilation Process

When a Jai program is compiled, the compiler first has to determine what source code belongs to the program. In broad terms, #load directives are evaluated by inserting the loaded file's exported contents into the loading file's scope. This is similar to pasting the contents of one file into another, while still respecting directives such as #scope_file.

#import works differently. Import directives are handed to the import resolver, which locates the requested modules, builds each module in its own scope, and makes only the exported identifiers visible to the importing file.

The import resolver exists so that importing code does not create the kinds of problems that plain textual inclusion would create. It keeps module scopes distinct, deduplicates repeated imports, applies visibility rules, prevents private implementation details from leaking into importers, and gives the compiler a consistent view of the program before type checking and code generation proceed.

Because #load behaves like inclusion, loading the same file twice can create duplicate declarations. Suppose modules/a.jai contains this declaration:

number :: 1;

Loading that file twice inserts number into the same file scope twice:

#load "modules/a.jai";
#load "modules/a.jai";

main :: () {
}

The result is a redeclaration error, because the same identifier has been introduced into the same scope more than once.

Importing the same module twice is different. These two imports do not create two separate copies of module a:

#import "a";
#import "a";

main :: () {
}

This compiles because the import resolver deduplicates repeated imports of the same module. The module is resolved and compiled once, and later imports refer to that same module scope rather than pasting another copy of its declarations into the importer.

This matters most in ordinary dependency graphs. For example, two modules may both depend on a shared math module:

.
|-- main.jai
`-- modules
    |-- a.jai
    |-- b.jai
    `-- c.jai

modules/c.jai might provide shared constants or procedures:

TAU :: 6.283185307179586;

Both a and b can import c:

// modules/a.jai
#import "c";

// modules/b.jai
#import "c";

Then main.jai can import both a and b:

#import "a";
#import "b";

main :: () {
}

Without deduplication, module c would be pulled in once through a and again through b, which would create duplicate module work and possible declaration conflicts. The import resolver avoids that by recognizing that both imports refer to the same module c, resolving it once, and sharing that resolved module wherever it is imported.