Think In Geek

In geek we trust

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.


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.

  std::unique_ptr C(TheDriver.BuildCompilation(argv));
  int Res = 0;
  SmallVector, 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.

  // 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.

  enum DriverMode {
  } Mode;

If you follow this function you will see that it does this

void Driver::ParseDriverMode(StringRef ProgramName,
                             ArrayRef Args) {
  auto Default = ToolChain::getTargetAndModeFromProgramName(ProgramName);

Function getTargetAndModeFromProgramName is defined in llvm/tools/clang/lib/Driver/ToolChain.cpp.

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.

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;
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.

  // 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.

types::ID types::lookupTypeForExtension(llvm::StringRef Ext) {
  return llvm::StringSwitch(Ext)
           .Case("c", TY_C)
           .Case("C", TY_CXX)
           .Case("F", TY_F_FixedForm)
           .Case("f", TY_PP_F_FixedForm)
           // (.. omitted ..)
           .Case("for", TY_PP_F_FixedForm)
           .Case("FOR", TY_PP_F_FixedForm)
           .Case("fpp", TY_F_FixedForm)
           .Case("FPP", TY_F_FixedForm)
           .Case("f90", TY_PP_F_FreeForm)
           .Case("f95", TY_PP_F_FreeForm)
           .Case("f03", TY_PP_F_FreeForm)
           .Case("f08", TY_PP_F_FreeForm)
           .Case("F90", TY_F_FreeForm)
           .Case("F95", TY_F_FreeForm)
           .Case("F03", TY_F_FreeForm)
           .Case("F08", TY_F_FreeForm)
           // (.. omitted ..)

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.

void Driver::BuildActions(Compilation &C, DerivedArgList &Args,
                          const InputList &Inputs, ActionList &Actions) const {
  // (.. omitted ..)
  Arg *FinalPhaseArg;
  phases::ID FinalPhase = getFinalPhase(Args, &FinalPhaseArg);

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.

// 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.

  llvm::SmallVector PL;
  for (auto &I : Inputs) {
    types::ID InputType = I.first;
    const Arg *InputArg = I.second;

    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.

void types::getCompilationPhases(ID Id, llvm::SmallVectorImpl &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)) {

    if (getPrecompiledType(Id) != TY_INVALID) {

    if (!onlyPrecompileType(Id)) {
      if (!onlyAssembleType(Id)) {
        if (isFortran(Id)) {
        } else {

  if (!onlyPrecompileType(Id)) {
  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.

    // Build the pipeline for this file.
    Action *Current = C.MakeAction(*InputArg, InputType);

    for (SmallVectorImpl::iterator i = PL.begin(), e = PL.end();
         i != e; ++i) {
      phases::ID Phase = *i;

      // We are done if this step is past what the user requested.
      if (Phase > FinalPhase)

      // Queue linker inputs.
      if (Phase == phases::Link) {
        assert((i + 1) == e && "linking must be final compilation step.");
        Current = nullptr;

      // Otherwise construct the appropriate action.
      auto *NewCurrent = ConstructPhaseAction(C, Args, Phase, Current);
      // (.. omitted ..)
      Current = NewCurrent;
      // (.. omitted ..)

   // If we ended with something, add to the output list.
   if (Current)

  // Add a link action if necessary.
  if (!LinkerInputs.empty()) {
    Action *LA = C.MakeAction(LinkerInputs, types::TY_Image);
    // (.. omitted ..)

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.

Action *Driver::ConstructPhaseAction(Compilation &C, const ArgList &Args,
                                     phases::ID Phase, Action *Input) const {

  // (.. omitted ..)
  case phases::Preprocess: {
    types::ID OutputTy;
    // (.. omitted ..)
        OutputTy = types::getPreprocessedType(OutputTy);
      return C.MakeAction(Input, OutputTy);
  case phases::FortranFrontend: {
    return C.MakeAction(Input,
  case phases::Compile: {
    if (Args.hasArg(options::OPT_fsyntax_only))
      return C.MakeAction(Input, types::TY_Nothing);
    // (.. omitted ..)
    return C.MakeAction(Input, types::TY_LLVM_BC);
  // (.. omitted ..)
  case phases::Backend: {
    // (.. omitted ..)
    return C.MakeAction(Input, types::TY_PP_Asm);
  case phases::Assemble:
    return C.MakeAction(std::move(Input), types::TY_Object);
  // (.. omitted ..)

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.

void Driver::BuildJobs(Compilation &C) const {
  // (.. omitted ..)
  for (Action *A : C.getActions()) {
    // (.. omitted ..)
    BuildJobsForAction(C, A, &C.getDefaultToolChain(),
                       /*BoundArch*/ StringRef(),
                       /*AtTopLevel*/ true,
                       /*MultipleArchs*/ ArchNames.size() > 1,
                       /*LinkingOutput*/ LinkingOutput, CachedResults,
                       /*TargetDeviceOffloadKind*/ Action::OFK_None);
  // (.. omitted ..)

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.

InputInfo Driver::BuildJobsForActionNoCache(
    Compilation &C, const Action *A, const ToolChain *TC, StringRef BoundArch,
    bool AtTopLevel, bool MultipleArchs, const char *LinkingOutput,
    std::map, InputInfo> &CachedResults,
    Action::OffloadKind TargetDeviceOffloadKind) const {

  // (.. omitted ..))

  if (const InputAction *IA = dyn_cast(A)) {
    // (.. omitted ..))
    return InputInfo(A, &Input, /* BaseInput = */ "");

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 Actions) 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).

  const ActionList *Inputs = &A->getInputs();

  const JobAction *JA = cast(A);
  ToolSelector TS(JA, *TC, C, isSaveTempsEnabled(), embedBitcodeInObject());
  const Tool *T = TS.getTool(Inputs, CollapsedOffloadActions);

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).

  for (const Action *Input : *Inputs) {
    // Treat dsymutil and verify sub-jobs as being at the top-level too, they
    // shouldn't get temporary output names.
    // FIXME: Clean this up.
    bool SubJobAtTopLevel =
        AtTopLevel && (isa(A) || isa(A));
        C, Input, TC, BoundArch, SubJobAtTopLevel, MultipleArchs, LinkingOutput,
        CachedResults, A->getOffloadingDeviceKind()));

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).

          C, *JA, Result, InputInfos,
          C.getArgsForToolChain(TC, BoundArch, JA->getOffloadingDeviceKind()),
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.

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.

  std::unique_ptr C(TheDriver.BuildCompilation(argv));
  int Res = 0;
  SmallVector, 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.

int Driver::ExecuteCompilation(
    Compilation &C,
    SmallVectorImpl> &FailingCommands) {
  // Just print if -### was present.
  if (C.getArgs().hasArg(options::OPT__HASH_HASH_HASH)) {
    C.getJobs().Print(llvm::errs(), "\n", true);
    return 0;

  // If there were errors building the compilation, quit now.
  if (Diags.hasErrorOccurred())
    return 1;

  // Set up response file names for each command, if necessary
  for (auto &Job : C.getJobs())
    setUpResponseFiles(C, Job);

  C.ExecuteJobs(C.getJobs(), FailingCommands);
  // (.. omitted ..)

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?


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.

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.

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.

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.

class LLVM_LIBRARY_VISIBILITY FlangFrontend : public Tool {
  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.

void FlangFrontend::ConstructJob(Compilation &C, const JobAction &JA,
                         const InputInfo &Output, const InputInfoList &Inputs,
                         const ArgList &Args, const char *LinkingOutput) const {
  // (.. omit ..)
  const char *UpperExec = Args.MakeArgString(getToolChain().GetProgramPath("flang1"));
  // (.. omit ..)
  C.addCommand(llvm::make_unique(JA, *this, UpperExec, UpperCmdArgs, Inputs));
  // (.. omit ..)
  const char *LowerExec = Args.MakeArgString(getToolChain().GetProgramPath("flang2"));
  // (.. omit ..)
  C.addCommand(llvm::make_unique(JA, *this, LowerExec, LowerCmdArgs, Inputs));

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.

$ flang -### -o test test.f90
clang version 4.0.1 ( 17f442716e8e6a5a642be5bd9e4f86b1aaf2f372) ( c8fccc53ed66d505898f8850bcc690c977a7c9a7)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: install/bin
 "install/bin/flang1" "test.f90" "-opt" "0" "-terse" "1" "-inform" "warn" "-nohpf" "-nostatic" "-y" "129" "2" "-inform" "warn" "-x" "19" "0x400000" "-quad" "-x" "59" "4" "-x" "15" "2" "-x" "49" "0x400004" "-x" "51" "0x20" "-x" "57" "0x4c" "-x" "58" "0x10000" "-x" "124" "0x1000" "-tp" "px" "-x" "57" "0xfb0000" "-x" "58" "0x78031040" "-x" "47" "0x08" "-x" "48" "4608" "-x" "49" "0x100" "-stdinc" "install/bin/../include:/usr/local/include:install/bin/../lib/clang/4.0.1/include:/usr/include/x86_64-linux-gnu:/include:/usr/include" "-def" "unix" "-def" "__unix" "-def" "__unix__" "-def" "linux" "-def" "__linux" "-def" "__linux__" "-def" "__NO_MATH_INLINES" "-def" "__LP64__" "-def" "__x86_64" "-def" "__x86_64__" "-def" "__LONG_MAX__=9223372036854775807L" "-def" "__SIZE_TYPE__=unsigned long int" "-def" "__PTRDIFF_TYPE__=long int" "-def" "__THROW=" "-def" "__extension__=" "-def" "__amd_64__amd64__" "-def" "__k8" "-def" "__k8__" "-def" "__PGLLVM__" "-freeform" "-vect" "48" "-y" "54" "1" "-x" "70" "0x40000000" "-y" "163" "0xc0000000" "-x" "189" "0x10" "-stbfile" "/tmp/test-82de19.stb" "-modexport" "/tmp/test-82de19.cmod" "-modindex" "/tmp/test-82de19.cmdx" "-output" "/tmp/test-82de19.ilm"
 "install/bin/flang2" "/tmp/test-82de19.ilm" "-ieee" "1" "-x" "6" "0x100" "-x" "42" "0x400000" "-y" "129" "4" "-x" "129" "0x400" "-fn" "test.f90" "-opt" "0" "-terse" "1" "-inform" "warn" "-y" "129" "2" "-inform" "warn" "-x" "51" "0x20" "-x" "119" "0xa10000" "-x" "122" "0x40" "-x" "123" "0x1000" "-x" "127" "4" "-x" "127" "17" "-x" "19" "0x400000" "-x" "28" "0x40000" "-x" "120" "0x10000000" "-x" "70" "0x8000" "-x" "122" "1" "-x" "125" "0x20000" "-quad" "-x" "59" "4" "-tp" "px" "-x" "120" "0x1000" "-x" "124" "0x1400" "-y" "15" "2" "-x" "57" "0x3b0000" "-x" "58" "0x48000000" "-x" "49" "0x100" "-astype" "0" "-x" "183" "4" "-x" "121" "0x800" "-x" "54" "0x10" "-x" "70" "0x40000000" "-x" "249" "40" "-x" "124" "1" "-y" "163" "0xc0000000" "-x" "189" "0x10" "-y" "189" "0x4000000" "-x" "183" "0x10" "-stbfile" "/tmp/test-82de19.stb" "-asm" "/tmp/test-82de19.ll"
 "install/bin/clang-4.0" "-cc1" "-triple" "x86_64-unknown-linux-gnu" "-emit-obj" "-mrelax-all" "-disable-free" "-main-file-name" "test.f90" "-mrelocation-model" "static" "-mthread-model" "posix" "-mdisable-fp-elim" "-fmath-errno" "-masm-verbose" "-mconstructor-aliases" "-munwind-tables" "-fuse-init-array" "-target-cpu" "x86-64" "-dwarf-column-info" "-debugger-tuning=gdb" "-resource-dir" "install/bin/../lib/clang/4.0.1" "-fdebug-compilation-dir" "test" "-ferror-limit" "19" "-fmessage-length" "190" "-fobjc-runtime=gcc" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-o" "/tmp/test-bcb4ec.o" "-x" "ir" "/tmp/test-82de19.ll"
 "/usr/bin/ld" "--hash-style=both" "--eh-frame-hdr" "-m" "elf_x86_64" "-dynamic-linker" "/lib64/" "-o" "test" "/usr/lib/gcc/x86_64-linux-gnu/6.3.0/../../../x86_64-linux-gnu/crt1.o" "/usr/lib/gcc/x86_64-linux-gnu/6.3.0/../../../x86_64-linux-gnu/crti.o" "/usr/lib/gcc/x86_64-linux-gnu/6.3.0/crtbegin.o" "-L/usr/lib/gcc/x86_64-linux-gnu/6.3.0" "-L/usr/lib/gcc/x86_64-linux-gnu/6.3.0/../../../x86_64-linux-gnu" "-L/lib/x86_64-linux-gnu" "-L/lib/../lib64" "-L/usr/lib/x86_64-linux-gnu" "-L/usr/lib/gcc/x86_64-linux-gnu/6.3.0/../../.." "-Linstall/bin/../lib" "-L/lib" "-L/usr/lib" "/tmp/test-bcb4ec.o" "-lflangmain" "-lflang" "-lflangrti" "-lompstub" "-lm" "-lrt" "-lpthread" "-lgcc" "--as-needed" "-lgcc_s" "--no-as-needed" "-lc" "-lgcc" "--as-needed" "-lgcc_s" "--no-as-needed" "/usr/lib/gcc/x86_64-linux-gnu/6.3.0/crtend.o" "/usr/lib/gcc/x86_64-linux-gnu/6.3.0/../../../x86_64-linux-gnu/crtn.o"

Wow, that is barely readable. But we can filter the output using grep. I annotate every invocation with its action.

$ flang -### -o test test.f90  2>&1  | grep -o "^ \"[^\"]\+\""
 "install/bin/flang1"     #             [FortranFrontEnd]
 "install/bin/flang2"     # .f/F → .ll [FortranFrontEnd]
 "install/bin/clang-4.0"  # .ll → .o   [Compile+Backend+Assemble]
 "/usr/bin/ld"            # .o → (exe) [Link)

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.

$ flang -### -o test test.f90 -save-temps 2>&1  | grep -o "^ \"[^\"]\+\"" 
 "install/bin/flang1"       #             [FortranFrontEnd]  
 "install/bin/flang2"       # .f/F → .ll [FortranFrontEnd]
 "install/bin/clang-4.0"    # .ll → .bc  [Compile]
 "install/bin/clang-4.0"    # .bc → .s   [Backend]
 "install/bin/clang-4.0"    # .s → .o    [Assemble]
 "/usr/bin/ld"              # .o → (exe) [Link]

Well, this post is too long already. In the next chapter we will see what flang1 does.

One thought on “Walk-through flang – Part 2

Leave a Reply

Your email address will not be published. Required fields are marked *