radFormatter / docs / conditional-compilation
Conditional compilation
Code guarded by compiler directives —
{$IFDEF}, {$IF}, {$ELSE},
{$ENDIF} — is the hardest thing any Delphi formatter has to
lay out. This page explains why, what radFormatter guarantees, and how to
steer it with the three conditional modes and a build context.
Why it is hard
A file with {$IFDEF} branches is not one program — it is
several overlapping programs that share the same lines of text. Two branches of
a single {$IFDEF} can end a statement differently, open a block in
one arm and close it in another, or give the same shared body two structurally
different parents depending on which symbols are defined. There is no single
syntax tree that is simultaneously correct for every build, so there is no
single "correct" layout either.
This is a fundamental limit, not a backlog item: no Delphi formatter handles conditional code perfectly, including RAD Studio's own Formatter.exe. radFormatter treats conditionally compiled code as best-effort in every mode and is explicit about the trade-off each mode makes, so you can pick the behavior you want rather than be surprised.
The one guarantee
Whatever mode you choose, radFormatter never joins, splits, aligns, or wraps a line across a conditional boundary. A directive line and the code on either side of it are never merged or reflowed into one another. Combined with the non-destructive safety check (a whitespace-excluded content hash plus a re-lexed token fingerprint), that means conditional code can never be silently corrupted — at worst a region is left unformatted.
The three modes
The strategy is set by conditional.mode in your config (or the
CLI -conditionalMode flag). All three parse; they differ
in how directives are tokenized.
activeBranch — the default
Directives are resolved against a build context (your defines, target platform, compiler version, and include paths), and only the active branch is parsed and formatted. The active code formats robustly; inactive branches are preserved exactly as written, together with their bounding directives — they are not reformatted and may not match the surrounding indentation.
Input — as authored
procedure TLogger.Emit(const S: string);
begin
{$IFDEF VERBOSE}
WriteLn('verbose: ' + S);
{$ELSE}
WriteLn(S);
{$ENDIF}
end;
activeBranch, with VERBOSE defined
procedure TLogger.Emit(const S: string);
begin
{$IFDEF VERBOSE}
WriteLn('verbose: ' + S); // active branch: reindented to structure
{$ELSE}
WriteLn(S); // inactive branch: preserved verbatim
{$ENDIF}
end;
This is the only mode aligned with the parser's core invariant (the parser
is designed to run on a conditional-filtered token stream), which is why it is
the default. For the best result, give it an accurate build context — see
below. With no context it falls back to deterministic defaults (the parser's
latest known compiler version and Win32Target), which is usually
fine but may select a different branch than your build would.
allBranches
Every branch is read together as a single raw stream, with no conditional processing (this was the default in earlier builds). Because mutually exclusive branches are parsed at once, code that is only valid in one specific build can mis-structure the parse — and therefore the layout — of the surrounding region. Reconcilable splits are handled; irreducible ones are preserved verbatim as opaque islands. No build context is used.
allBranches layout you do not like is
best-effort by policy, not a bug. The answer is almost always
“switch to activeBranch” (the default) with a good
build context.preserveConditionals
Every line that holds a conditional directive, or lies between an opener and its matching closer, is left exactly as authored; everything outside conditional regions is reformatted normally. It is the most conservative choice — nothing inside a conditional region is touched — and it is what the pass-through NoOp profile pins. No build context is used.
preserveConditionals — conditional lines untouched, surrounding code reformatted
{$IFDEF LOGGING}
Log( 'hello' ); // inside a conditional region: left verbatim
{$ENDIF}
DoStuff( x,y ); // outside: reformatted normally -> DoStuff(x, y);
Choosing a mode
| Mode | Formats | Build context | Best for |
|---|---|---|---|
activeBranch | The active branch; inactive branches preserved verbatim | Used | Almost everything — the default. Give it your real build context for clean results. |
allBranches | All branches together, best-effort | Ignored | Legacy behavior; rarely what you want on conditional-heavy code. |
preserveConditionals | Only code outside conditional regions | Ignored | Maximum caution — never touch anything inside a directive. |
The build context (activeBranch only)
To decide which branch is active, activeBranch needs to know how
your project builds. Four inputs feed the fold; they live in the
conditional section of your config so the IDE and the CLI resolve
the same branch for a given file (a checked-in setting, never an
environment default that would let format-on-save and a CI -check
disagree).
| Input | Meaning | Empty default |
|---|---|---|
compilerVersion | Compiler version as a product name ("Delphi 12"), an alias, or a VERxxx define | The parser's latest known version |
platform | Target-platform enum name (Win32Target, Win64Target, …) | Win32Target |
defines | Conditional symbols to define (NAME or NAME=VALUE; folding is name-only) | none |
includePaths | Extra directories searched for {$I} include files that gate conditionals | the source file's own directory |
Config — radFormatter.json
{
"conditional": {
"mode": "activeBranch",
"compilerVersion": "Delphi 12",
"platform": "Win64Target",
"defines": ["DEBUG", "MYFEATURE"],
"includePaths": ["./inc"]
}
}
The same, on the CLI
> radFormatter -conditionalMode activeBranch \
-compilerVersion "Delphi 12" -platform Win64Target \
-define DEBUG -define MYFEATURE -includePath ./inc \
source/MyUnit.pas
Win32Target), so the IDE and CLI produce identical output for the
same file.Deriving context from a project (-project)
Rather than list defines and include paths by hand, point radFormatter at a
Delphi project and let it derive the whole context — defines, include
paths, target platform, and a best-effort compiler version — from the
.dproj, resolving the MSBuild condition chain and
$(DCC_Define) accumulation for the selected configuration and
platform.
> radFormatter -project MyApp.dproj -projectConfig Release -projectPlatform Win64 -d source -r
In the IDE, the active project's context is used automatically — the same extraction code backs both, so the IDE and CLI resolve identical context for the same project. A few rules:
- A project context wins. When a project supplies the
context (IDE active project, or CLI
-project/-buildContext), it is used verbatim and the config's conditional context is ignored — a shared default could activate branches your project never builds. The config context is the no-project fallback only. -projectand-buildContextare mutually exclusive (both supply the base). Explicit-define/-includePath/-platform/-compilerVersionflags still layer on top as a per-invocation supplement.- Only conditional-folding inputs are read. Unit-scope names
(
DCC_Namespace) and library search paths are AST concerns and are deliberately not read. -projectis inert unless the mode isactiveBranch(radFormatter prints a warning otherwise).
Undecidable {$IF} and straddles
activeBranch is honest about what it cannot decide. If an
{$IF} / {$ELSEIF} controlling expression cannot be
resolved from the build context (for example, a comparison against a constant
the context does not pin down), radFormatter preserves the whole region
verbatim — every branch, opener through closer — instead of
silently committing to one.
Undecidable {$IF} — whole region preserved
{$IF MyTuningConstant > 10} // value not known from the build context
UseLargeBuffers;
{$ELSE}
UseSmallBuffers;
{$ENDIF}
Defined-ness, on the other hand, is always decidable:
{$IFDEF Sym} and {$IF DEFINED(Sym)} fold normally even
when Sym is unknown (an unknown symbol is simply not defined).
The hardest case is a straddle — a single shared body
that has two structurally different parents across a directive boundary (a
shared begin..end that is an anonymous-method body in one branch and
a for-loop body in the other, say). These cannot be reconciled into
one tree, so they are kept as verbatim opaque islands: fingerprint-stable and
safe, with indentation left best-effort.
Include files ({$I})
When a {$I foo.inc} gates conditionals, radFormatter expands the
include's tokens internally to evaluate directives — but the output shows
only the {$I} directive itself, never the spliced
include body. Includes resolve against the source file's own directory automatically;
use includePaths (or a project) to cover includes that live elsewhere.
With no path and no include dirs, an unresolved {$I} simply passes
through as text.
Config & CLI reference
The full option and flag definitions live in the configuration options reference and the CLI reference; the conditional-specific subset is collected here.
| Config key | CLI flag | Notes |
|---|---|---|
conditional.mode | -conditionalMode <mode> | activeBranch (default) / allBranches / preserveConditionals |
conditional.compilerVersion | -compilerVersion <ver> | activeBranch only; e.g. "Delphi 12" or VER360 |
conditional.platform | -platform <name> | activeBranch only; e.g. Win32Target, Win64Target |
conditional.defines | -define <NAME[=VALUE]> | activeBranch only; repeatable on the CLI |
conditional.includePaths | -includePath <dir> | activeBranch only; repeatable on the CLI |
| — | -buildContext <file> | Build-context JSON as the base; mutually exclusive with -project |
| — | -project <file.dproj> | Derive the context from a Delphi project; activeBranch only |
| — | -projectConfig <name> | Project build config to read with -project (e.g. Debug, Release) |
| — | -projectPlatform <name> | Project platform to read with -project (e.g. Win32, Win64) |
See also Known limitations
for the short version, and Profiles for how each
built-in profile sets conditional.mode.