xref: /llvm-project/flang/runtime/edit-input.cpp (revision e6873bfbcd356c6aeb0e0bc165326f9fc8f02cbd)
1 //===-- runtime/edit-input.cpp --------------------------------------------===//
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 "edit-input.h"
10 #include "namelist.h"
11 #include "utf.h"
12 #include "flang/Common/real.h"
13 #include "flang/Common/uint128.h"
14 #include <algorithm>
15 #include <cfenv>
16 
17 namespace Fortran::runtime::io {
18 
19 static bool EditBOZInput(IoStatementState &io, const DataEdit &edit, void *n,
20     int base, int totalBitSize) {
21   std::optional<int> remaining;
22   std::optional<char32_t> next{io.PrepareInput(edit, remaining)};
23   common::UnsignedInt128 value{0};
24   for (; next; next = io.NextInField(remaining, edit)) {
25     char32_t ch{*next};
26     if (ch == ' ' || ch == '\t') {
27       continue;
28     }
29     int digit{0};
30     if (ch >= '0' && ch <= '1') {
31       digit = ch - '0';
32     } else if (base >= 8 && ch >= '2' && ch <= '7') {
33       digit = ch - '0';
34     } else if (base >= 10 && ch >= '8' && ch <= '9') {
35       digit = ch - '0';
36     } else if (base == 16 && ch >= 'A' && ch <= 'Z') {
37       digit = ch + 10 - 'A';
38     } else if (base == 16 && ch >= 'a' && ch <= 'z') {
39       digit = ch + 10 - 'a';
40     } else {
41       io.GetIoErrorHandler().SignalError(
42           "Bad character '%lc' in B/O/Z input field", ch);
43       return false;
44     }
45     value *= base;
46     value += digit;
47   }
48   // TODO: check for overflow
49   std::memcpy(n, &value, totalBitSize >> 3);
50   return true;
51 }
52 
53 static inline char32_t GetDecimalPoint(const DataEdit &edit) {
54   return edit.modes.editingFlags & decimalComma ? char32_t{','} : char32_t{'.'};
55 }
56 
57 // Prepares input from a field, and consumes the sign, if any.
58 // Returns true if there's a '-' sign.
59 static bool ScanNumericPrefix(IoStatementState &io, const DataEdit &edit,
60     std::optional<char32_t> &next, std::optional<int> &remaining) {
61   bool bzMode{(edit.modes.editingFlags & blankZero) != 0};
62   next = io.PrepareInput(edit, remaining, !bzMode);
63   bool negative{false};
64   if (next) {
65     negative = *next == '-';
66     if (negative || *next == '+') {
67       if (!bzMode) {
68         io.SkipSpaces(remaining);
69       }
70       next = io.NextInField(remaining, edit);
71     }
72   }
73   return negative;
74 }
75 
76 bool EditIntegerInput(
77     IoStatementState &io, const DataEdit &edit, void *n, int kind) {
78   RUNTIME_CHECK(io.GetIoErrorHandler(), kind >= 1 && !(kind & (kind - 1)));
79   switch (edit.descriptor) {
80   case DataEdit::ListDirected:
81     if (IsNamelistName(io)) {
82       return false;
83     }
84     break;
85   case 'G':
86   case 'I':
87     break;
88   case 'B':
89     return EditBOZInput(io, edit, n, 2, kind << 3);
90   case 'O':
91     return EditBOZInput(io, edit, n, 8, kind << 3);
92   case 'Z':
93     return EditBOZInput(io, edit, n, 16, kind << 3);
94   case 'A': // legacy extension
95     return EditCharacterInput(io, edit, reinterpret_cast<char *>(n), kind);
96   default:
97     io.GetIoErrorHandler().SignalError(IostatErrorInFormat,
98         "Data edit descriptor '%c' may not be used with an INTEGER data item",
99         edit.descriptor);
100     return false;
101   }
102   std::optional<int> remaining;
103   std::optional<char32_t> next;
104   bool negate{ScanNumericPrefix(io, edit, next, remaining)};
105   common::UnsignedInt128 value{0};
106   bool any{negate};
107   for (; next; next = io.NextInField(remaining, edit)) {
108     char32_t ch{*next};
109     if (ch == ' ' || ch == '\t') {
110       if (edit.modes.editingFlags & blankZero) {
111         ch = '0'; // BZ mode - treat blank as if it were zero
112       } else {
113         continue;
114       }
115     }
116     int digit{0};
117     if (ch >= '0' && ch <= '9') {
118       digit = ch - '0';
119     } else {
120       io.GetIoErrorHandler().SignalError(
121           "Bad character '%lc' in INTEGER input field", ch);
122       return false;
123     }
124     value *= 10;
125     value += digit;
126     any = true;
127   }
128   if (negate) {
129     value = -value;
130   }
131   if (any || !io.GetConnectionState().IsAtEOF()) {
132     std::memcpy(n, &value, kind); // a blank field means zero
133   }
134   return any;
135 }
136 
137 // Parses a REAL input number from the input source as a normalized
138 // fraction into a supplied buffer -- there's an optional '-', a
139 // decimal point, and at least one digit.  The adjusted exponent value
140 // is returned in a reference argument.  The returned value is the number
141 // of characters that (should) have been written to the buffer -- this can
142 // be larger than the buffer size and can indicate overflow.  Replaces
143 // blanks with zeroes if appropriate.
144 static int ScanRealInput(char *buffer, int bufferSize, IoStatementState &io,
145     const DataEdit &edit, int &exponent) {
146   std::optional<int> remaining;
147   std::optional<char32_t> next;
148   int got{0};
149   std::optional<int> decimalPoint;
150   auto Put{[&](char ch) -> void {
151     if (got < bufferSize) {
152       buffer[got] = ch;
153     }
154     ++got;
155   }};
156   if (ScanNumericPrefix(io, edit, next, remaining)) {
157     Put('-');
158   }
159   bool bzMode{(edit.modes.editingFlags & blankZero) != 0};
160   if (!next || (!bzMode && *next == ' ')) { // empty/blank field means zero
161     remaining.reset();
162     if (!io.GetConnectionState().IsAtEOF()) {
163       Put('0');
164     }
165     return got;
166   }
167   char32_t decimal{GetDecimalPoint(edit)};
168   char32_t first{*next >= 'a' && *next <= 'z' ? *next + 'A' - 'a' : *next};
169   if (first == 'N' || first == 'I') {
170     // NaN or infinity - convert to upper case
171     // Subtle: a blank field of digits could be followed by 'E' or 'D',
172     for (; next &&
173          ((*next >= 'a' && *next <= 'z') || (*next >= 'A' && *next <= 'Z'));
174          next = io.NextInField(remaining, edit)) {
175       if (*next >= 'a' && *next <= 'z') {
176         Put(*next - 'a' + 'A');
177       } else {
178         Put(*next);
179       }
180     }
181     if (next && *next == '(') { // NaN(...)
182       while (next && *next != ')') {
183         next = io.NextInField(remaining, edit);
184       }
185     }
186     exponent = 0;
187   } else if (first == decimal || (first >= '0' && first <= '9') ||
188       (bzMode && (first == ' ' || first == '\t')) || first == 'E' ||
189       first == 'D' || first == 'Q') {
190     Put('.'); // input field is normalized to a fraction
191     auto start{got};
192     bool anyDigit{false};
193     for (; next; next = io.NextInField(remaining, edit)) {
194       char32_t ch{*next};
195       if (ch == ' ' || ch == '\t') {
196         if (bzMode) {
197           ch = '0'; // BZ mode - treat blank as if it were zero
198         } else {
199           continue;
200         }
201       }
202       if (ch == '0' && got == start && !decimalPoint) {
203         anyDigit = true;
204         // omit leading zeroes before the decimal
205       } else if (ch >= '0' && ch <= '9') {
206         anyDigit = true;
207         Put(ch);
208       } else if (ch == decimal && !decimalPoint) {
209         // the decimal point is *not* copied to the buffer
210         decimalPoint = got - start; // # of digits before the decimal point
211       } else {
212         break;
213       }
214     }
215     if (got == start) {
216       if (anyDigit) {
217         Put('0'); // emit at least one digit
218       } else {
219         return 0; // no digits, invalid input
220       }
221     }
222     if (next &&
223         (*next == 'e' || *next == 'E' || *next == 'd' || *next == 'D' ||
224             *next == 'q' || *next == 'Q')) {
225       // Optional exponent letter.  Blanks are allowed between the
226       // optional exponent letter and the exponent value.
227       io.SkipSpaces(remaining);
228       next = io.NextInField(remaining, edit);
229     }
230     // The default exponent is -kP, but the scale factor doesn't affect
231     // an explicit exponent.
232     exponent = -edit.modes.scale;
233     if (next &&
234         (*next == '-' || *next == '+' || (*next >= '0' && *next <= '9') ||
235             (bzMode && (*next == ' ' || *next == '\t')))) {
236       bool negExpo{*next == '-'};
237       if (negExpo || *next == '+') {
238         next = io.NextInField(remaining, edit);
239       }
240       for (exponent = 0; next; next = io.NextInField(remaining, edit)) {
241         if (*next >= '0' && *next <= '9') {
242           exponent = 10 * exponent + *next - '0';
243         } else if (bzMode && (*next == ' ' || *next == '\t')) {
244           exponent = 10 * exponent;
245         } else {
246           break;
247         }
248       }
249       if (negExpo) {
250         exponent = -exponent;
251       }
252     }
253     if (decimalPoint) {
254       exponent += *decimalPoint;
255     } else {
256       // When no decimal point (or comma) appears in the value, the 'd'
257       // part of the edit descriptor must be interpreted as the number of
258       // digits in the value to be interpreted as being to the *right* of
259       // the assumed decimal point (13.7.2.3.2)
260       exponent += got - start - edit.digits.value_or(0);
261     }
262   } else {
263     // TODO: hex FP input
264     exponent = 0;
265     return 0;
266   }
267   // Consume the trailing ')' of a list-directed or NAMELIST complex
268   // input value.
269   if (edit.descriptor == DataEdit::ListDirectedImaginaryPart) {
270     if (next && (*next == ' ' || *next == '\t')) {
271       next = io.NextInField(remaining, edit);
272     }
273     if (!next) { // NextInField fails on separators like ')'
274       std::size_t byteCount{0};
275       next = io.GetCurrentChar(byteCount);
276       if (next && *next == ')') {
277         io.HandleRelativePosition(byteCount);
278       }
279     }
280   } else if (remaining) {
281     while (next && (*next == ' ' || *next == '\t')) {
282       next = io.NextInField(remaining, edit);
283     }
284     if (next) {
285       return 0; // error: unused nonblank character in fixed-width field
286     }
287   }
288   return got;
289 }
290 
291 static void RaiseFPExceptions(decimal::ConversionResultFlags flags) {
292 #undef RAISE
293 #ifdef feraisexcept // a macro in some environments; omit std::
294 #define RAISE feraiseexcept
295 #else
296 #define RAISE std::feraiseexcept
297 #endif
298   if (flags & decimal::ConversionResultFlags::Overflow) {
299     RAISE(FE_OVERFLOW);
300   }
301   if (flags & decimal::ConversionResultFlags::Inexact) {
302     RAISE(FE_INEXACT);
303   }
304   if (flags & decimal::ConversionResultFlags::Invalid) {
305     RAISE(FE_INVALID);
306   }
307 #undef RAISE
308 }
309 
310 // If no special modes are in effect and the form of the input value
311 // that's present in the input stream is acceptable to the decimal->binary
312 // converter without modification, this fast path for real input
313 // saves time by avoiding memory copies and reformatting of the exponent.
314 template <int PRECISION>
315 static bool TryFastPathRealInput(
316     IoStatementState &io, const DataEdit &edit, void *n) {
317   if (edit.modes.editingFlags & (blankZero | decimalComma)) {
318     return false;
319   }
320   if (edit.modes.scale != 0) {
321     return false;
322   }
323   const char *str{nullptr};
324   std::size_t got{io.GetNextInputBytes(str)};
325   if (got == 0 || str == nullptr ||
326       !io.GetConnectionState().recordLength.has_value()) {
327     return false; // could not access reliably-terminated input stream
328   }
329   const char *p{str};
330   std::int64_t maxConsume{
331       std::min<std::int64_t>(got, edit.width.value_or(got))};
332   const char *limit{str + maxConsume};
333   decimal::ConversionToBinaryResult<PRECISION> converted{
334       decimal::ConvertToBinary<PRECISION>(p, edit.modes.round, limit)};
335   if (converted.flags & decimal::Invalid) {
336     return false;
337   }
338   if (edit.digits.value_or(0) != 0 &&
339       std::memchr(str, '.', p - str) == nullptr) {
340     // No explicit decimal point, and edit descriptor is Fw.d (or other)
341     // with d != 0, which implies scaling.
342     return false;
343   }
344   for (; p < limit && (*p == ' ' || *p == '\t'); ++p) {
345   }
346   if (edit.descriptor == DataEdit::ListDirectedImaginaryPart) {
347     // Need to consume a trailing ')' and any white space after
348     if (p >= limit || *p != ')') {
349       return false;
350     }
351     for (++p; p < limit && (*p == ' ' || *p == '\t'); ++p) {
352     }
353   }
354   if (edit.width && p < str + *edit.width) {
355     return false; // unconverted characters remain in fixed width field
356   }
357   // Success on the fast path!
358   *reinterpret_cast<decimal::BinaryFloatingPointNumber<PRECISION> *>(n) =
359       converted.binary;
360   io.HandleRelativePosition(p - str);
361   // Set FP exception flags
362   if (converted.flags != decimal::ConversionResultFlags::Exact) {
363     RaiseFPExceptions(converted.flags);
364   }
365   return true;
366 }
367 
368 template <int KIND>
369 bool EditCommonRealInput(IoStatementState &io, const DataEdit &edit, void *n) {
370   constexpr int binaryPrecision{common::PrecisionOfRealKind(KIND)};
371   if (TryFastPathRealInput<binaryPrecision>(io, edit, n)) {
372     return true;
373   }
374   // Fast path wasn't available or didn't work; go the more general route
375   static constexpr int maxDigits{
376       common::MaxDecimalConversionDigits(binaryPrecision)};
377   static constexpr int bufferSize{maxDigits + 18};
378   char buffer[bufferSize];
379   int exponent{0};
380   int got{ScanRealInput(buffer, maxDigits + 2, io, edit, exponent)};
381   if (got >= maxDigits + 2) {
382     io.GetIoErrorHandler().Crash("EditCommonRealInput: buffer was too small");
383     return false;
384   }
385   if (got == 0) {
386     io.GetIoErrorHandler().SignalError(IostatBadRealInput);
387     return false;
388   }
389   bool hadExtra{got > maxDigits};
390   if (exponent != 0) {
391     buffer[got++] = 'e';
392     if (exponent < 0) {
393       buffer[got++] = '-';
394       exponent = -exponent;
395     }
396     if (exponent > 9999) {
397       exponent = 9999; // will convert to +/-Inf
398     }
399     if (exponent > 999) {
400       int dig{exponent / 1000};
401       buffer[got++] = '0' + dig;
402       int rest{exponent - 1000 * dig};
403       dig = rest / 100;
404       buffer[got++] = '0' + dig;
405       rest -= 100 * dig;
406       dig = rest / 10;
407       buffer[got++] = '0' + dig;
408       buffer[got++] = '0' + (rest - 10 * dig);
409     } else if (exponent > 99) {
410       int dig{exponent / 100};
411       buffer[got++] = '0' + dig;
412       int rest{exponent - 100 * dig};
413       dig = rest / 10;
414       buffer[got++] = '0' + dig;
415       buffer[got++] = '0' + (rest - 10 * dig);
416     } else if (exponent > 9) {
417       int dig{exponent / 10};
418       buffer[got++] = '0' + dig;
419       buffer[got++] = '0' + (exponent - 10 * dig);
420     } else {
421       buffer[got++] = '0' + exponent;
422     }
423   }
424   buffer[got] = '\0';
425   const char *p{buffer};
426   decimal::ConversionToBinaryResult<binaryPrecision> converted{
427       decimal::ConvertToBinary<binaryPrecision>(p, edit.modes.round)};
428   if (hadExtra) {
429     converted.flags = static_cast<enum decimal::ConversionResultFlags>(
430         converted.flags | decimal::Inexact);
431   }
432   *reinterpret_cast<decimal::BinaryFloatingPointNumber<binaryPrecision> *>(n) =
433       converted.binary;
434   // Set FP exception flags
435   if (converted.flags != decimal::ConversionResultFlags::Exact) {
436     RaiseFPExceptions(converted.flags);
437   }
438   return true;
439 }
440 
441 template <int KIND>
442 bool EditRealInput(IoStatementState &io, const DataEdit &edit, void *n) {
443   constexpr int binaryPrecision{common::PrecisionOfRealKind(KIND)};
444   switch (edit.descriptor) {
445   case DataEdit::ListDirected:
446     if (IsNamelistName(io)) {
447       return false;
448     }
449     return EditCommonRealInput<KIND>(io, edit, n);
450   case DataEdit::ListDirectedRealPart:
451   case DataEdit::ListDirectedImaginaryPart:
452   case 'F':
453   case 'E': // incl. EN, ES, & EX
454   case 'D':
455   case 'G':
456     return EditCommonRealInput<KIND>(io, edit, n);
457   case 'B':
458     return EditBOZInput(
459         io, edit, n, 2, common::BitsForBinaryPrecision(binaryPrecision));
460   case 'O':
461     return EditBOZInput(
462         io, edit, n, 8, common::BitsForBinaryPrecision(binaryPrecision));
463   case 'Z':
464     return EditBOZInput(
465         io, edit, n, 16, common::BitsForBinaryPrecision(binaryPrecision));
466   case 'A': // legacy extension
467     return EditCharacterInput(io, edit, reinterpret_cast<char *>(n), KIND);
468   default:
469     io.GetIoErrorHandler().SignalError(IostatErrorInFormat,
470         "Data edit descriptor '%c' may not be used for REAL input",
471         edit.descriptor);
472     return false;
473   }
474 }
475 
476 // 13.7.3 in Fortran 2018
477 bool EditLogicalInput(IoStatementState &io, const DataEdit &edit, bool &x) {
478   switch (edit.descriptor) {
479   case DataEdit::ListDirected:
480     if (IsNamelistName(io)) {
481       return false;
482     }
483     break;
484   case 'L':
485   case 'G':
486     break;
487   default:
488     io.GetIoErrorHandler().SignalError(IostatErrorInFormat,
489         "Data edit descriptor '%c' may not be used for LOGICAL input",
490         edit.descriptor);
491     return false;
492   }
493   std::optional<int> remaining;
494   std::optional<char32_t> next{io.PrepareInput(edit, remaining)};
495   if (next && *next == '.') { // skip optional period
496     next = io.NextInField(remaining, edit);
497   }
498   if (!next) {
499     io.GetIoErrorHandler().SignalError("Empty LOGICAL input field");
500     return false;
501   }
502   switch (*next) {
503   case 'T':
504   case 't':
505     x = true;
506     break;
507   case 'F':
508   case 'f':
509     x = false;
510     break;
511   default:
512     io.GetIoErrorHandler().SignalError(
513         "Bad character '%lc' in LOGICAL input field", *next);
514     return false;
515   }
516   if (remaining) { // ignore the rest of the field
517     io.HandleRelativePosition(*remaining);
518   } else if (edit.descriptor == DataEdit::ListDirected) {
519     while (io.NextInField(remaining, edit)) { // discard rest of field
520     }
521   }
522   return true;
523 }
524 
525 // See 13.10.3.1 paragraphs 7-9 in Fortran 2018
526 template <typename CHAR>
527 static bool EditDelimitedCharacterInput(
528     IoStatementState &io, CHAR *x, std::size_t length, char32_t delimiter) {
529   bool result{true};
530   while (true) {
531     std::size_t byteCount{0};
532     auto ch{io.GetCurrentChar(byteCount)};
533     if (!ch) {
534       if (io.AdvanceRecord()) {
535         continue;
536       } else {
537         result = false; // EOF in character value
538         break;
539       }
540     }
541     io.HandleRelativePosition(byteCount);
542     if (*ch == delimiter) {
543       auto next{io.GetCurrentChar(byteCount)};
544       if (next && *next == delimiter) {
545         // Repeated delimiter: use as character value
546         io.HandleRelativePosition(byteCount);
547       } else {
548         break; // closing delimiter
549       }
550     }
551     if (length > 0) {
552       *x++ = *ch;
553       --length;
554     }
555   }
556   std::fill_n(x, length, ' ');
557   return result;
558 }
559 
560 template <typename CHAR>
561 static bool EditListDirectedCharacterInput(
562     IoStatementState &io, CHAR *x, std::size_t length, const DataEdit &edit) {
563   std::size_t byteCount{0};
564   auto ch{io.GetCurrentChar(byteCount)};
565   if (ch && (*ch == '\'' || *ch == '"')) {
566     io.HandleRelativePosition(byteCount);
567     return EditDelimitedCharacterInput(io, x, length, *ch);
568   }
569   if (IsNamelistName(io) || io.GetConnectionState().IsAtEOF()) {
570     return false;
571   }
572   // Undelimited list-directed character input: stop at a value separator
573   // or the end of the current record.  Subtlety: the "remaining" count
574   // here is a dummy that's used to avoid the interpretation of separators
575   // in NextInField.
576   std::optional<int> remaining{maxUTF8Bytes};
577   while (std::optional<char32_t> next{io.NextInField(remaining, edit)}) {
578     switch (*next) {
579     case ' ':
580     case '\t':
581     case ',':
582     case ';':
583     case '/':
584       remaining = 0; // value separator: stop
585       break;
586     default:
587       *x++ = *next;
588       --length;
589       remaining = maxUTF8Bytes;
590     }
591   }
592   std::fill_n(x, length, ' ');
593   return true;
594 }
595 
596 template <typename CHAR>
597 bool EditCharacterInput(
598     IoStatementState &io, const DataEdit &edit, CHAR *x, std::size_t length) {
599   switch (edit.descriptor) {
600   case DataEdit::ListDirected:
601     return EditListDirectedCharacterInput(io, x, length, edit);
602   case 'A':
603   case 'G':
604     break;
605   default:
606     io.GetIoErrorHandler().SignalError(IostatErrorInFormat,
607         "Data edit descriptor '%c' may not be used with a CHARACTER data item",
608         edit.descriptor);
609     return false;
610   }
611   const ConnectionState &connection{io.GetConnectionState()};
612   if (connection.IsAtEOF()) {
613     return false;
614   }
615   std::size_t remaining{length};
616   if (edit.width && *edit.width > 0) {
617     remaining = *edit.width;
618   }
619   // When the field is wider than the variable, we drop the leading
620   // characters.  When the variable is wider than the field, there's
621   // trailing padding.
622   const char *input{nullptr};
623   std::size_t ready{0};
624   bool hitEnd{false};
625   // Skip leading bytes.
626   // These bytes don't count towards INQUIRE(IOLENGTH=).
627   std::size_t skip{remaining > length ? remaining - length : 0};
628   // Transfer payload bytes; these do count.
629   while (remaining > 0) {
630     if (ready == 0) {
631       ready = io.GetNextInputBytes(input);
632       if (ready == 0) {
633         hitEnd = true;
634         break;
635       }
636     }
637     std::size_t chunk;
638     bool skipping{skip > 0};
639     if (connection.isUTF8) {
640       chunk = MeasureUTF8Bytes(*input);
641       if (skipping) {
642         --skip;
643       } else if (auto ucs{DecodeUTF8(input)}) {
644         *x++ = *ucs;
645         --length;
646       } else if (chunk == 0) {
647         // error recovery: skip bad encoding
648         chunk = 1;
649       }
650       --remaining;
651     } else {
652       if (skipping) {
653         chunk = std::min<std::size_t>(skip, ready);
654         skip -= chunk;
655       } else {
656         chunk = std::min<std::size_t>(remaining, ready);
657         std::memcpy(x, input, chunk);
658         x += chunk;
659         length -= chunk;
660       }
661       remaining -= chunk;
662     }
663     input += chunk;
664     if (!skipping) {
665       io.GotChar(chunk);
666     }
667     io.HandleRelativePosition(chunk);
668     ready -= chunk;
669   }
670   // Pad the remainder of the input variable, if any.
671   std::fill_n(x, length, ' ');
672   if (hitEnd) {
673     io.CheckForEndOfRecord(); // signal any needed error
674   }
675   return true;
676 }
677 
678 template bool EditRealInput<2>(IoStatementState &, const DataEdit &, void *);
679 template bool EditRealInput<3>(IoStatementState &, const DataEdit &, void *);
680 template bool EditRealInput<4>(IoStatementState &, const DataEdit &, void *);
681 template bool EditRealInput<8>(IoStatementState &, const DataEdit &, void *);
682 template bool EditRealInput<10>(IoStatementState &, const DataEdit &, void *);
683 // TODO: double/double
684 template bool EditRealInput<16>(IoStatementState &, const DataEdit &, void *);
685 
686 template bool EditCharacterInput(
687     IoStatementState &, const DataEdit &, char *, std::size_t);
688 template bool EditCharacterInput(
689     IoStatementState &, const DataEdit &, char16_t *, std::size_t);
690 template bool EditCharacterInput(
691     IoStatementState &, const DataEdit &, char32_t *, std::size_t);
692 
693 } // namespace Fortran::runtime::io
694