The preprocessor
Split a program across several files with #INCLUDE, #INSERT and IncludeFile, and compile lines only for the targets that need them with #IF and the target profiles.
In one line#
The preprocessor reads your program as lines of text, before the compiler does. It has two jobs: bring in the lines of other files, and keep or drop lines depending on where the program is compiled. Everything on this page is settled before a single statement is compiled.
Project modules: where the files come from#
An included file is a BASIC module of the project, nothing else. The compiler never reads a disk path or a URL.
- Create one in the script editor with File › New Script · BASIC
Module. It is named
modules/module_1.bas(thenmodule_2.bas…) and starts withOPTION EXPLICIT. - Import one: File › Import… with a
.basor.agcfile adds it as a module named after the file. (When the editor is open on an entity's script, Import replaces that script instead.) - Rename it (right-click, Rename) to place it in a folder. The name is
the path: a module renamed
lib/math.basis included with#INCLUDE "lib/math.bas". - A module never runs on its own. It is saved with the project, and exported with it to HTML and Bevy.
- After
#INCLUDE ",#INSERT "orIncludeFile(", the editor completes the module names.
#INCLUDE, #INSERT and IncludeFile#
#INCLUDE "<path>"
#INSERT "<path>"
IncludeFile("<path>")
The three put the lines of a module in place of the directive. They differ on one point:
| Directive | How many times |
|---|---|
#INCLUDE | once per program. Later requests for the same file are skipped, whichever file makes them. |
#INSERT | every time it appears |
IncludeFile | every time it appears (PureBasic's name for #INSERT) |
The module lib/math.bas declares FUNCTION Clamp(value, low, high):
FUNCTION Clamp(value, low, high)
IF value < low THEN EXITFUNCTION low
IF value > high THEN EXITFUNCTION high
ENDFUNCTION value
The main program can ask for it twice. #INCLUDE brings it in once:
#INCLUDE "lib/math.bas"
#INCLUDE "lib/math.bas"
Console(Str(Clamp(15, 0, 10))) ' 10
With #INSERT, the function would be declared twice, which is refused:
#INSERT "lib/math.bas"
#INSERT "lib/math.bas"
The error is "Ambiguous BASIC overload: Clamp", reported in lib/math.bas.
Use
#INCLUDEfor a file of declarations (functions,TYPEs,GLOBALs). Use#INSERTfor a piece of code you want repeated.
The module snippets/banner.bas is one line of code:
Console("==========")
#INSERT "snippets/banner.bas"
Console(" LEVEL 1")
#INSERT "snippets/banner.bas"
The banner line is printed twice, above and below LEVEL 1.
- One directive per line, alone on it. A trailing
'comment is fine. - ⚠️ Not after a
:.x = 1 : #INCLUDE "lib/math.bas"is not refused, but the directive stays in the program as text and the program cannot start. - The path is in double quotes. Parentheses are optional on all three:
IncludeFile "lib/math.bas"works too. - Keywords and paths ignore case:
#include "LIB/Math.bas"is the same line. - It is plain text insertion. A directive inside a
FUNCTIONputs the lines there. A directive inside a runtimeIFis inserted whatever the condition: theIFonly decides whether those lines run. To leave a file out, use#IF, below. #INSERTdoes not count for#INCLUDE: a file inserted earlier is still brought in by a later#INCLUDE, once.
Paths#
A path is looked up in two places, in this order:
- next to the file that holds the directive (its folder);
- from the root of the project.
/ and \ both separate folders, and .. goes up one folder. A path that
matches no module is a compile error, on the directive's line: "BASIC include
file not found: <path>".
The module game/player.bas declares FUNCTION ClampHealth(hp) and needs
lib/math.bas, one folder up:
#INCLUDE "../lib/math.bas"
FUNCTION ClampHealth(hp)
ENDFUNCTION Clamp(hp, 0, 100)
#INCLUDE "game/player.bas"
#INCLUDE "lib/math.bas"
Console(Str(ClampHealth(140))) ' 100
lib/math.bas is asked for twice, by game/player.bas and by the main program,
and is included once.
Cycles#
Two files that include each other form a cycle.
- With
#INCLUDE, a file already included is skipped, so the cycle simply stops. No error. - With
#INSERTorIncludeFile, a cycle is a compile error that names the chain: "Cyclic BASIC include: loop/a.bas -> loop/b.bas -> loop/a.bas".
The modules loop/a.bas and loop/b.bas insert each other:
#INSERT "b.bas"
#INSERT "a.bas"
#INSERT "loop/a.bas"Errors inside a module#
An error in an included line is reported with the module's name and its own line number, not with a line of the main program. Double-clicking it in the console opens that module. See Errors for the console format.
Conditional compilation#
#IF <condition>
<lines>
[#ELSEIF <condition>]
<lines>
[#ELSE]
<lines>
#ENDIF
#IFDEF <symbol>
#IFNDEF <symbol>
#DEFINE <symbol>
#UNDEF <symbol>
CompilerIf <condition>
CompilerElseIf <condition>
CompilerElse
CompilerEndIf
#IF keeps the lines of the first branch whose condition is true, and drops the
others. A condition tests symbols: names that are defined or not, with no
value.
#IF ENGINE AND DEBUG
Console("editor build")
#ELSEIF HTML
Console("web export")
#ELSE
Console("other target")
#ENDIF
In the editor this prints editor build. The HTML export of the same program
contains only the web export line.
| Spelling | Meaning |
|---|---|
#IF, #ELSEIF (or #ELIF), #ELSE, #ENDIF | the branches |
#IFDEF X | the same as #IF X, for one symbol only |
#IFNDEF X | the same as #IF NOT X, for one symbol only |
CompilerIf, CompilerElseIf, CompilerElse, CompilerEndIf | the PureBasic names for #IF, #ELSEIF, #ELSE, #ENDIF |
What a condition may contain#
| You write | True when |
|---|---|
WEB | the symbol WEB is defined |
DEFINED(WEB), DEFINED WEB | the same |
NOT WEB, !WEB | WEB is not defined |
WEB AND DEBUG, WEB && DEBUG | both are |
WEB OR BEVY, WEB || BEVY | at least one is |
DEBUG && (WEB OR BEVY) | parentheses group: DEBUG, and one of WEB or BEVY |
TRUE, FALSE | always, never |
NOTbinds tighter thanAND, andANDtighter thanOR. Add parentheses when in doubt.- An unknown symbol is simply false. A typo in a symbol name is not an error: check the spelling.
- Nothing else is allowed: no number, no string, no comparison, no constant.
#IF 1,#IF LEVEL = 3and PureBasic'sCompilerIf #PB_Compiler_OS = #PB_OS_Windowsare all refused with "Invalid BASIC compiler condition". PureBasic's two-argumentDefinedis refused too: writeDEFINED(X). - Directives and symbols ignore case:
#if webworks.
Rules for the blocks#
- One directive per line. A trailing
'comment is fine. - Blocks nest to any depth.
- A block opens and closes in the same file. An
#IFin the main program cannot end in a module. - The lines of a dropped branch are blanked, never compiled. They may contain anything, even text that is not BASIC, and the line numbers of the rest stay right.
- Directives written inside a
REMSTART…REMENDor/*…*/comment are ignored.
| Message | Cause |
|---|---|
#IF requires #ENDIF | a block left open at the end of its file |
#ENDIF without #IF, #ELSE without #IF, #ELSEIF without #IF | no open block |
#ELSEIF after #ELSE, Duplicate #ELSE | wrong order |
Invalid BASIC compiler condition: … | a condition with a number, a comparison, or #IFDEF with more than one symbol |
#DEFINE requires one symbol | #DEFINE with a value |
#DEFINE and #UNDEF#
#DEFINE adds a symbol, #UNDEF removes one. Use them for your own switches.
#DEFINE CHEATS
#IF DEFINED(CHEATS) AND NOT RELEASE
Console("cheat keys on")
#ENDIF
In the editor this prints cheat keys on. In an export, RELEASE is defined
and the line is dropped.
- A symbol has no value.
#DEFINE LEVEL 3is refused, and there are no macros. - It applies from its line on, for the rest of the program, files included
after it too. A module full of
#DEFINEs, included first, acts as a settings file. - A
#DEFINEinside a dropped branch does nothing. #UNDEFalso removes a profile symbol: after#UNDEF DEBUG,#IFDEF DEBUGis false even in the editor.
Target profiles#
Each place that compiles a program starts with a set of symbols, its profile:
| Where the program is compiled | Symbols defined |
|---|---|
| in the editor: Play, preview | ENGINE, WEB, DEBUG |
| the HTML export | HTML, WEB, RELEASE |
| the Bevy export | BEVY, RELEASE |
#IF DEBUG
GLOBAL lives AS integer = 99
#ELSE
GLOBAL lives AS integer = 3
#ENDIF
Console(Str(lives)) ' 99 in the editor, 3 in an export
DEBUGis only in the editor, andRELEASEin every export. That makes them the natural switch for test shortcuts.- Use
#IF ENGINEto keep code that only works in the editor out of the exports. - The compiler also knows two narrower Bevy profiles,
BEVY NATIVE RELEASEandBEVY WASM WEB RELEASE. At the time of writing the Bevy export compiles with the general one, so testBEVY, notNATIVEorWASM.
It runs before everything else#
The preprocessor works in one pass, top to bottom:
#IFblocks and#DEFINE/#UNDEFare settled line by line;- the kept
#INCLUDE,#INSERTandIncludeFilelines are replaced by their files, which go through the same two steps; - only then does the compiler read the result:
@expose,OPTION EXPLICIT, commands and all the rest.
Three consequences:
- A file in a dropped branch is never read. It may even be missing: no "not found" error.
- A condition cannot test a variable or a constant. They do not exist yet. Only symbols do.
@exposelines are handled after this step. See Variables and types.
#IF BEVY
#INCLUDE "bevy/only.bas"
#ENDIF
Console("compiled") ' compiled
In the editor, bevy/only.bas does not need to exist.
Compared with classic dialects#
| Dialect | What it has | In aukimi |
|---|---|---|
| AGK | #include (once) and #insert (every time) | the same words with the same meaning. The files are project modules. |
| PureBasic | IncludeFile (every time), XIncludeFile (once) | IncludeFile is the same. XIncludeFile is refused: write #INCLUDE. |
| PureBasic | CompilerIf with constants and comparisons | CompilerIf takes symbols only |
| C, FreeBASIC | #define NAME value, macros | #DEFINE defines a symbol without a value. No macros. |