Preprocessor
The preprocessor runs before the lexer and produces preprocessed source the rest of the compiler operates on. It exists alongside the language proper rather than as part of it — its job is meta (file inclusion, conditional compilation, simple macro substitution) rather than semantic.
File inclusion
Section titled “File inclusion”#include <file.xc> // search the system / -I paths only#include "file.xc" // search next to the current file first
#import <Stdio.xc> // include-once form, same search rules#import "Sprite.xc"The two quote forms select different search orders, matching C/C++ convention:
"file.xc"— look next to the file doing the include first, then fall through to the system directories and any-Ipaths. Use this for files that live alongside your source.<file.xc>— skip the current source’s directory and go straight to system /-Ipaths. Use this for library headers so a same-named file in your project can’t silently shadow the real library.
Filename matching is case-sensitive even on case-insensitive filesystems. #import <Sort.xc> will never match a sibling sort.xc, even on macOS’s HFS+/APFS — this prevents a classic collision where a user names their program after a library they import.
#import is identical to #include except that the named file is included only once across the entire compilation unit. Library headers should always use #import so a user can #import them freely without two copies of the contents ending up in scope.
Importing and promoting a class: #use
Section titled “Importing and promoting a class: #use”#use is sugar for the common case of “import a library class and let me call its static methods bare.” It expands to #import "ClassName.xc" followed by use ClassName; (the language-level directive) — one line replaces two.
#use Stdio // == #import "Stdio.xc" + use Stdio;#use Math#use <Time> // angle-bracket and quote forms also accepted#use "Sprite"
void main(void) { printf("answer = %u\n", 42); // resolves to Stdio.printf u8 r = rand((u8)100); // resolves to Math.rand}The class name may be written bare (#use Stdio), in angle brackets (#use <Stdio>), or in quotes (#use "Stdio"). The trailing .xc extension is stripped automatically if you include it. The same < > vs " " search-order rule as #include / #import applies to the underlying file lookup.
After #use Stdio, calling printf("hi") resolves the same way as Stdio.printf("hi") would — the receiver class is implied. This is bare-call sugar only; explicit Klass.method(...) calls, free functions, and local variables are unaffected. The full resolution rules — overload scoring, ambiguity diagnostics when multiple use’d classes both expose a method with the same name, and the file-local scope of the promotion — live with the language-level use directive.
Macros
Section titled “Macros”#define DBL(x) (double(x))#define ZP_BASE $80#define ENABLE_DOUBLE 1#define introduces a macro, optionally taking comma-separated arguments. At each later occurrence, the comma-separated actuals are substituted for the placeholders. Macros can be removed with #undef.
Variadic macros
Section titled “Variadic macros”A macro whose last parameter is ... is variadic; substitute the variadic tail with __VA_ARGS__ in the body:
#define LOG(level, ...) Stdio.printf("[" level "] " __VA_ARGS__)
LOG("warn", "value=%d\n", x);// expands to: Stdio.printf("[" "warn" "] " "value=%d\n", x);This follows the standard C model. GNU’s , ##__VA_ARGS__ comma-swallow is supported too — an
empty variadic tail removes the comma before it, so LOG("hi") expands cleanly:
#define LOG(fmt, ...) Stdio.printf(fmt, ##__VA_ARGS__)
LOG("done\n"); // → Stdio.printf("done\n") — no dangling commaLOG("x=%d\n", x); // → Stdio.printf("x=%d\n", x)Stringize (#) and token paste (##)
Section titled “Stringize (#) and token paste (##)”#param replaces the parameter with a string literal of the argument as written.
a ## b pastes two tokens into one.
Both operate on the argument unexpanded, which is why the idiomatic form uses two levels: the outer macro expands its arguments normally, and only the inner one applies the operator.
#define CAT2(a,b) a##b#define CAT(a,b) CAT2(a,b)#define STR2(x) #x#define STR(x) STR2(x)#define VER 7
CAT(x, VER) // → x7 — VER expanded first, then pastedCAT2(x, VER) // → xVER — pasted rawSTR(VER) // → "7"STR2(VER) // → "VER"The usual application is building a name from its parts — an ABI symbol from a version number, say — so that a mismatch fails at link time, by name, rather than surfacing later as a wild jump through a stale vtable.
Substitution is token-aware
Section titled “Substitution is token-aware”A parameter is substituted only where it appears as a whole token. It is not replaced inside a longer identifier, and not inside a string literal:
#define ABS_OK(a) a + abs_val // `a` does NOT rewrite `abs_val`#define INSTR(a) "a is here" // `a` does NOT rewrite the stringLikewise, a comma inside a string argument is part of that argument, not a separator:
P("a,b") passes one argument.
Conditional compilation
Section titled “Conditional compilation”#ifndef ENABLE_DOUBLE #define ENABLE_DOUBLE 1#endif
#if ENABLE_DOUBLE // …double-precision code…#elif ENABLE_FLOAT // …single-precision fallback…#else #error neither double nor float enabled#endif
#ifdef DEBUG Stdio.print("debug build\n");#endif#ifdef / #ifndef test for the presence (or absence) of a macro definition. #if evaluates a constant integer expression. The chain may include any number of #elif clauses and an optional #else, terminated by #endif.
Macros may be defined on the command line with -D:
xcc app.xc -DENABLE_DOUBLE=0 -DDEBUG -o app.xexPredefined macros
Section titled “Predefined macros”The driver predefines an ARCH_<arch> sentinel for the target being built, so a
single source file can serve every backend. This is how the standard library
keeps one copy of each class:
| Target | Defined |
|---|---|
-A arm64 | ARCH_arm64 |
-A x86_64 | ARCH_x86_64 |
-A win64 | ARCH_x86_64 and ARCH_win64 |
-A arm9 | ARCH_arm9 |
-A m68k | ARCH_m68k |
-A 6502 | ARCH_6502 |
Windows defines both because it is the x86-64 instruction set: ISA-guarded
code (inline assembly, register names) keys off ARCH_x86_64, while
OS-specific code (calling convention, system calls) keys off ARCH_win64.
#if ARCH_6502 // a byte-oriented path, and no i64 arithmetic#elif ARCH_win64 // kernel32, and the Microsoft x64 calling convention#else // the 64-bit hosts#endifThreading headers use exactly this to fail loudly rather than silently: on
xt6502 and m68k, Thread.xc is an #error, not a stub.
Also predefined: BANK_DATA, BANK_CODE, BANK_C (selector constants for the
bank(…) builtin), XTC_POINTER_WIDTH, and — on 6502 targets — a set of
layout-derived addresses (XT_PRINTF_BUF, XTC_HP_LO …) that inline assembly
in the runtime needs, since the preprocessor cannot test a memory-model
property directly. Those come from the active .lnk file, so a custom layout
changes them.
Diagnostics from source
Section titled “Diagnostics from source”#warning need to implement doFrobble()#error no supported target selected#warning produces a compile-time warning containing the text and lets the build continue. #error produces a fatal error and stops compilation. Both honour conditional compilation — use them inside #if chains to enforce build-configuration invariants.