2021-09-02 16:14:01 +08:00
|
|
|
//===-- runtime/file.cpp --------------------------------------------------===//
|
2020-01-24 08:10:00 +08:00
|
|
|
//
|
|
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
//
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
|
|
|
|
#include "file.h"
|
2021-09-02 07:00:53 +08:00
|
|
|
#include "flang/Runtime/magic-numbers.h"
|
|
|
|
#include "flang/Runtime/memory.h"
|
2020-07-07 04:03:00 +08:00
|
|
|
#include <algorithm>
|
2020-01-24 08:10:00 +08:00
|
|
|
#include <cerrno>
|
|
|
|
#include <cstring>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <stdlib.h>
|
2021-02-18 11:45:29 +08:00
|
|
|
#include <sys/stat.h>
|
2020-03-13 04:48:00 +08:00
|
|
|
#ifdef _WIN32
|
2020-08-13 03:21:30 +08:00
|
|
|
#define NOMINMAX
|
2020-03-13 04:48:00 +08:00
|
|
|
#include <io.h>
|
|
|
|
#include <windows.h>
|
|
|
|
#else
|
2020-01-24 08:10:00 +08:00
|
|
|
#include <unistd.h>
|
2020-03-13 04:48:00 +08:00
|
|
|
#endif
|
2020-01-24 08:10:00 +08:00
|
|
|
|
|
|
|
namespace Fortran::runtime::io {
|
|
|
|
|
2020-02-05 08:55:45 +08:00
|
|
|
void OpenFile::set_path(OwningPtr<char> &&path, std::size_t bytes) {
|
|
|
|
path_ = std::move(path);
|
|
|
|
pathLength_ = bytes;
|
|
|
|
}
|
|
|
|
|
2020-03-13 04:48:00 +08:00
|
|
|
static int openfile_mkstemp(IoErrorHandler &handler) {
|
|
|
|
#ifdef _WIN32
|
|
|
|
const unsigned int uUnique{0};
|
|
|
|
// GetTempFileNameA needs a directory name < MAX_PATH-14 characters in length.
|
|
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettempfilenamea
|
|
|
|
char tempDirName[MAX_PATH - 14];
|
|
|
|
char tempFileName[MAX_PATH];
|
|
|
|
unsigned long nBufferLength{sizeof(tempDirName)};
|
|
|
|
nBufferLength = ::GetTempPathA(nBufferLength, tempDirName);
|
|
|
|
if (nBufferLength > sizeof(tempDirName) || nBufferLength == 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (::GetTempFileNameA(tempDirName, "Fortran", uUnique, tempFileName) == 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
2021-07-27 04:02:08 +08:00
|
|
|
int fd{::_open(
|
|
|
|
tempFileName, _O_CREAT | _O_TEMPORARY | _O_RDWR, _S_IREAD | _S_IWRITE)};
|
2020-03-13 04:48:00 +08:00
|
|
|
#else
|
|
|
|
char path[]{"/tmp/Fortran-Scratch-XXXXXX"};
|
|
|
|
int fd{::mkstemp(path)};
|
|
|
|
#endif
|
|
|
|
if (fd < 0) {
|
|
|
|
handler.SignalErrno();
|
|
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
|
|
::unlink(path);
|
|
|
|
#endif
|
|
|
|
return fd;
|
|
|
|
}
|
|
|
|
|
2020-07-18 02:24:29 +08:00
|
|
|
void OpenFile::Open(OpenStatus status, std::optional<Action> action,
|
|
|
|
Position position, IoErrorHandler &handler) {
|
|
|
|
if (fd_ >= 0 &&
|
|
|
|
(status == OpenStatus::Old || status == OpenStatus::Unknown)) {
|
|
|
|
return;
|
|
|
|
}
|
2021-12-01 08:21:11 +08:00
|
|
|
CloseFd(handler);
|
2020-07-18 02:24:29 +08:00
|
|
|
if (status == OpenStatus::Scratch) {
|
2020-01-24 08:10:00 +08:00
|
|
|
if (path_.get()) {
|
2020-02-14 06:41:56 +08:00
|
|
|
handler.SignalError("FILE= must not appear with STATUS='SCRATCH'");
|
2020-01-24 08:10:00 +08:00
|
|
|
path_.reset();
|
|
|
|
}
|
2020-07-18 02:24:29 +08:00
|
|
|
if (!action) {
|
|
|
|
action = Action::ReadWrite;
|
|
|
|
}
|
2020-03-13 04:48:00 +08:00
|
|
|
fd_ = openfile_mkstemp(handler);
|
2020-07-18 02:24:29 +08:00
|
|
|
} else {
|
|
|
|
if (!path_.get()) {
|
2020-08-04 02:29:15 +08:00
|
|
|
handler.SignalError("FILE= is required");
|
2020-01-24 08:10:00 +08:00
|
|
|
return;
|
|
|
|
}
|
2020-07-18 02:24:29 +08:00
|
|
|
int flags{0};
|
|
|
|
if (status != OpenStatus::Old) {
|
|
|
|
flags |= O_CREAT;
|
|
|
|
}
|
|
|
|
if (status == OpenStatus::New) {
|
|
|
|
flags |= O_EXCL;
|
|
|
|
} else if (status == OpenStatus::Replace) {
|
|
|
|
flags |= O_TRUNC;
|
|
|
|
}
|
|
|
|
if (!action) {
|
|
|
|
// Try to open read/write, back off to read-only on failure
|
|
|
|
fd_ = ::open(path_.get(), flags | O_RDWR, 0600);
|
|
|
|
if (fd_ >= 0) {
|
|
|
|
action = Action::ReadWrite;
|
|
|
|
} else {
|
|
|
|
action = Action::Read;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (fd_ < 0) {
|
|
|
|
switch (*action) {
|
|
|
|
case Action::Read:
|
|
|
|
flags |= O_RDONLY;
|
|
|
|
break;
|
|
|
|
case Action::Write:
|
|
|
|
flags |= O_WRONLY;
|
|
|
|
break;
|
|
|
|
case Action::ReadWrite:
|
|
|
|
flags |= O_RDWR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
fd_ = ::open(path_.get(), flags, 0600);
|
|
|
|
if (fd_ < 0) {
|
|
|
|
handler.SignalErrno();
|
|
|
|
}
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
|
|
|
}
|
2020-07-18 02:24:29 +08:00
|
|
|
RUNTIME_CHECK(handler, action.has_value());
|
2020-01-24 08:10:00 +08:00
|
|
|
pending_.reset();
|
2020-02-05 08:55:45 +08:00
|
|
|
if (position == Position::Append && !RawSeekToEnd()) {
|
|
|
|
handler.SignalErrno();
|
|
|
|
}
|
|
|
|
isTerminal_ = ::isatty(fd_) == 1;
|
2020-07-18 02:24:29 +08:00
|
|
|
mayRead_ = *action != Action::Write;
|
|
|
|
mayWrite_ = *action != Action::Read;
|
|
|
|
if (status == OpenStatus::Old || status == OpenStatus::Unknown) {
|
|
|
|
knownSize_.reset();
|
2020-08-04 02:29:15 +08:00
|
|
|
#ifndef _WIN32
|
|
|
|
struct stat buf;
|
|
|
|
if (::fstat(fd_, &buf) == 0) {
|
|
|
|
mayPosition_ = S_ISREG(buf.st_mode);
|
|
|
|
knownSize_ = buf.st_size;
|
|
|
|
}
|
|
|
|
#else // TODO: _WIN32
|
|
|
|
mayPosition_ = true;
|
|
|
|
#endif
|
2020-07-18 02:24:29 +08:00
|
|
|
} else {
|
|
|
|
knownSize_ = 0;
|
2020-08-04 02:29:15 +08:00
|
|
|
mayPosition_ = true;
|
2020-07-18 02:24:29 +08:00
|
|
|
}
|
2021-11-25 08:05:37 +08:00
|
|
|
openPosition_ = position; // for INQUIRE(POSITION=)
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
void OpenFile::Predefine(int fd) {
|
|
|
|
fd_ = fd;
|
|
|
|
path_.reset();
|
|
|
|
pathLength_ = 0;
|
|
|
|
position_ = 0;
|
|
|
|
knownSize_.reset();
|
|
|
|
nextId_ = 0;
|
|
|
|
pending_.reset();
|
2020-07-18 02:24:29 +08:00
|
|
|
mayRead_ = fd == 0;
|
|
|
|
mayWrite_ = fd != 0;
|
|
|
|
mayPosition_ = false;
|
2020-01-24 08:59:27 +08:00
|
|
|
}
|
|
|
|
|
2020-02-05 08:55:45 +08:00
|
|
|
void OpenFile::Close(CloseStatus status, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
CheckOpen(handler);
|
|
|
|
pending_.reset();
|
|
|
|
knownSize_.reset();
|
2020-02-05 08:55:45 +08:00
|
|
|
switch (status) {
|
2020-03-29 12:00:16 +08:00
|
|
|
case CloseStatus::Keep:
|
|
|
|
break;
|
2020-02-05 08:55:45 +08:00
|
|
|
case CloseStatus::Delete:
|
2020-01-24 08:10:00 +08:00
|
|
|
if (path_.get()) {
|
|
|
|
::unlink(path_.get());
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
path_.reset();
|
2021-12-01 08:21:11 +08:00
|
|
|
CloseFd(handler);
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
std::size_t OpenFile::Read(FileOffset at, char *buffer, std::size_t minBytes,
|
2020-01-24 08:10:00 +08:00
|
|
|
std::size_t maxBytes, IoErrorHandler &handler) {
|
|
|
|
if (maxBytes == 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
CheckOpen(handler);
|
|
|
|
if (!Seek(at, handler)) {
|
|
|
|
return 0;
|
|
|
|
}
|
2020-07-04 01:56:08 +08:00
|
|
|
minBytes = std::min(minBytes, maxBytes);
|
2020-01-24 08:10:00 +08:00
|
|
|
std::size_t got{0};
|
|
|
|
while (got < minBytes) {
|
|
|
|
auto chunk{::read(fd_, buffer + got, maxBytes - got)};
|
|
|
|
if (chunk == 0) {
|
|
|
|
break;
|
2020-07-04 01:56:08 +08:00
|
|
|
} else if (chunk < 0) {
|
2020-01-24 08:10:00 +08:00
|
|
|
auto err{errno};
|
|
|
|
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
|
|
|
handler.SignalError(err);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
2021-11-25 08:05:37 +08:00
|
|
|
SetPosition(position_ + chunk);
|
2020-01-24 08:10:00 +08:00
|
|
|
got += chunk;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return got;
|
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
std::size_t OpenFile::Write(FileOffset at, const char *buffer,
|
|
|
|
std::size_t bytes, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
if (bytes == 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
CheckOpen(handler);
|
|
|
|
if (!Seek(at, handler)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
std::size_t put{0};
|
|
|
|
while (put < bytes) {
|
|
|
|
auto chunk{::write(fd_, buffer + put, bytes - put)};
|
|
|
|
if (chunk >= 0) {
|
2021-11-25 08:05:37 +08:00
|
|
|
SetPosition(position_ + chunk);
|
2020-01-24 08:10:00 +08:00
|
|
|
put += chunk;
|
|
|
|
} else {
|
|
|
|
auto err{errno};
|
|
|
|
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
|
|
|
handler.SignalError(err);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (knownSize_ && position_ > *knownSize_) {
|
|
|
|
knownSize_ = position_;
|
|
|
|
}
|
|
|
|
return put;
|
|
|
|
}
|
|
|
|
|
2020-03-13 04:48:00 +08:00
|
|
|
inline static int openfile_ftruncate(int fd, OpenFile::FileOffset at) {
|
|
|
|
#ifdef _WIN32
|
2021-07-27 04:02:08 +08:00
|
|
|
return ::_chsize(fd, at);
|
2020-03-13 04:48:00 +08:00
|
|
|
#else
|
|
|
|
return ::ftruncate(fd, at);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
void OpenFile::Truncate(FileOffset at, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
CheckOpen(handler);
|
|
|
|
if (!knownSize_ || *knownSize_ != at) {
|
2020-03-13 04:48:00 +08:00
|
|
|
if (openfile_ftruncate(fd_, at) != 0) {
|
2020-01-24 08:10:00 +08:00
|
|
|
handler.SignalErrno();
|
|
|
|
}
|
|
|
|
knownSize_ = at;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The operation is performed immediately; the results are saved
|
|
|
|
// to be claimed by a later WAIT statement.
|
|
|
|
// TODO: True asynchronicity
|
|
|
|
int OpenFile::ReadAsynchronously(
|
2020-01-24 08:59:27 +08:00
|
|
|
FileOffset at, char *buffer, std::size_t bytes, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
CheckOpen(handler);
|
|
|
|
int iostat{0};
|
|
|
|
for (std::size_t got{0}; got < bytes;) {
|
|
|
|
#if _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
|
|
|
|
auto chunk{::pread(fd_, buffer + got, bytes - got, at)};
|
|
|
|
#else
|
2020-01-24 08:59:27 +08:00
|
|
|
auto chunk{Seek(at, handler) ? ::read(fd_, buffer + got, bytes - got) : -1};
|
2020-01-24 08:10:00 +08:00
|
|
|
#endif
|
|
|
|
if (chunk == 0) {
|
|
|
|
iostat = FORTRAN_RUNTIME_IOSTAT_END;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (chunk < 0) {
|
|
|
|
auto err{errno};
|
|
|
|
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
|
|
|
iostat = err;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
at += chunk;
|
|
|
|
got += chunk;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return PendingResult(handler, iostat);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: True asynchronicity
|
2020-01-24 08:59:27 +08:00
|
|
|
int OpenFile::WriteAsynchronously(FileOffset at, const char *buffer,
|
|
|
|
std::size_t bytes, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
CheckOpen(handler);
|
|
|
|
int iostat{0};
|
|
|
|
for (std::size_t put{0}; put < bytes;) {
|
|
|
|
#if _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
|
|
|
|
auto chunk{::pwrite(fd_, buffer + put, bytes - put, at)};
|
|
|
|
#else
|
2020-01-24 08:59:27 +08:00
|
|
|
auto chunk{
|
|
|
|
Seek(at, handler) ? ::write(fd_, buffer + put, bytes - put) : -1};
|
2020-01-24 08:10:00 +08:00
|
|
|
#endif
|
|
|
|
if (chunk >= 0) {
|
|
|
|
at += chunk;
|
|
|
|
put += chunk;
|
|
|
|
} else {
|
|
|
|
auto err{errno};
|
|
|
|
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
|
|
|
iostat = err;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return PendingResult(handler, iostat);
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenFile::Wait(int id, IoErrorHandler &handler) {
|
|
|
|
std::optional<int> ioStat;
|
2020-02-14 06:41:56 +08:00
|
|
|
Pending *prev{nullptr};
|
|
|
|
for (Pending *p{pending_.get()}; p; p = (prev = p)->next.get()) {
|
|
|
|
if (p->id == id) {
|
|
|
|
ioStat = p->ioStat;
|
|
|
|
if (prev) {
|
|
|
|
prev->next.reset(p->next.release());
|
|
|
|
} else {
|
|
|
|
pending_.reset(p->next.release());
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
2020-02-14 06:41:56 +08:00
|
|
|
break;
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ioStat) {
|
|
|
|
handler.SignalError(*ioStat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenFile::WaitAll(IoErrorHandler &handler) {
|
|
|
|
while (true) {
|
|
|
|
int ioStat;
|
2020-02-14 06:41:56 +08:00
|
|
|
if (pending_) {
|
|
|
|
ioStat = pending_->ioStat;
|
|
|
|
pending_.reset(pending_->next.release());
|
|
|
|
} else {
|
|
|
|
return;
|
2020-01-24 08:10:00 +08:00
|
|
|
}
|
|
|
|
handler.SignalError(ioStat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-25 08:05:37 +08:00
|
|
|
Position OpenFile::InquirePosition() const {
|
|
|
|
if (openPosition_) { // from OPEN statement
|
|
|
|
return *openPosition_;
|
|
|
|
} else { // unit has been repositioned since opening
|
|
|
|
if (position_ == knownSize_.value_or(position_ + 1)) {
|
|
|
|
return Position::Append;
|
|
|
|
} else if (position_ == 0 && mayPosition_) {
|
|
|
|
return Position::Rewind;
|
|
|
|
} else {
|
|
|
|
return Position::AsIs; // processor-dependent & no common behavior
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-05 08:55:45 +08:00
|
|
|
void OpenFile::CheckOpen(const Terminator &terminator) {
|
2020-01-24 08:10:00 +08:00
|
|
|
RUNTIME_CHECK(terminator, fd_ >= 0);
|
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
bool OpenFile::Seek(FileOffset at, IoErrorHandler &handler) {
|
2020-01-24 08:10:00 +08:00
|
|
|
if (at == position_) {
|
|
|
|
return true;
|
|
|
|
} else if (RawSeek(at)) {
|
2021-11-25 08:05:37 +08:00
|
|
|
SetPosition(at);
|
2020-01-24 08:10:00 +08:00
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
handler.SignalErrno();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-24 08:59:27 +08:00
|
|
|
bool OpenFile::RawSeek(FileOffset at) {
|
2020-01-24 08:10:00 +08:00
|
|
|
#ifdef _LARGEFILE64_SOURCE
|
2020-02-05 08:55:45 +08:00
|
|
|
return ::lseek64(fd_, at, SEEK_SET) == at;
|
2020-01-24 08:10:00 +08:00
|
|
|
#else
|
2020-02-05 08:55:45 +08:00
|
|
|
return ::lseek(fd_, at, SEEK_SET) == at;
|
2020-01-24 08:10:00 +08:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2020-02-05 08:55:45 +08:00
|
|
|
bool OpenFile::RawSeekToEnd() {
|
|
|
|
#ifdef _LARGEFILE64_SOURCE
|
|
|
|
std::int64_t at{::lseek64(fd_, 0, SEEK_END)};
|
|
|
|
#else
|
|
|
|
std::int64_t at{::lseek(fd_, 0, SEEK_END)};
|
|
|
|
#endif
|
|
|
|
if (at >= 0) {
|
|
|
|
knownSize_ = at;
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int OpenFile::PendingResult(const Terminator &terminator, int iostat) {
|
2020-01-24 08:10:00 +08:00
|
|
|
int id{nextId_++};
|
2020-07-03 09:35:20 +08:00
|
|
|
pending_ = New<Pending>{terminator}(id, iostat, std::move(pending_));
|
2020-01-24 08:10:00 +08:00
|
|
|
return id;
|
|
|
|
}
|
2020-07-04 01:56:08 +08:00
|
|
|
|
2021-12-01 08:21:11 +08:00
|
|
|
void OpenFile::CloseFd(IoErrorHandler &handler) {
|
|
|
|
if (fd_ >= 0) {
|
|
|
|
if (fd_ <= 2) {
|
|
|
|
// don't actually close a standard file descriptor, we might need it
|
|
|
|
} else {
|
|
|
|
if (::close(fd_) != 0) {
|
|
|
|
handler.SignalErrno();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fd_ = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-04 01:56:08 +08:00
|
|
|
bool IsATerminal(int fd) { return ::isatty(fd); }
|
2020-08-04 02:29:15 +08:00
|
|
|
|
2020-09-30 05:57:05 +08:00
|
|
|
#ifdef WIN32
|
|
|
|
// Access flags are normally defined in unistd.h, which unavailable under
|
|
|
|
// Windows. Instead, define the flags as documented at
|
|
|
|
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/access-waccess
|
|
|
|
#define F_OK 00
|
|
|
|
#define W_OK 02
|
|
|
|
#define R_OK 04
|
|
|
|
#endif
|
|
|
|
|
2020-08-04 02:29:15 +08:00
|
|
|
bool IsExtant(const char *path) { return ::access(path, F_OK) == 0; }
|
|
|
|
bool MayRead(const char *path) { return ::access(path, R_OK) == 0; }
|
|
|
|
bool MayWrite(const char *path) { return ::access(path, W_OK) == 0; }
|
|
|
|
bool MayReadAndWrite(const char *path) {
|
|
|
|
return ::access(path, R_OK | W_OK) == 0;
|
|
|
|
}
|
2020-03-29 12:00:16 +08:00
|
|
|
} // namespace Fortran::runtime::io
|