Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Ordinary Fortran `&` continuation lines are now passed through unchanged (issue #537)
- Previously, the preprocessor incorrectly joined all `&`-continued lines, breaking
multi-line array constructors and embedding Fortran comments mid-statement
- Continuation joining now only applies to pFUnit `@`-directive lines

## [4.17.0] - 2026-04-08

### Changed
Expand Down
86 changes: 43 additions & 43 deletions bin/funit/pFUnitParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,9 @@ def getBaseName(fileName):

self.userTestMethods = [] # each entry is a dictionary

self.looking_for_test_name = False
self.current_method = {}

self.wrapModuleName = "Wrap" + getBaseName(inputFileName)
self.currentLineNumber = 0

Expand Down Expand Up @@ -864,8 +867,47 @@ def getBaseName(fileName):
def commentLine(self, line):
self.outputFile.write(re.sub("@", "!@", line))

def isDirectiveLine(self, line):
"""Return True if line begins a pFUnit @-directive (ignoring leading whitespace)."""
return bool(re.match(r"\s*@", line))

def joinContinuationLines(self, line):
"""Join Fortran & continuation lines for a single @-directive.

Reads additional physical lines from the input file as long as the
current logical line ends with '&' (before any inline comment).
Returns the complete logical line with a single trailing newline.
Only called for lines that start a pFUnit @-directive.
"""
logical = line.rstrip("\r\n")
while True:
stripped = logical.rstrip()
amp_pos = stripped.rfind("&")
comment_pos = stripped.find("!")
is_continued = (
amp_pos != -1
and (comment_pos == -1 or amp_pos < comment_pos)
and amp_pos == len(stripped) - 1
)
if not is_continued:
break
prefix = logical[:amp_pos]
self.currentLineNumber += 1
next_raw = self.inputFile.readline()
if not next_raw:
break
tail = next_raw.lstrip()
if tail.startswith("&"):
tail = tail[1:]
logical = prefix + tail.rstrip("\r\n")
return logical + "\n"

def run(self):
def parse(line):
# Join & continuation lines only for @-directive lines so that
# ordinary Fortran code is passed through unchanged (issue #537).
if self.isDirectiveLine(line):
line = self.joinContinuationLines(line)
for action in self.actions:
if action.apply(line):
return
Expand Down Expand Up @@ -903,58 +945,16 @@ def isComment(self, line):
return re.match(r"\s*(!.*|)$", line)

def nextLine(self):
# Loop until we get a non-comment, non-blank (possibly continued) line
logical_line = ""
start_line_number = None
while True:
self.currentLineNumber += 1
line = self.inputFile.readline()
if not line:
break
if self.isComment(line):
self.outputFile.write(line)
continue
else:
# Start tracking line number for multi-line error reporting
if start_line_number is None:
start_line_number = self.currentLineNumber

candidate_line = line.rstrip("\r\n")
# New logic for multi-line Fortran with ampersand continuation:
while True:
# Fortran-style continuation, check trailing '&' before any comment
stripped = candidate_line.rstrip()
comment_pos = stripped.find("!")
amp_pos = stripped.rfind("&")
is_continued = (
amp_pos != -1
and (comment_pos == -1 or amp_pos < comment_pos)
and amp_pos == len(stripped) - 1
)
if is_continued:
# Keep everything before the '&', preserving whitespace
prefix = candidate_line[:amp_pos]
logical_line += prefix
# Get the next physical line
self.currentLineNumber += 1
continuation = self.inputFile.readline()
if not continuation:
break
# Remove leading whitespace up to and including a leading '&'
# but preserve any whitespace after the '&'
tail = continuation.lstrip()
if tail.startswith("&"):
tail = tail[1:] # Remove only the '&', keep spaces after it
candidate_line = tail.rstrip("\r\n")
continue
else:
logical_line += candidate_line
break
break
# Add newline back since readline() includes it and callers expect it
if logical_line:
logical_line += "\n"
return logical_line
return line

def printHeader(self):
self.outputFile.write("\n")
Expand Down
Loading
Loading