1 /*
2  * A faster replacement for some subset of std.stdio
3  * Copyright © 2022, Siarhei Siamashka
4  *
5  * Boost Software License - Version 1.0 - August 17th, 2003
6  *
7  * Permission is hereby granted, free of charge, to any person or organization
8  * obtaining a copy of the software and accompanying documentation covered by
9  * this license (the "Software") to use, reproduce, display, distribute,
10  * execute, and transmit the Software, and to prepare derivative works of the
11  * Software, and to permit third-parties to whom the Software is furnished to
12  * do so, all subject to the following:
13  *
14  * The copyright notices in the Software and this entire statement, including
15  * the above license grant, this restriction and the following disclaimer,
16  * must be included in all copies of the Software, in whole or in part, and
17  * all derivative works of the Software, unless such copies or derivative
18  * works are solely in the form of machine-executable object code generated by
19  * a source language processor.
20  *
21  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
24  * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
25  * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
26  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27  * DEALINGS IN THE SOFTWARE.
28  */
29 module speedy.stdio;
30 @safe:
31 
32 private const BUFSIZE = 32 * 1024;
33 
34 public import std.stdio;
35 import std.range, std.format, std.algorithm, std.traits, core.bitop;
36 import std.exception;
37 
38 private enum SpeedySafety
39 {
40     Safe,
41     Unsafe
42 }
43 
44 package template SpeedyWriter(SpeedySafety safety)
45 {
46     private static bool isChar(T)()
47     {
48         return isSomeChar!T && T.sizeof == 1;
49     }
50 
51     private static bool isString(T)()
52     {
53         return is(T == string) || is(T == immutable(string));
54     }
55 
56     private static bool isNogcCompatibleNested(T)()
57     {
58         static if (isIntegral!T)
59             return true;
60         else static if (isInputRange!T || isArray!T)
61             return isNogcCompatibleNested!(ElementType!T);
62         else
63             return false;
64     }
65 
66     private static bool isNogcCompatible(T)()
67     {
68         static if (isIntegral!T || isString!T || isChar!T)
69             return true;
70         else static if (isInputRange!T || isArray!T)
71             return isNogcCompatibleNested!(ElementType!T);
72         else
73             return false;
74     }
75 
76     static if (safety == SpeedySafety.Safe)
77     {
78         static char[BUFSIZE] outbuf;
79         static ptrdiff_t outbuf_size = 0;
80     }
81     else
82     {
83         shared static char[BUFSIZE] outbuf;
84         shared static ptrdiff_t outbuf_size = 0;
85     }
86 
87     version (unittest)
88     {
89         static void rawWriteStdout()(char[] buffer)
90         {
91         }
92 
93         static bool buffer_contains()(string expected_data) @trusted
94         {
95             return expected_data == cast(string) outbuf[0 .. outbuf_size];
96         }
97     }
98     else static if (safety == SpeedySafety.Safe)
99     {
100         version (Posix)
101         {
102             import core.sys.posix.stdio;
103         }
104         version (Windows)
105         {
106             alias flockfile = _lock_file;
107             alias funlockfile = _unlock_file;
108         }
109         shared static FILE* fp;
110         shared static this() @trusted
111         {
112             fp = std.stdio.stdout.getFP;
113         }
114 
115         static bool flocked;
116         static void rawWriteStdout()(char[] buffer) @trusted
117         {
118             if (!flocked)
119             {
120                 flocked = true;
121                 flockfile(fp);
122             }
123             fwrite(cast(void*) buffer.ptr, 1, buffer.length, fp);
124         }
125     }
126     else version (Posix)
127     {
128 
129         static void rawWriteStdout()(char[] buffer) @trusted
130         {
131             import core.sys.posix.unistd : write, STDOUT_FILENO;
132             import core.sys.posix.fcntl, core.stdc.errno;
133 
134             while (buffer.length > 0)
135             {
136                 auto result = write(STDOUT_FILENO, cast(void*) buffer.ptr, buffer.length);
137                 if (result > 0)
138                 {
139                     buffer = buffer[result .. $];
140                     continue;
141                 }
142                 assert(result == -1 && (errno == EAGAIN || errno == EINTR), "unexpected error type");
143             }
144         }
145     }
146     else version (Windows)
147     {
148         static void rawWriteStdout()(char[] buffer) @trusted
149         {
150             import core.sys.windows.windows;
151 
152             auto h = GetStdHandle(STD_OUTPUT_HANDLE);
153             while (buffer.length > 0)
154             {
155                 uint actual_written;
156                 auto result = WriteFile(h, cast(void*) buffer.ptr, cast(uint) buffer.length, &actual_written, null);
157                 if (result && actual_written > 0)
158                 {
159                     buffer = buffer[actual_written .. $];
160                     continue;
161                 }
162                 assert(result, "unexpected error");
163             }
164         }
165     }
166 
167     static void flush()() @trusted
168     {
169         if (outbuf_size > 0)
170             rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]);
171         outbuf_size = 0;
172     }
173 
174     static void done()() @trusted
175     {
176         static if (safety == SpeedySafety.Safe)
177         {
178             if (flocked)
179             {
180                 flush();
181                 funlockfile(fp);
182                 flocked = false;
183             }
184             else
185             {
186                 flocked = true;
187                 flush();
188                 flocked = false;
189             }
190         }
191     }
192 
193     shared static ~this()
194     {
195         flush();
196     }
197 
198     static void put(T)(T ch) @trusted if (isChar!T)
199     {
200         if (outbuf_size < outbuf.sizeof)
201         {
202             outbuf[outbuf_size] = ch;
203             outbuf_size = outbuf_size + 1;
204         }
205         else
206         {
207             rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]);
208             outbuf[0] = ch;
209             outbuf_size = 1;
210         }
211     }
212 
213     static void ensure_outbuf_capacity()(ptrdiff_t size) @trusted
214     {
215         if (outbuf_size + size > outbuf.sizeof)
216         {
217             rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]);
218             outbuf_size = 0;
219             assert(size <= outbuf.sizeof, "write buffer is too small");
220         }
221     }
222 
223     // Compute the number of digits of an integer quickly. Public domain code from Daniel Lemire:
224     //  * https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog
225     //  * https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/
226     static int fast_digit_count()(uint x)
227     {
228         static immutable table = [
229             4294967296, 8589934582, 8589934582, 8589934582, 12884901788,
230             12884901788, 12884901788, 17179868184, 17179868184, 17179868184,
231             21474826480, 21474826480, 21474826480, 21474826480, 25769703776,
232             25769703776, 25769703776, 30063771072, 30063771072, 30063771072,
233             34349738368, 34349738368, 34349738368, 34349738368, 38554705664,
234             38554705664, 38554705664, 41949672960, 41949672960, 41949672960,
235             42949672960, 42949672960
236         ];
237         assert(x != 0);
238         return (x + table[bsr(x)]) >> 32;
239     }
240 
241     static int fast_digit_count()(ulong x)
242     {
243         static immutable table = [
244             4503599627370495, 9007199254740982, 9007199254740982,
245             9007199254740982, 13510798882111438, 13510798882111438,
246             13510798882111438, 18014398509481484, 18014398509481734,
247             18014398509481734, 22517998136849980, 22517998136849980,
248             22517998136851230, 22517998136851230, 27021597764210476,
249             27021597764210476, 27021597764216726, 31525197391530972,
250             31525197391530972, 31525197391530972, 36028797018651468,
251             36028797018651468, 36028797018651468, 36028797018651468,
252             40532396644771964, 40532396644771964, 40532396644771964,
253             45035996258079960, 45035996265892460, 45035996265892460,
254             49539595822950456, 49539595822950456, 49539595862012956,
255             49539595862012956, 54043195137820952, 54043195137820952,
256             54043195333133452, 58546793202691448, 58546793202691448,
257             58546793202691448, 63050385017561944, 63050385017561944,
258             63050385017561944, 63050385017561944, 67553945582432440,
259             67553945582432440, 67553945582432440, 72057105756677936,
260             72057349897302936, 72057349897302936, 76558752259048432,
261             76558752259048432, 76559972962173432, 76559972962173432,
262             81052586261418928, 81052586261418928, 81058689777043928,
263             85507357763789424, 85507357763789424, 85507357763789424,
264             89766816766159920, 89766816766159920, 89766816766159920,
265             89766816766159920
266         ];
267         assert(x != 0);
268         int floor_log2_x = bsr(x);
269         return (table[floor_log2_x] + (x >> (floor_log2_x / 4))) >> 52;
270     }
271 
272     // Construct digits conversion table at compile time
273 
274     static immutable digitsbatch = 4;
275 
276     static immutable tbl = {
277         char[(10 ^^ digitsbatch) * digitsbatch] result;
278         foreach (i; 0 .. (10 ^^ digitsbatch))
279         {
280             int v = i;
281             foreach_reverse (j; 0 .. digitsbatch)
282             {
283                 result[i * digitsbatch + j] = '0' + v % 10;
284                 v /= 10;
285             }
286         }
287         return result;
288     }();
289 
290     static void put(T)(T val) if (isIntegral!T)
291     {
292         if (val == 0)
293         {
294             put('0');
295             return;
296         }
297         if (val < 0)
298         {
299             put('-');
300             val = -val;
301         }
302         Unsigned!T v = val;
303 
304         ptrdiff_t ndigits = fast_digit_count(v);
305         ensure_outbuf_capacity(ndigits);
306 
307         ptrdiff_t i = outbuf_size + ndigits - digitsbatch;
308         while (i >= outbuf_size)
309         {
310             auto newv = v / (10 ^^ digitsbatch);
311             auto rem = v - newv * (10 ^^ digitsbatch);
312             v = newv;
313             ptrdiff_t j = cast(ptrdiff_t) rem * digitsbatch;
314             outbuf[i .. i + digitsbatch] = tbl[j .. j + digitsbatch];
315             i -= digitsbatch;
316         }
317         if (i < outbuf_size)
318         {
319             size_t lastbatch = digitsbatch - (outbuf_size - i);
320             ptrdiff_t j = cast(ptrdiff_t) v * digitsbatch + digitsbatch - lastbatch;
321             static if (digitsbatch <= 4)
322             {
323                 i = outbuf_size;
324                 if (lastbatch & 2)
325                 {
326                     outbuf[i .. i + 2] = tbl[j .. j + 2];
327                     i += 2;
328                     j += 2;
329                 }
330                 if (lastbatch & 1)
331                     outbuf[i] = tbl[j];
332             }
333             else
334             {
335                 outbuf[outbuf_size .. outbuf_size + lastbatch] = tbl[j .. j + lastbatch];
336             }
337         }
338         outbuf_size = outbuf_size + ndigits;
339     }
340 
341     static void put()(string str) @trusted
342     {
343         while (str.length > 0)
344         {
345             if (outbuf_size == outbuf.sizeof)
346             {
347                 rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]);
348                 outbuf_size = 0;
349             }
350             ptrdiff_t chunksize = min(str.length, outbuf.sizeof - outbuf_size);
351             outbuf[outbuf_size .. outbuf_size + chunksize] = str[0 .. chunksize];
352             str = str[chunksize .. $];
353             outbuf_size = outbuf_size + chunksize;
354         }
355     }
356 
357     static void put(Range)(Range r) if ((isInputRange!Range || isArray!Range) && isNogcCompatible!(ElementType!Range))
358     {
359         static if (isChar!(ElementType!Range))
360         {
361             put_with_joiner(r, "");
362         }
363         else
364         {
365             put('[');
366             put_with_joiner(r, ", ");
367             put(']');
368         }
369     }
370 
371     static void put_with_joiner(Range, T)(Range r, T sep)
372             if ((isInputRange!Range || isArray!Range) && (isString!T || isChar!T))
373     {
374         bool first = true;
375         foreach (x; r)
376         {
377             if (!first)
378                 put(sep);
379             static if (isNogcCompatible!(typeof(x)))
380                 put(x);
381             else
382                 put_gc(x);
383             first = false;
384         }
385     }
386 
387     // fallback to std.conv for the types, which are not supported
388     static void put_gc(T)(T val)
389     {
390         import std.conv;
391 
392         put(val.to!string);
393     }
394 
395     private struct SpeedyWriterSink
396     {
397         void put(T)(T arg)
398         {
399             SpeedyWriter!safety.put(arg);
400         }
401     }
402 
403     static SpeedyWriterSink s;
404 
405     static void writef_speedy(alias fmt, A...)(A args)
406     {
407         static immutable xfmt = parsefmt(fmt);
408         put(xfmt.prefix);
409         foreach (i, arg; args)
410         {
411             static if (xfmt.tokens[i].compound)
412                 put_with_joiner(arg, xfmt.tokens[i].separator);
413             else
414                 put(arg);
415             put(xfmt.tokens[i].suffix);
416         }
417     }
418 
419     public static void writef(alias fmt, A...)(A args)
420     {
421         static if (isSpeedyCompatibleFormat!(fmt, A))
422             writef_speedy!fmt(args);
423         else
424             formattedWrite!fmt(s, args);
425         done();
426     }
427 
428     public static void writef(Char, A...)(in Char[] fmt, A args)
429     {
430         formattedWrite(s, fmt, args);
431         done();
432     }
433 
434     public static void writefln(alias fmt, A...)(A args)
435     {
436         static if (isSpeedyCompatibleFormat!(fmt, A))
437             writef_speedy!fmt(args);
438         else
439             formattedWrite!fmt(s, args);
440         put('\n');
441         done();
442     }
443 
444     public static void writefln(Char, A...)(in Char[] fmt, A args)
445     {
446         formattedWrite(s, fmt, args);
447         put('\n');
448         done();
449     }
450 
451     public static void write(T...)(T args)
452     {
453         foreach (arg; args)
454             static if (isNogcCompatible!(typeof(arg)))
455                 put(arg);
456             else
457                 put_gc(arg);
458         done();
459     }
460 
461     public static void writeln(T...)(T args)
462     {
463         foreach (arg; args)
464             static if (isNogcCompatible!(typeof(arg)))
465                 put(arg);
466             else
467                 put_gc(arg);
468         put('\n');
469         done();
470     }
471 
472     // Similar to checkFormatException from Phobos, but only accepts a small
473     // subset of possible formats.
474     enum isSpeedyCompatibleFormat(alias fmt, Args...) =
475     {
476         try
477         {
478             void tassert(T)(T cond)
479             {
480                 if (!cond)
481                     throw new Exception("");
482             }
483 
484             static immutable xfmt = parsefmt(fmt);
485             tassert(xfmt.tokens.length == Args.length);
486             foreach (i, arg; Args.init)
487             {
488                 if (xfmt.tokens[i].compound == 1 && (isInputRange!(typeof(arg)) || isArray!(typeof(arg))))
489                 {
490                     if (isIntegral!(ElementType!(typeof(arg))))
491                         tassert(!find("ds", xfmt.tokens[i].spec).empty);
492                     else if (isNogcCompatibleNested!(ElementType!(typeof(arg))))
493                         tassert(!find("s", xfmt.tokens[i].spec).empty);
494                     else
495                         tassert(false);
496                 }
497                 else if (xfmt.tokens[i].compound == 2 && (isInputRange!(typeof(arg)) || isArray!(typeof(arg))))
498                 {
499                     if (isIntegral!(ElementType!(typeof(arg))))
500                         tassert(!find("ds", xfmt.tokens[i].spec).empty);
501                     else if (isNogcCompatible!(ElementType!(typeof(arg))))
502                         tassert(!find("s", xfmt.tokens[i].spec).empty);
503                     else
504                         tassert(false);
505                 }
506                 else if (!xfmt.tokens[i].compound)
507                 {
508                     if (isIntegral!(typeof(arg)))
509                         tassert(!find("ds", xfmt.tokens[i].spec).empty);
510                     else if (isNogcCompatible!(typeof(arg)))
511                         tassert(!find("s", xfmt.tokens[i].spec).empty);
512                     else
513                         tassert(false);
514                 }
515                 else
516                 {
517                     tassert(false);
518                 }
519 
520             }
521         }
522         catch (Exception e)
523             return false;
524         return true;
525     }();
526 
527 }
528 
529 ///////////////////////////////////////////////////////////////////////////////
530 
531 private struct FmtToken
532 {
533     char spec;
534     byte compound; /* 0 - not a compound, 1 - escaped, 2 - non-escaped */
535     string separator;
536     string suffix;
537 }
538 
539 private struct ParsedFmt
540 {
541     string prefix;
542     FmtToken[] tokens;
543 }
544 
545 /*
546  * This is a minimalistic parser, which recognizes the following patterns:
547  *  /%%/                 - escaped '%' character
548  *  /%d/                 - decimal number
549  *  /%s/                 - string
550  *  /%\(%[ds][^%]*%\)/   - escaped compound
551  *  /%\-\(%[ds][^%]*%\)/ - non-escaped compound
552  */
553 private ParsedFmt parsefmt(string fmt)
554 {
555     string scan_until_spec(ref string fmt)
556     {
557         string prefix = "";
558         while (!fmt.empty)
559         {
560             auto tmp = fmt.find('%');
561             if (tmp.empty)
562             {
563                 prefix ~= fmt;
564                 fmt = fmt[$ .. $];
565                 return prefix;
566             }
567             else
568             {
569                 enforce(tmp.length >= 2);
570                 if (tmp[1] == '%')
571                 {
572                     prefix ~= fmt[0 .. fmt.length - tmp.length + 1];
573                     fmt = fmt[fmt.length - tmp.length + 2 .. $];
574                 }
575                 else
576                 {
577                     prefix ~= fmt[0 .. fmt.length - tmp.length];
578                     fmt = fmt[fmt.length - tmp.length .. $];
579                     return prefix;
580                 }
581             }
582         }
583         return prefix;
584     }
585 
586     ParsedFmt result;
587     result.prefix = scan_until_spec(fmt);
588     while (!fmt.empty)
589     {
590         enforce(fmt.length >= 2 && fmt[0] == '%');
591         if (fmt[1] == '(' || (fmt.length >= 3 && fmt[1] == '-' && fmt[2] == '('))
592         {
593             byte compound = 1;
594             if (fmt[1] == '-')
595             {
596                 compound = 2;
597                 fmt = fmt[1 .. $];
598             }
599             enforce(fmt.length >= 6 && fmt[2] == '%'); // at least %(%s%)
600             char spec = fmt[3];
601             fmt = fmt[4 .. $];
602             string separator = scan_until_spec(fmt);
603             enforce(fmt.length >= 2 && fmt[1] == ')');
604             fmt = fmt[2 .. $];
605             string suffix = scan_until_spec(fmt);
606             result.tokens ~= FmtToken(spec, compound, separator, suffix);
607         }
608         else
609         {
610             byte spec = fmt[1];
611             fmt = fmt[2 .. $];
612             string suffix = scan_until_spec(fmt);
613             result.tokens ~= FmtToken(spec, 0, "", suffix);
614         }
615     }
616     return result;
617 }
618 
619 alias write = SpeedyWriter!(SpeedySafety.Safe).write;
620 alias writeln = SpeedyWriter!(SpeedySafety.Safe).writeln;
621 alias writef = SpeedyWriter!(SpeedySafety.Safe).writef;
622 alias writefln = SpeedyWriter!(SpeedySafety.Safe).writefln;
623 
624 alias unsafe_write = SpeedyWriter!(SpeedySafety.Unsafe).write;
625 alias unsafe_writeln = SpeedyWriter!(SpeedySafety.Unsafe).writeln;
626 alias unsafe_writef = SpeedyWriter!(SpeedySafety.Unsafe).writef;
627 alias unsafe_writefln = SpeedyWriter!(SpeedySafety.Unsafe).writefln;
628 alias unsafe_stdout_flush = SpeedyWriter!(SpeedySafety.Unsafe).flush;
629 
630 version (unittest)
631 {
632     alias unsafe_stdout_buffer_contains = SpeedyWriter!(SpeedySafety.Unsafe).buffer_contains;
633 }