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