[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