1#! /usr/bin/env bash
2#===-- tools/f18/flang.sh -----------------------------------------*- sh -*-===#
3#
4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7#
8#===------------------------------------------------------------------------===#
9# A wrapper script for Flang's compiler driver that was developed for testing and
10# experimenting. You should be able to use it as a regular compiler driver. It
11# will:
12#   * run Flang's compiler driver to unparse the input source files
13#   * use the external compiler (defined via F18_FC environment variable) to
14#   compile the unparsed source files
15#===------------------------------------------------------------------------===#
16set -euo pipefail
17
18# Global variables to make the parsing of input arguments a bit easier
19INPUT_FILES=()
20OPTIONS=()
21OUTPUT_FILE=""
22MODULE_DIR=""
23INTRINSICS_MOD_DIR=""
24COMPILE_ONLY="False"
25PREPROCESS_ONLY="False"
26PRINT_VERSION="False"
27
28# === parse_args ==============================================================
29#
30# Parse the input arguments passed to this script. Sets the global variables
31# declared at the top.
32#
33# INPUTS:
34#   $1 - all input arguments
35# OUTPUTS:
36#  Saved in the global variables for this script
37# =============================================================================
38parse_args()
39{
40  while [ "${1:-}" != "" ]; do
41      # CASE 1: Compiler option
42      if [[ "${1:0:1}" == "-" ]] ; then
43        # Output file - extract it into a global variable
44        if [[ "$1" == "-o" ]] ; then
45          shift
46          OUTPUT_FILE="$1"
47          shift
48          continue
49        fi
50
51        # Module directory - extract it into a global variable
52        if [[ "$1" == "-module-dir" ]]; then
53          shift
54          MODULE_DIR="$1"
55          shift
56          continue
57        fi
58
59        # Intrinsics module dir - extract it into a global var
60        if [[ "$1" == "-intrinsics-module-directory" ]]; then shift
61          INTRINSICS_MOD_DIR=$1
62          shift
63          continue
64        fi
65
66        # Module suffix cannot be modified - this script defines it before
67        # calling the driver.
68        if [[ "$1" == "-module-suffix" ]]; then
69          echo "ERROR: \'-module-suffix\' is not available when using the \'flang\' script"
70          exit 1
71        fi
72
73        # Special treatment for `J <dir>` and `-I <dir>`. We translate these
74        # into `J<dir>` and `-I<dir>` respectively.
75        if [[ "$1" == "-J" ]] || [[ "$1" == "-I" ]]; then
76          opt=$1
77          shift
78          OPTIONS+=("$opt$1")
79          shift
80          continue
81        fi
82
83        # This is a regular option - just add it to the list.
84        OPTIONS+=($1)
85        if [[ $1 == "-c" ]]; then
86          COMPILE_ONLY="True"
87        fi
88
89        if [[ $1 == "-E" ]]; then
90          PREPROCESS_ONLY="True"
91        fi
92
93        if [[ $1 == "-v" || $1 == "--version" ]]; then
94          PRINT_VERSION="True"
95        fi
96
97        shift
98        continue
99
100      # CASE 2: A regular file (either source or a library file)
101      elif [[ -f "$1" ]]; then
102        INPUT_FILES+=($1)
103        shift
104        continue
105
106      else
107        # CASE 3: Unsupported
108        echo "ERROR: unrecognised option format: \`$1\`. Perhaps non-existent file?"
109        exit 1
110      fi
111  done
112}
113
114# === categorise_files ========================================================
115#
116# Categorises input files into:
117#   * Fortran source files (to be compiled)
118#   * library files (to be linked into the final executable)
119#
120# INPUTS:
121#   $1 - all input files to be categorised (array, name reference)
122# OUTPUTS:
123#   $2 - Fortran source files extracted from $1 (array, name reference)
124#   $3 - other source files extracted from $1 (array, name reference)
125#   $4 - object files extracted from $1 (array, name reference)
126#   $4 - lib files extracted from $1 (array, name reference)
127# =============================================================================
128categorise_files()
129{
130  local -n -r all_files=$1
131  local -n fortran_sources=$2
132  local -n other_sources=$3
133  local -n libs=$4
134
135  for current_file in "${all_files[@]}"; do
136    file_ext=${current_file##*.}
137    if [[ $file_ext == "f" ]] || [[ $file_ext == "f90" ]] ||
138       [[ $file_ext == "f" ]] || [[ $file_ext == "F" ]] || [[ $file_ext == "ff" ]] ||
139       [[ $file_ext == "f90" ]] || [[ $file_ext == "F90" ]] || [[ $file_ext == "ff90" ]] ||
140       [[ $file_ext == "f95" ]] || [[ $file_ext == "F95" ]] || [[ $file_ext == "ff95" ]] ||
141       [[ $file_ext == "cuf" ]] || [[ $file_ext == "CUF" ]] || [[ $file_ext == "f18" ]] ||
142       [[ $file_ext == "F18" ]] || [[ $file_ext == "ff18" ]]; then
143      fortran_sources+=($current_file)
144    elif [[ $file_ext == "a" ]] || [[ $file_ext == "so" ]]; then
145      libs+=($current_file)
146    elif [[ $file_ext == "o" ]]; then
147      object_files+=($current_file)
148    else
149      other_sources+=($current_file)
150    fi
151  done
152}
153
154# === categorise_opts ==========================================================
155#
156# Categorises compiler options into options for:
157#   * the Flang driver (either new or the "throwaway" driver)
158#   * the external Fortran driver that will generate the code
159# Most options accepted by Flang will be claimed by it. The only exceptions are
160# `-I` and `-J`.
161#
162# INPUTS:
163#   $1 - all compiler options (array, name reference)
164# OUTPUTS:
165#   $2 - compiler options for the Flang driver (array, name reference)
166#   $3 - compiler options for the external driver (array, name reference)
167# =============================================================================
168categorise_opts()
169{
170  local -n all_opts=$1
171  local -n flang_opts=$2
172  local -n fc_opts=$3
173
174  for opt in "${all_opts[@]}"; do
175    # These options are claimed by Flang, but should've been dealt with in parse_args.
176    if  [[ $opt == "-module-dir" ]] ||
177      [[ $opt == "-o" ]] ||
178      [[ $opt == "-fintrinsic-modules-path" ]] ; then
179      echo "ERROR: $opt should've been fully processed by \`parse_args\`"
180      exit 1
181    fi
182
183    if
184      # The options claimed by Flang. This list needs to be compatible with
185      # what's supported by Flang's compiler driver (i.e. `flang-new` and f18).
186      [[ $opt == "-cpp" ]] ||
187      [[ $opt =~ ^-D.* ]] ||
188      [[ $opt == "-E" ]] ||
189      [[ $opt == "-falternative-parameter-statement" ]] ||
190      [[ $opt == "-fbackslash" ]] ||
191      [[ $opt == "-fcolor-diagnostics" ]] ||
192      [[ $opt == "-fdefault-double-8" ]] ||
193      [[ $opt == "-fdefault-integer-8" ]] ||
194      [[ $opt == "-fdefault-real-8" ]] ||
195      [[ $opt == "-ffixed-form" ]] ||
196      [[ $opt =~ ^-ffixed-line-length=.* ]] ||
197      [[ $opt == "-ffree-form" ]] ||
198      [[ $opt == "-fimplicit-none" ]] ||
199      [[ $opt =~ ^-finput-charset=.* ]] ||
200      [[ $opt == "-flarge-sizes" ]] ||
201      [[ $opt == "-flogical-abbreviations" ]] ||
202      [[ $opt == "-fno-color-diagnostics" ]] ||
203      [[ $opt == "-fopenacc" ]] ||
204      [[ $opt == "-fopenmp" ]] ||
205      [[ $opt == "-fxor-operator" ]] ||
206      [[ $opt == "-help" ]] ||
207      [[ $opt == "-nocpp" ]] ||
208      [[ $opt == "-pedantic" ]] ||
209      [[ $opt =~ ^-std=.* ]] ||
210      [[ $opt =~ ^-U.* ]] ||
211      [[ $opt == "-Werror" ]]; then
212      flang_opts+=($opt)
213    elif
214      # These options are not supported by `flang-new`. There is also no point
215      # in forwarding them to the host compiler as the output from
216      # `-fdebug-unparse` will always be in free form.
217      [[ $opt == "-Mfixed" ]] || [[ $opt == "-Mfree" ]]; then
218      :
219    elif [[ $opt =~ -I.* ]] || [[ $opt =~ -J.* ]]; then
220      # Options that are needed for both Flang and the external driver.
221      flang_opts+=($opt)
222      fc_opts+=($opt)
223    else
224      # All other options are claimed for the external driver.
225      fc_opts+=($opt)
226    fi
227  done
228}
229
230# === preprocess ==============================================================
231#
232# Runs the preprocessing. Fortran files are preprocessed using Flang. Other
233# files are preprocessed using the external Fortran compiler.
234#
235# INPUTS:
236#   $1 - Fortran source files (array, name reference)
237#   $2 - other source files (array, name reference)
238#   $3 - compiler flags (array, name reference)
239# =============================================================================
240preprocess() {
241  local -n fortran_srcs=$1
242  local -n other_srcs=$2
243  local -n opts=$3
244
245  local -r ext_fc="${F18_FC:-gfortran}"
246  local -r wd=$(cd "$(dirname "$0")/.." && pwd)
247
248  # Use the provided output file name.
249  if [[ ! -z ${OUTPUT_FILE:+x} ]]; then
250    output_definition="-o $OUTPUT_FILE"
251  fi
252
253  # Preprocess fortran sources using Flang
254  for idx in "${!fortran_srcs[@]}"; do
255    if ! "$wd/bin/@FLANG_DEFAULT_DRIVER@" -E "${opts[@]}" "${fortran_srcs[$idx]}" ${output_definition:+$output_definition}
256    then status=$?
257         echo flang: in "$PWD", @FLANG_DEFAULT_DRIVER@ failed with exit status $status: "$wd/bin/@FLANG_DEFAULT_DRIVER@" "${opts[@]}" "$@" >&2
258         exit $status
259    fi
260  done
261
262  # Preprocess other sources using Flang
263  for idx in "${!other_srcs[@]}"; do
264    if ! $ext_fc -E "${opts[@]}" "${other_srcs[$idx]}" ${output_definition:+$output_definition}
265    then status=$?
266         echo flang: in "$PWD", flang-new failed with exit status $status: "$wd/bin/flang-new" "${opts[@]}" "$@" >&2
267         exit $status
268    fi
269  done
270}
271
272# === get_relocatable_name ======================================================
273# This method generates the name of the output file for the compilation phase
274# (triggered with `-c`). If the user of this script is only interested in
275# compilation (`flang -c`), use $OUTPUT_FILE provided that it was defined.
276# Otherwise, use the usual heuristics:
277#   * file.f --> file.o
278#   * file.c --> file.o
279#
280# INPUTS:
281#   $1 - input source file for which to generate the output name
282# =============================================================================
283get_relocatable_name() {
284  local -r src_file=$1
285
286  if [[ $COMPILE_ONLY == "True" ]] && [[ ! -z ${OUTPUT_FILE:+x} ]]; then
287    out_file="$OUTPUT_FILE"
288  else
289    current_ext=${src_file##*.}
290    new_ext="o"
291
292    out_file=$(basename "${src_file}" "$current_ext")${new_ext}
293  fi
294
295  echo "$out_file"
296}
297
298# === main ====================================================================
299# Main entry point for this script
300# =============================================================================
301main() {
302  parse_args "$@"
303
304  if [[ $PRINT_VERSION == "True" ]]; then
305    echo "flang version @FLANG_VERSION@"
306    exit 0
307  fi
308
309  local fortran_source_files=()
310  local other_source_files=()
311  local object_files=()
312  local lib_files=()
313  categorise_files INPUT_FILES fortran_source_files other_source_files object_files lib_files
314
315  if [[ $PREPROCESS_ONLY == "True" ]]; then
316    preprocess fortran_source_files other_source_files OPTIONS
317    exit 0
318  fi
319
320  # Options for the Flang driver.
321  # NOTE: We need `-fc1` to make sure that the frontend driver rather than
322  # compiler driver is used. We also need to make sure that that's the first
323  # flag that the driver will see (otherwise it assumes compiler/toolchain
324  # driver mode).`f18` will just ignore this flag when uparsing, so it's fine
325  # to add it here unconditionally.
326  local flang_options=("-fc1")
327  # Options for the external Fortran Compiler
328  local ext_fc_options=()
329  categorise_opts OPTIONS flang_options ext_fc_options
330
331  local -r wd=$(cd "$(dirname "$0")/.." && pwd)
332
333  # STEP 1: Unparse
334  # Base-name for the unparsed files. These are just temporary files that are
335  # first generated and then deleted by this script.
336  # NOTE: We need to make sure that the base-name is unique to every
337  # invocation. Otherwise we can't use this script in parallel.
338  local -r unique_id=$(uuidgen | cut -b25-36)
339  local -r unparsed_file_base="flang_unparsed_file_$unique_id"
340
341  flang_options+=("-module-suffix")
342  flang_options+=(".f18.mod")
343  flang_options+=("-fdebug-unparse")
344  flang_options+=("-fno-analyzed-objects-for-unparse")
345
346  [[ ! -z ${MODULE_DIR} ]] && flang_options+=("-module-dir ${MODULE_DIR}")
347  [[ ! -z ${INTRINSICS_MOD_DIR} ]] && flang_options+=("-intrinsics-module-directory ${INTRINSICS_MOD_DIR}")
348  for idx in "${!fortran_source_files[@]}"; do
349    set +e
350    "$wd/bin/@FLANG_DEFAULT_DRIVER@" "${flang_options[@]}" "${fortran_source_files[$idx]}" -o "${unparsed_file_base}_${idx}.f90"
351    ret_status=$?
352    set -e
353    if [[ $ret_status != 0 ]]; then
354         echo flang: in "$PWD", @FLANG_DEFAULT_DRIVER@ failed with exit status "$ret_status": "$wd/bin/@FLANG_DEFAULT_DRIVER@" "${flang_options[@]}" "$@" >&2
355         exit "$ret_status"
356    fi
357  done
358
359  # STEP 2: Compile Fortran Source Files
360  readonly ext_fc="${F18_FC:-gfortran}"
361  for idx in "${!fortran_source_files[@]}"; do
362    # We always have to specify the output name with `-o <out_obj_file>`. This
363    # is because we are using the unparsed rather than the original source file
364    # below. As a result, we cannot rely on the compiler-generated output name.
365    out_obj_file=$(get_relocatable_name "${fortran_source_files[$idx]}")
366
367    set +e
368    $ext_fc "-c" "${ext_fc_options[@]}" "${unparsed_file_base}_${idx}.f90" "-o" "${out_obj_file}"
369    ret_status=$?
370    set -e
371    if [[ $ret_status != 0 ]]; then
372      echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2
373         exit "$ret_status"
374    fi
375    object_files+=(${out_obj_file})
376  done
377
378  # Delete the unparsed files
379  for idx in "${!fortran_source_files[@]}"; do
380    rm "${unparsed_file_base}_${idx}.f90"
381  done
382
383  # STEP 3: Compile Other Source Files
384  for idx in "${!other_source_files[@]}"; do
385    # We always specify the output name with `-o <out_obj_file>`. The user
386    # might have used `-o`, but we never add it to $OPTIONS (or
387    # $ext_fc_options). Hence we need to use `get_relocatable_name`.
388    out_obj_file=$(get_relocatable_name "${other_source_files[$idx]}")
389
390    set +e
391    $ext_fc "-c" "${ext_fc_options[@]}" "${other_source_files[${idx}]}" "-o" "${out_obj_file}"
392    ret_status=$?
393    set -e
394    if [[ $ret_status != 0 ]]; then
395      echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2
396         exit "$ret_status"
397    fi
398    object_files+=(${out_obj_file})
399  done
400
401  # STEP 4: Link
402  if [[ $COMPILE_ONLY == "True" ]]; then
403    exit 0;
404  fi
405
406  if [[ ${#object_files[@]} -ge 1 ]]; then
407    # If $OUTPUT_FILE was specified, use it for the output name.
408    if [[ ! -z ${OUTPUT_FILE:+x} ]]; then
409      output_definition="-o $OUTPUT_FILE"
410    else
411      output_definition=""
412    fi
413
414    set +e
415    $ext_fc "${ext_fc_options[@]}" "${object_files[@]}" "${lib_files[@]}" ${output_definition:+$output_definition}
416    ret_status=$?
417    set -e
418    if [[ $ret_status != 0 ]]; then
419         echo flang: in "$PWD", "$ext_fc" failed with exit status "$ret_status": "$ext_fc" "${ext_fc_options[@]}" "$@" >&2
420         exit "$ret_status"
421    fi
422  fi
423
424  # Delete intermediate object files
425  for idx in "${!fortran_source_files[@]}"; do
426    rm "${object_files[$idx]}"
427  done
428}
429
430main "${@}"
431