Macros & Preprocessor
Zym includes a built-in preprocessor that runs before compilation. It supports #define macros, function-like macros, multi-line block macros, conditional compilation, and expression evaluation — giving you compile-time code generation and configuration without runtime overhead.
Overview
The preprocessor processes your source code before the compiler sees it. Directives start with # at the beginning of a line (leading whitespace is allowed). The preprocessor strips comments, evaluates directives, expands macros, and passes the result to the compiler.
| Directive | Purpose |
|---|---|
#define | Define an object-like or function-like macro (single line) |
##define / ##enddefine | Define a multi-line block macro |
#undef | Remove a macro definition |
#if / #elif | Conditional compilation with expression evaluation |
#ifdef / #ifndef | Conditional compilation based on macro existence |
#else / #endif | Alternative branch / end conditional block |
#error | Abort compilation |
Object-like Macros
The simplest form of macro: a name that expands to a value. Anywhere the macro name appears in code, it is replaced with the defined value before compilation.
#define MAX_SIZE 100 #define GREETING "Hello, World" #define PI 3.14159 var arr = [null] * MAX_SIZE // expands to: [null] * 100 print(GREETING) // expands to: print("Hello, World") var circumference = 2 * PI * r
Flag macros
A macro can be defined without a value — it simply “exists” and can be tested with #ifdef or defined().
#define DEBUG
#define VERBOSE
#ifdef DEBUG
print("Debug mode is on")
#endif
Redefining macros
Defining a macro with the same name replaces the previous definition. Use #undef to remove a macro entirely.
#define MODE 1 // MODE expands to 1 #define MODE 2 // MODE now expands to 2 #undef MODE // MODE is no longer defined
Function-like Macros
Macros can take parameters, making them act like inline templates. The parameter list follows the macro name immediately (no space before the opening parenthesis).
#define SQUARE(x) x * x
#define ADD(a, b) a + b
#define MAX(a, b) if (a > b) { a } else { b }
var result = SQUARE(5) // expands to: 5 * 5
var sum = ADD(3, 4) // expands to: 3 + 4
Argument expansion
Arguments are expanded before substitution into the macro body. This means you can pass macro names or expressions as arguments and they will be resolved.
#define VALUE 10 #define DOUBLE(x) x + x DOUBLE(VALUE) // VALUE expands to 10, then: 10 + 10 DOUBLE(3 + 2) // expands to: 3 + 2 + 3 + 2
Multiple parameters
Function-like macros can have any number of parameters, separated by commas.
#define CLAMP(val, lo, hi) if (val < lo) { lo } else if (val > hi) { hi } else { val }
#define LERP(a, b, t) a + (b - a) * t
var clamped = CLAMP(x, 0, 100)
var mid = LERP(0, 10, 0.5)
Block Macros (Multi-line)
For macros that span multiple lines, use the ##define / ##enddefine syntax (double hash). Everything between the opening directive and ##enddefine becomes the macro body, preserving line breaks.
##define SETUP_PLAYER var health = 100 var mana = 50 var level = 1 func heal(amount) { health = health + amount if (health > 100) health = 100 } ##enddefine // Use it — expands to the full block SETUP_PLAYER heal(30)
Block macros with parameters
Block macros can also take parameters, just like single-line function macros.
##define MAKE_COUNTER(name, start) var name = start func increment_##name() { name = name + 1 } func get_##name() { return name } ##enddefine MAKE_COUNTER(score, 0) MAKE_COUNTER(lives, 3)
Line Continuation
A backslash (\) at the end of a line joins it with the next line. This lets you split long single-line #define directives across multiple lines for readability.
#define LONG_MACRO(a, b, c) \
a + b + c
// Equivalent to: #define LONG_MACRO(a, b, c) a + b + c
##define / ##enddefine) over line continuations. They are easier to read and maintain.Conditional Compilation
Conditional directives let you include or exclude code based on whether macros are defined and what values they hold. The preprocessor evaluates conditions and only passes the active branch to the compiler — excluded code is completely removed.
#ifdef / #ifndef
Test whether a macro is defined (or not defined).
#define DEBUG
#ifdef DEBUG
print("Debug: initializing...")
#endif
#ifndef RELEASE
print("Not a release build")
#endif
#if / #elif / #else
Evaluate expressions to decide which code to include. Supports integer comparisons, logical operators, and the defined() function.
#define VERSION 3
#if VERSION == 1
print("Version 1")
#elif VERSION == 2
print("Version 2")
#elif VERSION >= 3
print("Version 3 or later")
#else
print("Unknown version")
#endif
Expression operators
The following operators are available inside #if and #elif expressions:
| Operator | Meaning | Example |
|---|---|---|
== | Equal | #if VERSION == 2 |
!= | Not equal | #if MODE != 0 |
&& | Logical AND | #if DEBUG && VERBOSE |
|| | Logical OR | #if A || B |
! | Logical NOT | #if !RELEASE |
defined() | Check if macro exists | #if defined(FEATURE_X) |
( ) | Grouping | #if (A || B) && C |
defined() function
The defined() function returns 1 if a macro is defined, 0 otherwise. It can be used with or without parentheses.
#define FEATURE_A
#if defined(FEATURE_A) && !defined(FEATURE_B)
// Only FEATURE_A is enabled
print("Feature A only")
#endif
// Without parentheses
#if defined FEATURE_A
print("Feature A is defined")
#endif
Nesting conditionals
Conditional blocks can be nested. Inner conditions are only evaluated if all outer conditions are active.
#define PLATFORM 1
#define DEBUG
#if PLATFORM == 1
print("Platform 1")
#ifdef DEBUG
print("Platform 1, debug mode")
#endif
#elif PLATFORM == 2
print("Platform 2")
#endif
Undefining Macros
#undef removes a previously defined macro. After undefining, the name is no longer expanded and #ifdef will be false.
#define TEMP 42 var x = TEMP // expands to: var x = 42 #undef TEMP var y = TEMP // TEMP is no longer a macro — treated as an identifier #ifdef TEMP // This block is skipped — TEMP is no longer defined #endif
#error
The #error directive immediately aborts compilation. Use it inside conditional blocks to enforce configuration requirements.
#ifndef PLATFORM
#error
#endif
// Compilation only reaches here if PLATFORM is defined
Macro Expansion
Macros are expanded recursively — if a macro body contains another macro name, that name is expanded too. The preprocessor includes an infinite-recursion guard: if a macro references itself (directly or through a chain), the self-reference is left unexpanded.
#define A B + 1 #define B 10 var x = A // A → B + 1 → 10 + 1 // x is 11
#define FOO FOO + 1 var x = FOO // FOO expands once, but the inner FOO is NOT expanded again // Result: FOO + 1 (FOO treated as identifier in expansion)
Expansion in conditionals
Macros inside #if and #elif expressions are expanded before the expression is evaluated. This means you can use macros in condition expressions.
#define MAJOR 2
#define MINOR 5
#define VERSION MAJOR
#if VERSION == 2
print("Major version 2") // this branch is taken
#endif
Comment Handling
The preprocessor strips comments before processing directives and expanding macros. Both line comments (//) and block comments (/* */) are removed, while preserving line numbers for accurate error reporting.
#define VALUE 42 // this comment is stripped before the macro is stored /* Block comments are also stripped before preprocessing */ var x = VALUE // expands to 42
Comments inside strings are not stripped — string literals are preserved exactly as written.
Common Patterns
Feature flags
Use macros as compile-time feature toggles to include or exclude functionality.
#define ENABLE_LOGGING #define ENABLE_METRICS func processRequest(req) { #ifdef ENABLE_LOGGING log("Processing: " + str(req)) #endif var result = handle(req) #ifdef ENABLE_METRICS recordMetric("request_processed") #endif return result }
Platform-specific code
#define PLATFORM 1 // 1 = desktop, 2 = mobile, 3 = web #if PLATFORM == 1 func getInput() { return readKeyboard() } #elif PLATFORM == 2 func getInput() { return readTouch() } #else func getInput() { return readEvent() } #endif
Code generation with block macros
Block macros are powerful for generating repetitive boilerplate code.
##define MAKE_ACCESSOR(field) func get_##field(obj) { return obj.field } func set_##field(obj, value) { obj.field = value } ##enddefine struct Player { name; health; score } MAKE_ACCESSOR(name) MAKE_ACCESSOR(health) MAKE_ACCESSOR(score)
Configuration constants
#define MAX_PLAYERS 16 #define TICK_RATE 60 #define MAP_WIDTH 1024 #define MAP_HEIGHT 768 var players = [] for (var i = 0; i < MAX_PLAYERS; i = i + 1) { push(players, null) }
Guard patterns
Use #ifndef to prevent double-definition, or #error to enforce requirements.
// Ensure a required config is set #ifndef API_VERSION #error #endif // Default values #ifndef MAX_RETRIES #define MAX_RETRIES 3 #endif
Directive Reference
| Directive | Syntax | Description |
|---|---|---|
#define |
#define NAME value |
Define an object-like macro that expands to value |
#define |
#define NAME(a, b) body |
Define a function-like macro with parameters |
##define |
##define NAME |
Define a multi-line block macro (with optional parameters) |
#undef |
#undef NAME |
Remove a macro definition |
#if |
#if expression |
Include following code if expression is non-zero |
#ifdef |
#ifdef NAME |
Include following code if NAME is defined |
#ifndef |
#ifndef NAME |
Include following code if NAME is not defined |
#elif |
#elif expression |
Alternative branch with expression check |
#else |
#else |
Alternative branch (no condition) |
#endif |
#endif |
End a conditional block |
#error |
#error |
Abort compilation immediately |
See also: Language Guide — Embedding Guide — Continuations