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.

An 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

ModeFormatsBuild contextBest for
activeBranchThe active branch; inactive branches preserved verbatimUsedAlmost everything — the default. Give it your real build context for clean results.
allBranchesAll branches together, best-effortIgnoredLegacy behavior; rarely what you want on conditional-heavy code.
preserveConditionalsOnly code outside conditional regionsIgnoredMaximum 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).

InputMeaningEmpty default
compilerVersionCompiler version as a product name ("Delphi 12"), an alias, or a VERxxx defineThe parser's latest known version
platformTarget-platform enum name (Win32Target, Win64Target, …)Win32Target
definesConditional symbols to define (NAME or NAME=VALUE; folding is name-only)none
includePathsExtra directories searched for {$I} include files that gate conditionalsthe 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
Deterministic by design: with zero configuration the fold still resolves the same way everywhere (latest known version, 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:

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 keyCLI flagNotes
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.