Skip to content

Preprocessor macro expansion #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 13, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### Added

- Added support for preprocessor macro expansions
([#368](https://github.com/fortran-lang/fortls/pull/368))
- Added support for leading white spaces in preprocessor directives
([#297](https://github.com/fortran-lang/fortls/issues/297))
- Added hover messages for Types and Modules
([#208](https://github.com/fortran-lang/fortls/issues/208))
- Added support for Markdown intrinsics from the M_intrinsics repository
Expand Down
9 changes: 8 additions & 1 deletion fortls/langserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,11 +751,18 @@ def get_definition(
return None
# Search in Preprocessor defined variables
if def_name in def_file.pp_defs:
def_value = def_file.pp_defs.get(def_name)
def_arg_str = ""
if isinstance(def_value, tuple):
def_arg_str, def_value = def_value
def_arg_str = ", ".join([x.strip() for x in def_arg_str.split(",")])
def_arg_str = f"({def_arg_str})"

var = Variable(
def_file.ast,
def_line + 1,
def_name,
f"#define {def_name} {def_file.pp_defs.get(def_name)}",
f"#define {def_name}{def_arg_str} {def_value}",
[],
)
return var
Expand Down
60 changes: 46 additions & 14 deletions fortls/parsers/internal/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,23 @@ def replace_vars(line: str):
else:
return line_res

def expand_func_macro(def_name: str, def_value: tuple[str, str]):
def_args, sub = def_value
def_args = def_args.split(",")
regex = re.compile(rf"\b{def_name}\s*\({','.join(['(.*)']*len(def_args))}\)")

for i, arg in enumerate(def_args, start=1):
sub = re.sub(rf"\b({arg.strip()})\b", rf"\\{i}", sub)

return regex, sub

def append_multiline_macro(def_value: str | tuple, line: str):
if isinstance(def_value, tuple):
def_args, def_value = def_value
def_value += line
return (def_args, def_value)
return def_value + line

if pp_defs is None:
pp_defs = {}
if include_dirs is None:
Expand All @@ -2097,11 +2114,13 @@ def replace_vars(line: str):
# Handle multiline macro continuation
if def_cont_name is not None:
output_file.append("")
if line.rstrip()[-1] != "\\":
defs_tmp[def_cont_name] += line.strip()
is_multiline = line.strip()[-1] != "\\"
line_to_append = line.strip() if is_multiline else line[0:-1].strip()
defs_tmp[def_cont_name] = append_multiline_macro(
defs_tmp[def_cont_name], line_to_append
)
if is_multiline:
def_cont_name = None
else:
defs_tmp[def_cont_name] += line[0:-1].strip()
continue
# Handle conditional statements
match = FRegex.PP_REGEX.match(line)
Expand All @@ -2110,14 +2129,14 @@ def replace_vars(line: str):
def_name = None
if_start = False
# Opening conditional statements
if match.group(1) == "if ":
if match.group(1).lower() == "if ":
is_path = eval_pp_if(line[match.end(1) :], defs_tmp)
if_start = True
elif match.group(1) == "ifdef":
elif match.group(1).lower() == "ifdef":
if_start = True
def_name = line[match.end(0) :].strip()
is_path = def_name in defs_tmp
elif match.group(1) == "ifndef":
elif match.group(1).lower() == "ifndef":
if_start = True
def_name = line[match.end(0) :].strip()
is_path = not (def_name in defs_tmp)
Expand All @@ -2135,7 +2154,7 @@ def replace_vars(line: str):
inc_start = False
exc_start = False
exc_continue = False
if match.group(1) == "elif":
if match.group(1).lower() == "elif":
if (not pp_stack_group) or (pp_stack_group[-1][0] != len(pp_stack)):
# First elif statement for this elif group
if pp_stack[-1][0] < 0:
Expand All @@ -2155,7 +2174,7 @@ def replace_vars(line: str):
inc_start = True
else:
exc_start = True
elif match.group(1) == "else":
elif match.group(1).lower() == "else":
if pp_stack[-1][0] < 0:
pp_stack[-1][0] = i + 1
exc_start = True
Expand All @@ -2171,7 +2190,7 @@ def replace_vars(line: str):
pp_skips.append(pp_stack.pop())
pp_stack.append([-1, -1])
inc_start = True
elif match.group(1) == "endif":
elif match.group(1).lower() == "endif":
if pp_stack_group and (pp_stack_group[-1][0] == len(pp_stack)):
pp_stack_group.pop()
if pp_stack[-1][0] < 0:
Expand Down Expand Up @@ -2209,12 +2228,18 @@ def replace_vars(line: str):
if eq_ind >= 0:
# Handle multiline macros
if line.rstrip()[-1] == "\\":
defs_tmp[def_name] = line[match.end(0) + eq_ind : -1].strip()
def_value = line[match.end(0) + eq_ind : -1].strip()
def_cont_name = def_name
else:
defs_tmp[def_name] = line[match.end(0) + eq_ind :].strip()
def_value = line[match.end(0) + eq_ind :].strip()
else:
defs_tmp[def_name] = "True"
def_value = "True"

# are there arguments to parse?
if match.group(3):
def_value = (match.group(4), def_value)

defs_tmp[def_name] = def_value
elif (match.group(1) == "undef") and (def_name in defs_tmp):
defs_tmp.pop(def_name, None)
log.debug(f"{line.strip()} !!! Define statement({i + 1})")
Expand Down Expand Up @@ -2265,8 +2290,15 @@ def replace_vars(line: str):
continue
def_regex = def_regexes.get(def_tmp)
if def_regex is None:
def_regex = re.compile(rf"\b{def_tmp}\b")
if isinstance(value, tuple):
def_regex = expand_func_macro(def_tmp, value)
else:
def_regex = re.compile(rf"\b{def_tmp}\b")
def_regexes[def_tmp] = def_regex

if isinstance(def_regex, tuple):
def_regex, value = def_regex

line_new, nsubs = def_regex.subn(value, line)
if nsubs > 0:
log.debug(
Expand Down
11 changes: 7 additions & 4 deletions fortls/regex_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ class FortranRegularExpressions:
FREE_FORMAT_TEST: Pattern = compile(r"[ ]{1,4}[a-z]", I)
# Preprocessor matching rules
DEFINED: Pattern = compile(r"defined[ ]*\(?[ ]*([a-z_]\w*)[ ]*\)?", I)
PP_REGEX: Pattern = compile(r"#(if |ifdef|ifndef|else|elif|endif)")
PP_DEF: Pattern = compile(r"#(define|undef)[ ]*([\w]+)(\((\w+(,[ ]*)?)+\))?", I)
PP_REGEX: Pattern = compile(r"[ ]*#[ ]*(if |ifdef|ifndef|else|elif|endif)", I)
PP_DEF: Pattern = compile(
r"[ ]*#[ ]*(define|undef|undefined)[ ]*(\w+)(\([ ]*([ \w,]*?)[ ]*\))?",
I,
)
PP_DEF_TEST: Pattern = compile(r"(![ ]*)?defined[ ]*\([ ]*(\w*)[ ]*\)$", I)
PP_INCLUDE: Pattern = compile(r"#include[ ]*([\"\w\.]*)", I)
PP_ANY: Pattern = compile(r"(^#:?\w+)")
PP_INCLUDE: Pattern = compile(r"[ ]*#[ ]*include[ ]*([\"\w\.]*)", I)
PP_ANY: Pattern = compile(r"^[ ]*#:?[ ]*(\w+)")
# Context matching rules
CALL: Pattern = compile(r"[ ]*CALL[ ]+[\w%]*$", I)
INT_STMNT: Pattern = compile(r"^[ ]*[a-z]*$", I)
Expand Down
8 changes: 4 additions & 4 deletions test/test_preproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ def check_return(result_array, checks):
"```fortran90\n#define PETSC_ERR_INT_OVERFLOW 84\n```",
"```fortran90\n#define varVar 55\n```",
(
"```fortran90\n#define ewrite if (priority <= 3) write((priority),"
" format)\n```"
"```fortran90\n#define ewrite(priority, format)"
" if (priority <= 3) write((priority), format)\n```"
),
(
"```fortran90\n#define ewrite2 if (priority <= 3) write((priority),"
" format)\n```"
"```fortran90\n#define ewrite2(priority, format)"
" if (priority <= 3) write((priority), format)\n```"
),
"```fortran90\n#define SUCCESS .true.\n```",
"```fortran90\nREAL, CONTIGUOUS, POINTER, DIMENSION(:) :: var1\n```",
Expand Down
38 changes: 38 additions & 0 deletions test/test_preproc_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

from fortls.parsers.internal.parser import preprocess_file


def test_pp_leading_spaces():
lines = [
" #define LEADING_SPACES_INDENT 1",
" # define LEADING_SPACES_INDENT2",
" # define FILE_ENCODING ,encoding='UTF-8'",
"program pp_intentation",
" implicit none",
" print*, LEADING_SPACES_INDENT",
" open(unit=1,file='somefile.txt' FILE_ENCODING)",
"end program pp_intentation",
]
_, _, _, defs = preprocess_file(lines)
ref = {
"LEADING_SPACES_INDENT": "1",
"LEADING_SPACES_INDENT2": "True",
"FILE_ENCODING": ",encoding='UTF-8'",
}
assert defs == ref


def test_pp_macro_expansion():
lines = [
"# define WRAP(PROCEDURE) PROCEDURE , wrap_/**/PROCEDURE",
"generic, public :: set => WRAP(abc)",
"procedure :: WRAP(abc)",
]
ref = [
"# define WRAP(PROCEDURE) PROCEDURE , wrap_/**/PROCEDURE",
"generic, public :: set => abc , wrap_/**/abc",
"procedure :: abc , wrap_/**/abc",
]
output, _, _, _ = preprocess_file(lines)
assert output == ref