Preprocessor

SafeC includes a preprocessor that supports a safe subset of C preprocessing directives. By default, it operates in safe mode, which restricts features that lead to hard-to-debug macro issues. A compatibility mode is available for full C macro support.

Directives

#include

Include other SafeC files or C headers:

c
#include "mymodule.h"          // local include (quoted)
#include <stdio.h>             // system include (angle brackets)

System C headers are imported natively via the CHeaderImporter, which extracts function and typedef declarations from clang -ast-dump=json. See C Interop for details.

#include names headers only. Including a .sc source file is a compile error: a .sc file is a translation unit of its own, and pasting it into another duplicates every definition it holds — multiple-definition link errors, or two diverging copies of its static state. Compile each .sc separately and include its .h; generic code that every user must instantiate lives in the header itself (the standard library's containers are header-only for exactly this reason).

Use -I <dir> to add include search paths.

#define (Object-Like)

Object-like macros define simple text substitutions:

c
#define MAX_SIZE 1024
#define VERSION "2.0"
#define DEBUG

In safe mode, function-like macros are rejected:

c
// ERROR in safe mode:
#define MAX(a, b) ((a) > (b) ? (a) : (b))

Use const variables and generic functions instead:

c
const int MAX_SIZE = 1024;

generic<T: Numeric>
T max(T a, T b) {
    if (a > b) return a;
    return b;
}

#undef

Remove a previously defined macro:

c
#define FEATURE_X
// ... use FEATURE_X ...
#undef FEATURE_X

#pragma once

Prevent a header from being included multiple times:

c
#pragma once

// header contents...

Conditional Compilation

#ifdef / #ifndef

Test whether a macro is defined:

c
#ifdef DEBUG
    printf("debug: x = %d\n", x);
#endif

#ifndef NDEBUG
    assert(x > 0);
#endif

#if / #elif / #else / #endif

General conditional compilation with constant expressions:

c
#if PLATFORM == 1
    // Linux-specific code
#elif PLATFORM == 2
    // macOS-specific code
#else
    // Generic fallback
#endif

Nesting

Conditional blocks can be nested:

c
#ifdef FEATURE_A
    #ifdef FEATURE_B
        // both A and B enabled
    #else
        // only A enabled
    #endif
#endif

#error and #warning

#error message stops compilation with that message; #warning message reports it and carries on. Together with defined(NAME) in #if they guard a header against configurations it doesn't support:

c
#if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32) && !defined(__SAFEC_FREESTANDING__)
#error "unsupported target"
#endif
#ifndef NDEBUG
#warning "debug build"
#endif

Any other #pragma than #pragma once is accepted and ignored.

Predefined Macros

Macro Value Description
__FILE__ String literal Current source file name
__LINE__ Integer literal Current line number
__SIZEOF_POINTER__ 4 or 8 Pointer width of the target
__SAFEC_FREESTANDING__ 1 --freestanding, or any bare-metal/WebAssembly --target alias
__SAFEC_COMPAT_PREPROCESSOR__ 1 --compat-preprocessor is on
__linux__, __APPLE__, _WIN32 1 Hosted builds for that OS (never on bare-metal targets)
__x86_64__/__amd64__, __i386__, __aarch64__ 1 Target architecture
__riscv, __riscv_xlen 1, 32/64 RISC-V targets
__wasm__, __wasm32__/__wasm64__ 1 WebAssembly targets
__arm__, __ARM_ARCH, __ARM_ARCH_PROFILE, __ARM_ARCH_<name>__, __thumb__, __thumb2__ 1, 6/7/8, 'A'/'R'/'M' 32-bit ARM, spelled as clang does (__ARM_ARCH_7EM__, __ARM_ARCH_8M_MAIN__, …)

Unlike C, SafeC deliberately leaves out __TIME__ and __DATE__ to keep builds reproducible: using either is a compile error ("TIME and DATE are disabled in SafeC").

Command-Line Flags

-D <name>[=value]

Define a macro from the command line:

bash
./build/safec program.sc -D DEBUG -D MAX_SIZE=2048 --emit-llvm -o program.ll

-I <dir>

Add an include search directory:

bash
./build/safec program.sc -I ./include -I ../lib/include --emit-llvm -o program.ll

--no-import-c-headers

Disable the native C header import mechanism. With this flag, #include <stdio.h> will not invoke clang for AST extraction. You must provide extern declarations manually.

--compat-preprocessor

Enable compatibility mode with full C preprocessor features (see below).

Safe Mode vs Compatibility Mode

Safe Mode (Default)

The default preprocessor mode restricts features that are common sources of bugs in C:

Feature Safe Mode Rationale
Object-like #define Allowed Simple constant substitution
Function-like macros Rejected Use const + generics instead
## (token pasting) Rejected Obfuscates code
# (stringification) Rejected Obfuscates code
#include Allowed File inclusion is necessary
#ifdef / #if Allowed Conditional compilation is necessary
#pragma once Allowed Header guard

Compatibility Mode (--compat-preprocessor)

When working with legacy C code or headers that require full macro support, enable compatibility mode:

bash
./build/safec legacy.sc --compat-preprocessor --emit-llvm -o legacy.ll

A safeguard package turns it on for its own sources with [build] compat_preprocessor = true (see Package Manager).

In compatibility mode, function-like macros, token pasting, and stringification are all permitted, and the preprocessor defines __SAFEC_COMPAT_PREPROCESSOR__. Headers that offer optional function-like convenience macros guard them with it, so the same header works in both modes — the standard library does this for ASSERT_* (std/test/test.h), RING_STATIC, the logging and panic helpers, and the rest:

c
#ifdef __SAFEC_COMPAT_PREPROCESSOR__   // function-like macro: compat mode only
#define ASSERT_TRUE(e) test_assert_true((e) != 0, #e, __FILE__, __LINE__)
#endif

Function-like macros are hygienic for the locals they declare: a variable declared in a macro body is renamed at each expansion, so it can't capture or shadow a caller's variable of the same name:

c
#define SWAP(a, b) { int tmp = a; a = b; b = tmp; }

int tmp = 1, x = 2;
SWAP(tmp, x);      // expands to { int __hyg1_tmp = tmp; tmp = x; x = __hyg1_tmp; }

Fields of a struct declared in a macro body keep their names.

Compile-Time Alternatives

SafeC provides first-class language features that replace most preprocessor use cases:

C Preprocessor Pattern SafeC Alternative
#define PI 3.14159 const double PI = 3.14159;
#define MAX(a,b) ... generic<T> T max(T a, T b) { ... }
#ifdef DEBUG if const (DEBUG) { ... }
#define ARRAY_SIZE 100 const int ARRAY_SIZE = 100;
Conditional function body if const with consteval conditions

Using language-level constructs instead of macros provides:

  • Type safety: const and generics are type-checked
  • Scoping: const variables respect lexical scope
  • Debuggability: no invisible text substitution
  • IDE support: go-to-definition, rename, hover all work correctly

Example

c
#pragma once
#define VERSION 1

#ifdef DEBUG
    #define LOG_LEVEL 3
#else
    #define LOG_LEVEL 0
#endif

const int MAX_CONNECTIONS = 128;

// Prefer const over #define for typed constants
const double TIMEOUT_SEC = 30.0;

// Prefer generic functions over function-like macros
generic<T: Numeric>
T clamp(T val, T lo, T hi) {
    if (val < lo) return lo;
    if (val > hi) return hi;
    return val;
}