Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
cmake_minimum_required(VERSION 3.24)

project (PFUNIT
VERSION 4.17.0
VERSION 4.17.1
LANGUAGES Fortran C)

cmake_policy(SET CMP0077 NEW)
Expand Down
9 changes: 9 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [4.17.1] - 2026-04-09

### 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