Walk-through flang – Part 2
In the previous installment of this series we saw basically how to install flang and we ran a simple smoke test. In this post we will see a high level overview of what happens when we compile a Fortran program using flang. We will also compare it with what usually happens with clang.
Driver
The process of obtaining a program, from source code to something that we can execute in some system, has been traditionally called compilation. But actually it is a process that involves several steps. The set of tools that allows us to perform these steps is usually called a toolchain. In general, nothing prevents us from using each tool individually when building a program but commonly a tool called driver is used instead. The driver knows how the toolchain works and how it has to invoke the different tools in order to achieve the expected result.
In LLVM the current driver for C/C++ is called clang. Unfortunately this is confusing, because clang can also act as a compiler (when the first option is -cc1
) and as an assembler (when the first option is -cc1as
). When we need to distinguish between clang the driver and clang the compiler/assembler, we will call the former clang (or the driver) and the latter cc1/cc1as.
When clang (the driver) runs, it analyzes its command line. Depending on the flags it knows it has to do more or less steps. By default a C/C++ driver has to do the following steps: preprocess, compile, assemble and link. Clang can do a few more specific steps but these are not relevant. Some steps may have to be omitted, for instance a preprocessed file (usually with extension .i
or .ii
) does not have to be processed. If the command line includes -c
, no linking happens, if the command line includes -S
the code is not assembled so the output of the driver is just assembly code. Some flags are only relevant for some part of the whole process: some apply only to the preprocessor (like -D and -I flags for macros and include paths), others apply only to the compilation itself (like -W
flags for warnings) and others apply to the link step (like -L
or -l
for library paths and libraries). The driver by default behaves like the gcc
driver. There is also clang-cl
which behaves like the cl.exe
driver of Microsoft Visual Studio C/C++ .
Entry point: the driver tool
The entry point of the driver is in llvm/tools/clang/tools/driver/driver.cpp
. It does a few checks to see if it is cc1
or cc1as
and if it is not it will create a Compilation
and it will execute it. And this is where all the magic happens. Everything else in that file just takes care of handling the errors that might have happened during compilation. in the snippet below argv
is the argv
of the main
function. Line 459 below simply checks if the compilation C
has been created successfully, if so it executes it.
456
457
458
459
460
std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(argv));
int Res = 0;
SmallVector<std::pair<int, const Command *>, 4> FailingCommands;
if (C.get())
Res = TheDriver.ExecuteCompilation(*C, FailingCommands);
The long journey of bulding a compilation
The intriguing function BuildCompilation
is defined in llvm/tools/clang/lib/Driver/Driver.cpp
. Note that this is inside lib
which means this code belongs ot the reusable and modular parts of clang (so if you need a C/C++ compiler, you can use the classes and functions defined in lib
to build your personalised C/C++ compiler). The file driver.cpp
inside tools
is just a tool that uses these components.
One of the first things BuildCompilation
does is determining what the driver is expected to do.
Detecting the mode of the driver
Detecting the mode of the driver lets us know whether we have to behave like a gcc-style driver, cl.exe driver or flang driver. This impacts how command options are analyzed and also may impact some of the steps done by the driver.
568
569
570
// We look for the driver mode option early, because the mode can affect
// how other options are parsed.
ParseDriverMode(ClangExecutable, ArgList.slice(1));
This function infers the mode of the driver, the set of modes it supports are described in llvm/tools/clang/include/clang/Driver/Driver.h
and by default the GCCMode
is chosen.
71
72
73
74
75
76
77
enum DriverMode {
GCCMode,
GXXMode,
CPPMode,
CLMode,
FortranMode
} Mode;
If you follow this function you will see that it does this
94
95
96
void Driver::ParseDriverMode(StringRef ProgramName,
ArrayRef<const char *> Args) {
auto Default = ToolChain::getTargetAndModeFromProgramName(ProgramName);
Function getTargetAndModeFromProgramName
is defined in llvm/tools/clang/lib/Driver/ToolChain.cpp
.
166
167
168
169
std::pair<std::string, std::string>
ToolChain::getTargetAndModeFromProgramName(StringRef PN) {
std::string ProgName = normalizeProgramName(PN);
const DriverSuffix *DS = parseDriverSuffix(ProgName);
The function parseDriverSuffix
tries hard to infer the driver mode in many different ways but eventually calls FindDriverSuffix
.
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
const DriverSuffix *parseDriverSuffix(StringRef ProgName) {
// Try to infer frontend type and default target from the program name by
// comparing it against DriverSuffixes in order.
// If there is a match, the function tries to identify a target as prefix.
// E.g. "x86_64-linux-clang" as interpreted as suffix "clang" with target
// prefix "x86_64-linux". If such a target prefix is found, it may be
// added via -target as implicit first argument.
const DriverSuffix *DS = FindDriverSuffix(ProgName);
if (!DS) {
// Try again after stripping any trailing version number:
// clang++3.5 -> clang++
ProgName = ProgName.rtrim("0123456789.");
DS = FindDriverSuffix(ProgName);
}
if (!DS) {
// Try again after stripping trailing -component.
// clang++-tot -> clang++
ProgName = ProgName.slice(0, ProgName.rfind('-'));
DS = FindDriverSuffix(ProgName);
}
return DS;
}
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
const DriverSuffix *FindDriverSuffix(StringRef ProgName) {
// A list of known driver suffixes. Suffixes are compared against the
// program name in order. If there is a match, the frontend type is updated as
// necessary by applying the ModeFlag.
static const DriverSuffix DriverSuffixes[] = {
{"clang", nullptr},
{"clang++", "--driver-mode=g++"},
{"clang-c++", "--driver-mode=g++"},
{"clang-cc", nullptr},
{"clang-cpp", "--driver-mode=cpp"},
{"clang-g++", "--driver-mode=g++"},
{"clang-gcc", nullptr},
{"clang-cl", "--driver-mode=cl"},
{"cc", nullptr},
{"cpp", "--driver-mode=cpp"},
{"cl", "--driver-mode=cl"},
{"++", "--driver-mode=g++"},
{"flang", "--driver-mode=fortran"},
};
for (size_t i = 0; i < llvm::array_lengthof(DriverSuffixes); ++i)
if (ProgName.endswith(DriverSuffixes[i].Suffix))
return &DriverSuffixes[i];
return nullptr;
}
What all this means? It means that if we invoke the driver as flang
it will configure itself in Fortran mode. We are not seeing it in this chapter, but when the driver is in Fortran mode it adds a few extra libraries that are required at runtime by Fortran programs.
Analysis of the command line
Let's go back to BuildCompilation
. At some point the arguments of the driver are processed and a set of inputs is constructed.
669
670
671
// Construct the list of inputs.
InputList Inputs;
BuildInputs(C->getDefaultToolChain(), *TranslatedArgs, Inputs);
The function BuildInputs
is a bit complicated basically because, even if we know the mode of the driver, we allow passing inputs of different kind (source, objects, archives, etc.) or if they are source code, of different languages. This means that an invocation like flang -c myfileA.c myfileB.f90
the driver must compile myfileA.c
as a C file and myfileB.f90
as a Fortran file. This is done using the extension file. Sadly things are not that easy as there are positional flags, like -x, that can override the meaning of the later files (so no checking of the extension occurs), but we do not need to know so much detail. The function that checks the extension is in Types.cpp
and is called lookupTypeForExtension
. It is a bit long to paste it all so I shortened it a bit.
For the case we care, which is basically Fortran, we see that there is a plethora of extensions assumed to be Fortran. Files with .f
are not to be preprocessed, while files with .F
are to be preprocessed. All the cases are there to accomodate names that historically have been given to Fortran files, including those that encode the Fortran version like .f95
or .f03
. BuildInputs
in Driver.cpp
, above, will build a list of pairs 〈input file, file type〉 that encodes the type of input we found.
It's time to take action
With the list of inputs built in BuildInputs
it is time to build the actions that the driver has to do, based on the arguments of the command line. This is done in the function BuildActions
. A first step of this function is determine the final phase of the driver. As mentioned above the driver has to do more or less steps depending on the options.
Function getFinalPhase
uses the arguments in the command line to determine which is the last phase to perform. For instance if there is the -c
option, it only assembles while if there is the -S
option it runs only up to the backend (for the generation of the assembly). There are several other flags that impact the final phase.
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Determine which compilation mode we are in. We look for options which
// affect the phase, starting with the earliest phases, and record which
// option we used to determine the final phase.
phases::ID Driver::getFinalPhase(const DerivedArgList &DAL,
Arg **FinalPhaseArg) const {
Arg *PhaseArg = nullptr;
phases::ID FinalPhase;
// -{E,EP,P,M,MM} only run the preprocessor.
if (CCCIsCPP() || (PhaseArg = DAL.getLastArg(options::OPT_E)) ||
(PhaseArg = DAL.getLastArg(options::OPT__SLASH_EP)) ||
(PhaseArg = DAL.getLastArg(options::OPT_M, options::OPT_MM)) ||
(PhaseArg = DAL.getLastArg(options::OPT__SLASH_P))) {
FinalPhase = phases::Preprocess;
// --precompile only runs up to precompilation.
} else if ((PhaseArg = DAL.getLastArg(options::OPT__precompile))) {
FinalPhase = phases::Precompile;
// -{fsyntax-only,-analyze,emit-ast} only run up to the compiler.
} else if ((PhaseArg = DAL.getLastArg(options::OPT_fsyntax_only)) ||
(PhaseArg = DAL.getLastArg(options::OPT_module_file_info)) ||
(PhaseArg = DAL.getLastArg(options::OPT_verify_pch)) ||
(PhaseArg = DAL.getLastArg(options::OPT_rewrite_objc)) ||
(PhaseArg = DAL.getLastArg(options::OPT_rewrite_legacy_objc)) ||
(PhaseArg = DAL.getLastArg(options::OPT__migrate)) ||
(PhaseArg = DAL.getLastArg(options::OPT__analyze,
options::OPT__analyze_auto)) ||
(PhaseArg = DAL.getLastArg(options::OPT_emit_ast))) {
FinalPhase = phases::Compile;
// -S only runs up to the backend.
} else if ((PhaseArg = DAL.getLastArg(options::OPT_S))) {
FinalPhase = phases::Backend;
// -c compilation only runs up to the assembler.
} else if ((PhaseArg = DAL.getLastArg(options::OPT_c))) {
FinalPhase = phases::Assemble;
// Otherwise do everything.
} else
FinalPhase = phases::Link;
if (FinalPhaseArg)
*FinalPhaseArg = PhaseArg;
return FinalPhase;
}
Back to BuildActions
, now we can actually build the actions required for each input file.
2455
2456
2457
2458
2459
2460
2461
llvm::SmallVector<phases::ID, phases::MaxNumberOfPhases> PL;
for (auto &I : Inputs) {
types::ID InputType = I.first;
const Arg *InputArg = I.second;
PL.clear();
types::getCompilationPhases(InputType, PL);
The required compilation phases will depend on the input type, so this is again defined in Types.cpp
. Generally preprocessed files have their own preprocessing phase but Fortran files can be preprocessed at the same time as they are compiled (in a step that we will see later, called the "upper part" of the Fortran compiler). In fact, Fortran files, line 298-301, go through a different sequence of phases compared to other inputs: FortranFrontEnd
, then Compile
, then Backend
.
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
void types::getCompilationPhases(ID Id, llvm::SmallVectorImpl<phases::ID> &P) {
if (Id != TY_Object) {
// Delegate preprocessing to the "upper" part of Fortran compiler,
// preprocess for other preprocessable inputs
if (getPreprocessedType(Id) != TY_INVALID && !isFortran(Id)) {
P.push_back(phases::Preprocess);
}
if (getPrecompiledType(Id) != TY_INVALID) {
P.push_back(phases::Precompile);
}
if (!onlyPrecompileType(Id)) {
if (!onlyAssembleType(Id)) {
if (isFortran(Id)) {
P.push_back(phases::FortranFrontend);
P.push_back(phases::Compile);
P.push_back(phases::Backend);
} else {
P.push_back(phases::Compile);
P.push_back(phases::Backend);
}
}
P.push_back(phases::Assemble);
}
}
if (!onlyPrecompileType(Id)) {
P.push_back(phases::Link);
}
assert(0 < P.size() && "Not enough phases in list");
assert(P.size() <= phases::MaxNumberOfPhases && "Too many phases in list");
}
Back to BuildActions
again, now that we know the phases that the input has to perform, it is time to construct an action for each phase. When we are linking, all inputs will end in the Link phase, so we queue these phases aside and then we create a final action for them (after the loop). If not linking, a step generates some output that we will add to our list of actions.
Each action, except the InputAction
that receives the input argument and its type, is built in ConstructPhaseAction
using the previous phase and the output type it generates. Note that this function does not handle a Link
phase as it handled in BuildActions
(see code above wehre a LinkJobAction
is built). Again this is a long function so let's see a shortened version of it.
If we recall the set of phases for C/C++ we have that first they are Preprocess (.c
→ .i
or for C++ .cpp
→ .ii
), then, Compile (.i
/.ii
→ .bc
), then Backend (.bc
→ .s
) and then Assemble (.s
→ .o
). A .bc
file is a LLVM bitcode file which is a binary representation of the LLVM IR (represented in the code above as types::TY_LLVM_BC)
. But as we saw above, a Fortran file follows a slightly different process: FortranFrontend (.f
/.F
→ .llvm
), Compile (.llvm
→ .bc
), Backend (.bc
→ .s
) and Assemble (.s
→ .o
). A .llvm file is the textual representation of the LLVM IR and can represent exactly the same as a .bc file (but note that the backend step only consumes .bc
files). These actions are defined in llvm/tools/clang/include/clang/Action.h
and in general they are very thin classes that inherit from the Action
class.
Jobs for everyone
Now function BuildActions
ends and we go back to BuildCompilation
. The code now proceeds to BuildJobs
. It's main purpose is to build one or more jobs for each action.
There is a bit of complexity here because the driver caches invocation of the same inputs more than once (to be honest I fail to see a non-obvious example beyond repeating the same input in the command line, maybe this happens in offloading contexts that are very out of the scope of this post). So BuildJobsForAction
calls BuildJobsForActionNoCache
and in practice the two functions do the same, the former does caching and calls the latter if the result has not been cached yet. The name job is a bit misleading here because these two functions return an InputInfo
object which basically is a tuple containing the original file that motivated the creation of the input, the associated action and the type of the file (e.g.: object, Fortran free-form source, C code preprocessed, etc.). That said, at the end of BuildJobsForActionNoCache
, the function Tool::ConstructJob
is invoked. So now let's try to spell out this function because it does so many things at once. Fortunately, for us, most of the complexity is caused by extra steps required by offloading which we do not care at all.
It may not be obvious from what we have seen so far, but because of the way we built the actions, each action has a reference to another action that represents its inputs. For instance, when linking, there will be just a single (top-level) LinkJobAction
action that will have as inputs all the objects built by some AssembleJobAction
action. So this function traverses this graph starting from the final actions towards its inputs. Recall that the for every input there is at least one InputAction
from them (we created it in BuildActions
above), so this is where we stop the graph traversal: we simply return an InputInfo
using the current Action
(an InputAction
) and the argument in the command line that motivated the creation of this InputAction
.
From now on all the actions are going to be JobAction
which is a common class for actions that use a tool to do its job (in contrast to InputAction
). Thus we need some tool to generate the output of this action (which may be the input of another action). This is done using a class called ToolSelector
. An object of class ToolSelector
is created passing the JobAction
and the ToolChain
(and a couple more of flags we don't mind now).
Then we ask that object for a tool suitable for the current action using the Inputs of this JobAction
(they will be other Action
s) to give us a suitable Tool
. This function can use a function from the ToolChain
class called SelectTool
which returns the tool suitable for an action. But before it does this, ToolSelector::getTool
attempts to collapse several actions in a single tool. For example, cc1
(the C/C++ compiler proper) can genrate directly object files from C input files. So it does not make sense to do .c
→ .i
, then .i
→ .bc
, .bc
→ .s
and then .s
→ .o
if the tool can actually do .c
→ .o
in a single invocation. When the function ToolSelector::getTool
combines the action (like in the case of C/C++) it also updates the Inputs
, so for the case of Assembly
(.s
→ .o
) actions in C/C++ the inputs where Backend
actions (.bc
→ .s
) but now they are the InputAction
(the .c
/.cpp
source files in the command line).
In the case we care the most, Fortran, ToolSelector::getTool
does not combine anything as the flang frontend will just take a .F
/.f
and generate a .llvm
(textual LLVM IR).
Once we have a suitable tool for the action we can build jobs for the inputs. We do this recursively invoking BuildJobsForAction
. This will compute the jobs of the actions previous to this one (the inputs to the current action). Note that SubJobAtTopLevel
for the kind of actions we care will always evaluate as false. This is because when linking only the Link action is top level. When compiling only (-c
option) more than one Assemble
action can be TopLevel
(depending on the number of .c
files in the input).
Once we have built the jobs of the inputs of our task we can create the job of the current task (now that we have the tool).
<p.
This will construct each tool individually. There are classes for each possible tool: Clang
(for cc1), ClangAs
(for cc1as) and FlangFrontend
(for flang). These are defined in llvm/tools/clang/lib/Driver/Tools.cpp
. We’ll get back to them later.
</p>
After the jobs have been built, BuildJobsForActionNoCache
, BuildJobsForAction
and BuildJobs
do a few more of uninteresting bookkeeping but in practical terms they are done. So we go back to BuildCompilation
. Building the jobs is the last thing it does, so we're back to the driver. As a reminder where we started.
456
457
458
459
460
std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(argv));
int Res = 0;
SmallVector<std::pair<int, const Command *>, 4> FailingCommands;
if (C.get())
Res = TheDriver.ExecuteCompilation(*C, FailingCommands);
Now, the built compilation is executed in ExecuteCompilation
. It basically executes the jobs we created when building the compilation.
And the compilation has fully completed!
A tool for every task
Above we mentioned that every JobAction
invokes a tool to generate its output using the inputs. How does this really work?
Toolchain
We have mentioned several times above the class ToolChain
. Objects of this class know how to invoke the tools in every environment. The toolchain depends on the target which in LLVM is represented using a triple. The triple is loosely based on the GNU triple as used by the GNU toolchain. For instance a triple like x86_64-pc-linux-gnu
uses a Linux toolchain while a triple like x86_64-apple-darwin16.6.0
uses a Darwin toolchain style. When it comes to tools, usually only the assembler and the linker change, the tools provided by LLVM (like cc1 or the flang front end) are not configurable. That said, toolchains may have different defaults or require different configurations. All this information is encoded in the ToolChain
class.
We stated above that when creating the jobs for an action we need a tool. ToolSelector::getTool
calls ToolChain::SelectTool
to do this. By default it prioritizes cc1
or cc1as
if it can process the input specified, otherwise it falls back to ToolChain::getTool
that can be overriden by the ToolChain
.
347
348
349
350
351
352
353
Tool *ToolChain::SelectTool(const JobAction &JA) const {
if (getDriver().ShouldUseClangCompiler(JA)) return getClang();
Action::ActionClass AC = JA.getKind();
if (AC == Action::AssembleJobClass && useIntegratedAs())
return getClangAs();
return getTool(AC);
}
The base ToolChain
class provides the following getTool
implementation. The few ToolChain
that override this function forward to the base implementation if they do not have to provide specific behaviour.
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
Tool *ToolChain::getTool(Action::ActionClass AC) const {
switch (AC) {
case Action::AssembleJobClass:
return getAssemble();
case Action::LinkJobClass:
return getLink();
case Action::InputClass:
case Action::BindArchClass:
case Action::OffloadClass:
case Action::LipoJobClass:
case Action::DsymutilJobClass:
case Action::VerifyDebugInfoJobClass:
llvm_unreachable("Invalid tool kind.");
case Action::CompileJobClass:
case Action::PrecompileJobClass:
case Action::PreprocessJobClass:
case Action::AnalyzeJobClass:
case Action::MigrateJobClass:
case Action::VerifyPCHJobClass:
case Action::BackendJobClass:
return getClang();
case Action::OffloadBundlingJobClass:
case Action::OffloadUnbundlingJobClass:
return getOffloadBundler();
case Action::FortranFrontendJobClass:
return getFlangFrontend();
}
llvm_unreachable("Invalid tool kind.");
}
Flang frontend
Recall that when the input is a Fortran file, the phase for the action is a FortranFrontend
which generates a FortranFrontendJobAction
and this one uses the tool specified in ToolChain::getFlangFrontend
.
231
232
233
234
235
Tool *ToolChain::getFlangFrontend() const {
if (!FlangFrontend)
FlangFrontend.reset(new tools::FlangFrontend(*this));
return FlangFrontend.get();
}
This is the tool that will execute the Fortran front end phases. It is defined in Tools.h
.
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class LLVM_LIBRARY_VISIBILITY FlangFrontend : public Tool {
public:
FlangFrontend(const ToolChain &TC)
: Tool("flang:frontend",
"Fortran frontend to LLVM", TC,
RF_Full) {}
bool hasGoodDiagnostics() const override { return true; }
bool hasIntegratedAssembler() const override { return false; }
bool hasIntegratedCPP() const override { return false; }
void ConstructJob(Compilation &C, const JobAction &JA,
const InputInfo &Output, const InputInfoList &Inputs,
const llvm::opt::ArgList &TCArgs,
const char *LinkingOutput) const override;
};
The function ConstructJob
(that is effectively called from BuildJobsForActionNoCache
as we saw above) is a very long function, but the key elements of it is that it adds to the compilation a couple of commands.
Basically the execution of the FlangFrontend
entails running two programs, called flang1
and flang2
. Now it is a bit early to explain what they do but basically flang1
(upper Fortran) parses the Fortran code and generates an intermediate representation that is handed to flang2
(lower Fortran). flang2
is the responsible to generate the LLVM IR output.
Overall workflow
The option -### of the driver can be used to see the commands that it would invoke (in ExecuteCompilation
). If we retake our test.f90
from the last chapter, we can ask flang what commands it wil execute.
Wow, that is barely readable. But we can filter the output using grep. I annotate every invocation with its action.
The first two commands (flang1 and flang2) are due to the FortranFrontendJobAction
. The invocation for clang-4.0
is caused by the collapsing the phases of Assemble
(.ll
→ .bc
) and Backend
(.bc
→ .s
) and Assemble (.s
→ .o
) into a single step (.ll
→ .o
). A way to disable the collapsing of phases is using the flag -save-temps
.
Well, this post is too long already. In the next chapter we will see what flang1 does.