Compiler Directives

In Asa, there are a number of directives for modifying the behavior of the compiler during compilation. These all start with the character #. Any compiler directive is an expression that will be evaluated at compile time.


Primary Compiler Directives:

Beginner Level

This topic is intended for beginner programmers.


#import

One of the most used directives, used for importing modules by name. For example:

Asa
#import Rendering.Window;
#import Rendering.Drawing.Line;

This imports the module Window from the directory modules/Rendering and makes its exported members available without qualifying them with the module name.


The module expression can be more complex, for example sub-directories: #import A.B.C.ModuleName;, which would be in modules/A/B/C.

You can also wildcard import modules by using the base path followed by an asterisk: #import Builtin.*;, which would import all modules of all files in the directory modules/Builtin/.


#import_qualified

Like #import, this imports the module, but #import_qualified requires qualified access through the module name, like Foo.someFunc();.


Both #import and #import_qualified look in multiple locations for the specified module:

  • First, they look in the local directory from where they are called. If there is a matching module (including all path components), then it will stop there.
  • If it does not find a matching module locally, it will look in the global modules folder, which is installed next to the asa executable. This allows you to override builtin modules for specific use cases.

#embed

This is similar to #import, but instead of only including a single module, it loads the entire file. Also, it does so by an explicit path. For example:

Asa
#embed "./otherFile.asa";

This would compile and import the entire other file at the given path. The path is evaluated relative to the file which #embed is in.


#library

Used to specify the name of a library to link against. For example:

Asa
#library "ssl";

This would link against libssl. It is equivalent to passing -lssl with --clangoptions.


#library_static

Used to specify the name of a library to statically link against. For example:

Asa
#library_static "/path/to/libssl.a";

This would link against libssl. It is equivalent to passing /path/to/libssl.a with --clangoptions.


#linenum

Gets the line number as an integer of its own location in the source code file. For example:

linenum.asa
// some line
print(#linenum); // Prints 4
Shell
$ asa linenum.asa
$ ./build/linenum
4

#linecol

Gets the line column as an integer of its own location in the source code file. For example:

linecol.asa
print(#linecol); // Prints 6
Shell
$ asa linecol.asa
$ ./build/linecol
6

#line

Gets the line contents as a string at its own location in the source code file. For example:

line_contents.asa
print(#line);
Shell
$ asa line_contents.asa
$ ./build/line_contents
print(#line);

#filepath

Gets the full path of the source code file as a string. For example:

file_path.asa
// In a file at /home/user/main.asa
print(#filepath);
Shell
$ asa file_path.asa
$ ./build/file_path
/home/user/main.asa

#funcname

Gets the name of the function it is in as a string. For example:

funcname.asa
someFunc :: int(){
    print(#funcname);
    ...
}
Shell
$ asa funcname.asa
$ ./build/funcname
someFunc

#modulename

Gets the name of the immediate module it is in as a string. For example:

modulename.asa
moduleA :: module{
    print(#modulename);
}
Shell
$ asa modulename.asa
$ ./build/modulename
moduleA

The above would print moduleA. But in the case of nested modules like so:

modulename_nested.asa
moduleA :: module{
    moduleB :: module{
        print(#modulename);
    }
}
Shell
$ asa modulename_nested.asa
$ ./build/modulename_nested
moduleB

It would print the name of the module it is immediately within, moduleB.


#asaversion

Gets the version of the Asa compiler as a string as it was when compiled. For example:

asaversion.asa
print(#asaversion);
Shell
$ asa asaversion.asa
$ ./build/asaversion
1.2.3

The above would print the current version string of the asa compiler.


#counter

Acts as an automatically incrementing integer. For example:

counter.asa
printl(#counter); // Prints 0
printl(#counter); // Prints 1
printl(#counter); // Prints 2
Shell
$ asa counter.asa
$ ./build/counter
0
1
2

#nameof(I)

Gets the name of an identifier as a string. For example:

nameof.asa
someVar : int = 0;
print(#nameof(someVar));
Shell
$ asa nameof.asa
$ ./build/nameof
someVar

#typeof(T)

Gets the type name of an identifier or type as a string. For example:

typeof.asa
someVar : int = 0;
printl(#typeof(someVar)); // Would print `int`
printl(#typeof(string));  // Would print `string`
Shell
$ asa typeof.asa
$ ./build/typeof
int
string

#sizeof(T)

Gets the size in bytes of an identifier or type as an integer. For example:

sizeof.asa
someVar : int = 0;
printl(#sizeof(someVar)); // Would print `4`
printl(#sizeof(int8));    // Would print `1`
Shell
$ asa sizeof.asa
$ ./build/sizeof
4
1

Advanced Compiler Directives:

Intermediate Level

This topic is intended for intermediate programmers.


#extern

This is a method to reference external functions from libraries. For example:

Asa
#extern printf :: int32 (s : const *char, ...);

The above would allow you to use the printf function from the C standard library. #extern use is simply stating the function name and parameters as they are defined, and not including a body.


#compiles(expression)

Checks at compile time if expression will compile successfully, or cause a compiler error. Returns true if it compiled without error, and false if it failed. The error message is thrown out, and never printed to stdout or stderr.

compiles.asa
printl(#compiles(1 + 1)); // Prints `true`
printl(#compiles(undefinedVariable + 1)); // Prints `false`
Shell
$ asa compiles.asa
$ ./build/compiles.asa
true
false

#variant


TODO:
This directive will likely be removed, since the variant system will be more like the C++ template system, see variants

This is a function modifier that creates variants of the function. For example:

Asa
foo :: ()
    #variant w = 0;
{
    printl(w);
}

Then you can use it like so:

Asa
foo<w=7>(); // -> 7
foo<w=2>(); // -> 2

This looks like arguments with extra steps, but the main difference is that it permanently creates a completely different function for each combination during the compilation process. So the above would have equivalent code to:

Asa
foo :: (){
    printl(7);
}

foo :: (){
    printl(2);
}

This is useful if you want to allow for a function to work with multiple types:

Asa
foo :: T(v : T)
    #variant T = int;
{
    return v * 2;
}

#set_attribute(ast_node, attribute_name, value)

Sets or changes the given AST node's attribute to the new value

set_attribute.asa
@public:
x : int = 0;

#set_attribute(#parent(x), "public", false);

printl(x.@public);
Shell
$ asa set_attribute.asa
$ ./build/set_attribute.asa
false

#setflag(name, value)

Sets a global flag to the given value.

Asa
#setflag(FOO, true);
// OR:
#setflag FOO true;

#getflag(name)

Returns the value of the named global flag.

Asa
#getflag(FOO);

#recommend_options(string)

Sets compiler options at compile time. For security, it asks the user with a prompt before applying, and the user can reject the options. Uses the same syntax as options applied through the command line:

Asa
#recommend_options("-O3 -w all");  // Applies optimization level 3 and enables all warnings

The same as this command:

Shell
$ asa main.asa -O3 -w all

#error(string, ast_node)

Causes a compiler error at compile time, using the same error system that the compiler uses for it's builtin errors. It uses the string as the error message, and the ast_node as the line context shown in the error message.


TODO:
Update this with valid runnable code
compiler_error.asa
#error("This is an error", SOME_AST);
Shell
$ asa compiler_error.asa
$ ./build/compiler_error.asa

Note

This requires that the second argument is a reference to an AST node. It will work if you pass something else, such as a variable name: #error("...", x);. But it will be a new node generated for this line, rather than for the definition or set of x


#warning(string, ast_node)

Causes a compiler warning at compile time, using the same error system that the compiler uses for it's builtin errors. It uses the string as the warning message, and the ast_node as the line context shown in the warning message.


TODO:
Update this with valid runnable code
compiler_warning.asa
#warning("This is a warning", SOME_AST);
Shell
$ asa compiler_warning.asa
$ ./build/compiler_warning.asa

Note

This requires that the second argument is a reference to an AST node. It will work if you pass something else, such as a variable name: #warning("...", x);. But it will be a new node generated for this line, rather than for the definition or set of x


#stack_push(string, ast_node)

This pushes a given AST node to the named stack. If the stack does not exist yet, it creates a new one. Example:

Asa
#stack_push("some stack", true);

Note

Like mentioned above, the second argument is an AST node. If you pass a literal or expression, this will pass a new AST node representing that expression.


#stack_pop(string)

This pops a single item off of the given stack. It does not return anything.

Asa
// "some stack" => {0, 4, 2, 1}

#stack_pop("some stack");

// "some stack" => {0, 4, 2}

#stack_last(string)

This returns the last AST node added to the named stack.

stack_last.asa
// "some stack" => {0, 4, 2, 1}

printl(#stack_last("some stack"));  // Prints: 1
Shell
$ asa stack_last.asa
$ ./build/stack_last.asa
1

#print_ast(ast_node)

This prints out the given AST node to the terminal.

printast.asa
x :: 4;

#print_ast(x);
Shell
$ asa print_ast.asa
$ ./build/print_ast.asa
:(Expression_Term){
    6:(Integer_Node){}
}

Note

For compile time equals definitions using ::, it is the AST of its value, rather than its definition.


#parent(ast_node)

This returns the parent node of the given AST node. You can use this for cases like above, where you want the AST node of the definition rather than the right-hand-side.

parent.asa
x :: 4;

#print_ast(#parent(x));
Shell
$ asa parent.asa
$ ./build/parent.asa
x:(Compiler_Define){
    :(Expression_Term){
        6:(Integer_Node){}
    }
}

#print(string)

Different from regular print(); function, in that it prints the given string to the terminal at compile time only. Does nothing at runtime.

compile_time_print.asa
#print("This prints only during compilation");
Shell
$ asa compile_time_print.asa
$ ./build/compile_time_print.asa
This prints only during compilation

#printl(string)

Different from regular printl(); function, in that it prints the given string to the terminal with a newline at compile time only. Does nothing at runtime.

compile_time_printl.asa
#printl("This prints only during compilation");
Shell
$ asa compile_time_printl.asa
$ ./build/compile_time_printl.asa
This prints only during compilation

#if(condition, ast_node)

Evaluates condition as a boolean. If it is "true", the ast_node is included in compilation. If it is "false", it is ignored.

if.asa
// The print statement is included in the compiled program
#if(true, printl("Was true!"));
Shell
$ asa if.asa
$ ./build/if.asa
Was true!
Asa
x :: { i : int = 9 };
#if(1 == 2, x);
// The code `i : int = 9` is not included in the program

#make_directive(name, (ast_arguments), ast_node)

Creates a new custom compiler directive with the given name, arguments, and body.

Asa
#make_directive("custom_add", (A, B), {
    #return A + B;
});

#custom_add(1, 2);  // Returns AST node of {1+2}

#return(ast_node)

Used in conjunction with #make_directive to let the expression result in a given AST node.

Asa
#make_directive("custom_add", (A, B), {
    #return A + B;
});

#context

Returns the AST node of the current context. For example, if used inside a ::asa #make_directive definition, #context will be the node where it is called.

Asa
#make_directive("custom_add", (A, B), {
    #printast(#context);
});

#custom_add(1, 2);  // `#context` refers to this line