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