[flang-commits] [flang] [llvm] [flang-rt] Split large unformatted sequential records into subrecords (PR #213003)
via flang-commits
flang-commits at lists.llvm.org
Fri Oct 16 02:44:46 PDT 2026
https://github.com/ejose02 updated https://github.com/llvm/llvm-project/pull/213003
>From b8b7f4247cd16a13ca4fcd72d9bff78024c83b40 Mon Sep 17 00:00:00 2001
From: ejose <ejose at amd.com>
Date: Thu, 30 Jul 2026 12:15:10 +0000
Subject: [PATCH 1/2] [flang-rt] Split large unformatted sequential records
into subrecords
Implement the TODO in AdvanceRecord to break very large unformatted sequential records into a chain of signed 4-byte subrecord markers, compatible with gfortran on-disk layout:
// TODO: Break very large records up into subrecords with negative
// headers &/or footers
A single unsigned 32-bit header/footer cannot represent records at or above 4 GiB. The sign of each marker indicates continuation and the magnitude gives the payload size (at most ~2 GiB per subrecord). Records below that limit keep the original single header/footer layout.
Larger records are laid out in the frame buffer on output and read back by streaming across interior markers; backspace and positioning walk the full on-disk extent.
Added a unit test for reading and backspacing over a multi-subrecord file.
---
flang-rt/include/flang-rt/runtime/unit.h | 6 +-
flang-rt/lib/runtime/unit.cpp | 360 +++++++++++++-----
flang-rt/unittests/Runtime/ExternalIOTest.cpp | 104 +++++
3 files changed, 366 insertions(+), 104 deletions(-)
diff --git a/flang-rt/include/flang-rt/runtime/unit.h b/flang-rt/include/flang-rt/runtime/unit.h
index a9419daf3c052b..761cc249f2c82d 100644
--- a/flang-rt/include/flang-rt/runtime/unit.h
+++ b/flang-rt/include/flang-rt/runtime/unit.h
@@ -225,7 +225,7 @@ class ExternalFileUnit : public ConnectionState,
RT_API_ATTRS void CommitWrites();
RT_API_ATTRS bool CheckDirectAccess(IoErrorHandler &);
RT_API_ATTRS void HitEndOnRead(IoErrorHandler &);
- RT_API_ATTRS std::uint32_t ReadHeaderOrFooter(std::int64_t frameOffset);
+ RT_API_ATTRS void ResetSequentialUnformattedStream();
Lock lock_;
@@ -241,6 +241,10 @@ class ExternalFileUnit : public ConnectionState,
// manage the frame and the current record therein separately.
std::int64_t frameOffsetInFile_{0};
std::size_t recordOffsetInFrame_{0}; // of currentRecordNumber
+ std::int64_t unformattedDiskExtent_{0};
+ FileOffset streamReadPos_{0};
+ std::size_t streamReadRemain_{0};
+ bool streamRead_{false};
bool swapEndianness_{false};
bool createdForInternalChildIo_{false};
common::BitSet<64> asyncIdAvailable_[maxAsyncIds / 64];
diff --git a/flang-rt/lib/runtime/unit.cpp b/flang-rt/lib/runtime/unit.cpp
index cbf802d3b8225c..a33eacb5b9e954 100644
--- a/flang-rt/lib/runtime/unit.cpp
+++ b/flang-rt/lib/runtime/unit.cpp
@@ -101,6 +101,61 @@ static inline RT_API_ATTRS void SwapEndianness(
}
}
+static constexpr std::size_t kRecMarkSize{4};
+static constexpr std::int32_t kMaxSubrecPayload{0x7fffffff - 8};
+
+static inline RT_API_ATTRS std::size_t SubrecordPayloadBytes(
+ std::int32_t marker) {
+ return static_cast<std::size_t>(marker < 0 ? -marker : marker);
+}
+
+static inline RT_API_ATTRS std::int32_t SignedRecordMarker(
+ std::int32_t length, bool negative) {
+ return negative ? -length : length;
+}
+
+static inline RT_API_ATTRS void StoreRecordMarker(
+ char *p, std::int32_t value, bool swapEndianness) {
+ runtime::memcpy(p, &value, kRecMarkSize);
+ if (swapEndianness) {
+ SwapEndianness(p, kRecMarkSize, kRecMarkSize);
+ }
+}
+
+static inline RT_API_ATTRS std::int32_t LoadRecordMarker(
+ const char *p, bool swapEndianness) {
+ std::int32_t word;
+ runtime::memcpy(&word, p, kRecMarkSize);
+ if (swapEndianness) {
+ SwapEndianness(reinterpret_cast<char *>(&word), kRecMarkSize, kRecMarkSize);
+ }
+ return word;
+}
+
+static RT_API_ATTRS void LayoutBufferedSubrecords(
+ char *base, std::size_t payload, bool swapEndianness) {
+ std::size_t maxChunk{static_cast<std::size_t>(kMaxSubrecPayload)};
+ std::size_t subrecords{(payload + maxChunk - 1) / maxChunk};
+ for (std::size_t i{subrecords}; i-- > 0;) {
+ std::size_t chunk{i + 1 < subrecords ? maxChunk : payload - i * maxChunk};
+ std::size_t src{kRecMarkSize + i * maxChunk};
+ std::size_t dst{kRecMarkSize + i * (maxChunk + 2 * kRecMarkSize)};
+ if (dst != src && chunk > 0) {
+ runtime::memmove(base + dst, base + src, chunk);
+ }
+ std::int32_t length{static_cast<std::int32_t>(chunk)};
+ StoreRecordMarker(base + dst - kRecMarkSize,
+ SignedRecordMarker(length, i + 1 < subrecords), swapEndianness);
+ StoreRecordMarker(
+ base + dst + chunk, SignedRecordMarker(length, i > 0), swapEndianness);
+ }
+}
+
+void ExternalFileUnit::ResetSequentialUnformattedStream() {
+ streamRead_ = false;
+ streamReadRemain_ = 0;
+}
+
bool ExternalFileUnit::Emit(const char *data, std::size_t bytes,
std::size_t elementBytes, IoErrorHandler &handler) {
auto furthestAfter{std::max(furthestPositionInRecord,
@@ -173,6 +228,46 @@ bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
static_cast<std::intmax_t>(*recordLength));
return false;
}
+ if (streamRead_) {
+ std::size_t copied{0};
+ while (copied < bytes) {
+ if (streamReadRemain_ == 0) {
+ streamReadPos_ += kRecMarkSize;
+ char mark[kRecMarkSize];
+ if (Read(streamReadPos_, mark, kRecMarkSize, kRecMarkSize, handler) <
+ kRecMarkSize) {
+ HitEndOnRead(handler);
+ return false;
+ }
+ streamReadPos_ += kRecMarkSize;
+ streamReadRemain_ =
+ SubrecordPayloadBytes(LoadRecordMarker(mark, swapEndianness_));
+ }
+ std::size_t chunk{bytes - copied};
+ if (chunk > streamReadRemain_) {
+ chunk = streamReadRemain_;
+ }
+ auto logicalRemain{
+ static_cast<std::size_t>(*recordLength - positionInRecord)};
+ if (chunk > logicalRemain) {
+ chunk = logicalRemain;
+ }
+ if (Read(streamReadPos_, data + copied, chunk, chunk, handler) < chunk) {
+ HitEndOnRead(handler);
+ return false;
+ }
+ if (swapEndianness_) {
+ SwapEndianness(data + copied, chunk, elementBytes);
+ }
+ streamReadPos_ += static_cast<FileOffset>(chunk);
+ streamReadRemain_ -= chunk;
+ copied += chunk;
+ positionInRecord += static_cast<std::int64_t>(chunk);
+ }
+ furthestPositionInRecord =
+ std::max(furthestPositionInRecord, positionInRecord);
+ return true;
+ }
auto need{recordOffsetInFrame_ + furthestAfter};
auto got{ReadFrame(frameOffsetInFile_, need, handler)};
if (got >= need) {
@@ -325,9 +420,9 @@ void ExternalFileUnit::FinishReadingRecord(IoErrorHandler &handler) {
RUNTIME_CHECK(handler, isUnformatted.has_value());
recordLength.reset();
if (isUnformatted.value_or(false)) {
- // Retain footer in frame for more efficient BACKSPACE
- frameOffsetInFile_ += recordOffsetInFrame_;
- recordOffsetInFrame_ = sizeof(std::uint32_t);
+ frameOffsetInFile_ += unformattedDiskExtent_;
+ unformattedDiskExtent_ = 0;
+ recordOffsetInFrame_ = 0;
} else { // formatted
if (FrameLength() > recordOffsetInFrame_ &&
Frame()[recordOffsetInFrame_] == '\r') {
@@ -351,6 +446,7 @@ void ExternalFileUnit::FinishReadingRecord(IoErrorHandler &handler) {
recordOffsetInFrame_ = 0;
}
BeginRecord();
+ ResetSequentialUnformattedStream();
leftTabLimit.reset();
}
@@ -376,21 +472,28 @@ bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
}
} else if (*isUnformatted) {
if (access == Access::Sequential) {
- // Append the length of a sequential unformatted variable-length record
- // as its footer, then overwrite the reserved first four bytes of the
- // record with its length as its header. These four bytes were skipped
- // over in BeginUnformattedIO<Output>().
- // TODO: Break very large records up into subrecords with negative
- // headers &/or footers
- std::uint32_t length;
- length = furthestPositionInRecord - sizeof length;
- ok = ok &&
- Emit(reinterpret_cast<const char *>(&length), sizeof length,
- sizeof length, handler);
- positionInRecord = 0;
- ok = ok &&
- Emit(reinterpret_cast<const char *>(&length), sizeof length,
- sizeof length, handler);
+ std::size_t payload{
+ static_cast<std::size_t>(furthestPositionInRecord - kRecMarkSize)};
+ if (payload <= static_cast<std::size_t>(kMaxSubrecPayload)) {
+ std::int32_t length{static_cast<std::int32_t>(payload)};
+ ok = ok &&
+ Emit(reinterpret_cast<const char *>(&length), kRecMarkSize,
+ kRecMarkSize, handler);
+ positionInRecord = 0;
+ ok = ok &&
+ Emit(reinterpret_cast<const char *>(&length), kRecMarkSize,
+ kRecMarkSize, handler);
+ } else {
+ std::size_t maxChunk{static_cast<std::size_t>(kMaxSubrecPayload)};
+ std::size_t subrecords{(payload + maxChunk - 1) / maxChunk};
+ std::size_t diskExtent{payload + 2 * subrecords * kRecMarkSize};
+ WriteFrame(
+ frameOffsetInFile_, recordOffsetInFrame_ + diskExtent, handler);
+ char *base{Frame() + recordOffsetInFrame_};
+ LayoutBufferedSubrecords(base, payload, swapEndianness_);
+ furthestPositionInRecord = static_cast<std::int64_t>(diskExtent);
+ positionInRecord = furthestPositionInRecord;
+ }
} else {
// Unformatted stream: nothing to do
}
@@ -516,6 +619,7 @@ void ExternalFileUnit::Rewind(IoErrorHandler &handler) {
void ExternalFileUnit::SetPosition(std::int64_t pos) {
frameOffsetInFile_ = pos;
recordOffsetInFrame_ = 0;
+ ResetSequentialUnformattedStream();
if (access == Access::Direct) {
directAccessRecWasSet_ = true;
}
@@ -607,67 +711,130 @@ void ExternalFileUnit::EndIoStatement() {
void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord(
IoErrorHandler &handler) {
RUNTIME_CHECK(handler, access == Access::Sequential);
- std::uint32_t header{0}, footer{0};
- std::size_t need{recordOffsetInFrame_ + sizeof header};
- std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)};
- // Try to emit informative errors to help debug corrupted files.
+ unformattedDiskExtent_ = 0;
+ streamRead_ = false;
+ FileOffset recordStart{
+ frameOffsetInFile_ + static_cast<FileOffset>(recordOffsetInFrame_)};
const char *error{nullptr};
- if (got < need) {
- if (got == recordOffsetInFrame_) {
- HitEndOnRead(handler);
+ std::int32_t header{0}, footer{0}, firstHeader{0};
+ std::size_t totalPayload{0};
+ FileOffset filePos{recordStart};
+
+ enum class MarkerRead { Ok, Eof, Short };
+ enum class ScanOutcome { Ok, Eof, Fail };
+ const char *scanError{nullptr};
+
+ auto readMarkerAt = [&](FileOffset &pos, std::int32_t &marker, bool swap) {
+ char buf[kRecMarkSize];
+ auto got{Read(pos, buf, kRecMarkSize, kRecMarkSize, handler)};
+ if (got < kRecMarkSize) {
+ return got == 0 ? MarkerRead::Eof : MarkerRead::Short;
+ }
+ pos += kRecMarkSize;
+ marker = LoadRecordMarker(buf, swap);
+ return MarkerRead::Ok;
+ };
+
+ auto scanRecord = [&](bool swap, std::int32_t &outHeader,
+ std::int32_t &outFooter, std::int32_t &outFirstHeader,
+ std::size_t &outTotalPayload,
+ FileOffset &outFilePos) -> ScanOutcome {
+ scanError = nullptr;
+ outFilePos = recordStart;
+ outTotalPayload = 0;
+ switch (readMarkerAt(outFilePos, outHeader, swap)) {
+ case MarkerRead::Ok:
+ break;
+ case MarkerRead::Eof:
+ return ScanOutcome::Eof;
+ case MarkerRead::Short:
+ scanError = "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): truncated record header";
+ return ScanOutcome::Fail;
+ }
+ outFirstHeader = outHeader;
+ do {
+ outTotalPayload += SubrecordPayloadBytes(outHeader);
+ outFilePos += static_cast<FileOffset>(SubrecordPayloadBytes(outHeader));
+ if (readMarkerAt(outFilePos, outFooter, swap) != MarkerRead::Ok) {
+ scanError =
+ "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): truncated record footer";
+ return ScanOutcome::Fail;
+ }
+ if (SubrecordPayloadBytes(outFooter) !=
+ SubrecordPayloadBytes(outHeader)) {
+ scanError =
+ "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): record header has length %jd "
+ "that does not match record footer (%jd)";
+ return ScanOutcome::Fail;
+ }
+ if (outHeader >= 0) {
+ return ScanOutcome::Ok;
+ }
+ if (readMarkerAt(outFilePos, outHeader, swap) != MarkerRead::Ok) {
+ scanError =
+ "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): truncated record header";
+ return ScanOutcome::Fail;
+ }
+ } while (true);
+ };
+
+ auto outcome{scanRecord(
+ swapEndianness_, header, footer, firstHeader, totalPayload, filePos)};
+ if (outcome == ScanOutcome::Eof) {
+ HitEndOnRead(handler);
+ return;
+ }
+ if (outcome == ScanOutcome::Fail) {
+ const char *primaryError{scanError};
+ if (frameOffsetInFile_ == 0 && recordOffsetInFrame_ == 0 && [&]() {
+ std::int32_t swappedHeader{0}, swappedFooter{0},
+ swappedFirstHeader{0};
+ std::size_t swappedTotalPayload{0};
+ FileOffset swappedFilePos{recordStart};
+ return scanRecord(!swapEndianness_, swappedHeader, swappedFooter,
+ swappedFirstHeader, swappedTotalPayload,
+ swappedFilePos) == ScanOutcome::Ok;
+ }()) {
+ error =
+ "Unformatted variable-length sequential file input failed on the "
+ "first record, probably due to a need for byte order data "
+ "conversion; consider adding CONVERT='SWAP' to the OPEN statement "
+ "or adding FORT_CONVERT=SWAP to the execution environment";
} else {
- error = "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): truncated record header";
+ error = primaryError
+ ? primaryError
+ : "Unformatted variable-length sequential file input "
+ "failed at record #%jd (file offset %jd): truncated "
+ "record header";
}
} else {
- header = ReadHeaderOrFooter(recordOffsetInFrame_);
- recordLength = sizeof header + header; // does not include footer
- need = recordOffsetInFrame_ + *recordLength + sizeof footer;
- got = ReadFrame(frameOffsetInFile_, need, handler);
- if (got >= need) {
- footer = ReadHeaderOrFooter(recordOffsetInFrame_ + *recordLength);
- }
- if (frameOffsetInFile_ == 0 && recordOffsetInFrame_ == 0 &&
- (got < need || footer != header)) {
- // Maybe an omitted or incorrect byte swap flag setting?
- // Try it the other way, since this is the first record.
- // (N.B. Won't work on files starting with empty records, but there's
- // no good way to know later if all preceding records were empty.)
- swapEndianness_ = !swapEndianness_;
- std::uint32_t header2{ReadHeaderOrFooter(0)};
- std::size_t recordLength2{sizeof header2 + header2};
- std::size_t need2{recordLength2 + sizeof footer};
- std::size_t got2{ReadFrame(0, need2, handler)};
- if (got2 >= need2) {
- std::uint32_t footer2{ReadHeaderOrFooter(recordLength2)};
- if (footer2 == header2) {
- error = "Unformatted variable-length sequential file input "
- "failed on the first record, probably due to a need "
- "for byte order data conversion; consider adding "
- "CONVERT='SWAP' to the OPEN statement or adding "
- "FORT_CONVERT=SWAP to the execution environment";
- }
+ auto diskBytes{filePos - recordStart};
+ recordLength = kRecMarkSize + totalPayload;
+ unformattedDiskExtent_ = diskBytes;
+ positionInRecord = kRecMarkSize;
+ if (diskBytes >
+ static_cast<FileOffset>(kRecMarkSize + totalPayload + kRecMarkSize)) {
+ streamRead_ = true;
+ streamReadPos_ = recordStart + kRecMarkSize;
+ streamReadRemain_ = SubrecordPayloadBytes(firstHeader);
+ } else {
+ auto need{recordOffsetInFrame_ + static_cast<std::size_t>(diskBytes)};
+ if (ReadFrame(recordStart, need, handler) < need) {
+ error = "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): hit EOF reading record with "
+ "length %jd bytes";
}
- swapEndianness_ = !swapEndianness_;
- }
- if (error) {
- } else if (got < need) {
- error = "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): hit EOF reading record with "
- "length %jd bytes";
- } else if (footer != header) {
- error = "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): record header has length %jd "
- "that does not match record footer (%jd)";
}
}
if (error) {
handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber),
- static_cast<std::intmax_t>(frameOffsetInFile_),
+ static_cast<std::intmax_t>(recordStart),
static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer));
- // TODO: error recovery
}
- positionInRecord = sizeof header;
}
void ExternalFileUnit::BeginVariableFormattedInputRecord(
@@ -710,40 +877,37 @@ void ExternalFileUnit::BackspaceFixedRecord(IoErrorHandler &handler) {
void ExternalFileUnit::BackspaceVariableUnformattedRecord(
IoErrorHandler &handler) {
- std::uint32_t header{0};
- auto headerBytes{static_cast<std::int64_t>(sizeof header)};
frameOffsetInFile_ += recordOffsetInFrame_;
recordOffsetInFrame_ = 0;
- if (frameOffsetInFile_ <= headerBytes) {
+ if (frameOffsetInFile_ <= static_cast<FileOffset>(kRecMarkSize)) {
handler.SignalError(IostatBackspaceAtFirstRecord);
return;
}
- // Error conditions here cause crashes, not file format errors, because the
- // validity of the file structure before the current record will have been
- // checked informatively in NextSequentialVariableUnformattedInputRecord().
- std::size_t got{
- ReadFrame(frameOffsetInFile_ - headerBytes, headerBytes, handler)};
- if (static_cast<std::int64_t>(got) < headerBytes) {
- handler.SignalError(IostatShortRead);
- return;
- }
- recordLength = ReadHeaderOrFooter(0);
- if (frameOffsetInFile_ < *recordLength + 2 * headerBytes) {
- handler.SignalError(IostatBadUnformattedRecord);
- return;
- }
- frameOffsetInFile_ -= *recordLength + 2 * headerBytes;
- auto need{static_cast<std::size_t>(
- recordOffsetInFrame_ + sizeof header + *recordLength)};
- got = ReadFrame(frameOffsetInFile_, need, handler);
- if (got < need) {
+ std::int32_t marker{0};
+ do {
+ if (ReadFrame(frameOffsetInFile_ - kRecMarkSize, kRecMarkSize, handler) <
+ kRecMarkSize) {
+ handler.SignalError(IostatShortRead);
+ return;
+ }
+ marker = LoadRecordMarker(Frame(), swapEndianness_);
+ auto payload{SubrecordPayloadBytes(marker)};
+ if (frameOffsetInFile_ <
+ static_cast<FileOffset>(payload + 2 * kRecMarkSize)) {
+ handler.SignalError(IostatBadUnformattedRecord);
+ return;
+ }
+ frameOffsetInFile_ -= static_cast<FileOffset>(payload + 2 * kRecMarkSize);
+ } while (marker < 0);
+ auto need{
+ static_cast<std::size_t>(kRecMarkSize + SubrecordPayloadBytes(marker))};
+ if (ReadFrame(frameOffsetInFile_, need, handler) < need) {
handler.SignalError(IostatShortRead);
return;
}
- header = ReadHeaderOrFooter(recordOffsetInFrame_);
- if (header != *recordLength) {
+ if (SubrecordPayloadBytes(LoadRecordMarker(Frame(), swapEndianness_)) !=
+ SubrecordPayloadBytes(marker)) {
handler.SignalError(IostatBadUnformattedRecord);
- return;
}
}
@@ -902,16 +1066,6 @@ void ExternalFileUnit::PopChildIo(ChildIo &child) {
child_ = previous;
}
-std::uint32_t ExternalFileUnit::ReadHeaderOrFooter(std::int64_t frameOffset) {
- std::uint32_t word;
- char *wordPtr{reinterpret_cast<char *>(&word)};
- runtime::memcpy(wordPtr, Frame() + frameOffset, sizeof word);
- if (swapEndianness_) {
- SwapEndianness(wordPtr, sizeof word, sizeof word);
- }
- return word;
-}
-
void ChildIo::EndIoStatement() {
io_.reset();
u_.emplace<std::monostate>();
diff --git a/flang-rt/unittests/Runtime/ExternalIOTest.cpp b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
index 1fe3b08126c5d0..3e3da7f2bc5b03 100644
--- a/flang-rt/unittests/Runtime/ExternalIOTest.cpp
+++ b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
@@ -332,6 +332,110 @@ TEST(ExternalIOTests, TestSequentialVariableUnformatted) {
<< "EndIoStatement() for Close";
}
+TEST(ExternalIOTests, TestSequentialUnformattedSubrecords) {
+ static constexpr const char *fileName{"subrecord_test.tmp"};
+
+ auto emitMarker{[](std::FILE *fp, std::int32_t marker) {
+ ASSERT_EQ(std::fwrite(&marker, sizeof marker, 1, fp), 1u);
+ }};
+ auto emitValue{[](std::FILE *fp, std::int64_t value) {
+ ASSERT_EQ(std::fwrite(&value, sizeof value, 1, fp), 1u);
+ }};
+
+ static constexpr std::int64_t kRecord1[]{100, 101, 102, 103, 104, 105};
+ static constexpr std::int64_t kRecord2{999};
+ static constexpr std::int32_t kSub{16};
+
+ {
+ std::FILE *fp{std::fopen(fileName, "wb")};
+ ASSERT_NE(fp, nullptr) << "fopen(" << fileName << ") for write";
+ emitMarker(fp, -kSub);
+ emitValue(fp, kRecord1[0]);
+ emitValue(fp, kRecord1[1]);
+ emitMarker(fp, kSub);
+ emitMarker(fp, -kSub);
+ emitValue(fp, kRecord1[2]);
+ emitValue(fp, kRecord1[3]);
+ emitMarker(fp, -kSub);
+ emitMarker(fp, kSub);
+ emitValue(fp, kRecord1[4]);
+ emitValue(fp, kRecord1[5]);
+ emitMarker(fp, -kSub);
+ emitMarker(fp, sizeof kRecord2);
+ emitValue(fp, kRecord2);
+ emitMarker(fp, sizeof kRecord2);
+ ASSERT_EQ(std::fclose(fp), 0) << "fclose after write";
+ }
+
+ // OPEN(UNIT=unit,FILE=fileName,ACCESS='SEQUENTIAL',ACTION='READ',&
+ // FORM='UNFORMATTED',STATUS='OLD')
+ static constexpr int unit{20};
+ Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
+ ASSERT_TRUE(IONAME(SetFile)(io, fileName, std::strlen(fileName)))
+ << "SetFile()";
+ ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+ << "SetAccess(SEQUENTIAL)";
+ ASSERT_TRUE(IONAME(SetAction)(io, "READ", 4)) << "SetAction(READ)";
+ ASSERT_TRUE(IONAME(SetForm)(io, "UNFORMATTED", 11)) << "SetForm(UNFORMATTED)";
+ ASSERT_TRUE(IONAME(SetStatus)(io, "OLD", 3)) << "SetStatus(OLD)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for OPEN";
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+
+ auto readRecord1{[&](const char *what) {
+ std::int64_t buffer[6]{};
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(InputDescriptor)(io, desc))
+ << "InputDescriptor() " << what;
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for READ record #1 " << what;
+ for (int k{0}; k < 6; ++k) {
+ ASSERT_EQ(buffer[k], kRecord1[k])
+ << "record #1 " << what << " element " << k << " read back "
+ << buffer[k] << ", expected " << kRecord1[k];
+ }
+ }};
+
+ auto readRecord2{[&](const char *what) {
+ std::int64_t value{0};
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ desc.Establish(TypeCode{sizeof value}, sizeof value, &value, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(InputDescriptor)(io, desc))
+ << "InputDescriptor() " << what;
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for READ record #2 " << what;
+ ASSERT_EQ(value, kRecord2) << "record #2 " << what << " read back " << value
+ << ", expected " << kRecord2;
+ }};
+
+ readRecord1("forward");
+ readRecord2("forward");
+
+ io = IONAME(BeginBackspace)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Backspace over record #2";
+ readRecord2("after backspace");
+
+ io = IONAME(BeginBackspace)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Backspace over record #2 (second time)";
+ io = IONAME(BeginBackspace)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Backspace over record #1";
+ readRecord1("after backspace");
+
+ // CLOSE(UNIT=unit,STATUS='DELETE')
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_TRUE(IONAME(SetStatus)(io, "DELETE", 6)) << "SetStatus(DELETE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+}
+
TEST(ExternalIOTests, TestDirectFormatted) {
// OPEN(NEWUNIT=unit,ACCESS='DIRECT',ACTION='READWRITE',&
// FORM='FORMATTED',RECL=8,STATUS='SCRATCH')
>From c6f2b45a2e783cb48c390485c3b86b7a28c0c17a Mon Sep 17 00:00:00 2001
From: ejose <ejose at amd.com>
Date: Fri, 16 Oct 2026 09:42:17 +0000
Subject: [PATCH 2/2] [flang-rt] Split large unformatted sequential records
into subrecords
Write gfortran-compatible subrecord chains for payloads above the
signed-marker limit. Address review: SWAP after assemble, non-seekable
single-marker path, restore small-record performance, hybrid streaming,
RECL before subrecord layout, legacy 2-4 GiB uint32 markers, advance
on read errors, cheap BACKSPACE, INT32_MIN-safe payload size.
Add FLANG_RT_MAX_SUBRECORD_LENGTH and unit tests (layout, SWAP, RECL,
bad-record recovery, FIFO). Document the env var. Fix GCC ambiguity on
streamReadPos_ (std::int64_t).
---
.../include/flang-rt/runtime/environment.h | 4 +
flang-rt/include/flang-rt/runtime/unit.h | 2 +-
flang-rt/lib/runtime/environment.cpp | 13 +
flang-rt/lib/runtime/unit.cpp | 288 +++++++--------
flang-rt/unittests/Runtime/ExternalIOTest.cpp | 332 +++++++++++++++++-
flang/docs/RuntimeEnvironment.md | 11 +
6 files changed, 508 insertions(+), 142 deletions(-)
diff --git a/flang-rt/include/flang-rt/runtime/environment.h b/flang-rt/include/flang-rt/runtime/environment.h
index 332330e651c94d..c1f0c9898333b6 100644
--- a/flang-rt/include/flang-rt/runtime/environment.h
+++ b/flang-rt/include/flang-rt/runtime/environment.h
@@ -92,6 +92,10 @@ struct ExecutionEnvironment {
// (CopyOutAssign then copies every element back instead of only the
// suffix from the first modified element through the end).
bool copyOutModifiedOnly{true}; // FLANG_RT_COPYOUT_MODIFIED_ONLY
+ // Largest payload of one subrecord of an unformatted sequential record,
+ // like gfortran's -fmax-subrecord-length. Zero selects the default, which
+ // is the largest value that a signed 32-bit record marker can describe.
+ std::size_t maxSubrecordLength{0}; // FLANG_RT_MAX_SUBRECORD_LENGTH
enum InternalDebugging { WorkQueue = 1 };
int internalDebugging{0}; // FLANG_RT_DEBUG
diff --git a/flang-rt/include/flang-rt/runtime/unit.h b/flang-rt/include/flang-rt/runtime/unit.h
index 761cc249f2c82d..3139f11343f8c1 100644
--- a/flang-rt/include/flang-rt/runtime/unit.h
+++ b/flang-rt/include/flang-rt/runtime/unit.h
@@ -242,7 +242,7 @@ class ExternalFileUnit : public ConnectionState,
std::int64_t frameOffsetInFile_{0};
std::size_t recordOffsetInFrame_{0}; // of currentRecordNumber
std::int64_t unformattedDiskExtent_{0};
- FileOffset streamReadPos_{0};
+ std::int64_t streamReadPos_{0};
std::size_t streamReadRemain_{0};
bool streamRead_{false};
bool swapEndianness_{false};
diff --git a/flang-rt/lib/runtime/environment.cpp b/flang-rt/lib/runtime/environment.cpp
index 0203d48443a3b1..9097e853f524e4 100644
--- a/flang-rt/lib/runtime/environment.cpp
+++ b/flang-rt/lib/runtime/environment.cpp
@@ -385,6 +385,19 @@ void ExecutionEnvironment::Configure(int ac, const char *av[],
}
}
+ if (auto *x{std::getenv("FLANG_RT_MAX_SUBRECORD_LENGTH")}) {
+ char *end;
+ auto n{std::strtoul(x, &end, 10)};
+ if (n > 0 && n <= 2147483639u && *end == '\0') {
+ maxSubrecordLength = n;
+ } else {
+ std::fprintf(stderr,
+ "Fortran runtime: FLANG_RT_MAX_SUBRECORD_LENGTH=%s is invalid; "
+ "ignored\n",
+ x);
+ }
+ }
+
if (auto *x{std::getenv("FLANG_RT_DEBUG")}) {
internalDebugging = std::strtol(x, nullptr, 10);
}
diff --git a/flang-rt/lib/runtime/unit.cpp b/flang-rt/lib/runtime/unit.cpp
index a33eacb5b9e954..2c8f193fa50fe9 100644
--- a/flang-rt/lib/runtime/unit.cpp
+++ b/flang-rt/lib/runtime/unit.cpp
@@ -103,10 +103,13 @@ static inline RT_API_ATTRS void SwapEndianness(
static constexpr std::size_t kRecMarkSize{4};
static constexpr std::int32_t kMaxSubrecPayload{0x7fffffff - 8};
+// Transfers at least this large bypass the frame and are read directly.
+static constexpr std::size_t kStreamWindow{65536};
static inline RT_API_ATTRS std::size_t SubrecordPayloadBytes(
std::int32_t marker) {
- return static_cast<std::size_t>(marker < 0 ? -marker : marker);
+ std::int64_t m{marker};
+ return static_cast<std::size_t>(m < 0 ? -m : m);
}
static inline RT_API_ATTRS std::int32_t SignedRecordMarker(
@@ -132,9 +135,14 @@ static inline RT_API_ATTRS std::int32_t LoadRecordMarker(
return word;
}
-static RT_API_ATTRS void LayoutBufferedSubrecords(
- char *base, std::size_t payload, bool swapEndianness) {
- std::size_t maxChunk{static_cast<std::size_t>(kMaxSubrecPayload)};
+static inline RT_API_ATTRS std::size_t MaxSubrecordPayload() {
+ std::size_t limit{static_cast<std::size_t>(kMaxSubrecPayload)};
+ std::size_t configured{executionEnvironment.maxSubrecordLength};
+ return configured > 0 && configured < limit ? configured : limit;
+}
+
+static RT_API_ATTRS void LayoutBufferedSubrecords(char *base,
+ std::size_t payload, std::size_t maxChunk, bool swapEndianness) {
std::size_t subrecords{(payload + maxChunk - 1) / maxChunk};
for (std::size_t i{subrecords}; i-- > 0;) {
std::size_t chunk{i + 1 < subrecords ? maxChunk : payload - i * maxChunk};
@@ -233,37 +241,41 @@ bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
while (copied < bytes) {
if (streamReadRemain_ == 0) {
streamReadPos_ += kRecMarkSize;
- char mark[kRecMarkSize];
- if (Read(streamReadPos_, mark, kRecMarkSize, kRecMarkSize, handler) <
- kRecMarkSize) {
+ if (ReadFrame(streamReadPos_, kRecMarkSize, handler) < kRecMarkSize) {
HitEndOnRead(handler);
return false;
}
- streamReadPos_ += kRecMarkSize;
streamReadRemain_ =
- SubrecordPayloadBytes(LoadRecordMarker(mark, swapEndianness_));
+ SubrecordPayloadBytes(LoadRecordMarker(Frame(), swapEndianness_));
+ streamReadPos_ += kRecMarkSize;
}
std::size_t chunk{bytes - copied};
if (chunk > streamReadRemain_) {
chunk = streamReadRemain_;
}
- auto logicalRemain{
- static_cast<std::size_t>(*recordLength - positionInRecord)};
- if (chunk > logicalRemain) {
- chunk = logicalRemain;
- }
- if (Read(streamReadPos_, data + copied, chunk, chunk, handler) < chunk) {
- HitEndOnRead(handler);
- return false;
- }
- if (swapEndianness_) {
- SwapEndianness(data + copied, chunk, elementBytes);
+ if (chunk >= kStreamWindow) {
+ if (Read(streamReadPos_, data + copied, chunk, chunk, handler) <
+ chunk) {
+ HitEndOnRead(handler);
+ return false;
+ }
+ } else {
+ if (ReadFrame(streamReadPos_, chunk, handler) < chunk) {
+ HitEndOnRead(handler);
+ return false;
+ }
+ runtime::memcpy(data + copied, Frame(), chunk);
}
- streamReadPos_ += static_cast<FileOffset>(chunk);
+ streamReadPos_ += static_cast<std::int64_t>(chunk);
streamReadRemain_ -= chunk;
copied += chunk;
positionInRecord += static_cast<std::int64_t>(chunk);
}
+ // Swap over the assembled buffer so that elements split across a
+ // subrecord boundary are byte-reversed as a whole.
+ if (swapEndianness_) {
+ SwapEndianness(data, bytes, elementBytes);
+ }
furthestPositionInRecord =
std::max(furthestPositionInRecord, positionInRecord);
return true;
@@ -474,7 +486,8 @@ bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
if (access == Access::Sequential) {
std::size_t payload{
static_cast<std::size_t>(furthestPositionInRecord - kRecMarkSize)};
- if (payload <= static_cast<std::size_t>(kMaxSubrecPayload)) {
+ std::size_t maxChunk{MaxSubrecordPayload()};
+ if (payload <= maxChunk) {
std::int32_t length{static_cast<std::int32_t>(payload)};
ok = ok &&
Emit(reinterpret_cast<const char *>(&length), kRecMarkSize,
@@ -484,13 +497,19 @@ bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
Emit(reinterpret_cast<const char *>(&length), kRecMarkSize,
kRecMarkSize, handler);
} else {
- std::size_t maxChunk{static_cast<std::size_t>(kMaxSubrecPayload)};
+ if (openRecl && payload > static_cast<std::size_t>(*openRecl)) {
+ handler.SignalError(IostatRecordWriteOverrun,
+ "Attempt to write %zd bytes to position 0 in a fixed-size "
+ "record of %jd bytes",
+ payload, static_cast<std::intmax_t>(*openRecl));
+ return false;
+ }
std::size_t subrecords{(payload + maxChunk - 1) / maxChunk};
std::size_t diskExtent{payload + 2 * subrecords * kRecMarkSize};
WriteFrame(
frameOffsetInFile_, recordOffsetInFrame_ + diskExtent, handler);
char *base{Frame() + recordOffsetInFrame_};
- LayoutBufferedSubrecords(base, payload, swapEndianness_);
+ LayoutBufferedSubrecords(base, payload, maxChunk, swapEndianness_);
furthestPositionInRecord = static_cast<std::int64_t>(diskExtent);
positionInRecord = furthestPositionInRecord;
}
@@ -713,127 +732,120 @@ void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord(
RUNTIME_CHECK(handler, access == Access::Sequential);
unformattedDiskExtent_ = 0;
streamRead_ = false;
- FileOffset recordStart{
- frameOffsetInFile_ + static_cast<FileOffset>(recordOffsetInFrame_)};
- const char *error{nullptr};
- std::int32_t header{0}, footer{0}, firstHeader{0};
- std::size_t totalPayload{0};
- FileOffset filePos{recordStart};
-
- enum class MarkerRead { Ok, Eof, Short };
- enum class ScanOutcome { Ok, Eof, Fail };
- const char *scanError{nullptr};
-
- auto readMarkerAt = [&](FileOffset &pos, std::int32_t &marker, bool swap) {
- char buf[kRecMarkSize];
- auto got{Read(pos, buf, kRecMarkSize, kRecMarkSize, handler)};
- if (got < kRecMarkSize) {
- return got == 0 ? MarkerRead::Eof : MarkerRead::Short;
+ std::int64_t recordStart{
+ frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_)};
+ std::size_t need{recordOffsetInFrame_ + kRecMarkSize};
+ std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)};
+ positionInRecord = kRecMarkSize;
+ if (got < need) {
+ if (got <= recordOffsetInFrame_) {
+ HitEndOnRead(handler);
+ } else {
+ handler.SignalError(
+ "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): truncated record header",
+ static_cast<std::intmax_t>(currentRecordNumber),
+ static_cast<std::intmax_t>(recordStart));
}
- pos += kRecMarkSize;
- marker = LoadRecordMarker(buf, swap);
- return MarkerRead::Ok;
- };
+ return;
+ }
+ std::int32_t header{
+ LoadRecordMarker(Frame() + recordOffsetInFrame_, swapEndianness_)};
- auto scanRecord = [&](bool swap, std::int32_t &outHeader,
- std::int32_t &outFooter, std::int32_t &outFirstHeader,
- std::size_t &outTotalPayload,
- FileOffset &outFilePos) -> ScanOutcome {
- scanError = nullptr;
- outFilePos = recordStart;
- outTotalPayload = 0;
- switch (readMarkerAt(outFilePos, outHeader, swap)) {
- case MarkerRead::Ok:
- break;
- case MarkerRead::Eof:
- return ScanOutcome::Eof;
- case MarkerRead::Short:
- scanError = "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): truncated record header";
- return ScanOutcome::Fail;
- }
- outFirstHeader = outHeader;
- do {
- outTotalPayload += SubrecordPayloadBytes(outHeader);
- outFilePos += static_cast<FileOffset>(SubrecordPayloadBytes(outHeader));
- if (readMarkerAt(outFilePos, outFooter, swap) != MarkerRead::Ok) {
- scanError =
- "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): truncated record footer";
- return ScanOutcome::Fail;
+ // A negative header starts a subrecord chain, which has to be walked to
+ // learn the record length. Seeking is required, so this is skipped for
+ // non-positionable files; a chain that does not check out falls through to
+ // the single-marker interpretation below.
+ if (mayPosition() && header < 0) {
+ auto readMarkerAt = [&](std::int64_t &pos, std::int32_t &marker) {
+ char buf[kRecMarkSize];
+ if (Read(pos, buf, kRecMarkSize, kRecMarkSize, handler) < kRecMarkSize) {
+ return false;
}
- if (SubrecordPayloadBytes(outFooter) !=
- SubrecordPayloadBytes(outHeader)) {
- scanError =
- "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): record header has length %jd "
- "that does not match record footer (%jd)";
- return ScanOutcome::Fail;
+ pos += kRecMarkSize;
+ marker = LoadRecordMarker(buf, swapEndianness_);
+ return true;
+ };
+ std::size_t totalPayload{0};
+ std::int64_t filePos{recordStart + static_cast<std::int64_t>(kRecMarkSize)};
+ std::int32_t marker{header};
+ bool ok{true};
+ while (true) {
+ totalPayload += SubrecordPayloadBytes(marker);
+ filePos += static_cast<std::int64_t>(SubrecordPayloadBytes(marker));
+ std::int32_t subrecordFooter{0};
+ if (!readMarkerAt(filePos, subrecordFooter) ||
+ SubrecordPayloadBytes(subrecordFooter) !=
+ SubrecordPayloadBytes(marker)) {
+ ok = false;
+ break;
}
- if (outHeader >= 0) {
- return ScanOutcome::Ok;
+ if (marker >= 0) {
+ break;
}
- if (readMarkerAt(outFilePos, outHeader, swap) != MarkerRead::Ok) {
- scanError =
- "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): truncated record header";
- return ScanOutcome::Fail;
+ if (!readMarkerAt(filePos, marker)) {
+ ok = false;
+ break;
}
- } while (true);
- };
-
- auto outcome{scanRecord(
- swapEndianness_, header, footer, firstHeader, totalPayload, filePos)};
- if (outcome == ScanOutcome::Eof) {
- HitEndOnRead(handler);
- return;
- }
- if (outcome == ScanOutcome::Fail) {
- const char *primaryError{scanError};
- if (frameOffsetInFile_ == 0 && recordOffsetInFrame_ == 0 && [&]() {
- std::int32_t swappedHeader{0}, swappedFooter{0},
- swappedFirstHeader{0};
- std::size_t swappedTotalPayload{0};
- FileOffset swappedFilePos{recordStart};
- return scanRecord(!swapEndianness_, swappedHeader, swappedFooter,
- swappedFirstHeader, swappedTotalPayload,
- swappedFilePos) == ScanOutcome::Ok;
- }()) {
- error =
- "Unformatted variable-length sequential file input failed on the "
- "first record, probably due to a need for byte order data "
- "conversion; consider adding CONVERT='SWAP' to the OPEN statement "
- "or adding FORT_CONVERT=SWAP to the execution environment";
- } else {
- error = primaryError
- ? primaryError
- : "Unformatted variable-length sequential file input "
- "failed at record #%jd (file offset %jd): truncated "
- "record header";
}
- } else {
- auto diskBytes{filePos - recordStart};
- recordLength = kRecMarkSize + totalPayload;
- unformattedDiskExtent_ = diskBytes;
- positionInRecord = kRecMarkSize;
- if (diskBytes >
- static_cast<FileOffset>(kRecMarkSize + totalPayload + kRecMarkSize)) {
+ if (ok) {
+ recordLength = static_cast<std::int64_t>(kRecMarkSize + totalPayload);
+ unformattedDiskExtent_ = filePos - recordStart;
streamRead_ = true;
- streamReadPos_ = recordStart + kRecMarkSize;
- streamReadRemain_ = SubrecordPayloadBytes(firstHeader);
- } else {
- auto need{recordOffsetInFrame_ + static_cast<std::size_t>(diskBytes)};
- if (ReadFrame(recordStart, need, handler) < need) {
- error = "Unformatted variable-length sequential file input failed at "
- "record #%jd (file offset %jd): hit EOF reading record with "
- "length %jd bytes";
- }
+ streamReadPos_ = recordStart + static_cast<std::int64_t>(kRecMarkSize);
+ streamReadRemain_ = SubrecordPayloadBytes(header);
+ return;
}
}
+
+ // Single header/footer pair. The marker is unsigned here, so records
+ // written by earlier versions of this runtime with a length in 2 GiB to
+ // 4 GiB remain readable.
+ std::uint32_t length{static_cast<std::uint32_t>(header)};
+ recordLength = static_cast<std::int64_t>(kRecMarkSize + length);
+ unformattedDiskExtent_ =
+ static_cast<std::int64_t>(kRecMarkSize + length + kRecMarkSize);
+ need = recordOffsetInFrame_ + static_cast<std::size_t>(*recordLength) +
+ kRecMarkSize;
+ got = ReadFrame(frameOffsetInFile_, need, handler);
+ std::uint32_t footer{0};
+ if (got >= need) {
+ footer = static_cast<std::uint32_t>(LoadRecordMarker(
+ Frame() + recordOffsetInFrame_ + *recordLength, swapEndianness_));
+ }
+ const char *error{nullptr};
+ if (frameOffsetInFile_ == 0 && recordOffsetInFrame_ == 0 &&
+ (got < need || footer != length)) {
+ // Maybe an omitted or incorrect byte swap flag setting?
+ // Try it the other way, since this is the first record.
+ std::uint32_t swapped{static_cast<std::uint32_t>(
+ LoadRecordMarker(Frame(), !swapEndianness_))};
+ std::size_t length2{kRecMarkSize + swapped};
+ std::size_t need2{length2 + kRecMarkSize};
+ if (ReadFrame(0, need2, handler) >= need2 &&
+ static_cast<std::uint32_t>(
+ LoadRecordMarker(Frame() + length2, !swapEndianness_)) == swapped) {
+ error = "Unformatted variable-length sequential file input failed on the "
+ "first record, probably due to a need for byte order data "
+ "conversion; consider adding CONVERT='SWAP' to the OPEN "
+ "statement or adding FORT_CONVERT=SWAP to the execution "
+ "environment";
+ }
+ }
+ if (error) {
+ } else if (got < need) {
+ error = "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): hit EOF reading record with "
+ "length %jd bytes";
+ } else if (footer != length) {
+ error = "Unformatted variable-length sequential file input failed at "
+ "record #%jd (file offset %jd): record header has length %jd "
+ "that does not match record footer (%jd)";
+ }
if (error) {
handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber),
static_cast<std::intmax_t>(recordStart),
- static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer));
+ static_cast<std::intmax_t>(length), static_cast<std::intmax_t>(footer));
}
}
@@ -879,7 +891,7 @@ void ExternalFileUnit::BackspaceVariableUnformattedRecord(
IoErrorHandler &handler) {
frameOffsetInFile_ += recordOffsetInFrame_;
recordOffsetInFrame_ = 0;
- if (frameOffsetInFile_ <= static_cast<FileOffset>(kRecMarkSize)) {
+ if (frameOffsetInFile_ <= static_cast<std::int64_t>(kRecMarkSize)) {
handler.SignalError(IostatBackspaceAtFirstRecord);
return;
}
@@ -893,15 +905,15 @@ void ExternalFileUnit::BackspaceVariableUnformattedRecord(
marker = LoadRecordMarker(Frame(), swapEndianness_);
auto payload{SubrecordPayloadBytes(marker)};
if (frameOffsetInFile_ <
- static_cast<FileOffset>(payload + 2 * kRecMarkSize)) {
+ static_cast<std::int64_t>(payload + 2 * kRecMarkSize)) {
handler.SignalError(IostatBadUnformattedRecord);
return;
}
- frameOffsetInFile_ -= static_cast<FileOffset>(payload + 2 * kRecMarkSize);
+ frameOffsetInFile_ -= static_cast<std::int64_t>(payload + 2 * kRecMarkSize);
} while (marker < 0);
- auto need{
- static_cast<std::size_t>(kRecMarkSize + SubrecordPayloadBytes(marker))};
- if (ReadFrame(frameOffsetInFile_, need, handler) < need) {
+ // Only the header is needed here; reading the payload too would require a
+ // frame as large as the subrecord.
+ if (ReadFrame(frameOffsetInFile_, kRecMarkSize, handler) < kRecMarkSize) {
handler.SignalError(IostatShortRead);
return;
}
diff --git a/flang-rt/unittests/Runtime/ExternalIOTest.cpp b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
index 3e3da7f2bc5b03..1fa3d975215940 100644
--- a/flang-rt/unittests/Runtime/ExternalIOTest.cpp
+++ b/flang-rt/unittests/Runtime/ExternalIOTest.cpp
@@ -13,6 +13,7 @@
#include "CrashHandlerFixture.h"
#include "gtest/gtest.h"
#include "flang-rt/runtime/descriptor.h"
+#include "flang-rt/runtime/environment.h"
#include "flang/Runtime/extensions.h"
#include "flang/Runtime/io-api.h"
#include "flang/Runtime/main.h"
@@ -20,6 +21,11 @@
#include <cstdio>
#include <cstring>
#include <string_view>
+#if defined(__unix__) || defined(__APPLE__)
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#endif
using namespace Fortran::runtime;
using namespace Fortran::runtime::io;
@@ -332,8 +338,64 @@ TEST(ExternalIOTests, TestSequentialVariableUnformatted) {
<< "EndIoStatement() for Close";
}
+namespace {
+// Shrinks the subrecord payload limit so that tests can drive the subrecord
+// paths without writing multi-gigabyte records.
+struct ScopedMaxSubrecordLength {
+ explicit ScopedMaxSubrecordLength(std::size_t bytes)
+ : saved_{executionEnvironment.maxSubrecordLength} {
+ executionEnvironment.maxSubrecordLength = bytes;
+ }
+ ~ScopedMaxSubrecordLength() {
+ executionEnvironment.maxSubrecordLength = saved_;
+ }
+
+private:
+ std::size_t saved_;
+};
+
+// Removes the file at the end of the test, including on an early return from
+// a failed assertion. Each test uses its own name, so the names stay unique
+// across the shards that llvm-lit runs concurrently.
+struct ScopedTempFile {
+ explicit ScopedTempFile(const char *name) : name_{name} { std::remove(name); }
+ ~ScopedTempFile() { std::remove(name_); }
+ const char *name() const { return name_; }
+
+private:
+ const char *name_;
+};
+
+// OPEN(UNIT=unit,FILE=fileName,ACCESS='SEQUENTIAL',ACTION='READWRITE',&
+// FORM='UNFORMATTED',STATUS='REPLACE')
+void OpenSequentialUnformatted(
+ int unit, const char *fileName, const char *convert) {
+ Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
+ ASSERT_TRUE(IONAME(SetFile)(io, fileName, std::strlen(fileName)))
+ << "SetFile";
+ ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+ << "SetAccess(SEQUENTIAL)";
+ ASSERT_TRUE(IONAME(SetAction)(io, "READWRITE", 9)) << "SetAction(READWRITE)";
+ ASSERT_TRUE(IONAME(SetForm)(io, "UNFORMATTED", 11)) << "SetForm(UNFORMATTED)";
+ if (convert) {
+ ASSERT_TRUE(IONAME(SetConvert)(io, convert, std::strlen(convert)))
+ << "SetConvert";
+ }
+ ASSERT_TRUE(IONAME(SetStatus)(io, "REPLACE", 7)) << "SetStatus(REPLACE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for OPEN";
+}
+
+std::int32_t MarkerAt(const char *raw, std::size_t at) {
+ std::int32_t marker;
+ std::memcpy(&marker, raw + at, sizeof marker);
+ return marker;
+}
+} // namespace
+
TEST(ExternalIOTests, TestSequentialUnformattedSubrecords) {
- static constexpr const char *fileName{"subrecord_test.tmp"};
+ ScopedTempFile tempFile{"subrecord_read_test.tmp"};
+ const char *fileName{tempFile.name()};
auto emitMarker{[](std::FILE *fp, std::int32_t marker) {
ASSERT_EQ(std::fwrite(&marker, sizeof marker, 1, fp), 1u);
@@ -348,7 +410,7 @@ TEST(ExternalIOTests, TestSequentialUnformattedSubrecords) {
{
std::FILE *fp{std::fopen(fileName, "wb")};
- ASSERT_NE(fp, nullptr) << "fopen(" << fileName << ") for write";
+ ASSERT_NE(fp, nullptr) << "fopen for write";
emitMarker(fp, -kSub);
emitValue(fp, kRecord1[0]);
emitValue(fp, kRecord1[1]);
@@ -372,7 +434,7 @@ TEST(ExternalIOTests, TestSequentialUnformattedSubrecords) {
static constexpr int unit{20};
Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
ASSERT_TRUE(IONAME(SetFile)(io, fileName, std::strlen(fileName)))
- << "SetFile()";
+ << "SetFile";
ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
<< "SetAccess(SEQUENTIAL)";
ASSERT_TRUE(IONAME(SetAction)(io, "READ", 4)) << "SetAction(READ)";
@@ -436,6 +498,270 @@ TEST(ExternalIOTests, TestSequentialUnformattedSubrecords) {
<< "EndIoStatement() for Close";
}
+TEST(ExternalIOTests, TestSequentialUnformattedSubrecordWrite) {
+ // A 48-byte record with a 13-byte subrecord limit is laid out by gfortran
+ // -fmax-subrecord-length=13 as
+ // -13 [13] 13 | -13 [13] -13 | -13 [13] -13 | 9 [9] -9
+ ScopedMaxSubrecordLength maxSubrecordLength{13};
+ ScopedTempFile tempFile{"subrecord_write_test.tmp"};
+ const char *fileName{tempFile.name()};
+ static constexpr std::int64_t values[]{100, 101, 102, 103, 104, 105};
+ static constexpr int unit{21};
+
+ OpenSequentialUnformatted(unit, fileName, nullptr);
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+ std::int64_t buffer[6];
+ std::memcpy(buffer, values, sizeof buffer);
+
+ // WRITE(UNIT=unit) buffer
+ Cookie io{IONAME(BeginUnformattedOutput)(unit, __FILE__, __LINE__)};
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(OutputDescriptor)(io, desc)) << "OutputDescriptor()";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for WRITE";
+
+ // REWIND(UNIT=unit); READ(UNIT=unit) buffer
+ io = IONAME(BeginRewind)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Rewind";
+ std::memset(buffer, 0, sizeof buffer);
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(InputDescriptor)(io, desc)) << "InputDescriptor()";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for READ";
+ for (int j{0}; j < 6; ++j) {
+ EXPECT_EQ(buffer[j], values[j]) << "element " << j;
+ }
+
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_TRUE(IONAME(SetStatus)(io, "KEEP", 4)) << "SetStatus(KEEP)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+
+ std::FILE *fp{std::fopen(fileName, "rb")};
+ ASSERT_NE(fp, nullptr) << "fopen for read";
+ char raw[80];
+ ASSERT_EQ(std::fread(raw, 1, sizeof raw, fp), sizeof raw) << "fread";
+ EXPECT_EQ(std::fgetc(fp), EOF) << "file is longer than 80 bytes";
+ ASSERT_EQ(std::fclose(fp), 0) << "fclose";
+
+ EXPECT_EQ(MarkerAt(raw, 0), -13) << "subrecord #1 header";
+ EXPECT_EQ(MarkerAt(raw, 17), 13) << "subrecord #1 footer";
+ EXPECT_EQ(MarkerAt(raw, 21), -13) << "subrecord #2 header";
+ EXPECT_EQ(MarkerAt(raw, 38), -13) << "subrecord #2 footer";
+ EXPECT_EQ(MarkerAt(raw, 42), -13) << "subrecord #3 header";
+ EXPECT_EQ(MarkerAt(raw, 59), -13) << "subrecord #3 footer";
+ EXPECT_EQ(MarkerAt(raw, 63), 9) << "subrecord #4 header";
+ EXPECT_EQ(MarkerAt(raw, 76), -9) << "subrecord #4 footer";
+
+ char payload[sizeof values];
+ std::memcpy(payload, raw + 4, 13);
+ std::memcpy(payload + 13, raw + 25, 13);
+ std::memcpy(payload + 26, raw + 46, 13);
+ std::memcpy(payload + 39, raw + 67, 9);
+ EXPECT_EQ(std::memcmp(payload, values, sizeof payload), 0)
+ << "concatenated subrecord payloads";
+}
+
+TEST(ExternalIOTests, TestSequentialUnformattedSubrecordSwap) {
+ // 13 is not a multiple of 8, so every subrecord boundary splits an
+ // INTEGER(8) element; byte swapping must still reverse whole elements.
+ ScopedMaxSubrecordLength maxSubrecordLength{13};
+ ScopedTempFile tempFile{"subrecord_swap_test.tmp"};
+ const char *fileName{tempFile.name()};
+ static constexpr std::int64_t values[]{0x0102030405060708, 2, 3, 4, 5, 6};
+ static constexpr int unit{22};
+
+ OpenSequentialUnformatted(unit, fileName, "SWAP");
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+ std::int64_t buffer[6];
+ std::memcpy(buffer, values, sizeof buffer);
+
+ Cookie io{IONAME(BeginUnformattedOutput)(unit, __FILE__, __LINE__)};
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(OutputDescriptor)(io, desc)) << "OutputDescriptor()";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for WRITE";
+
+ io = IONAME(BeginRewind)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Rewind";
+ std::memset(buffer, 0, sizeof buffer);
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ ASSERT_TRUE(IONAME(InputDescriptor)(io, desc)) << "InputDescriptor()";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for READ";
+ for (int j{0}; j < 6; ++j) {
+ EXPECT_EQ(buffer[j], values[j]) << "element " << j;
+ }
+
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_TRUE(IONAME(SetStatus)(io, "DELETE", 6)) << "SetStatus(DELETE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+}
+
+TEST(ExternalIOTests, TestSequentialUnformattedSubrecordRecl) {
+ // RECL= is checked by the footer Emit() on the single-marker path; the
+ // subrecord path emits no footer and has to check for itself. RECL=46
+ // is large enough that writing the data does not trip the Emit() check.
+ ScopedMaxSubrecordLength maxSubrecordLength{13};
+ ScopedTempFile tempFile{"subrecord_recl_test.tmp"};
+ const char *fileName{tempFile.name()};
+ static constexpr int unit{23};
+
+ Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
+ ASSERT_TRUE(IONAME(SetFile)(io, fileName, std::strlen(fileName)))
+ << "SetFile";
+ ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+ << "SetAccess(SEQUENTIAL)";
+ ASSERT_TRUE(IONAME(SetAction)(io, "READWRITE", 9)) << "SetAction(READWRITE)";
+ ASSERT_TRUE(IONAME(SetForm)(io, "UNFORMATTED", 11)) << "SetForm(UNFORMATTED)";
+ ASSERT_TRUE(IONAME(SetRecl)(io, 46)) << "SetRecl(46)";
+ ASSERT_TRUE(IONAME(SetStatus)(io, "REPLACE", 7)) << "SetStatus(REPLACE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for OPEN";
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+ std::int64_t buffer[6]{1, 2, 3, 4, 5, 6}; // 48 bytes > RECL=46
+
+ io = IONAME(BeginUnformattedOutput)(unit, __FILE__, __LINE__);
+ IONAME(EnableHandlers)(io, true /*IOSTAT=*/);
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ IONAME(OutputDescriptor)(io, desc);
+ EXPECT_EQ(IONAME(EndIoStatement)(io), IostatRecordWriteOverrun)
+ << "EndIoStatement() for WRITE past RECL=";
+
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_TRUE(IONAME(SetStatus)(io, "DELETE", 6)) << "SetStatus(DELETE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+}
+
+TEST(ExternalIOTests, TestSequentialUnformattedBadRecordRecovery) {
+ // A record whose header and footer disagree is reported once; the next
+ // READ must make progress rather than report the same record forever.
+ ScopedTempFile tempFile{"bad_record_test.tmp"};
+ const char *fileName{tempFile.name()};
+ {
+ std::FILE *fp{std::fopen(fileName, "wb")};
+ ASSERT_NE(fp, nullptr) << "fopen for write";
+ auto emit32{[&](std::int32_t v) {
+ ASSERT_EQ(std::fwrite(&v, sizeof v, 1, fp), 1u);
+ }};
+ auto emit64{[&](std::int64_t v) {
+ ASSERT_EQ(std::fwrite(&v, sizeof v, 1, fp), 1u);
+ }};
+ emit32(8), emit64(11), emit32(9); // corrupt: footer != header
+ emit32(8), emit64(22), emit32(8); // valid
+ ASSERT_EQ(std::fclose(fp), 0) << "fclose after write";
+ }
+
+ static constexpr int unit{24};
+ Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
+ ASSERT_TRUE(IONAME(SetFile)(io, fileName, std::strlen(fileName)))
+ << "SetFile";
+ ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+ << "SetAccess(SEQUENTIAL)";
+ ASSERT_TRUE(IONAME(SetAction)(io, "READ", 4)) << "SetAction(READ)";
+ ASSERT_TRUE(IONAME(SetForm)(io, "UNFORMATTED", 11)) << "SetForm(UNFORMATTED)";
+ ASSERT_TRUE(IONAME(SetStatus)(io, "OLD", 3)) << "SetStatus(OLD)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for OPEN";
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+ auto readValue{[&](std::int64_t &value) {
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ IONAME(EnableHandlers)(io, true /*IOSTAT=*/, false, true /*END=*/);
+ desc.Establish(TypeCode{sizeof value}, sizeof value, &value, 0);
+ desc.Check();
+ IONAME(InputDescriptor)(io, desc);
+ return IONAME(EndIoStatement)(io);
+ }};
+
+ std::int64_t value{0};
+ EXPECT_NE(readValue(value), IostatOk) << "READ of the corrupt record";
+ value = 0;
+ EXPECT_EQ(readValue(value), IostatOk) << "READ after the corrupt record";
+ EXPECT_EQ(value, 22) << "value of the record after the corrupt one";
+ EXPECT_EQ(readValue(value), IostatEnd) << "READ at end of file";
+
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_TRUE(IONAME(SetStatus)(io, "DELETE", 6)) << "SetStatus(DELETE)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+}
+
+#if defined(__unix__) || defined(__APPLE__)
+TEST(ExternalIOTests, TestSequentialUnformattedFromPipe) {
+ // Sequential unformatted input must not depend on being able to seek.
+ ScopedTempFile tempFifo{"sequential_unformatted_fifo.tmp"};
+ const char *fifoName{tempFifo.name()};
+ ASSERT_EQ(mkfifo(fifoName, 0600), 0) << "mkfifo";
+
+ static constexpr std::int64_t values[]{100, 101, 102};
+ pid_t pid{fork()};
+ ASSERT_NE(pid, -1) << "fork";
+ if (pid == 0) {
+ std::FILE *fp{std::fopen(fifoName, "wb")};
+ if (!fp) {
+ _exit(1);
+ }
+ std::int32_t marker(sizeof values);
+ bool ok{std::fwrite(&marker, sizeof marker, 1, fp) == 1 &&
+ std::fwrite(values, sizeof values, 1, fp) == 1 &&
+ std::fwrite(&marker, sizeof marker, 1, fp) == 1};
+ _exit(std::fclose(fp) == 0 && ok ? 0 : 1);
+ }
+
+ static constexpr int unit{25};
+ Cookie io{IONAME(BeginOpenUnit)(unit, __FILE__, __LINE__)};
+ ASSERT_TRUE(IONAME(SetFile)(io, fifoName, std::strlen(fifoName)))
+ << "SetFile";
+ ASSERT_TRUE(IONAME(SetAccess)(io, "SEQUENTIAL", 10))
+ << "SetAccess(SEQUENTIAL)";
+ ASSERT_TRUE(IONAME(SetAction)(io, "READ", 4)) << "SetAction(READ)";
+ ASSERT_TRUE(IONAME(SetForm)(io, "UNFORMATTED", 11)) << "SetForm(UNFORMATTED)";
+ ASSERT_TRUE(IONAME(SetStatus)(io, "OLD", 3)) << "SetStatus(OLD)";
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for OPEN";
+
+ StaticDescriptor<0> staticDescriptor;
+ Descriptor &desc{staticDescriptor.descriptor()};
+ std::int64_t buffer[3]{};
+ io = IONAME(BeginUnformattedInput)(unit, __FILE__, __LINE__);
+ desc.Establish(TypeCode{sizeof buffer[0]}, sizeof buffer, buffer, 0);
+ desc.Check();
+ EXPECT_TRUE(IONAME(InputDescriptor)(io, desc)) << "InputDescriptor()";
+ EXPECT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for READ from a FIFO";
+ for (int j{0}; j < 3; ++j) {
+ EXPECT_EQ(buffer[j], values[j]) << "element " << j;
+ }
+
+ io = IONAME(BeginClose)(unit, __FILE__, __LINE__);
+ ASSERT_EQ(IONAME(EndIoStatement)(io), IostatOk)
+ << "EndIoStatement() for Close";
+
+ int status{0};
+ ASSERT_EQ(waitpid(pid, &status, 0), pid) << "waitpid";
+ EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) << "writer";
+}
+#endif
+
TEST(ExternalIOTests, TestDirectFormatted) {
// OPEN(NEWUNIT=unit,ACCESS='DIRECT',ACTION='READWRITE',&
// FORM='FORMATTED',RECL=8,STATUS='SCRATCH')
diff --git a/flang/docs/RuntimeEnvironment.md b/flang/docs/RuntimeEnvironment.md
index 8b1ead298f45dd..71f63c76b24554 100644
--- a/flang/docs/RuntimeEnvironment.md
+++ b/flang/docs/RuntimeEnvironment.md
@@ -117,6 +117,17 @@ restore the unconditional whole-object copy-out. Note that this restores
stores of unmodified data as well (including into read-only storage), so it
is an escape hatch and A/B-comparison aid, not a safer mode.
+## `FLANG_RT_MAX_SUBRECORD_LENGTH`
+
+Set to an integer value to limit the number of payload bytes that the runtime
+places in one subrecord of an unformatted sequential record
+A record longer than this limit is written as a chain of subrecords whose
+4-byte length markers are negative when another subrecord follows, which is
+the layout that gfortran uses.
+The default, which also applies when the value is not a positive integer no
+greater than 2147483639, is the largest length that a signed 32-bit marker
+can describe; records shorter than that keep a single header and footer.
+
## `FORT_CHECK_POINTER_DEALLOCATION`
Fortran requires that a pointer that appears in a `DEALLOCATE` statement
More information about the flang-commits
mailing list