Skip to content

Commit e1b2400

Browse files
authored
Merge pull request #368 from fortran-lang/feat/generic-macro-expansions
feat(pp): adds preprocessor macro expansion
2 parents 922e984 + b2e74ad commit e1b2400

File tree

6 files changed

+107
-23
lines changed

6 files changed

+107
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
### Added
88

9+
- Added support for preprocessor macro expansions
10+
([#368](https://github.com/fortran-lang/fortls/pull/368))
11+
- Added support for leading white spaces in preprocessor directives
12+
([#297](https://github.com/fortran-lang/fortls/issues/297))
913
- Added hover messages for Types and Modules
1014
([#208](https://github.com/fortran-lang/fortls/issues/208))
1115
- Added support for Markdown intrinsics from the M_intrinsics repository

fortls/langserver.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,11 +751,18 @@ def get_definition(
751751
return None
752752
# Search in Preprocessor defined variables
753753
if def_name in def_file.pp_defs:
754+
def_value = def_file.pp_defs.get(def_name)
755+
def_arg_str = ""
756+
if isinstance(def_value, tuple):
757+
def_arg_str, def_value = def_value
758+
def_arg_str = ", ".join([x.strip() for x in def_arg_str.split(",")])
759+
def_arg_str = f"({def_arg_str})"
760+
754761
var = Variable(
755762
def_file.ast,
756763
def_line + 1,
757764
def_name,
758-
f"#define {def_name} {def_file.pp_defs.get(def_name)}",
765+
f"#define {def_name}{def_arg_str} {def_value}",
759766
[],
760767
)
761768
return var

fortls/parsers/internal/parser.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,6 +2079,23 @@ def replace_vars(line: str):
20792079
else:
20802080
return line_res
20812081

2082+
def expand_func_macro(def_name: str, def_value: tuple[str, str]):
2083+
def_args, sub = def_value
2084+
def_args = def_args.split(",")
2085+
regex = re.compile(rf"\b{def_name}\s*\({','.join(['(.*)']*len(def_args))}\)")
2086+
2087+
for i, arg in enumerate(def_args, start=1):
2088+
sub = re.sub(rf"\b({arg.strip()})\b", rf"\\{i}", sub)
2089+
2090+
return regex, sub
2091+
2092+
def append_multiline_macro(def_value: str | tuple, line: str):
2093+
if isinstance(def_value, tuple):
2094+
def_args, def_value = def_value
2095+
def_value += line
2096+
return (def_args, def_value)
2097+
return def_value + line
2098+
20822099
if pp_defs is None:
20832100
pp_defs = {}
20842101
if include_dirs is None:
@@ -2097,11 +2114,13 @@ def replace_vars(line: str):
20972114
# Handle multiline macro continuation
20982115
if def_cont_name is not None:
20992116
output_file.append("")
2100-
if line.rstrip()[-1] != "\\":
2101-
defs_tmp[def_cont_name] += line.strip()
2117+
is_multiline = line.strip()[-1] != "\\"
2118+
line_to_append = line.strip() if is_multiline else line[0:-1].strip()
2119+
defs_tmp[def_cont_name] = append_multiline_macro(
2120+
defs_tmp[def_cont_name], line_to_append
2121+
)
2122+
if is_multiline:
21022123
def_cont_name = None
2103-
else:
2104-
defs_tmp[def_cont_name] += line[0:-1].strip()
21052124
continue
21062125
# Handle conditional statements
21072126
match = FRegex.PP_REGEX.match(line)
@@ -2110,14 +2129,14 @@ def replace_vars(line: str):
21102129
def_name = None
21112130
if_start = False
21122131
# Opening conditional statements
2113-
if match.group(1) == "if ":
2132+
if match.group(1).lower() == "if ":
21142133
is_path = eval_pp_if(line[match.end(1) :], defs_tmp)
21152134
if_start = True
2116-
elif match.group(1) == "ifdef":
2135+
elif match.group(1).lower() == "ifdef":
21172136
if_start = True
21182137
def_name = line[match.end(0) :].strip()
21192138
is_path = def_name in defs_tmp
2120-
elif match.group(1) == "ifndef":
2139+
elif match.group(1).lower() == "ifndef":
21212140
if_start = True
21222141
def_name = line[match.end(0) :].strip()
21232142
is_path = not (def_name in defs_tmp)
@@ -2135,7 +2154,7 @@ def replace_vars(line: str):
21352154
inc_start = False
21362155
exc_start = False
21372156
exc_continue = False
2138-
if match.group(1) == "elif":
2157+
if match.group(1).lower() == "elif":
21392158
if (not pp_stack_group) or (pp_stack_group[-1][0] != len(pp_stack)):
21402159
# First elif statement for this elif group
21412160
if pp_stack[-1][0] < 0:
@@ -2155,7 +2174,7 @@ def replace_vars(line: str):
21552174
inc_start = True
21562175
else:
21572176
exc_start = True
2158-
elif match.group(1) == "else":
2177+
elif match.group(1).lower() == "else":
21592178
if pp_stack[-1][0] < 0:
21602179
pp_stack[-1][0] = i + 1
21612180
exc_start = True
@@ -2171,7 +2190,7 @@ def replace_vars(line: str):
21712190
pp_skips.append(pp_stack.pop())
21722191
pp_stack.append([-1, -1])
21732192
inc_start = True
2174-
elif match.group(1) == "endif":
2193+
elif match.group(1).lower() == "endif":
21752194
if pp_stack_group and (pp_stack_group[-1][0] == len(pp_stack)):
21762195
pp_stack_group.pop()
21772196
if pp_stack[-1][0] < 0:
@@ -2209,12 +2228,18 @@ def replace_vars(line: str):
22092228
if eq_ind >= 0:
22102229
# Handle multiline macros
22112230
if line.rstrip()[-1] == "\\":
2212-
defs_tmp[def_name] = line[match.end(0) + eq_ind : -1].strip()
2231+
def_value = line[match.end(0) + eq_ind : -1].strip()
22132232
def_cont_name = def_name
22142233
else:
2215-
defs_tmp[def_name] = line[match.end(0) + eq_ind :].strip()
2234+
def_value = line[match.end(0) + eq_ind :].strip()
22162235
else:
2217-
defs_tmp[def_name] = "True"
2236+
def_value = "True"
2237+
2238+
# are there arguments to parse?
2239+
if match.group(3):
2240+
def_value = (match.group(4), def_value)
2241+
2242+
defs_tmp[def_name] = def_value
22182243
elif (match.group(1) == "undef") and (def_name in defs_tmp):
22192244
defs_tmp.pop(def_name, None)
22202245
log.debug(f"{line.strip()} !!! Define statement({i + 1})")
@@ -2265,8 +2290,15 @@ def replace_vars(line: str):
22652290
continue
22662291
def_regex = def_regexes.get(def_tmp)
22672292
if def_regex is None:
2268-
def_regex = re.compile(rf"\b{def_tmp}\b")
2293+
if isinstance(value, tuple):
2294+
def_regex = expand_func_macro(def_tmp, value)
2295+
else:
2296+
def_regex = re.compile(rf"\b{def_tmp}\b")
22692297
def_regexes[def_tmp] = def_regex
2298+
2299+
if isinstance(def_regex, tuple):
2300+
def_regex, value = def_regex
2301+
22702302
line_new, nsubs = def_regex.subn(value, line)
22712303
if nsubs > 0:
22722304
log.debug(

fortls/regex_patterns.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,14 @@ class FortranRegularExpressions:
124124
FREE_FORMAT_TEST: Pattern = compile(r"[ ]{1,4}[a-z]", I)
125125
# Preprocessor matching rules
126126
DEFINED: Pattern = compile(r"defined[ ]*\(?[ ]*([a-z_]\w*)[ ]*\)?", I)
127-
PP_REGEX: Pattern = compile(r"#(if |ifdef|ifndef|else|elif|endif)")
128-
PP_DEF: Pattern = compile(r"#(define|undef)[ ]*([\w]+)(\((\w+(,[ ]*)?)+\))?", I)
127+
PP_REGEX: Pattern = compile(r"[ ]*#[ ]*(if |ifdef|ifndef|else|elif|endif)", I)
128+
PP_DEF: Pattern = compile(
129+
r"[ ]*#[ ]*(define|undef|undefined)[ ]*(\w+)(\([ ]*([ \w,]*?)[ ]*\))?",
130+
I,
131+
)
129132
PP_DEF_TEST: Pattern = compile(r"(![ ]*)?defined[ ]*\([ ]*(\w*)[ ]*\)$", I)
130-
PP_INCLUDE: Pattern = compile(r"#include[ ]*([\"\w\.]*)", I)
131-
PP_ANY: Pattern = compile(r"(^#:?\w+)")
133+
PP_INCLUDE: Pattern = compile(r"[ ]*#[ ]*include[ ]*([\"\w\.]*)", I)
134+
PP_ANY: Pattern = compile(r"^[ ]*#:?[ ]*(\w+)")
132135
# Context matching rules
133136
CALL: Pattern = compile(r"[ ]*CALL[ ]+[\w%]*$", I)
134137
INT_STMNT: Pattern = compile(r"^[ ]*[a-z]*$", I)

test/test_preproc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ def check_return(result_array, checks):
5252
"```fortran90\n#define PETSC_ERR_INT_OVERFLOW 84\n```",
5353
"```fortran90\n#define varVar 55\n```",
5454
(
55-
"```fortran90\n#define ewrite if (priority <= 3) write((priority),"
56-
" format)\n```"
55+
"```fortran90\n#define ewrite(priority, format)"
56+
" if (priority <= 3) write((priority), format)\n```"
5757
),
5858
(
59-
"```fortran90\n#define ewrite2 if (priority <= 3) write((priority),"
60-
" format)\n```"
59+
"```fortran90\n#define ewrite2(priority, format)"
60+
" if (priority <= 3) write((priority), format)\n```"
6161
),
6262
"```fortran90\n#define SUCCESS .true.\n```",
6363
"```fortran90\nREAL, CONTIGUOUS, POINTER, DIMENSION(:) :: var1\n```",

test/test_preproc_parser.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from fortls.parsers.internal.parser import preprocess_file
4+
5+
6+
def test_pp_leading_spaces():
7+
lines = [
8+
" #define LEADING_SPACES_INDENT 1",
9+
" # define LEADING_SPACES_INDENT2",
10+
" # define FILE_ENCODING ,encoding='UTF-8'",
11+
"program pp_intentation",
12+
" implicit none",
13+
" print*, LEADING_SPACES_INDENT",
14+
" open(unit=1,file='somefile.txt' FILE_ENCODING)",
15+
"end program pp_intentation",
16+
]
17+
_, _, _, defs = preprocess_file(lines)
18+
ref = {
19+
"LEADING_SPACES_INDENT": "1",
20+
"LEADING_SPACES_INDENT2": "True",
21+
"FILE_ENCODING": ",encoding='UTF-8'",
22+
}
23+
assert defs == ref
24+
25+
26+
def test_pp_macro_expansion():
27+
lines = [
28+
"# define WRAP(PROCEDURE) PROCEDURE , wrap_/**/PROCEDURE",
29+
"generic, public :: set => WRAP(abc)",
30+
"procedure :: WRAP(abc)",
31+
]
32+
ref = [
33+
"# define WRAP(PROCEDURE) PROCEDURE , wrap_/**/PROCEDURE",
34+
"generic, public :: set => abc , wrap_/**/abc",
35+
"procedure :: abc , wrap_/**/abc",
36+
]
37+
output, _, _, _ = preprocess_file(lines)
38+
assert output == ref

0 commit comments

Comments
 (0)