1 //===-- runtime/unit.cpp ----------------------------------------*- C++ -*-===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "unit.h" 10 #include "io-error.h" 11 #include "lock.h" 12 #include "unit-map.h" 13 #include <cstdio> 14 15 namespace Fortran::runtime::io { 16 17 // The per-unit data structures are created on demand so that Fortran I/O 18 // should work without a Fortran main program. 19 static Lock unitMapLock; 20 static UnitMap *unitMap{nullptr}; 21 static ExternalFileUnit *defaultInput{nullptr}; 22 static ExternalFileUnit *defaultOutput{nullptr}; 23 24 void FlushOutputOnCrash(const Terminator &terminator) { 25 if (!defaultOutput) { 26 return; 27 } 28 CriticalSection critical{unitMapLock}; 29 if (defaultOutput) { 30 IoErrorHandler handler{terminator}; 31 handler.HasIoStat(); // prevent nested crash if flush has error 32 defaultOutput->Flush(handler); 33 } 34 } 35 36 ExternalFileUnit *ExternalFileUnit::LookUp(int unit) { 37 return GetUnitMap().LookUp(unit); 38 } 39 40 ExternalFileUnit &ExternalFileUnit::LookUpOrCrash( 41 int unit, const Terminator &terminator) { 42 ExternalFileUnit *file{LookUp(unit)}; 43 if (!file) { 44 terminator.Crash("Not an open I/O unit number: %d", unit); 45 } 46 return *file; 47 } 48 49 ExternalFileUnit &ExternalFileUnit::LookUpOrCreate( 50 int unit, const Terminator &terminator, bool &wasExtant) { 51 return GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant); 52 } 53 54 ExternalFileUnit &ExternalFileUnit::LookUpOrCreateAnonymous( 55 int unit, Direction dir, bool isUnformatted, const Terminator &terminator) { 56 bool exists{false}; 57 ExternalFileUnit &result{ 58 GetUnitMap().LookUpOrCreate(unit, terminator, exists)}; 59 if (!exists) { 60 // I/O to an unconnected unit reads/creates a local file, e.g. fort.7 61 std::size_t pathMaxLen{32}; 62 auto path{SizedNew<char>{terminator}(pathMaxLen)}; 63 std::snprintf(path.get(), pathMaxLen, "fort.%d", unit); 64 IoErrorHandler handler{terminator}; 65 result.OpenUnit( 66 dir == Direction::Input ? OpenStatus::Old : OpenStatus::Replace, 67 Position::Rewind, std::move(path), std::strlen(path.get()), handler); 68 result.isUnformatted = isUnformatted; 69 } 70 return result; 71 } 72 73 ExternalFileUnit &ExternalFileUnit::CreateNew( 74 int unit, const Terminator &terminator) { 75 bool wasExtant{false}; 76 ExternalFileUnit &result{ 77 GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant)}; 78 RUNTIME_CHECK(terminator, !wasExtant); 79 return result; 80 } 81 82 ExternalFileUnit *ExternalFileUnit::LookUpForClose(int unit) { 83 return GetUnitMap().LookUpForClose(unit); 84 } 85 86 int ExternalFileUnit::NewUnit(const Terminator &terminator) { 87 return GetUnitMap().NewUnit(terminator).unitNumber(); 88 } 89 90 void ExternalFileUnit::OpenUnit(OpenStatus status, Position position, 91 OwningPtr<char> &&newPath, std::size_t newPathLength, 92 IoErrorHandler &handler) { 93 if (IsOpen()) { 94 if (status == OpenStatus::Old && 95 (!newPath.get() || 96 (path() && pathLength() == newPathLength && 97 std::memcmp(path(), newPath.get(), newPathLength) == 0))) { 98 // OPEN of existing unit, STATUS='OLD', not new FILE= 99 newPath.reset(); 100 return; 101 } 102 // Otherwise, OPEN on open unit with new FILE= implies CLOSE 103 DoImpliedEndfile(handler); 104 Flush(handler); 105 Close(CloseStatus::Keep, handler); 106 } 107 set_path(std::move(newPath), newPathLength); 108 Open(status, position, handler); 109 auto totalBytes{knownSize()}; 110 if (access == Access::Direct) { 111 if (!isFixedRecordLength || !recordLength) { 112 handler.SignalError(IostatOpenBadRecl, 113 "OPEN(UNIT=%d,ACCESS='DIRECT'): record length is not known", 114 unitNumber()); 115 } else if (*recordLength <= 0) { 116 handler.SignalError(IostatOpenBadRecl, 117 "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is invalid", 118 unitNumber(), static_cast<std::intmax_t>(*recordLength)); 119 } else if (!totalBytes) { 120 handler.SignalError(IostatOpenUnknownSize, 121 "OPEN(UNIT=%d,ACCESS='DIRECT'): file size is not known"); 122 } else if (*totalBytes % *recordLength != 0) { 123 handler.SignalError(IostatOpenBadAppend, 124 "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is not an " 125 "even divisor of the file size %jd", 126 unitNumber(), static_cast<std::intmax_t>(*recordLength), 127 static_cast<std::intmax_t>(*totalBytes)); 128 } 129 } 130 if (position == Position::Append) { 131 if (*totalBytes && recordLength && *recordLength) { 132 endfileRecordNumber = 1 + (*totalBytes / *recordLength); 133 } else { 134 // Fake it so that we can backspace relative from the end 135 endfileRecordNumber = std::numeric_limits<std::int64_t>::max() - 1; 136 } 137 currentRecordNumber = *endfileRecordNumber; 138 } else { 139 currentRecordNumber = 1; 140 } 141 } 142 143 void ExternalFileUnit::CloseUnit(CloseStatus status, IoErrorHandler &handler) { 144 DoImpliedEndfile(handler); 145 Flush(handler); 146 Close(status, handler); 147 } 148 149 void ExternalFileUnit::DestroyClosed() { 150 GetUnitMap().DestroyClosed(*this); // destroys *this 151 } 152 153 bool ExternalFileUnit::SetDirection( 154 Direction direction, IoErrorHandler &handler) { 155 if (direction == Direction::Input) { 156 if (mayRead()) { 157 direction_ = Direction::Input; 158 return true; 159 } else { 160 handler.SignalError(IostatReadFromWriteOnly, 161 "READ(UNIT=%d) with ACTION='WRITE'", unitNumber()); 162 return false; 163 } 164 } else { 165 if (mayWrite()) { 166 direction_ = Direction::Output; 167 return true; 168 } else { 169 handler.SignalError(IostatWriteToReadOnly, 170 "WRITE(UNIT=%d) with ACTION='READ'", unitNumber()); 171 return false; 172 } 173 } 174 } 175 176 UnitMap &ExternalFileUnit::GetUnitMap() { 177 if (unitMap) { 178 return *unitMap; 179 } 180 CriticalSection critical{unitMapLock}; 181 if (unitMap) { 182 return *unitMap; 183 } 184 Terminator terminator{__FILE__, __LINE__}; 185 IoErrorHandler handler{terminator}; 186 unitMap = New<UnitMap>{terminator}().release(); 187 ExternalFileUnit &out{ExternalFileUnit::CreateNew(6, terminator)}; 188 out.Predefine(1); 189 out.set_mayRead(false); 190 out.set_mayWrite(true); 191 out.set_mayPosition(false); 192 out.SetDirection(Direction::Output, handler); 193 defaultOutput = &out; 194 ExternalFileUnit &in{ExternalFileUnit::CreateNew(5, terminator)}; 195 in.Predefine(0); 196 in.set_mayRead(true); 197 in.set_mayWrite(false); 198 in.set_mayPosition(false); 199 in.SetDirection(Direction::Input, handler); 200 defaultInput = ∈ 201 // TODO: Set UTF-8 mode from the environment 202 return *unitMap; 203 } 204 205 void ExternalFileUnit::CloseAll(IoErrorHandler &handler) { 206 CriticalSection critical{unitMapLock}; 207 if (unitMap) { 208 unitMap->CloseAll(handler); 209 FreeMemoryAndNullify(unitMap); 210 } 211 defaultOutput = nullptr; 212 } 213 214 void ExternalFileUnit::FlushAll(IoErrorHandler &handler) { 215 CriticalSection critical{unitMapLock}; 216 if (unitMap) { 217 unitMap->FlushAll(handler); 218 } 219 } 220 221 bool ExternalFileUnit::Emit( 222 const char *data, std::size_t bytes, IoErrorHandler &handler) { 223 auto furthestAfter{std::max(furthestPositionInRecord, 224 positionInRecord + static_cast<std::int64_t>(bytes))}; 225 if (furthestAfter > recordLength.value_or(furthestAfter)) { 226 handler.SignalError(IostatRecordWriteOverrun, 227 "Attempt to write %zd bytes to position %jd in a fixed-size record of " 228 "%jd bytes", 229 bytes, static_cast<std::intmax_t>(positionInRecord), 230 static_cast<std::intmax_t>(*recordLength)); 231 return false; 232 } 233 WriteFrame(frameOffsetInFile_, recordOffsetInFrame_ + furthestAfter, handler); 234 if (positionInRecord > furthestPositionInRecord) { 235 std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord, ' ', 236 positionInRecord - furthestPositionInRecord); 237 } 238 std::memcpy(Frame() + recordOffsetInFrame_ + positionInRecord, data, bytes); 239 positionInRecord += bytes; 240 furthestPositionInRecord = furthestAfter; 241 return true; 242 } 243 244 bool ExternalFileUnit::Receive( 245 char *data, std::size_t bytes, IoErrorHandler &handler) { 246 RUNTIME_CHECK(handler, direction_ == Direction::Input); 247 auto furthestAfter{std::max(furthestPositionInRecord, 248 positionInRecord + static_cast<std::int64_t>(bytes))}; 249 if (furthestAfter > recordLength.value_or(furthestAfter)) { 250 handler.SignalError(IostatRecordReadOverrun, 251 "Attempt to read %zd bytes at position %jd in a record of %jd bytes", 252 bytes, static_cast<std::intmax_t>(positionInRecord), 253 static_cast<std::intmax_t>(*recordLength)); 254 return false; 255 } 256 auto need{recordOffsetInFrame_ + furthestAfter}; 257 auto got{ReadFrame(frameOffsetInFile_, need, handler)}; 258 if (got >= need) { 259 std::memcpy(data, Frame() + recordOffsetInFrame_ + positionInRecord, bytes); 260 positionInRecord += bytes; 261 furthestPositionInRecord = furthestAfter; 262 return true; 263 } else { 264 handler.SignalEnd(); 265 endfileRecordNumber = currentRecordNumber; 266 return false; 267 } 268 } 269 270 std::optional<char32_t> ExternalFileUnit::GetCurrentChar( 271 IoErrorHandler &handler) { 272 RUNTIME_CHECK(handler, direction_ == Direction::Input); 273 if (const char *p{FrameNextInput(handler, 1)}) { 274 // TODO: UTF-8 decoding; may have to get more bytes in a loop 275 return *p; 276 } 277 return std::nullopt; 278 } 279 280 const char *ExternalFileUnit::FrameNextInput( 281 IoErrorHandler &handler, std::size_t bytes) { 282 RUNTIME_CHECK(handler, !isUnformatted); 283 if (static_cast<std::int64_t>(positionInRecord + bytes) <= 284 recordLength.value_or(positionInRecord + bytes)) { 285 auto at{recordOffsetInFrame_ + positionInRecord}; 286 auto need{static_cast<std::size_t>(at + bytes)}; 287 auto got{ReadFrame(frameOffsetInFile_, need, handler)}; 288 SetSequentialVariableFormattedRecordLength(); 289 if (got >= need) { 290 return Frame() + at; 291 } 292 handler.SignalEnd(); 293 endfileRecordNumber = currentRecordNumber; 294 } 295 return nullptr; 296 } 297 298 bool ExternalFileUnit::SetSequentialVariableFormattedRecordLength() { 299 if (recordLength || access != Access::Sequential) { 300 return true; 301 } 302 if (FrameLength() > recordOffsetInFrame_) { 303 const char *record{Frame() + recordOffsetInFrame_}; 304 if (const char *nl{reinterpret_cast<const char *>( 305 std::memchr(record, '\n', FrameLength() - recordOffsetInFrame_))}) { 306 recordLength = nl - record; 307 if (*recordLength > 0 && record[*recordLength - 1] == '\r') { 308 --*recordLength; 309 } 310 return true; 311 } 312 } 313 return false; 314 } 315 316 void ExternalFileUnit::SetLeftTabLimit() { 317 leftTabLimit = furthestPositionInRecord; 318 positionInRecord = furthestPositionInRecord; 319 } 320 321 void ExternalFileUnit::BeginReadingRecord(IoErrorHandler &handler) { 322 RUNTIME_CHECK(handler, direction_ == Direction::Input); 323 if (access == Access::Sequential) { 324 if (endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber) { 325 handler.SignalEnd(); 326 } else if (isFixedRecordLength) { 327 RUNTIME_CHECK(handler, recordLength.has_value()); 328 auto need{static_cast<std::size_t>(recordOffsetInFrame_ + *recordLength)}; 329 auto got{ReadFrame(frameOffsetInFile_, need, handler)}; 330 if (got < need) { 331 handler.SignalEnd(); 332 } 333 } else if (isUnformatted) { 334 BeginSequentialVariableUnformattedInputRecord(handler); 335 } else { // formatted 336 BeginSequentialVariableFormattedInputRecord(handler); 337 } 338 } 339 } 340 341 bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) { 342 bool ok{true}; 343 if (direction_ == Direction::Input) { 344 if (access == Access::Sequential) { 345 RUNTIME_CHECK(handler, recordLength.has_value()); 346 if (isFixedRecordLength) { 347 frameOffsetInFile_ += recordOffsetInFrame_ + *recordLength; 348 recordOffsetInFrame_ = 0; 349 } else if (isUnformatted) { 350 // Retain footer in frame for more efficient BACKSPACE 351 frameOffsetInFile_ += recordOffsetInFrame_ + *recordLength; 352 recordOffsetInFrame_ = sizeof(std::uint32_t); 353 recordLength.reset(); 354 } else { // formatted 355 if (Frame()[recordOffsetInFrame_ + *recordLength] == '\r') { 356 ++recordOffsetInFrame_; 357 } 358 recordOffsetInFrame_ += *recordLength + 1; 359 RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ - 1] == '\n'); 360 recordLength.reset(); 361 } 362 } 363 } else { // Direction::Output 364 if (!isUnformatted) { 365 if (isFixedRecordLength && recordLength) { 366 if (furthestPositionInRecord < *recordLength) { 367 WriteFrame(frameOffsetInFile_, *recordLength, handler); 368 std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord, 369 ' ', *recordLength - furthestPositionInRecord); 370 } 371 } else { 372 positionInRecord = furthestPositionInRecord; 373 ok &= Emit("\n", 1, handler); // TODO: Windows CR+LF 374 } 375 } 376 frameOffsetInFile_ += 377 recordOffsetInFrame_ + recordLength.value_or(furthestPositionInRecord); 378 recordOffsetInFrame_ = 0; 379 impliedEndfile_ = true; 380 } 381 ++currentRecordNumber; 382 BeginRecord(); 383 return ok; 384 } 385 386 void ExternalFileUnit::BackspaceRecord(IoErrorHandler &handler) { 387 if (access != Access::Sequential) { 388 handler.SignalError(IostatBackspaceNonSequential, 389 "BACKSPACE(UNIT=%d) on non-sequential file", unitNumber()); 390 } else { 391 DoImpliedEndfile(handler); 392 --currentRecordNumber; 393 BeginRecord(); 394 if (isFixedRecordLength) { 395 BackspaceFixedRecord(handler); 396 } else if (isUnformatted) { 397 BackspaceVariableUnformattedRecord(handler); 398 } else { 399 BackspaceVariableFormattedRecord(handler); 400 } 401 } 402 } 403 404 void ExternalFileUnit::FlushIfTerminal(IoErrorHandler &handler) { 405 if (isTerminal()) { 406 Flush(handler); 407 } 408 } 409 410 void ExternalFileUnit::Endfile(IoErrorHandler &handler) { 411 if (access != Access::Sequential) { 412 handler.SignalError(IostatEndfileNonSequential, 413 "ENDFILE(UNIT=%d) on non-sequential file", unitNumber()); 414 } else if (!mayWrite()) { 415 handler.SignalError(IostatEndfileUnwritable, 416 "ENDFILE(UNIT=%d) on read-only file", unitNumber()); 417 } else { 418 DoEndfile(handler); 419 } 420 } 421 422 void ExternalFileUnit::Rewind(IoErrorHandler &handler) { 423 if (access == Access::Direct) { 424 handler.SignalError(IostatRewindNonSequential, 425 "REWIND(UNIT=%d) on non-sequential file", unitNumber()); 426 } else { 427 DoImpliedEndfile(handler); 428 SetPosition(0); 429 currentRecordNumber = 1; 430 // TODO: reset endfileRecordNumber? 431 } 432 } 433 434 void ExternalFileUnit::EndIoStatement() { 435 frameOffsetInFile_ += recordOffsetInFrame_; 436 recordOffsetInFrame_ = 0; 437 io_.reset(); 438 u_.emplace<std::monostate>(); 439 lock_.Drop(); 440 } 441 442 void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord( 443 IoErrorHandler &handler) { 444 std::int32_t header{0}, footer{0}; 445 std::size_t need{recordOffsetInFrame_ + sizeof header}; 446 std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)}; 447 // Try to emit informative errors to help debug corrupted files. 448 const char *error{nullptr}; 449 if (got < need) { 450 if (got == recordOffsetInFrame_) { 451 handler.SignalEnd(); 452 } else { 453 error = "Unformatted variable-length sequential file input failed at " 454 "record #%jd (file offset %jd): truncated record header"; 455 } 456 } else { 457 std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header); 458 recordLength = sizeof header + header; // does not include footer 459 need = recordOffsetInFrame_ + *recordLength + sizeof footer; 460 got = ReadFrame(frameOffsetInFile_, need, handler); 461 if (got < need) { 462 error = "Unformatted variable-length sequential file input failed at " 463 "record #%jd (file offset %jd): hit EOF reading record with " 464 "length %jd bytes"; 465 } else { 466 std::memcpy(&footer, Frame() + recordOffsetInFrame_ + *recordLength, 467 sizeof footer); 468 if (footer != header) { 469 error = "Unformatted variable-length sequential file input failed at " 470 "record #%jd (file offset %jd): record header has length %jd " 471 "that does not match record footer (%jd)"; 472 } 473 } 474 } 475 if (error) { 476 handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber), 477 static_cast<std::intmax_t>(frameOffsetInFile_), 478 static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer)); 479 // TODO: error recovery 480 } 481 positionInRecord = sizeof header; 482 } 483 484 void ExternalFileUnit::BeginSequentialVariableFormattedInputRecord( 485 IoErrorHandler &handler) { 486 if (this == defaultInput && defaultOutput) { 487 defaultOutput->Flush(handler); 488 } 489 std::size_t length{0}; 490 do { 491 std::size_t need{recordOffsetInFrame_ + length + 1}; 492 length = ReadFrame(frameOffsetInFile_, need, handler); 493 if (length < need) { 494 handler.SignalEnd(); 495 break; 496 } 497 } while (!SetSequentialVariableFormattedRecordLength()); 498 } 499 500 void ExternalFileUnit::BackspaceFixedRecord(IoErrorHandler &handler) { 501 RUNTIME_CHECK(handler, recordLength.has_value()); 502 if (frameOffsetInFile_ < *recordLength) { 503 handler.SignalError(IostatBackspaceAtFirstRecord); 504 } else { 505 frameOffsetInFile_ -= *recordLength; 506 } 507 } 508 509 void ExternalFileUnit::BackspaceVariableUnformattedRecord( 510 IoErrorHandler &handler) { 511 std::int32_t header{0}, footer{0}; 512 auto headerBytes{static_cast<std::int64_t>(sizeof header)}; 513 frameOffsetInFile_ += recordOffsetInFrame_; 514 recordOffsetInFrame_ = 0; 515 if (frameOffsetInFile_ <= headerBytes) { 516 handler.SignalError(IostatBackspaceAtFirstRecord); 517 return; 518 } 519 // Error conditions here cause crashes, not file format errors, because the 520 // validity of the file structure before the current record will have been 521 // checked informatively in NextSequentialVariableUnformattedInputRecord(). 522 std::size_t got{ 523 ReadFrame(frameOffsetInFile_ - headerBytes, headerBytes, handler)}; 524 RUNTIME_CHECK(handler, got >= sizeof footer); 525 std::memcpy(&footer, Frame(), sizeof footer); 526 recordLength = footer; 527 RUNTIME_CHECK(handler, frameOffsetInFile_ >= *recordLength + 2 * headerBytes); 528 frameOffsetInFile_ -= *recordLength + 2 * headerBytes; 529 if (frameOffsetInFile_ >= headerBytes) { 530 frameOffsetInFile_ -= headerBytes; 531 recordOffsetInFrame_ = headerBytes; 532 } 533 auto need{static_cast<std::size_t>( 534 recordOffsetInFrame_ + sizeof header + *recordLength)}; 535 got = ReadFrame(frameOffsetInFile_, need, handler); 536 RUNTIME_CHECK(handler, got >= need); 537 std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header); 538 RUNTIME_CHECK(handler, header == *recordLength); 539 } 540 541 // There's no portable memrchr(), unfortunately, and strrchr() would 542 // fail on a record with a NUL, so we have to do it the hard way. 543 static const char *FindLastNewline(const char *str, std::size_t length) { 544 for (const char *p{str + length}; p-- > str;) { 545 if (*p == '\n') { 546 return p; 547 } 548 } 549 return nullptr; 550 } 551 552 void ExternalFileUnit::BackspaceVariableFormattedRecord( 553 IoErrorHandler &handler) { 554 // File offset of previous record's newline 555 auto prevNL{ 556 frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_) - 1}; 557 if (prevNL < 0) { 558 handler.SignalError(IostatBackspaceAtFirstRecord); 559 return; 560 } 561 while (true) { 562 if (frameOffsetInFile_ < prevNL) { 563 if (const char *p{ 564 FindLastNewline(Frame(), prevNL - 1 - frameOffsetInFile_)}) { 565 recordOffsetInFrame_ = p - Frame() + 1; 566 *recordLength = prevNL - (frameOffsetInFile_ + recordOffsetInFrame_); 567 break; 568 } 569 } 570 if (frameOffsetInFile_ == 0) { 571 recordOffsetInFrame_ = 0; 572 *recordLength = prevNL; 573 break; 574 } 575 frameOffsetInFile_ -= std::min<std::int64_t>(frameOffsetInFile_, 1024); 576 auto need{static_cast<std::size_t>(prevNL + 1 - frameOffsetInFile_)}; 577 auto got{ReadFrame(frameOffsetInFile_, need, handler)}; 578 RUNTIME_CHECK(handler, got >= need); 579 } 580 RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ + *recordLength] == '\n'); 581 if (*recordLength > 0 && 582 Frame()[recordOffsetInFrame_ + *recordLength - 1] == '\r') { 583 --*recordLength; 584 } 585 } 586 587 void ExternalFileUnit::DoImpliedEndfile(IoErrorHandler &handler) { 588 if (impliedEndfile_) { 589 impliedEndfile_ = false; 590 if (access == Access::Sequential && mayPosition()) { 591 DoEndfile(handler); 592 } 593 } 594 } 595 596 void ExternalFileUnit::DoEndfile(IoErrorHandler &handler) { 597 endfileRecordNumber = currentRecordNumber; 598 Truncate(frameOffsetInFile_ + recordOffsetInFrame_, handler); 599 BeginRecord(); 600 impliedEndfile_ = false; 601 } 602 } // namespace Fortran::runtime::io 603