├── .codecov.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── dub.sdl ├── dub.selections.json ├── scripts └── ci.sh └── source └── bc ├── core ├── demangle.d ├── intrinsics.d ├── memory.d ├── system │ ├── backtrace.d │ └── linux │ │ ├── dwarf.d │ │ ├── elf.d │ │ └── execinfo.d └── traits.d ├── internal └── utf.d └── string ├── ascii.d ├── conv.d ├── format.d ├── numeric.d ├── package.d └── string.d /.codecov.yml: -------------------------------------------------------------------------------- 1 | # Documentation: https://github.com/codecov/support/wiki/codecov.yml 2 | # Validate with: curl --data-binary @.codecov.yml https://codecov.io/validate 3 | 4 | codecov: 5 | notify: 6 | # We don't want to wait for the CodeCov report 7 | # See https://github.com/codecov/support/issues/312 8 | require_ci_to_pass: false 9 | after_n_builds: 1 # send notifications after the first upload 10 | wait_for_ci: false 11 | 12 | # At Travis, the PR is merged into `master` before the testsuite is run. 13 | # This allows CodeCov to adjust the resulting coverage diff, s.t. it matches 14 | # with the GitHub diff. 15 | # https://github.com/codecov/support/issues/363 16 | # https://docs.codecov.io/v4.3.6/docs/comparing-commits 17 | allow_coverage_offsets: true 18 | 19 | coverage: 20 | precision: 3 21 | round: down 22 | range: 50...100 23 | 24 | status: 25 | # Learn more at https://codecov.io/docs#yaml_default_commit_status 26 | project: off 27 | patch: 28 | default: 29 | informational: true 30 | changes: off 31 | 32 | comment: false 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | name: ci 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | test: 15 | name: tests 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macOS-latest] 19 | dc: [dmd-latest, ldc-latest] 20 | include: 21 | - { os: ubuntu-latest, dc: dmd-2.102.2, arch: x86_64 } 22 | - { os: ubuntu-latest, dc: dmd-2.094.2, arch: x86_64 } 23 | - { os: ubuntu-latest, dc: ldc-1.24.0, arch: x86_64 } 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Install D compiler 28 | uses: dlang-community/setup-dlang@v1 29 | with: 30 | compiler: ${{ matrix.dc }} 31 | - name: Run tests 32 | run: scripts/ci.sh 33 | - name: Upload codecov 34 | if: matrix.os == 'ubuntu-latest' && matrix.dc == 'dmd-latest' 35 | env: 36 | COVERAGE: true 37 | run: scripts/ci.sh 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | *test* 6 | *.lib 7 | *.a 8 | *.exe 9 | *.o 10 | *.obj 11 | *.lst 12 | bc-string 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bc-string 2 | [![Actions Status](https://github.com/tchaloupka/bc-string/workflows/ci/badge.svg)](https://github.com/tchaloupka/bc-string/actions) 3 | [![Latest version](https://img.shields.io/dub/v/bc-string.svg)](https://code.dlang.org/packages/bc-string) 4 | [![Dub downloads](https://img.shields.io/dub/dt/bc-string.svg)](http://code.dlang.org/packages/bc-string) 5 | [![codecov](https://codecov.io/gh/tchaloupka/bc-string/branch/master/graph/badge.svg)](https://codecov.io/gh/tchaloupka/bc-string) 6 | [![license](https://img.shields.io/github/license/tchaloupka/bc-string.svg)](https://github.com/tchaloupka/bc-string/blob/master/LICENSE) 7 | 8 | Set of utilities to help working with strings in a `@nogc` or `betterC` codes. 9 | 10 | It uses parts of [druntime](https://github.com/dlang/druntime) or [phobos](https://github.com/dlang/phobos) for `betterC` builds to allow otherwise impossible compilation or problems with `@nogc` uses. 11 | 12 | ## `format.d` 13 | 14 | This module contains `@nogc` formatter with a compile time format specifier parser. 15 | 16 | * doesn't have problems with `Nullable` - see [issue 17269](https://issues.dlang.org/show_bug.cgi?id=17269) 17 | * can be used with any `Nullable` like type defining a common interface (isNull, nullify, get) - ie [mir.algebraic.Nullable](http://mir-core.libmir.org/mir_algebraic.html#Nullable) 18 | * can format exception backtrace (currently on Linux only) - and not in `betterC` as there are none 19 | * can be used to determine resulting string size (ie for preallocations) 20 | * can be used to format texts for asserts or format something in class destructors while being cleaned up by GC, etc. 21 | 22 | **Note:** It's not feature complete compared to `std.format.format`, but pretty close (can be improved, but it's just ok for logging purposes). 23 | 24 | Main functions are: 25 | 26 | * `nogcFormatTo` - formats text to the provided sink (must be `@nogc`) 27 | * `nogcFormat` - formats text to internally stored static `String` - content is replaced on each use 28 | * `getFormatSize` - gets size needed to format the text with a provided arguments 29 | 30 | Example: 31 | 32 | ```D 33 | import bc.core.memory; 34 | import bc.internal.utf : byCodeUnit; 35 | import core.stdc.string : strlen; 36 | import std.algorithm : filter; 37 | import std.range : chunks; 38 | 39 | assert(nogcFormat!"abcd abcd" == "abcd abcd"); 40 | assert(nogcFormat!"123456789a" == "123456789a"); 41 | version (D_NoBoundsChecks) {} 42 | else version (D_BetterC) {} 43 | else 44 | { 45 | () @trusted 46 | { 47 | import core.exception : RangeError; 48 | import std.exception : assertThrown; 49 | char[5] buf; 50 | assertThrown!RangeError(buf.nogcFormatTo!"123412341234"); 51 | }(); 52 | } 53 | 54 | // literal escape 55 | assert(nogcFormat!"123 %%" == "123 %"); 56 | assert(nogcFormat!"%%%%" == "%%"); 57 | 58 | // %d 59 | assert(nogcFormat!"%d"(1234) == "1234"); 60 | assert(nogcFormat!"%4d"(42) == " 42"); 61 | assert(nogcFormat!"%04d"(42) == "0042"); 62 | assert(nogcFormat!"%04d"(-42) == "-042"); 63 | assert(nogcFormat!"ab%dcd"(1234) == "ab1234cd"); 64 | assert(nogcFormat!"ab%d%d"(1234, 56) == "ab123456"); 65 | 66 | // %x 67 | assert(nogcFormat!"0x%x"(0x1234) == "0x1234"); 68 | 69 | // %p 70 | assert(nogcFormat!("%p")(0x1234) == "0000000000001234"); 71 | 72 | // %s 73 | assert(nogcFormat!"12345%s"("12345") == "1234512345"); 74 | assert(nogcFormat!"12345%s"(12345) == "1234512345"); 75 | enum Floop {XXX, YYY, ZZZ} 76 | assert(nogcFormat!"12345%s"(Floop.YYY) == "12345YYY"); 77 | char[4] str = "foo\0"; 78 | assert(nogcFormat!"%s"(&str[0]) == "foo"); 79 | 80 | version (D_BetterC) {} // can't import std.uuid in betterC 81 | else 82 | { 83 | import std.uuid; 84 | assert(nogcFormat!"%s"( 85 | parseUUID("22390768-cced-325f-8f0f-cfeaa19d0ccd")) 86 | == "22390768-cced-325f-8f0f-cfeaa19d0ccd"); 87 | } 88 | 89 | // array format 90 | version (D_BetterC) 91 | { 92 | int[] arr = (cast(int*)enforceMalloc(int.sizeof*10))[0..10]; 93 | foreach (i; 0..10) arr[i] = i; 94 | scope (exit) pureFree(arr.ptr); 95 | } 96 | else auto arr = [0,1,2,3,4,5,6,7,8,9]; 97 | 98 | assert(nogcFormat!"foo %(%d %)"(arr[1..4]) == "foo 1 2 3"); 99 | assert(nogcFormat!"foo %-(%d %)"(arr[1..4]) == "foo 1 2 3"); 100 | assert(nogcFormat!"foo %(-%d-%|, %)"(arr[1..4]) == "foo -1-, -2-, -3-"); 101 | assert(nogcFormat!"%(0x%02x %)"(arr[1..4]) == "0x01 0x02 0x03"); 102 | assert(nogcFormat!"%(%(%d %)\n%)"(arr[1..$].chunks(3)) == "1 2 3\n4 5 6\n7 8 9"); 103 | 104 | // range format 105 | auto r = arr.filter!(a => a < 5); 106 | assert(nogcFormat!"%s"(r) == "[0, 1, 2, 3, 4]"); 107 | 108 | // Arg num 109 | assert(!__traits(compiles, nogcFormat!"abc"(5))); 110 | assert(!__traits(compiles, nogcFormat!"%d"())); 111 | assert(!__traits(compiles, nogcFormat!"%d a %d"(5))); 112 | 113 | // Format error 114 | assert(!__traits(compiles, nogcFormat!"%"())); 115 | assert(!__traits(compiles, nogcFormat!"abcd%d %"(15))); 116 | assert(!__traits(compiles, nogcFormat!"%$"(1))); 117 | assert(!__traits(compiles, nogcFormat!"%d"("hello"))); 118 | assert(!__traits(compiles, nogcFormat!"%x"("hello"))); 119 | 120 | assert(nogcFormat!"Hello %s"(5) == "Hello 5"); 121 | 122 | struct Foo { int x, y; } 123 | assert(nogcFormat!("Hello %s")(Foo(1, 2)) == "Hello Foo(x=1, y=2)"); 124 | 125 | version (D_BetterC) 126 | { 127 | struct Nullable(T) // can't be instanciated in betterC - fake just for the UT 128 | { 129 | T get() { return T.init; } 130 | bool isNull() { return true; } 131 | void nullify() {} 132 | } 133 | } 134 | else import std.typecons : Nullable; 135 | struct Msg { Nullable!string foo; } 136 | assert(nogcFormat!"%s"(Msg.init) == "Msg(foo=null)"); 137 | ``` 138 | 139 | Example formating current stack (no `betterC` and linux only): 140 | 141 | ```D 142 | import bc.core.system.backtrace : TraceInfo; 143 | printf("%s\n", nogcFormat!"where am i: %s"(TraceInfo.current).ptr); 144 | ``` 145 | 146 | ## `numeric.d` 147 | 148 | Just a helper to determine required number of digits of integral numbers - `numDigits`. 149 | 150 | Example: 151 | 152 | ```D 153 | assert(numDigits(0) == 1); 154 | assert(numDigits(11) == 2); 155 | assert(numDigits(-1) == 2); 156 | 157 | assert(numDigits(int.min) == 11); 158 | assert(numDigits(int.max) == 10); 159 | assert(numDigits(long.min) == 20); 160 | assert(numDigits(long.max) == 19); 161 | assert(numDigits(ulong.min) == 1); 162 | assert(numDigits(ulong.max) == 20); 163 | ``` 164 | 165 | ## `string.d` 166 | 167 | String holding structures for various usecases. 168 | Has char, wchar and dchar variants. 169 | All has leading `\0` character so can be used as a C string as is without the need to use `toStringz`. 170 | 171 | * `TempCString` - temporary zero terminated string - inspired by not visible `std.internal.cstring.TempCStringBuffer` 172 | * `RCString` - refcounted string that can be passed as needed and is freed with last instance 173 | * `String` - non copyable variant that can be only moved (passing ownership) 174 | * `dedent` - CT variant of `std.string.outdent` that can be used to directly outdent string literals in CT (ie when writing in code indented SQL commands) 175 | 176 | Example: 177 | 178 | ```D 179 | string str2 = "abc"; 180 | 181 | // Intended usage 182 | assert(strlen(str2.tempCString()) == 3); 183 | 184 | // Correct usage 185 | auto tmp = str2.tempCString(); 186 | assert(strlen(tmp) == 3); // or `tmp.ptr`, or `tmp.buffPtr` 187 | 188 | // Incorrect usage 189 | auto pInvalid1 = str2.tempCString().ptr; 190 | const char* pInvalid2 = str2.tempCString(); 191 | 192 | RCString rcs; 193 | rcs ~= "fo"; 194 | rcs ~= 'o'; 195 | rcs ~= "bar"; 196 | rcs ~= "baz".byCodeUnit.filter!(a => a == 'z'); 197 | assert(rcs.length == "foobarz".length); 198 | assert(rcs.data == "foobarz"); 199 | assert(rcs == "foobarz"); 200 | assert(rcs.ptr == &rcs.data[0]); 201 | assert((rcs.ptr["foobarz".length]) == 0); 202 | 203 | // construction from write like params 204 | rcs = RCString.from("foo", 42, "bar"); 205 | assert(rcs == "foo42bar"); 206 | 207 | auto ss = String("Hello"); 208 | assert(ss[] == "Hello", s[]); 209 | ss ~= " String"; 210 | assert(ss[] == "Hello String", ss[]); 211 | auto ss2 = ss.clone(); 212 | assert(ss[] == ss2[]); 213 | assert(ss.ptr != ss2.ptr); 214 | 215 | auto ss3 = ss.move(); 216 | assert(ss.length == 0); 217 | assert(ss3 == "Hello String"); 218 | 219 | enum indented = ` 220 | SELECT foo, bar, baz 221 | FROM foos 222 | WHERE id=ANY($1) AND type_id IN ( 223 | SELECT id FROM bars WHERE owner=$2 224 | )`; 225 | 226 | enum dedented = 227 | "SELECT foo, bar, baz\n" ~ 228 | " FROM foos\n" ~ 229 | " WHERE id=ANY($1) AND type_id IN (\n" ~ 230 | " SELECT id FROM bars WHERE owner=$2\n" ~ 231 | ")"; 232 | 233 | assert(dedent!indented == dedented); 234 | ``` 235 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "bc-string" 2 | description "Various string structures and utilities useable in @nogc or betterC code (RCString, String, TempCString, format, ...)" 3 | authors "Tomáš Chaloupka" 4 | copyright "Copyright © 2020, Tomáš Chaloupka" 5 | license "BSL-1.0" 6 | 7 | configuration "default" { 8 | } 9 | 10 | configuration "betterC" { 11 | buildOptions "betterC" 12 | } 13 | 14 | configuration "bc-string-test-betterC" { 15 | targetType "executable" 16 | buildOptions "betterC" 17 | dflags "-mcpu=native" 18 | versions "CI_MAIN" 19 | } 20 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SRC_FILES="-Isource 4 | source/bc/core/system/linux/elf.d 5 | source/bc/core/system/linux/execinfo.d 6 | source/bc/core/system/linux/dwarf.d 7 | source/bc/core/system/backtrace.d 8 | source/bc/core/demangle.d 9 | source/bc/core/memory.d 10 | source/bc/core/intrinsics.d 11 | source/bc/core/traits.d 12 | source/bc/internal/utf.d 13 | source/bc/string/ascii.d 14 | source/bc/string/conv.d 15 | source/bc/string/format.d 16 | source/bc/string/string.d 17 | source/bc/string/package.d 18 | source/bc/string/numeric.d" 19 | 20 | set -v -e -o pipefail 21 | 22 | if [ -z $DC ]; then DC="dmd"; fi 23 | 24 | if [ $DC = "ldc2" ]; then DC="ldmd2"; fi 25 | 26 | rm -f bc-string-* 27 | 28 | if [ "$COVERAGE" = true ]; then 29 | $DC -version=CI_MAIN -cov -debug -g -unittest -w -vcolumns -of=bc-string-cov $SRC_FILES 30 | ./bc-string-cov 31 | wget https://codecov.io/bash -O codecov.sh 32 | bash codecov.sh 33 | else 34 | # test with dub 35 | dub test --compiler=$DC 36 | 37 | # test release build 38 | echo "Building release test build" 39 | $DC -version=CI_MAIN -release -g -O -w -mcpu=native -inline -of=bc-string-release-test $SRC_FILES 40 | ./bc-string-release-test 41 | 42 | echo "Building betterC unittest runner" 43 | $DC -c -version=CI_MAIN -debug -unittest -g -w -vcolumns -betterC -of=bc-string.o $SRC_FILES 44 | $DC -ofbc-string-bc-test bc-string.o -g -betterC 45 | ./bc-string-bc-test 46 | 47 | echo "Building betterC test build" 48 | $DC -c -version=CI_MAIN -debug -g -w -vcolumns -betterC -of=bc-string.o $SRC_FILES 49 | $DC -ofbc-string-bc-test bc-string.o -g -betterC 50 | ./bc-string-bc-test 51 | 52 | echo "Building betterC release build" 53 | $DC -c -version=CI_MAIN -release -g -O -boundscheck=off -w -vcolumns -betterC -of=bc-string.o $SRC_FILES 54 | $DC -ofbc-string-bc-test bc-string.o -g -betterC 55 | ./bc-string-bc-test 56 | fi 57 | -------------------------------------------------------------------------------- /source/bc/core/intrinsics.d: -------------------------------------------------------------------------------- 1 | module bc.core.intrinsics; 2 | 3 | version(LDC) 4 | { 5 | import ldc.intrinsics: llvm_expect; 6 | import ldc.gccbuiltins_x86; 7 | public import core.simd; 8 | 9 | enum LDC_with_SSE42 = __traits(targetHasFeature, "sse4.2"); 10 | 11 | // some definition aliases to commonly used names 12 | alias __m128i = int4; 13 | 14 | // some used methods aliases 15 | alias _expect = llvm_expect; 16 | alias _mm_loadu_si128 = loadUnaligned!__m128i; 17 | alias _mm_cmpestri = __builtin_ia32_pcmpestri128; 18 | 19 | // These specify the type of data that we're comparing. 20 | enum _SIDD_UBYTE_OPS = 0x00; 21 | enum _SIDD_UWORD_OPS = 0x01; 22 | enum _SIDD_SBYTE_OPS = 0x02; 23 | enum _SIDD_SWORD_OPS = 0x03; 24 | 25 | // These specify the type of comparison operation. 26 | enum _SIDD_CMP_EQUAL_ANY = 0x00; 27 | enum _SIDD_CMP_RANGES = 0x04; 28 | enum _SIDD_CMP_EQUAL_EACH = 0x08; 29 | enum _SIDD_CMP_EQUAL_ORDERED = 0x0c; 30 | 31 | // These are used in _mm_cmpXstri() to specify the return. 32 | enum _SIDD_LEAST_SIGNIFICANT = 0x00; 33 | enum _SIDD_MOST_SIGNIFICANT = 0x40; 34 | 35 | // These macros are used in _mm_cmpXstri() to specify the return. 36 | enum _SIDD_BIT_MASK = 0x00; 37 | enum _SIDD_UNIT_MASK = 0x40; 38 | } 39 | else version(GNU) 40 | { 41 | import gcc.builtins: __builtin_expect, __builtin_clong; 42 | 43 | /// 44 | T _expect(T)(in T val, in T expected_val) if (__traits(isIntegral, T)) 45 | { 46 | static if (T.sizeof <= __builtin_clong.sizeof) 47 | return cast(T) __builtin_expect(val, expected_val); 48 | else 49 | return val; 50 | } 51 | 52 | enum LDC_with_SSE42 = false; 53 | } 54 | else 55 | { 56 | /// 57 | T _expect(T)(T val, T expected_val) if (__traits(isIntegral, T)) 58 | { 59 | return val; 60 | } 61 | 62 | enum LDC_with_SSE42 = false; 63 | } 64 | 65 | version (unittest) pragma(msg, "SSE: ", LDC_with_SSE42); 66 | 67 | // Workarounds for betterC 68 | version (D_BetterC) 69 | { 70 | pragma(mangle, "_D4core8lifetime16testEmplaceChunkFNaNbNiNfAvmmZv") 71 | nothrow @nogc @safe pure void testEmplaceChunk(void[] chunk, size_t typeSize, size_t typeAlignment) {} 72 | 73 | version (DigitalMars) 74 | { 75 | // see: https://issues.dlang.org/show_bug.cgi?id=19946 76 | extern (C) nothrow @nogc: 77 | 78 | short* _memset16(short *p, short value, size_t count) 79 | { 80 | short *pstart = p; 81 | short *ptop; 82 | 83 | for (ptop = &p[count]; p < ptop; p++) 84 | *p = value; 85 | return pstart; 86 | } 87 | 88 | int*_memset32(int *p, int value, size_t count) 89 | { 90 | version (D_InlineAsm_X86) 91 | { 92 | asm 93 | { 94 | mov EDI,p ; 95 | mov EAX,value ; 96 | mov ECX,count ; 97 | mov EDX,EDI ; 98 | rep ; 99 | stosd ; 100 | mov EAX,EDX ; 101 | } 102 | } 103 | else 104 | { 105 | int *pstart = p; 106 | int *ptop; 107 | 108 | for (ptop = &p[count]; p < ptop; p++) 109 | *p = value; 110 | return pstart; 111 | } 112 | } 113 | } 114 | 115 | version (assert) 116 | { 117 | version (LDC) 118 | { 119 | // See: https://github.com/ldc-developers/ldc/issues/2425 120 | // See: https://forum.dlang.org/post/heksucpdamkgwnztyitr@forum.dlang.org 121 | extern(C) 122 | nothrow @nogc 123 | void _d_array_slice_copy(void* dst, size_t dstlen, void* src, size_t srclen, size_t elemsz) 124 | { 125 | import ldc.intrinsics : llvm_memcpy; 126 | llvm_memcpy!size_t(dst, src, dstlen * elemsz, 0); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /source/bc/core/memory.d: -------------------------------------------------------------------------------- 1 | module bc.core.memory; 2 | 3 | import std.traits : hasIndirections, isDelegate, isFunctionPointer, isPointer, PointerTarget; 4 | public import core.memory : pureFree; 5 | 6 | version (D_Exceptions) 7 | { 8 | import core.exception : onOutOfMemoryError; 9 | private enum allocationFailed = `onOutOfMemoryError();`; 10 | } 11 | else 12 | { 13 | private enum allocationFailed = `assert(0, "Memory allocation failed");`; 14 | } 15 | 16 | /** 17 | * Allocates class or struct on the heap. 18 | * It automatically emplaces it to tyhe allocated memory with provided args. 19 | * On error, `onOutOfMemoryError` assert is called. 20 | * 21 | * When `gcRoot` is set, it also sets the memory range to be scanned by GC for pointers (off by default). 22 | */ 23 | auto heapAlloc(T, Args...) (Args args) @trusted 24 | if (is(T == struct)) 25 | { 26 | pragma(inline); 27 | import core.lifetime : emplace; 28 | 29 | // allocate memory for the object 30 | auto memory = enforceMalloc(T.sizeof); 31 | 32 | // call T's constructor and emplace instance on newly allocated memory 33 | return emplace!(T, Args)(memory[0..T.sizeof], args); 34 | } 35 | 36 | /** 37 | * Deallocates `heapAlloc` allocated memory. 38 | * It automatically calls the object destructor and removes it from GC scanning (no effect if not 39 | * added there) 40 | */ 41 | void heapDealloc(T)(ref T obj) @trusted 42 | if (isPointer!T && is(PointerTarget!T == struct)) 43 | { 44 | pragma(inline); 45 | import std.traits : hasElaborateDestructor; 46 | 47 | alias U = PointerTarget!T; 48 | static if (hasElaborateDestructor!(PointerTarget!T)) destroy(*obj); 49 | 50 | // free memory occupied by object 51 | pureFree(cast(void*)obj); 52 | obj = null; 53 | } 54 | 55 | @safe unittest 56 | { 57 | struct Foo 58 | { 59 | int i; 60 | this(int i) { this.i = i; } 61 | } 62 | 63 | Foo* f = heapAlloc!Foo(42); 64 | assert(f.i == 42); 65 | f.heapDealloc(); 66 | assert(f is null); 67 | } 68 | 69 | // NOTE: these are copy pasted from: https://github.com/dlang/phobos/blob/master/std/internal/memory.d 70 | 71 | /+ 72 | Mnemonic for `enforce!OutOfMemoryError(malloc(size))` that (unlike malloc) 73 | can be considered pure because it causes the program to abort if the result 74 | of the allocation is null, with the consequence that errno will not be 75 | visibly changed by calling this function. Note that `malloc` can also 76 | return `null` in non-failure situations if given an argument of 0. Hence, 77 | it is a programmer error to use this function if the requested allocation 78 | size is logically permitted to be zero. `enforceCalloc` and `enforceRealloc` 79 | work analogously. 80 | All these functions are usable in `betterC`. 81 | +/ 82 | void* enforceMalloc()(size_t size) @nogc nothrow pure @safe 83 | { 84 | auto result = fakePureMalloc(size); 85 | if (!result) mixin(allocationFailed); 86 | return result; 87 | } 88 | 89 | // ditto 90 | void* enforceCalloc()(size_t nmemb, size_t size) @nogc nothrow pure @safe 91 | { 92 | auto result = fakePureCalloc(nmemb, size); 93 | if (!result) mixin(allocationFailed); 94 | return result; 95 | } 96 | 97 | // ditto 98 | void* enforceRealloc()(void* ptr, size_t size) @nogc nothrow pure @system 99 | { 100 | auto result = fakePureRealloc(ptr, size); 101 | if (!result) mixin(allocationFailed); 102 | return result; 103 | } 104 | 105 | // Purified for local use only. 106 | extern (C) @nogc nothrow pure private 107 | { 108 | pragma(mangle, "malloc") void* fakePureMalloc(size_t) @safe; 109 | pragma(mangle, "calloc") void* fakePureCalloc(size_t nmemb, size_t size) @safe; 110 | pragma(mangle, "realloc") void* fakePureRealloc(void* ptr, size_t size) @system; 111 | } 112 | -------------------------------------------------------------------------------- /source/bc/core/system/backtrace.d: -------------------------------------------------------------------------------- 1 | module bc.core.system.backtrace; 2 | 3 | version (D_BetterC) {} 4 | else version (linux): 5 | 6 | /** 7 | * This struct us used to mimic private class in https://github.com/dlang/druntime/blob/master/src/core/runtime.d#L734 8 | * so we can access callstack. 9 | */ 10 | struct TraceInfo 11 | { 12 | // class fields 13 | void* _vtbl; 14 | void* _monitor; 15 | void* _interface; // introduced in DMD 2.071 16 | 17 | // make sure the ABI matches 18 | static assert ( 19 | {static interface I {} static class C: I {} return __traits(classInstanceSize, C);}() == (void*[3]).sizeof 20 | ); 21 | 22 | // actual fields we care about 23 | static enum MAXFRAMES = 128; 24 | int numframes; 25 | void*[MAXFRAMES] callstack; 26 | 27 | @property void*[] frames() return nothrow @trusted @nogc { 28 | return callstack.ptr[0 .. numframes]; 29 | } 30 | 31 | version (Posix) 32 | { 33 | /// Gather trace info from Throwable 34 | this(Throwable ex) nothrow @trusted @nogc pure 35 | { 36 | this(ex.info); 37 | } 38 | 39 | this(Throwable.TraceInfo ti) nothrow @trusted @nogc pure 40 | { 41 | if (ti !is null) 42 | { 43 | auto obj = cast(Object)ti; 44 | 45 | // this can change in druntime 46 | assert(typeid(obj).name == "core.runtime.DefaultTraceInfo", "Unexpected trace info type"); 47 | 48 | auto trace = cast(TraceInfo*)(cast(void*)obj); 49 | if (trace.numframes) 50 | { 51 | this.numframes = trace.numframes; 52 | this.callstack[0..numframes] = trace.callstack[0..numframes]; 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// Gets current trace info 59 | static TraceInfo current()() nothrow @trusted @nogc 60 | { 61 | version (Posix) 62 | { 63 | import bc.core.system.linux.execinfo : backtrace, thread_stackBottom; 64 | 65 | // just a copy from: https://github.com/dlang/druntime/blob/master/src/core/runtime.d#L742 66 | // again, cant't use directly as it's not @nogc 67 | 68 | // it may not be 1 but it is good enough to get 69 | // in CALL instruction address range for backtrace 70 | enum CALL_INSTRUCTION_SIZE = 1; 71 | 72 | TraceInfo ret; 73 | 74 | static if (__traits(compiles, backtrace((void**).init, int.init))) 75 | ret.numframes = backtrace(ret.callstack.ptr, MAXFRAMES); 76 | // Backtrace succeeded, adjust the frame to point to the caller 77 | if (ret.numframes >= 2) 78 | foreach (ref elem; ret.callstack) 79 | elem -= CALL_INSTRUCTION_SIZE; 80 | else // backtrace() failed, do it ourselves 81 | { 82 | static void** getBasePtr() nothrow @nogc 83 | { 84 | version (D_InlineAsm_X86) 85 | asm nothrow @nogc { naked; mov EAX, EBP; ret; } 86 | else 87 | version (D_InlineAsm_X86_64) 88 | asm nothrow @nogc { naked; mov RAX, RBP; ret; } 89 | else 90 | return null; 91 | } 92 | 93 | auto stackTop = getBasePtr(); 94 | auto stackBottom = cast(void**)thread_stackBottom(); 95 | void* dummy; 96 | 97 | if (stackTop && &dummy < stackTop && stackTop < stackBottom) 98 | { 99 | auto stackPtr = stackTop; 100 | 101 | for (ret.numframes = 0; stackTop <= stackPtr && stackPtr < stackBottom && ret.numframes < MAXFRAMES; ) 102 | { 103 | ret.callstack[ret.numframes++] = *(stackPtr + 1) - CALL_INSTRUCTION_SIZE; 104 | stackPtr = cast(void**) *stackPtr; 105 | } 106 | } 107 | } 108 | } 109 | else static assert(0, "Unsupported platform"); 110 | 111 | if (ret.numframes > 1) 112 | { 113 | // drop first frame as it points to this method 114 | import std.algorithm : copy; 115 | ret.numframes--; 116 | ret.callstack[1..ret.numframes+1].copy(ret.callstack[0..ret.numframes]); 117 | } 118 | 119 | return ret; 120 | } 121 | 122 | /// Dumps trace info to the provided sink. 123 | /// Returns: size of written data 124 | size_t dumpTo(S)(ref S sink) nothrow @nogc @trusted // TODO: well.. 125 | { 126 | if (numframes) 127 | { 128 | version (Posix) 129 | { 130 | import bc.core.system.linux.dwarf : dumpCallstack, getFirstFrame; 131 | import bc.core.system.linux.elf : Image; 132 | import bc.core.system.linux.execinfo : backtrace_symbols; 133 | import core.sys.posix.stdlib : free; 134 | 135 | const char** frameList = () @trusted { return backtrace_symbols(&callstack[0], cast(int) numframes); }(); 136 | scope(exit) () @trusted { free(cast(void*)frameList); }(); 137 | 138 | auto first = getFirstFrame(callstack[0..numframes], frameList); 139 | version (LDC) {} 140 | else { 141 | static if (__VERSION__ < 2092) enum FIRSTFRAME = 4; 142 | else static if (__VERSION__ < 2096) enum FIRSTFRAME = 5; 143 | else enum FIRSTFRAME = 0; 144 | 145 | // getFirstFrame searches for throw in stack, that is not always the case (ie when printing out just the current stack) 146 | if (!first) first = FIRSTFRAME; 147 | } 148 | 149 | auto image = Image.openSelf(); 150 | if (image.isValid) 151 | return image.processDebugLineSectionData(sink, callstack[first..numframes], &frameList[first], &dumpCallstack!S); 152 | 153 | return dumpCallstack(sink, image, callstack[first..numframes], &frameList[first], null); 154 | } 155 | else static assert(0, "Unsupported platform"); 156 | } 157 | return 0; 158 | } 159 | } 160 | 161 | version (Posix) 162 | { 163 | // get current callstack 164 | unittest 165 | { 166 | import bc.string.string : String; 167 | 168 | auto ti = TraceInfo.current(); 169 | String buf, buf2; 170 | immutable ret = ti.dumpTo(buf); 171 | assert(ret == buf.length); 172 | 173 | // import std.stdio : writeln; 174 | // writeln(cast(const(char)[])buf[]); 175 | } 176 | 177 | // get callstack from defaultTraceHandler 178 | unittest 179 | { 180 | import bc.string.string : String; 181 | import core.runtime : defaultTraceHandler; 182 | 183 | auto dti = defaultTraceHandler(); 184 | assert(dti !is null); 185 | 186 | String buf, buf2; 187 | auto ti = TraceInfo(dti); 188 | immutable ret = ti.dumpTo(buf); 189 | assert(ret == buf.length); 190 | 191 | foreach (ln; dti) { buf2 ~= ln; buf2 ~= '\n'; } 192 | 193 | // import std.stdio : writeln; 194 | // writeln("-----------------"); 195 | // writeln("our: ", cast(const(char)[])buf[]); 196 | // writeln("-----------------"); 197 | // writeln("orig: ", cast(const(char)[])buf2[]); 198 | // writeln("-----------------"); 199 | static if (__VERSION__ >= 2095) 200 | { 201 | // we try to reflect last compiler behavior, previous might differ 202 | assert(buf[] == buf2[0..$-1]); 203 | } 204 | else 205 | { 206 | import std.algorithm : countUntil; 207 | immutable ln = buf[].countUntil('\n'); 208 | assert(ln>0); 209 | assert(buf[0..ln] == buf2[0..ln]); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /source/bc/core/system/linux/dwarf.d: -------------------------------------------------------------------------------- 1 | module bc.core.system.linux.dwarf; 2 | 3 | version (D_BetterC) {} 4 | else version (linux): 5 | 6 | import bc.core.system.linux.elf : Image; 7 | import core.exception : onOutOfMemoryErrorNoGC; 8 | import core.internal.traits : dtorIsNothrow; 9 | import core.stdc.stdio : snprintf; 10 | import core.stdc.string : strlen; 11 | 12 | // Selective copy from normally unavailable: https://github.com/dlang/druntime/blob/master/src/rt/backtrace/dwarf.d 13 | // Also combined (and noted) with some only here used module parts 14 | 15 | size_t getFirstFrame(const(void*)[] callstack, const char** frameList) nothrow @nogc 16 | { 17 | import core.internal.execinfo : getMangledSymbolName; 18 | 19 | version (LDC) enum BaseExceptionFunctionName = "_d_throw_exception"; 20 | else enum BaseExceptionFunctionName = "_d_throwdwarf"; 21 | 22 | foreach (i; 0..callstack.length) 23 | { 24 | auto proc = getMangledSymbolName(frameList[i][0 .. strlen(frameList[i])]); 25 | if (proc == BaseExceptionFunctionName) return i+1; 26 | } 27 | return 0; 28 | } 29 | 30 | /// Our customized @nogc variant of https://github.com/dlang/druntime/blob/master/src/rt/backtrace/dwarf.d#L94 31 | size_t dumpCallstack(S)(ref S sink, ref Image image, const(void*)[] callstack, const char** frameList, 32 | const(ubyte)[] debugLineSectionData) nothrow @nogc 33 | { 34 | // find address -> file, line mapping using dwarf debug_line 35 | Array!Location locations; 36 | if (debugLineSectionData) 37 | { 38 | locations.length = callstack.length; 39 | foreach (size_t i; 0 .. callstack.length) 40 | locations[i].address = cast(size_t) callstack[i]; 41 | 42 | resolveAddresses(debugLineSectionData, locations[], image.baseAddress); 43 | } 44 | 45 | size_t ret = 0; 46 | foreach (size_t i; 0 .. callstack.length) 47 | { 48 | char[1536] buffer = void; 49 | size_t bufferLength = 0; 50 | 51 | void appendToBuffer(Args...)(const(char)* format, Args args) 52 | { 53 | const count = snprintf(buffer.ptr + bufferLength, buffer.length - bufferLength, format, args); 54 | assert(count >= 0); 55 | bufferLength += count; 56 | if (bufferLength >= buffer.length) 57 | bufferLength = buffer.length - 1; 58 | } 59 | 60 | if (i) { sink.put('\n'); ret++; } 61 | 62 | if (locations.length > 0 && locations[i].line != -1) 63 | { 64 | bool includeSlash = locations[i].directory.length > 0 && locations[i].directory[$ - 1] != '/'; 65 | if (locations[i].line) 66 | { 67 | string printFormat = includeSlash ? "%.*s/%.*s:%d " : "%.*s%.*s:%d "; 68 | appendToBuffer( 69 | printFormat.ptr, 70 | cast(int) locations[i].directory.length, locations[i].directory.ptr, 71 | cast(int) locations[i].file.length, locations[i].file.ptr, 72 | locations[i].line, 73 | ); 74 | } 75 | else 76 | { 77 | string printFormat = includeSlash ? "%.*s/%.*s " : "%.*s%.*s "; 78 | appendToBuffer( 79 | printFormat.ptr, 80 | cast(int) locations[i].directory.length, locations[i].directory.ptr, 81 | cast(int) locations[i].file.length, locations[i].file.ptr, 82 | ); 83 | } 84 | } 85 | else 86 | { 87 | buffer[0 .. 5] = "??:? "; 88 | bufferLength = 5; 89 | } 90 | 91 | char[1024] symbolBuffer = void; 92 | auto symbol = getDemangledSymbol(frameList[i][0 .. strlen(frameList[i])], symbolBuffer); 93 | if (symbol.length > 0) 94 | appendToBuffer("%.*s ", cast(int) symbol.length, symbol.ptr); 95 | 96 | const addressLength = 20; 97 | const maxBufferLength = buffer.length - addressLength; 98 | if (bufferLength > maxBufferLength) 99 | { 100 | buffer[maxBufferLength-4 .. maxBufferLength] = "... "; 101 | bufferLength = maxBufferLength; 102 | } 103 | appendToBuffer("[0x%zx]", callstack[i]); 104 | 105 | auto output = buffer[0 .. bufferLength]; 106 | sink.put(output); 107 | ret += bufferLength; 108 | if (symbol == "_Dmain") break; 109 | } 110 | 111 | return ret; 112 | } 113 | 114 | // Copy: https://github.com/dlang/druntime/blob/master/src/rt/backtrace/dwarf.d#L172 115 | // the lifetime of the Location data is bound to the lifetime of debugLineSectionData 116 | void resolveAddresses(const(ubyte)[] debugLineSectionData, Location[] locations, size_t baseAddress) @nogc nothrow 117 | { 118 | debug(DwarfDebugMachine) import core.stdc.stdio; 119 | 120 | size_t numberOfLocationsFound = 0; 121 | 122 | const(ubyte)[] dbg = debugLineSectionData; 123 | while (dbg.length > 0) 124 | { 125 | debug(DwarfDebugMachine) printf("new debug program\n"); 126 | const lp = readLineNumberProgram(dbg); 127 | 128 | LocationInfo lastLoc = LocationInfo(-1, -1); 129 | size_t lastAddress = 0x0; 130 | 131 | debug(DwarfDebugMachine) printf("program:\n"); 132 | runStateMachine(lp, 133 | (size_t address, LocationInfo locInfo, bool isEndSequence) 134 | { 135 | // adjust to ASLR offset 136 | address += baseAddress; 137 | debug (DwarfDebugMachine) 138 | printf("-- offsetting 0x%zx to 0x%zx\n", address - baseAddress, address); 139 | 140 | foreach (ref loc; locations) 141 | { 142 | // If loc.line != -1, then it has been set previously. 143 | // Some implementations (eg. dmd) write an address to 144 | // the debug data multiple times, but so far I have found 145 | // that the first occurrence to be the correct one. 146 | if (loc.line != -1) 147 | continue; 148 | 149 | // Can be called with either `locInfo` or `lastLoc` 150 | void update(const ref LocationInfo match) 151 | { 152 | const sourceFile = lp.sourceFiles[match.file - 1]; 153 | debug (DwarfDebugMachine) 154 | { 155 | printf("-- found for [0x%zx]:\n", loc.address); 156 | printf("-- file: %.*s\n", 157 | cast(int) sourceFile.file.length, sourceFile.file.ptr); 158 | printf("-- line: %d\n", match.line); 159 | } 160 | // DMD emits entries with FQN, but other implmentations 161 | // (e.g. LDC) make use of directories 162 | // See https://github.com/dlang/druntime/pull/2945 163 | if (sourceFile.dirIndex != 0) 164 | loc.directory = lp.includeDirectories[sourceFile.dirIndex - 1]; 165 | 166 | loc.file = sourceFile.file; 167 | loc.line = match.line; 168 | numberOfLocationsFound++; 169 | } 170 | 171 | // The state machine will not contain an entry for each 172 | // address, as consecutive addresses with the same file/line 173 | // are merged together to save on space, so we need to 174 | // check if our address is within two addresses we get 175 | // called with. 176 | // 177 | // Specs (DWARF v4, Section 6.2, PDF p.109) says: 178 | // "We shrink it with two techniques. First, we delete from 179 | // the matrix each row whose file, line, source column and 180 | // discriminator information is identical with that of its 181 | // predecessors. 182 | if (loc.address == address) 183 | update(locInfo); 184 | else if (lastAddress && 185 | loc.address > lastAddress && loc.address < address) 186 | update(lastLoc); 187 | } 188 | 189 | if (isEndSequence) 190 | { 191 | lastAddress = 0; 192 | } 193 | else 194 | { 195 | lastAddress = address; 196 | lastLoc = locInfo; 197 | } 198 | 199 | return numberOfLocationsFound < locations.length; 200 | } 201 | ); 202 | 203 | if (numberOfLocationsFound == locations.length) return; 204 | } 205 | } 206 | 207 | const(char)[] getDemangledSymbol(const(char)[] btSymbol, return ref char[1024] buffer) nothrow @nogc 208 | { 209 | //import core.demangle; // isn't @nogc :( 210 | import bc.core.demangle : demangle; 211 | import core.internal.execinfo : getMangledSymbolName; 212 | 213 | const mangledName = getMangledSymbolName(btSymbol); 214 | return !mangledName.length ? buffer[0..0] : demangle(mangledName, buffer[]); 215 | // return mangledName; 216 | } 217 | 218 | struct LineNumberProgram 219 | { 220 | ulong unitLength; 221 | ushort dwarfVersion; 222 | ulong headerLength; 223 | ubyte minimumInstructionLength; 224 | ubyte maximumOperationsPerInstruction; 225 | bool defaultIsStatement; 226 | byte lineBase; 227 | ubyte lineRange; 228 | ubyte opcodeBase; 229 | const(ubyte)[] standardOpcodeLengths; 230 | Array!(const(char)[]) includeDirectories; 231 | Array!SourceFile sourceFiles; 232 | const(ubyte)[] program; 233 | } 234 | 235 | struct SourceFile 236 | { 237 | const(char)[] file; 238 | size_t dirIndex; 239 | } 240 | 241 | struct LocationInfo 242 | { 243 | int file; 244 | int line; 245 | } 246 | 247 | LineNumberProgram readLineNumberProgram(ref const(ubyte)[] data) @nogc nothrow 248 | { 249 | // import core.stdc.stdio : printf; 250 | // printf("!my readLineNumberProgram: "); 251 | // foreach (b; data[0..data.length > 256 ? 256 : $]) printf("%02X", b); 252 | // printf("\n"); 253 | 254 | const originalData = data; 255 | 256 | LineNumberProgram lp; 257 | 258 | bool is64bitDwarf = false; 259 | lp.unitLength = data.read!uint(); 260 | if (lp.unitLength == uint.max) 261 | { 262 | is64bitDwarf = true; 263 | lp.unitLength = data.read!ulong(); 264 | } 265 | 266 | const dwarfVersionFieldOffset = cast(size_t) (data.ptr - originalData.ptr); 267 | lp.dwarfVersion = data.read!ushort(); 268 | debug(DwarfDebugMachine) printf("DWARF version: %d\n", lp.dwarfVersion); 269 | assert(lp.dwarfVersion < 5, "DWARF v5+ not supported yet"); 270 | 271 | lp.headerLength = (is64bitDwarf ? data.read!ulong() : data.read!uint()); 272 | 273 | const minimumInstructionLengthFieldOffset = cast(size_t) (data.ptr - originalData.ptr); 274 | lp.minimumInstructionLength = data.read!ubyte(); 275 | 276 | lp.maximumOperationsPerInstruction = (lp.dwarfVersion >= 4 ? data.read!ubyte() : 1); 277 | lp.defaultIsStatement = (data.read!ubyte() != 0); 278 | lp.lineBase = data.read!byte(); 279 | lp.lineRange = data.read!ubyte(); 280 | lp.opcodeBase = data.read!ubyte(); 281 | 282 | lp.standardOpcodeLengths = data[0 .. lp.opcodeBase - 1]; 283 | data = data[lp.opcodeBase - 1 .. $]; 284 | 285 | // A sequence ends with a null-byte. 286 | static auto readSequence(alias ReadEntry)(ref const(ubyte)[] data) 287 | { 288 | alias ResultType = typeof(ReadEntry(data)); 289 | 290 | static size_t count(const(ubyte)[] data) 291 | { 292 | size_t count = 0; 293 | while (data.length && data[0] != 0) 294 | { 295 | ReadEntry(data); 296 | ++count; 297 | } 298 | return count; 299 | } 300 | 301 | const numEntries = count(data); 302 | 303 | Array!ResultType result; 304 | result.length = numEntries; 305 | 306 | foreach (i; 0 .. numEntries) 307 | result[i] = ReadEntry(data); 308 | 309 | data = data[1 .. $]; // skip over sequence-terminating null 310 | 311 | return result; 312 | } 313 | 314 | static const(char)[] readIncludeDirectoryEntry(ref const(ubyte)[] data) 315 | { 316 | const length = strlen(cast(char*) data.ptr); 317 | auto result = cast(const(char)[]) data[0 .. length]; 318 | debug(DwarfDebugMachine) printf("dir: %.*s\n", cast(int) length, result.ptr); 319 | data = data[length + 1 .. $]; 320 | return result; 321 | } 322 | lp.includeDirectories = readSequence!readIncludeDirectoryEntry(data); 323 | 324 | static SourceFile readFileNameEntry(ref const(ubyte)[] data) 325 | { 326 | const length = strlen(cast(char*) data.ptr); 327 | auto file = cast(const(char)[]) data[0 .. length]; 328 | debug(DwarfDebugMachine) printf("file: %.*s\n", cast(int) length, file.ptr); 329 | data = data[length + 1 .. $]; 330 | 331 | auto dirIndex = cast(size_t) data.readULEB128(); 332 | 333 | data.readULEB128(); // last mod 334 | data.readULEB128(); // file len 335 | 336 | return SourceFile( 337 | file, 338 | dirIndex, 339 | ); 340 | } 341 | lp.sourceFiles = readSequence!readFileNameEntry(data); 342 | 343 | const programStart = cast(size_t) (minimumInstructionLengthFieldOffset + lp.headerLength); 344 | const programEnd = cast(size_t) (dwarfVersionFieldOffset + lp.unitLength); 345 | lp.program = originalData[programStart .. programEnd]; 346 | 347 | data = originalData[programEnd .. $]; 348 | 349 | return lp; 350 | } 351 | 352 | T read(T)(ref const(ubyte)[] buffer) @nogc nothrow 353 | { 354 | version (X86) enum hasUnalignedLoads = true; 355 | else version (X86_64) enum hasUnalignedLoads = true; 356 | else enum hasUnalignedLoads = false; 357 | 358 | static if (hasUnalignedLoads || T.alignof == 1) 359 | { 360 | T result = *(cast(T*) buffer.ptr); 361 | } 362 | else 363 | { 364 | import core.stdc.string : memcpy; 365 | T result = void; 366 | memcpy(&result, buffer.ptr, T.sizeof); 367 | } 368 | 369 | buffer = buffer[T.sizeof .. $]; 370 | return result; 371 | } 372 | 373 | ulong readULEB128(ref const(ubyte)[] buffer) @nogc nothrow 374 | { 375 | ulong val = 0; 376 | uint shift = 0; 377 | 378 | while (true) 379 | { 380 | ubyte b = buffer.read!ubyte(); 381 | 382 | val |= (b & 0x7f) << shift; 383 | if ((b & 0x80) == 0) break; 384 | shift += 7; 385 | } 386 | 387 | return val; 388 | } 389 | 390 | long readSLEB128(ref const(ubyte)[] buffer) @nogc nothrow 391 | { 392 | long val = 0; 393 | uint shift = 0; 394 | int size = 8 << 3; 395 | ubyte b; 396 | 397 | while (true) 398 | { 399 | b = buffer.read!ubyte(); 400 | val |= (b & 0x7f) << shift; 401 | shift += 7; 402 | if ((b & 0x80) == 0) 403 | break; 404 | } 405 | 406 | if (shift < size && (b & 0x40) != 0) 407 | val |= -(1 << shift); 408 | 409 | return val; 410 | } 411 | 412 | alias RunStateMachineCallback = 413 | bool delegate(size_t address, LocationInfo info, bool isEndSequence) 414 | @nogc nothrow; 415 | 416 | enum StandardOpcode : ubyte 417 | { 418 | extendedOp = 0, 419 | copy = 1, 420 | advancePC = 2, 421 | advanceLine = 3, 422 | setFile = 4, 423 | setColumn = 5, 424 | negateStatement = 6, 425 | setBasicBlock = 7, 426 | constAddPC = 8, 427 | fixedAdvancePC = 9, 428 | setPrologueEnd = 10, 429 | setEpilogueBegin = 11, 430 | setISA = 12, 431 | } 432 | 433 | enum ExtendedOpcode : ubyte 434 | { 435 | endSequence = 1, 436 | setAddress = 2, 437 | defineFile = 3, 438 | setDiscriminator = 4, 439 | } 440 | 441 | struct StateMachine 442 | { 443 | size_t address = 0; 444 | uint operationIndex = 0; 445 | uint fileIndex = 1; 446 | uint line = 1; 447 | uint column = 0; 448 | uint isa = 0; 449 | uint discriminator = 0; 450 | bool isStatement; 451 | bool isBasicBlock = false; 452 | bool isEndSequence = false; 453 | bool isPrologueEnd = false; 454 | bool isEpilogueBegin = false; 455 | } 456 | 457 | bool runStateMachine(ref const(LineNumberProgram) lp, scope RunStateMachineCallback callback) @nogc nothrow 458 | { 459 | StateMachine machine; 460 | machine.isStatement = lp.defaultIsStatement; 461 | 462 | const(ubyte)[] program = lp.program; 463 | while (program.length > 0) 464 | { 465 | size_t advanceAddressAndOpIndex(size_t operationAdvance) 466 | { 467 | const addressIncrement = lp.minimumInstructionLength * ((machine.operationIndex + operationAdvance) / lp.maximumOperationsPerInstruction); 468 | machine.address += addressIncrement; 469 | machine.operationIndex = (machine.operationIndex + operationAdvance) % lp.maximumOperationsPerInstruction; 470 | return addressIncrement; 471 | } 472 | 473 | ubyte opcode = program.read!ubyte(); 474 | if (opcode < lp.opcodeBase) 475 | { 476 | switch (opcode) with (StandardOpcode) 477 | { 478 | case extendedOp: 479 | size_t len = cast(size_t) program.readULEB128(); 480 | ubyte eopcode = program.read!ubyte(); 481 | 482 | switch (eopcode) with (ExtendedOpcode) 483 | { 484 | case endSequence: 485 | machine.isEndSequence = true; 486 | debug(DwarfDebugMachine) printf("endSequence 0x%zx\n", machine.address); 487 | if (!callback(machine.address, LocationInfo(machine.fileIndex, machine.line), true)) return true; 488 | machine = StateMachine.init; 489 | machine.isStatement = lp.defaultIsStatement; 490 | break; 491 | 492 | case setAddress: 493 | size_t address = program.read!size_t(); 494 | debug(DwarfDebugMachine) printf("setAddress 0x%zx\n", address); 495 | machine.address = address; 496 | machine.operationIndex = 0; 497 | break; 498 | 499 | case defineFile: // TODO: add proper implementation 500 | debug(DwarfDebugMachine) printf("defineFile\n"); 501 | program = program[len - 1 .. $]; 502 | break; 503 | 504 | case setDiscriminator: 505 | const discriminator = cast(uint) program.readULEB128(); 506 | debug(DwarfDebugMachine) printf("setDiscriminator %d\n", discriminator); 507 | machine.discriminator = discriminator; 508 | break; 509 | 510 | default: 511 | // unknown opcode 512 | debug(DwarfDebugMachine) printf("unknown extended opcode %d\n", cast(int) eopcode); 513 | program = program[len - 1 .. $]; 514 | break; 515 | } 516 | 517 | break; 518 | 519 | case copy: 520 | debug(DwarfDebugMachine) printf("copy 0x%zx\n", machine.address); 521 | if (!callback(machine.address, LocationInfo(machine.fileIndex, machine.line), false)) return true; 522 | machine.isBasicBlock = false; 523 | machine.isPrologueEnd = false; 524 | machine.isEpilogueBegin = false; 525 | machine.discriminator = 0; 526 | break; 527 | 528 | case advancePC: 529 | const operationAdvance = cast(size_t) readULEB128(program); 530 | advanceAddressAndOpIndex(operationAdvance); 531 | debug(DwarfDebugMachine) printf("advancePC %d to 0x%zx\n", cast(int) operationAdvance, machine.address); 532 | break; 533 | 534 | case advanceLine: 535 | long ad = readSLEB128(program); 536 | machine.line += ad; 537 | debug(DwarfDebugMachine) printf("advanceLine %d to %d\n", cast(int) ad, cast(int) machine.line); 538 | break; 539 | 540 | case setFile: 541 | uint index = cast(uint) readULEB128(program); 542 | debug(DwarfDebugMachine) printf("setFile to %d\n", cast(int) index); 543 | machine.fileIndex = index; 544 | break; 545 | 546 | case setColumn: 547 | uint col = cast(uint) readULEB128(program); 548 | debug(DwarfDebugMachine) printf("setColumn %d\n", cast(int) col); 549 | machine.column = col; 550 | break; 551 | 552 | case negateStatement: 553 | debug(DwarfDebugMachine) printf("negateStatement\n"); 554 | machine.isStatement = !machine.isStatement; 555 | break; 556 | 557 | case setBasicBlock: 558 | debug(DwarfDebugMachine) printf("setBasicBlock\n"); 559 | machine.isBasicBlock = true; 560 | break; 561 | 562 | case constAddPC: 563 | const operationAdvance = (255 - lp.opcodeBase) / lp.lineRange; 564 | advanceAddressAndOpIndex(operationAdvance); 565 | debug(DwarfDebugMachine) printf("constAddPC 0x%zx\n", machine.address); 566 | break; 567 | 568 | case fixedAdvancePC: 569 | const add = program.read!ushort(); 570 | machine.address += add; 571 | machine.operationIndex = 0; 572 | debug(DwarfDebugMachine) printf("fixedAdvancePC %d to 0x%zx\n", cast(int) add, machine.address); 573 | break; 574 | 575 | case setPrologueEnd: 576 | machine.isPrologueEnd = true; 577 | debug(DwarfDebugMachine) printf("setPrologueEnd\n"); 578 | break; 579 | 580 | case setEpilogueBegin: 581 | machine.isEpilogueBegin = true; 582 | debug(DwarfDebugMachine) printf("setEpilogueBegin\n"); 583 | break; 584 | 585 | case setISA: 586 | machine.isa = cast(uint) readULEB128(program); 587 | debug(DwarfDebugMachine) printf("setISA %d\n", cast(int) machine.isa); 588 | break; 589 | 590 | default: 591 | debug(DwarfDebugMachine) printf("unknown opcode %d\n", cast(int) opcode); 592 | return false; 593 | } 594 | } 595 | else 596 | { 597 | opcode -= lp.opcodeBase; 598 | const operationAdvance = opcode / lp.lineRange; 599 | const addressIncrement = advanceAddressAndOpIndex(operationAdvance); 600 | const lineIncrement = lp.lineBase + (opcode % lp.lineRange); 601 | machine.line += lineIncrement; 602 | 603 | debug (DwarfDebugMachine) 604 | printf("special %d %d to 0x%zx line %d\n", cast(int) addressIncrement, 605 | cast(int) lineIncrement, machine.address, machine.line); 606 | 607 | if (!callback(machine.address, LocationInfo(machine.fileIndex, machine.line), false)) return true; 608 | 609 | machine.isBasicBlock = false; 610 | machine.isPrologueEnd = false; 611 | machine.isEpilogueBegin = false; 612 | machine.discriminator = 0; 613 | } 614 | } 615 | 616 | return true; 617 | } 618 | 619 | struct Location 620 | { 621 | const(char)[] file = null; 622 | const(char)[] directory = null; 623 | int line = -1; 624 | size_t address; 625 | } 626 | 627 | // See: module rt.util.container.array; 628 | struct Array(T) 629 | { 630 | nothrow: 631 | @disable this(this); 632 | 633 | ~this() 634 | { 635 | reset(); 636 | } 637 | 638 | void reset() 639 | { 640 | length = 0; 641 | } 642 | 643 | @property size_t length() const 644 | { 645 | return _length; 646 | } 647 | 648 | @property void length(size_t nlength) 649 | { 650 | import core.checkedint : mulu; 651 | 652 | bool overflow = false; 653 | size_t reqsize = mulu(T.sizeof, nlength, overflow); 654 | if (!overflow) 655 | { 656 | if (nlength < _length) 657 | foreach (ref val; _ptr[nlength .. _length]) .destroy(val); 658 | _ptr = cast(T*).xrealloc(_ptr, reqsize); 659 | if (nlength > _length) 660 | foreach (ref val; _ptr[_length .. nlength]) .initialize(val); 661 | _length = nlength; 662 | } 663 | else 664 | onOutOfMemoryErrorNoGC(); 665 | 666 | } 667 | 668 | @property bool empty() const 669 | { 670 | return !length; 671 | } 672 | 673 | @property ref inout(T) front() inout 674 | in { assert(!empty); } 675 | do 676 | { 677 | return _ptr[0]; 678 | } 679 | 680 | @property ref inout(T) back() inout 681 | in { assert(!empty); } 682 | do 683 | { 684 | return _ptr[_length - 1]; 685 | } 686 | 687 | ref inout(T) opIndex(size_t idx) inout 688 | in { assert(idx < length); } 689 | do 690 | { 691 | return _ptr[idx]; 692 | } 693 | 694 | inout(T)[] opSlice() inout 695 | { 696 | return _ptr[0 .. _length]; 697 | } 698 | 699 | inout(T)[] opSlice(size_t a, size_t b) inout 700 | in { assert(a < b && b <= length); } 701 | do 702 | { 703 | return _ptr[a .. b]; 704 | } 705 | 706 | alias length opDollar; 707 | 708 | void insertBack()(auto ref T val) 709 | { 710 | import core.checkedint : addu; 711 | 712 | bool overflow = false; 713 | size_t newlength = addu(length, 1, overflow); 714 | if (!overflow) 715 | { 716 | length = newlength; 717 | back = val; 718 | } 719 | else 720 | onOutOfMemoryErrorNoGC(); 721 | } 722 | 723 | void popBack() 724 | { 725 | length = length - 1; 726 | } 727 | 728 | void remove(size_t idx) 729 | in { assert(idx < length); } 730 | do 731 | { 732 | foreach (i; idx .. length - 1) 733 | _ptr[i] = _ptr[i+1]; 734 | popBack(); 735 | } 736 | 737 | void swap(ref Array other) 738 | { 739 | auto ptr = _ptr; 740 | _ptr = other._ptr; 741 | other._ptr = ptr; 742 | immutable len = _length; 743 | _length = other._length; 744 | other._length = len; 745 | } 746 | 747 | invariant 748 | { 749 | assert(!_ptr == !_length); 750 | } 751 | 752 | private: 753 | T* _ptr; 754 | size_t _length; 755 | } 756 | 757 | import core.stdc.stdlib : malloc, realloc, free; 758 | 759 | // See: rt.util.container.common 760 | void* xrealloc(void* ptr, size_t sz) nothrow @nogc 761 | { 762 | import core.exception; 763 | 764 | if (!sz) { .free(ptr); return null; } 765 | if (auto nptr = .realloc(ptr, sz)) return nptr; 766 | .free(ptr); onOutOfMemoryErrorNoGC(); 767 | assert(0); 768 | } 769 | 770 | void destroy(T)(ref T t) if (is(T == struct) && dtorIsNothrow!T) 771 | { 772 | scope (failure) assert(0); // nothrow hack 773 | object.destroy(t); 774 | } 775 | 776 | void destroy(T)(ref T t) if (!is(T == struct)) 777 | { 778 | t = T.init; 779 | } 780 | 781 | void initialize(T)(ref T t) if (is(T == struct)) 782 | { 783 | import core.stdc.string; 784 | static if (__traits(isPOD, T)) // implies !hasElaborateAssign!T && !hasElaborateDestructor!T 785 | t = T.init; 786 | else static if (__traits(isZeroInit, T)) 787 | memset(&t, 0, T.sizeof); 788 | else 789 | memcpy(&t, typeid(T).initializer().ptr, T.sizeof); 790 | } 791 | 792 | void initialize(T)(ref T t) if (!is(T == struct)) 793 | { 794 | t = T.init; 795 | } 796 | -------------------------------------------------------------------------------- /source/bc/core/system/linux/elf.d: -------------------------------------------------------------------------------- 1 | module bc.core.system.linux.elf; 2 | 3 | version (D_BetterC) {} 4 | else: 5 | 6 | // Copy from normally unavailable https://github.com/dlang/druntime/blob/master/src/rt/backtrace/elf.d 7 | 8 | version (linux) 9 | { 10 | import core.sys.linux.elf; 11 | version = LinuxOrBSD; 12 | } 13 | else version (FreeBSD) 14 | { 15 | import core.sys.freebsd.sys.elf; 16 | version = LinuxOrBSD; 17 | } 18 | else version (DragonFlyBSD) 19 | { 20 | import core.sys.dragonflybsd.sys.elf; 21 | version = LinuxOrBSD; 22 | } 23 | 24 | version (LinuxOrBSD): 25 | 26 | import core.internal.elf.dl; 27 | import core.internal.elf.io; 28 | 29 | struct Image 30 | { 31 | private ElfFile file; 32 | 33 | nothrow @nogc: 34 | 35 | static Image openSelf() 36 | { 37 | // see: https://github.com/dlang/druntime/commit/e3c8f38141cd148e92bc949fd88a3e50d0054807 38 | // const(char)* selfPath = SharedObject.thisExecutable().name().ptr; 39 | const(char)* selfPath; 40 | foreach (object; SharedObjects) 41 | { 42 | // the first object is the main binary 43 | selfPath = object.name().ptr; 44 | break; 45 | } 46 | 47 | Image image; 48 | if (!ElfFile.open(selfPath, image.file)) 49 | image.file = ElfFile.init; 50 | 51 | return image; 52 | } 53 | 54 | @property bool isValid() 55 | { 56 | return file != ElfFile.init; 57 | } 58 | 59 | T processDebugLineSectionData(S, T)(ref S sink, const(void*)[] callstack, const char** frameList, 60 | scope T function(ref S, ref Image, const(void*)[], const char**, const(ubyte)[]) nothrow @nogc processor) 61 | { 62 | ElfSectionHeader dbgSectionHeader; 63 | ElfSection dbgSection; 64 | 65 | if (file.findSectionHeaderByName(".debug_line", dbgSectionHeader)) 66 | { 67 | // we don't support compressed debug sections 68 | if (!(dbgSectionHeader.shdr.sh_flags & SHF_COMPRESSED)) 69 | dbgSection = ElfSection(file, dbgSectionHeader); 70 | } 71 | 72 | return processor(sink, this, callstack, frameList, cast(const(ubyte)[])dbgSection.data()); 73 | } 74 | 75 | @property size_t baseAddress() 76 | { 77 | // the DWARF addresses for DSOs are relative 78 | const isDynamicSharedObject = (file.ehdr.e_type == ET_DYN); 79 | if (!isDynamicSharedObject) 80 | return 0; 81 | 82 | // see: https://github.com/dlang/druntime/commit/e3c8f38141cd148e92bc949fd88a3e50d0054807 83 | // return cast(size_t) SharedObject.thisExecutable().baseAddress; 84 | 85 | size_t base = 0; 86 | foreach (object; SharedObjects) 87 | { 88 | // only take the first address as this will be the main binary 89 | base = cast(size_t) object.baseAddress; 90 | break; 91 | } 92 | 93 | return base; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /source/bc/core/system/linux/execinfo.d: -------------------------------------------------------------------------------- 1 | module bc.core.system.linux.execinfo; 2 | 3 | // Copy from (missing @nogc): https://github.com/dlang/druntime/blob/master/src/core/sys/linux/execinfo.d 4 | 5 | version(linux): 6 | 7 | extern (C): 8 | nothrow: 9 | @system: 10 | @nogc: // missing in druntime - see https://github.com/dlang/druntime/pull/3317 11 | 12 | int backtrace(void** buffer, int size); 13 | char** backtrace_symbols(const(void*)* buffer, int size); 14 | void backtrace_symbols_fd(const(void*)* buffer, int size, int fd); 15 | 16 | // added from https://github.com/dlang/druntime/blob/master/src/core/runtime.d#L101 17 | void* thread_stackBottom(); 18 | -------------------------------------------------------------------------------- /source/bc/core/traits.d: -------------------------------------------------------------------------------- 1 | module bc.core.traits; 2 | 3 | /** 4 | * Match types like `std.typecons.Nullable` ie `mir.core.Nullable` 5 | */ 6 | template isStdNullable(T) 7 | { 8 | import std.traits : hasMember; 9 | 10 | T* aggregate; 11 | 12 | enum bool isStdNullable = 13 | hasMember!(T, "isNull") && 14 | hasMember!(T, "get") && 15 | hasMember!(T, "nullify") && 16 | is(typeof(__traits(getMember, aggregate, "isNull")()) == bool) && 17 | !is(typeof(__traits(getMember, aggregate, "get")()) == void) && 18 | is(typeof(__traits(getMember, aggregate, "nullify")()) == void); 19 | } 20 | 21 | version (D_BetterC) {} 22 | else: 23 | 24 | unittest 25 | { 26 | import std.typecons : Nullable; 27 | static assert(isStdNullable!(Nullable!string)); 28 | } 29 | -------------------------------------------------------------------------------- /source/bc/internal/utf.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Normally just a public import of std.utf, but for betterC internally used functions are added 3 | * here (stripped from unneeded stuff not compatible with betterC). 4 | * 5 | * For details see: https://github.com/dlang/phobos/blob/master/std/utf.d 6 | * 7 | * Last revision from commit: 9351e91fe466e315baa6c82205d2563ce80e4c91 8 | */ 9 | module bc.internal.utf; 10 | 11 | version (D_BetterC) version = BC_UTF; 12 | else 13 | public import std.utf; 14 | 15 | version (BC_UTF): 16 | 17 | import std.meta : AliasSeq; 18 | import std.range.primitives; 19 | import std.traits : isAutodecodableString, isConvertibleToString, isSomeChar, isSomeString; 20 | 21 | @safe pure nothrow @nogc: 22 | 23 | enum dchar replacementDchar = '\uFFFD'; 24 | 25 | bool isValidDchar(dchar c) pure nothrow @safe @nogc 26 | { 27 | return c < 0xD800 || (c > 0xDFFF && c <= 0x10FFFF); 28 | } 29 | 30 | // workaround for non working std.traits isInputRange 31 | template isDecodableRange(R) 32 | { 33 | enum isDecodableRange = 34 | isInputRange!R || 35 | ( 36 | isAutodecodableString!R && !__traits(hasMember, R, "empty") && 37 | !__traits(hasMember, R, "front") && !__traits(hasMember, R, "popFront") 38 | ); 39 | } 40 | 41 | auto byCodeUnit(R)(R r) 42 | if ((isConvertibleToString!R && !isStaticArray!R) || 43 | (isDecodableRange!R && isSomeChar!(ElementEncodingType!R))) 44 | { 45 | import std.traits : StringTypeOf; 46 | static if ((isAutodecodableString!R && !__traits(hasMember, R, "empty") && 47 | !__traits(hasMember, R, "front") && !__traits(hasMember, R, "popFront"))) 48 | { 49 | static struct ByCodeUnitImpl 50 | { 51 | @safe pure nothrow @nogc: 52 | 53 | @property bool empty() const { return source.length == 0; } 54 | @property auto ref front() inout { return source[0]; } 55 | void popFront() { source = source[1 .. $]; } 56 | 57 | @property auto save() { return ByCodeUnitImpl(source.save); } 58 | 59 | @property auto ref back() inout { return source[$ - 1]; } 60 | void popBack() { source = source[0 .. $-1]; } 61 | 62 | auto ref opIndex(size_t index) inout { return source[index]; } 63 | auto opSlice(size_t lower, size_t upper) { return ByCodeUnitImpl(source[lower .. upper]); } 64 | 65 | @property size_t length() const { return source.length; } 66 | alias opDollar = length; 67 | 68 | StringTypeOf!R source; 69 | } 70 | 71 | static assert(isRandomAccessRange!ByCodeUnitImpl); 72 | 73 | return ByCodeUnitImpl(r); 74 | } 75 | else static if (!isInputRange!R || 76 | (is(R : const dchar[]) && !__traits(hasMember, R, "empty") && 77 | !__traits(hasMember, R, "front") && !__traits(hasMember, R, "popFront"))) 78 | { 79 | return cast(StringTypeOf!R) r; 80 | } 81 | else return r; // byCodeUnit for ranges and dchar[] is a no-op 82 | } 83 | 84 | template byUTF(C) if (isSomeChar!C) 85 | { 86 | static if (is(immutable C == immutable UC, UC) && !is(C == UC)) 87 | alias byUTF = byUTF!UC; 88 | else: 89 | 90 | auto ref byUTF(R)(R r) 91 | if (isAutodecodableString!R && isDecodableRange!R && isSomeChar!(ElementEncodingType!R)) 92 | { 93 | return byUTF(r.byCodeUnit()); 94 | } 95 | 96 | auto ref byUTF(R)(R r) 97 | if (!isAutodecodableString!R && isDecodableRange!R && isSomeChar!(ElementEncodingType!R)) 98 | { 99 | static if (is(immutable ElementEncodingType!R == immutable RC, RC) && is(RC == C)) 100 | { 101 | return r.byCodeUnit(); 102 | } 103 | else static if (is(C == dchar)) 104 | { 105 | static struct Result 106 | { 107 | enum Empty = uint.max; // range is empty or just constructed 108 | 109 | this(return R r) { this.r = r; } 110 | this(return R r, uint buff) 111 | { 112 | this.r = r; 113 | this.buff = buff; 114 | } 115 | 116 | @property bool empty() 117 | { 118 | return buff == Empty && r.empty; 119 | } 120 | 121 | @property dchar front() scope // 'scope' required by call to decodeFront() below 122 | { 123 | if (buff == Empty) 124 | { 125 | auto c = r.front; 126 | 127 | static if (is(RC == wchar)) 128 | enum firstMulti = 0xD800; // First high surrogate. 129 | else 130 | enum firstMulti = 0x80; // First non-ASCII. 131 | if (c < firstMulti) 132 | { 133 | r.popFront; 134 | buff = cast(dchar) c; 135 | } 136 | else 137 | buff = () @trusted { return decodeFront(r); }(); 138 | } 139 | return cast(dchar) buff; 140 | } 141 | 142 | void popFront() 143 | { 144 | if (buff == Empty) 145 | front(); 146 | buff = Empty; 147 | } 148 | 149 | static if (isForwardRange!R) 150 | { 151 | @property auto save() 152 | { 153 | return Result(r.save, buff); 154 | } 155 | } 156 | 157 | private: 158 | 159 | R r; 160 | uint buff = Empty; // one character lookahead buffer 161 | } 162 | 163 | return Result(r); 164 | } 165 | else 166 | { 167 | static struct Result 168 | { 169 | this(return R r) 170 | { 171 | this.r = r; 172 | } 173 | 174 | this(return R r, ushort pos, ushort fill, C[4 / C.sizeof] buf) 175 | { 176 | this.r = r; 177 | this.pos = pos; 178 | this.fill = fill; 179 | this.buf = buf; 180 | } 181 | 182 | @property bool empty() 183 | { 184 | return pos == fill && r.empty; 185 | } 186 | 187 | @property auto front() scope // 'scope' required by call to decodeFront() below 188 | { 189 | if (pos == fill) 190 | { 191 | pos = 0; 192 | auto c = r.front; 193 | 194 | static if (C.sizeof >= 2 && RC.sizeof >= 2) 195 | enum firstMulti = 0xD800; // First high surrogate. 196 | else 197 | enum firstMulti = 0x80; // First non-ASCII. 198 | if (c < firstMulti) 199 | { 200 | fill = 1; 201 | r.popFront; 202 | buf[pos] = cast(C) c; 203 | } 204 | else 205 | { 206 | static if (is(RC == dchar)) 207 | { 208 | r.popFront; 209 | dchar dc = c; 210 | } 211 | else 212 | dchar dc = () @trusted { return decodeFront(r); }(); 213 | fill = cast(ushort) encode(buf, dc); 214 | } 215 | } 216 | return buf[pos]; 217 | } 218 | 219 | void popFront() 220 | { 221 | if (pos == fill) 222 | front; 223 | ++pos; 224 | } 225 | 226 | static if (isForwardRange!R) 227 | { 228 | @property auto save() 229 | { 230 | return Result(r.save, pos, fill, buf); 231 | } 232 | } 233 | 234 | private: 235 | 236 | R r; 237 | ushort pos, fill; 238 | C[4 / C.sizeof] buf = void; 239 | } 240 | 241 | return Result(r); 242 | } 243 | } 244 | } 245 | 246 | dchar decodeFront(S)(ref S str, out size_t numCodeUnits) 247 | if (!isSomeString!S && isInputRange!S && isSomeChar!(ElementType!S)) 248 | in { assert(!str.empty); } 249 | out (result) { assert(isValidDchar(result)); } 250 | do 251 | { 252 | immutable fst = str.front; 253 | 254 | if (fst < codeUnitLimit!S) 255 | { 256 | str.popFront(); 257 | numCodeUnits = 1; 258 | return fst; 259 | } 260 | else 261 | { 262 | // https://issues.dlang.org/show_bug.cgi?id=14447 forces canIndex to be 263 | // done outside of decodeImpl, which is undesirable, since not all 264 | // overloads of decodeImpl need it. So, it should be moved back into 265 | // decodeImpl once https://issues.dlang.org/show_bug.cgi?id=8521 266 | // has been fixed. 267 | enum canIndex = is(S : const char[]) || isRandomAccessRange!S && hasSlicing!S && hasLength!S; 268 | immutable retval = decodeImpl!(canIndex)(str, numCodeUnits); 269 | 270 | // The other range types were already popped by decodeImpl. 271 | static if (isRandomAccessRange!S && hasSlicing!S && hasLength!S) 272 | str = str[numCodeUnits .. str.length]; 273 | 274 | return retval; 275 | } 276 | } 277 | 278 | /// ditto 279 | dchar decodeFront(S)(ref S str, out size_t numCodeUnits) @trusted pure 280 | if (isSomeString!S) 281 | in { assert(!str.empty); } 282 | out (result) { assert(isValidDchar(result)); } 283 | do 284 | { 285 | if (str[0] < codeUnitLimit!S) 286 | { 287 | numCodeUnits = 1; 288 | immutable retval = str[0]; 289 | str = str[1 .. $]; 290 | return retval; 291 | } 292 | else static if (is(immutable S == immutable C[], C)) 293 | { 294 | immutable retval = decodeImpl!true(cast(const(C)[]) str, numCodeUnits); 295 | str = str[numCodeUnits .. $]; 296 | return retval; 297 | } 298 | } 299 | 300 | /// ditto 301 | dchar decodeFront(S)(ref S str) if (isInputRange!S && isSomeChar!(ElementType!S)) 302 | { 303 | size_t numCodeUnits; 304 | return decodeFront(str, numCodeUnits); 305 | } 306 | 307 | // Gives the maximum value that a code unit for the given range type can hold. 308 | package template codeUnitLimit(S) 309 | if (isSomeChar!(ElementEncodingType!S)) 310 | { 311 | static if (is(immutable ElementEncodingType!S == immutable char)) 312 | enum char codeUnitLimit = 0x80; 313 | else static if (is(immutable ElementEncodingType!S == immutable wchar)) 314 | enum wchar codeUnitLimit = 0xD800; 315 | else 316 | enum dchar codeUnitLimit = 0xD800; 317 | } 318 | 319 | private dchar decodeImpl(bool canIndex, S)(auto ref S str, ref size_t index) 320 | if (is(S : const char[]) || (isInputRange!S && is(immutable ElementEncodingType!S == immutable char))) 321 | { 322 | /* The following encodings are valid, except for the 5 and 6 byte 323 | * combinations: 324 | * 0xxxxxxx 325 | * 110xxxxx 10xxxxxx 326 | * 1110xxxx 10xxxxxx 10xxxxxx 327 | * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 328 | * 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 329 | * 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 330 | */ 331 | 332 | /* Dchar bitmask for different numbers of UTF-8 code units. 333 | */ 334 | alias bitMask = AliasSeq!((1 << 7) - 1, (1 << 11) - 1, (1 << 16) - 1, (1 << 21) - 1); 335 | 336 | static if (is(S : const char[])) 337 | auto pstr = str.ptr + index; // this is what makes decodeImpl() @system code 338 | else static if (isRandomAccessRange!S && hasSlicing!S && hasLength!S) 339 | auto pstr = str[index .. str.length]; 340 | else 341 | alias pstr = str; 342 | 343 | // https://issues.dlang.org/show_bug.cgi?id=14447 forces this to be done 344 | // outside of decodeImpl 345 | //enum canIndex = is(S : const char[]) || (isRandomAccessRange!S && hasSlicing!S && hasLength!S); 346 | 347 | static if (canIndex) 348 | { 349 | immutable length = str.length - index; 350 | ubyte fst = pstr[0]; 351 | } 352 | else 353 | { 354 | ubyte fst = pstr.front; 355 | pstr.popFront(); 356 | } 357 | 358 | if ((fst & 0b1100_0000) != 0b1100_0000) 359 | { 360 | ++index; // always consume bad input to avoid infinite loops 361 | return replacementDchar; 362 | } 363 | ubyte tmp = void; 364 | dchar d = fst; // upper control bits are masked out later 365 | fst <<= 1; 366 | 367 | foreach (i; AliasSeq!(1, 2, 3)) 368 | { 369 | static if (canIndex) 370 | { 371 | if (i == length) 372 | { 373 | index += i; 374 | return replacementDchar; 375 | } 376 | } 377 | else 378 | { 379 | if (pstr.empty) 380 | { 381 | index += i; 382 | return replacementDchar; 383 | } 384 | } 385 | 386 | static if (canIndex) 387 | tmp = pstr[i]; 388 | else 389 | { 390 | tmp = pstr.front; 391 | pstr.popFront(); 392 | } 393 | 394 | if ((tmp & 0xC0) != 0x80) 395 | { 396 | index += i + 1; 397 | return replacementDchar; 398 | } 399 | 400 | d = (d << 6) | (tmp & 0x3F); 401 | fst <<= 1; 402 | 403 | if (!(fst & 0x80)) // no more bytes 404 | { 405 | d &= bitMask[i]; // mask out control bits 406 | 407 | // overlong, could have been encoded with i bytes 408 | if ((d & ~bitMask[i - 1]) == 0) 409 | { 410 | index += i + 1; 411 | return replacementDchar; 412 | } 413 | 414 | // check for surrogates only needed for 3 bytes 415 | static if (i == 2) 416 | { 417 | if (!isValidDchar(d)) 418 | { 419 | index += i + 1; 420 | return replacementDchar; 421 | } 422 | } 423 | 424 | index += i + 1; 425 | static if (i == 3) 426 | { 427 | if (d > dchar.max) d = replacementDchar; 428 | } 429 | return d; 430 | } 431 | } 432 | 433 | index += 4; // read 4 chars by now 434 | return replacementDchar; 435 | } 436 | 437 | private dchar decodeImpl(bool canIndex, S)(auto ref S str, ref size_t index) 438 | if (is(S : const wchar[]) || (isInputRange!S && is(immutable ElementEncodingType!S == immutable wchar))) 439 | { 440 | static if (is(S : const wchar[])) 441 | auto pstr = str.ptr + index; 442 | else static if (isRandomAccessRange!S && hasSlicing!S && hasLength!S) 443 | auto pstr = str[index .. str.length]; 444 | else 445 | alias pstr = str; 446 | 447 | // https://issues.dlang.org/show_bug.cgi?id=14447 forces this to be done 448 | // outside of decodeImpl 449 | //enum canIndex = is(S : const wchar[]) || (isRandomAccessRange!S && hasSlicing!S && hasLength!S); 450 | 451 | static if (canIndex) 452 | { 453 | immutable length = str.length - index; 454 | uint u = pstr[0]; 455 | } 456 | else 457 | { 458 | uint u = pstr.front; 459 | pstr.popFront(); 460 | } 461 | 462 | // The < case must be taken care of before decodeImpl is called. 463 | assert(u >= 0xD800); 464 | 465 | if (u <= 0xDBFF) 466 | { 467 | static if (canIndex) 468 | immutable onlyOneCodeUnit = length == 1; 469 | else 470 | immutable onlyOneCodeUnit = pstr.empty; 471 | 472 | if (onlyOneCodeUnit) 473 | { 474 | ++index; 475 | return replacementDchar; 476 | } 477 | 478 | static if (canIndex) 479 | immutable uint u2 = pstr[1]; 480 | else 481 | { 482 | immutable uint u2 = pstr.front; 483 | pstr.popFront(); 484 | } 485 | 486 | if (u2 < 0xDC00 || u2 > 0xDFFF) 487 | u = replacementDchar; 488 | else 489 | u = ((u - 0xD7C0) << 10) + (u2 - 0xDC00); 490 | ++index; 491 | } 492 | else if (u >= 0xDC00 && u <= 0xDFFF) 493 | u = replacementDchar; 494 | 495 | ++index; 496 | 497 | // Note: u+FFFE and u+FFFF are specifically permitted by the 498 | // Unicode standard for application internal use (see isValidDchar) 499 | 500 | return cast(dchar) u; 501 | } 502 | 503 | private dchar decodeImpl(bool canIndex, S)(auto ref S str, ref size_t index) 504 | if (is(S : const dchar[]) || (isInputRange!S && is(immutable ElementEncodingType!S == immutable dchar))) 505 | { 506 | static if (is(S : const dchar[])) 507 | auto pstr = str.ptr; 508 | else 509 | alias pstr = str; 510 | 511 | static if (is(S : const dchar[]) || isRandomAccessRange!S) 512 | { 513 | dchar dc = pstr[index]; 514 | if (!isValidDchar(dc)) 515 | dc = replacementDchar; 516 | 517 | ++index; 518 | return dc; 519 | } 520 | else 521 | { 522 | dchar dc = pstr.front; 523 | if (!isValidDchar(dc)) 524 | dc = replacementDchar; 525 | 526 | ++index; 527 | pstr.popFront(); 528 | return dc; 529 | } 530 | } 531 | 532 | size_t encode()(out char[4] buf, dchar c) @safe pure 533 | { 534 | if (c <= 0x7F) 535 | { 536 | assert(isValidDchar(c)); 537 | buf[0] = cast(char) c; 538 | return 1; 539 | } 540 | if (c <= 0x7FF) 541 | { 542 | assert(isValidDchar(c)); 543 | buf[0] = cast(char)(0xC0 | (c >> 6)); 544 | buf[1] = cast(char)(0x80 | (c & 0x3F)); 545 | return 2; 546 | } 547 | if (c <= 0xFFFF) 548 | { 549 | if (0xD800 <= c && c <= 0xDFFF) 550 | c = replacementDchar; 551 | 552 | assert(isValidDchar(c)); 553 | L3: 554 | buf[0] = cast(char)(0xE0 | (c >> 12)); 555 | buf[1] = cast(char)(0x80 | ((c >> 6) & 0x3F)); 556 | buf[2] = cast(char)(0x80 | (c & 0x3F)); 557 | return 3; 558 | } 559 | if (c <= 0x10FFFF) 560 | { 561 | assert(isValidDchar(c)); 562 | buf[0] = cast(char)(0xF0 | (c >> 18)); 563 | buf[1] = cast(char)(0x80 | ((c >> 12) & 0x3F)); 564 | buf[2] = cast(char)(0x80 | ((c >> 6) & 0x3F)); 565 | buf[3] = cast(char)(0x80 | (c & 0x3F)); 566 | return 4; 567 | } 568 | 569 | assert(!isValidDchar(c)); 570 | c = replacementDchar; 571 | goto L3; 572 | } 573 | 574 | size_t encode()(out wchar[2] buf, dchar c) @safe pure 575 | { 576 | if (c <= 0xFFFF) 577 | { 578 | if (0xD800 <= c && c <= 0xDFFF) 579 | c = replacementDchar; 580 | 581 | assert(isValidDchar(c)); 582 | L1: 583 | buf[0] = cast(wchar) c; 584 | return 1; 585 | } 586 | if (c <= 0x10FFFF) 587 | { 588 | assert(isValidDchar(c)); 589 | buf[0] = cast(wchar)((((c - 0x10000) >> 10) & 0x3FF) + 0xD800); 590 | buf[1] = cast(wchar)(((c - 0x10000) & 0x3FF) + 0xDC00); 591 | return 2; 592 | } 593 | 594 | c = replacementDchar; 595 | goto L1; 596 | } 597 | 598 | size_t encode()(out dchar[1] buf, dchar c) @safe pure 599 | { 600 | if ((0xD800 <= c && c <= 0xDFFF) || 0x10FFFF < c) 601 | c = replacementDchar; 602 | else 603 | assert(isValidDchar(c)); 604 | buf[0] = c; 605 | return 1; 606 | } 607 | -------------------------------------------------------------------------------- /source/bc/string/ascii.d: -------------------------------------------------------------------------------- 1 | module bc.string.ascii; 2 | 3 | /** 4 | * Simplified ASCII strings case insensitve comparison. 5 | * Returns: 6 | * - < 0 - when first string is lesser that the second 7 | * - = 0 - when both string are equal 8 | * - > 0 - when first string is greater than the second 9 | */ 10 | int sicmp(const(char)[] a, const(char)[] b) @safe nothrow @nogc 11 | { 12 | immutable len = a.length > b.length ? b.length : a.length; 13 | version (Posix) { 14 | import core.sys.posix.strings : strncasecmp; 15 | immutable diff = () @trusted { return strncasecmp(a.ptr, b.ptr, len); }(); 16 | if (diff) return diff; 17 | } else { 18 | // TODO: manual loop unroll 19 | immutable end = min(a.length, b.length); 20 | for (int i=0; i < len; ++i) { 21 | auto lhs = a[i].toLower; 22 | auto rhs = b[i].toLower; 23 | auto diff = lhs - rhs; 24 | if (diff) return diff; 25 | } 26 | } 27 | return (a.length > b.length) - (b.length > a.length); 28 | } 29 | 30 | @safe unittest 31 | { 32 | assert(sicmp("fOoo", "FoOo") == 0); 33 | assert(sicmp("abcd", "efgh") < 0); 34 | assert(sicmp("efgh", "abcd") > 0); 35 | assert(sicmp("fOoox", "FoOo") > 0); 36 | assert(sicmp("fOoo", "FoOox") < 0); 37 | } 38 | 39 | /// Converts ASCII characters 'A'..'Z' to a lower 'a'..'z'. 40 | char toLower(char c) @safe pure nothrow @nogc 41 | { 42 | pragma(inline, true); 43 | static immutable chmap = () 44 | { 45 | char[256] res = void; 46 | 47 | for (int i=0; i < 256; ++i) { 48 | if (i >= 'A' && i <= 'Z') res[i] = cast(char)(i+32); 49 | else res[i] = cast(char)i; 50 | } 51 | return res; 52 | }(); 53 | 54 | return chmap[c]; 55 | } 56 | 57 | /// Converts ASCII characters 'a'..'z' to a upper 'A'..'Z'. 58 | char toUpper(char c) @safe pure nothrow @nogc 59 | { 60 | pragma(inline, true); 61 | static immutable chmap = () 62 | { 63 | char[256] res = void; 64 | 65 | for (int i=0; i < 256; ++i) { 66 | if (i >= 'a' && i <= 'z') res[i] = cast(char)(i-32); 67 | else res[i] = cast(char)i; 68 | } 69 | return res; 70 | }(); 71 | 72 | return chmap[c]; 73 | } 74 | 75 | //@safe 76 | unittest 77 | { 78 | assert('A'.toLower == 'a'); 79 | assert('b'.toLower == 'b'); 80 | assert('2'.toLower == '2'); 81 | assert('Z'.toLower == 'z'); 82 | 83 | assert('a'.toUpper == 'A'); 84 | assert('B'.toUpper == 'B'); 85 | assert('2'.toUpper == '2'); 86 | assert('z'.toUpper == 'Z'); 87 | } 88 | 89 | /// Checks if character is a digit ('0'..'9') 90 | bool isDigit(char c) @safe pure nothrow @nogc 91 | { 92 | pragma(inline, true); 93 | static immutable chmap = () 94 | { 95 | bool[256] res = void; 96 | 97 | for (int i=0; i < 256; ++i) { 98 | if (i >= '0' && i <= '9') res[i] = true; 99 | else res[i] = false; 100 | } 101 | return res; 102 | }(); 103 | 104 | return chmap[c]; 105 | } 106 | 107 | @safe unittest 108 | { 109 | assert('0'.isDigit); 110 | assert('9'.isDigit); 111 | assert(!'a'.isDigit); 112 | assert(!'+'.isDigit); 113 | } 114 | -------------------------------------------------------------------------------- /source/bc/string/conv.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Parsing from string to standard types. 3 | */ 4 | module bc.string.conv; 5 | 6 | import bc.string.ascii; 7 | import bc.core.intrinsics; 8 | import std.traits; 9 | 10 | alias cstring = const(char)[]; 11 | 12 | //TODO: check for owerloads? 13 | //TODO: use parseToken to fast check length of input number 14 | 15 | ParseResult!T parse(T)(cstring str) if (isIntegral!T && !is(T == enum)) 16 | { 17 | pragma(inline, true); 18 | if (!str.length) return ParseResult!T.init; 19 | 20 | size_t count; 21 | static if (isSigned!T) { 22 | ptrdiff_t res; 23 | bool sign; 24 | if (str[0] == '-') { 25 | sign = true; 26 | count++; 27 | } else if (str[0] == '+') 28 | count++; 29 | } else size_t res; 30 | 31 | for (; count < str.length; ++count) 32 | { 33 | if (_expect(!str[count].isDigit, false)) return ParseResult!T.init; 34 | res = res*10 + (str[count]-'0'); 35 | } 36 | 37 | if (_expect(res > T.max, false)) return ParseResult!T.init; 38 | 39 | static if (isSigned!T) { 40 | if (sign) res = -res; 41 | } 42 | return ParseResult!T(cast(T)res, count); 43 | } 44 | 45 | @safe unittest 46 | { 47 | assert("42".parse!int == ParseResult!int(42, 2)); 48 | assert("42".parse!uint == ParseResult!uint(42, 2)); 49 | assert("-42".parse!int == ParseResult!int(-42, 3)); 50 | assert("+42".parse!int == ParseResult!int(42, 3)); 51 | } 52 | 53 | /// Result of the parse functions 54 | struct ParseResult(T) 55 | { 56 | T data; /// parsed value 57 | size_t count; /// Number of characters consumed while parsing (0 means that input couldn't be parsed into specified type) 58 | 59 | /// Checks if some data was actually parsed from the input 60 | bool opCast(T)() const if(is(T == bool)) { return count > 0; } 61 | // alias data this; 62 | } 63 | -------------------------------------------------------------------------------- /source/bc/string/format.d: -------------------------------------------------------------------------------- 1 | /** 2 | * @nogc formatting utilities 3 | * 4 | * Inspired by: https://github.com/weka-io/mecca/blob/master/src/mecca/lib/string.d 5 | * 6 | * Sink Types: 7 | * various functions in this module use "sinks" which are buffers or objects that get filled with 8 | * the formatting data while the format functions are running. The following sink types are 9 | * supported to be passed into these arguments: 10 | * - Arrays (`isArray!S && is(ForeachType!S : char))`) 11 | * - $(LREF NullSink) 12 | * - Object with `put(const(char)[])` and `put(char)` functions 13 | * 14 | * Passing in arrays will make the sink `@nogc pure nothrow @safe` as everything will be written 15 | * into that memory. Passing in arrays that are too short to hold all the data will trigger a 16 | * `RangeError` or terminate the program in betterC. 17 | * 18 | * Passing in a $(LREF NullSink) instance will not allocate any memory and just count the bytes that 19 | * will be allocated. 20 | * 21 | * Otherwise any type that contains a `put` method that can be called both with `const(char)[]` and 22 | * with `char` arguments can be used. 23 | */ 24 | module bc.string.format; 25 | 26 | import bc.core.intrinsics; 27 | import bc.core.system.backtrace; 28 | import bc.core.traits; 29 | import bc.string.string; 30 | import std.algorithm : among; 31 | import std.datetime.date : TimeOfDay; 32 | import std.traits : 33 | EnumMembers, FieldNameTuple, ForeachType, hasMember, 34 | isArray, isPointer, isSigned, isSomeChar, isStaticArray, 35 | PointerTarget, Unqual; 36 | import std.range : ElementEncodingType, isForwardRange, isInputRange; 37 | import std.typecons : Flag, Tuple, isTuple; 38 | 39 | version (D_BetterC) {} 40 | else 41 | { 42 | import core.time : Duration; 43 | import std.datetime.systime : SysTime; 44 | import std.uuid : UUID; 45 | } 46 | 47 | private template isUUID(T) 48 | { 49 | version (D_BetterC) enum isUUID = false; 50 | else enum isUUID = is(T == UUID); 51 | } 52 | 53 | private template isSysTime(T) 54 | { 55 | version (D_BetterC) enum isSysTime = false; 56 | else enum isSysTime = is(T == SysTime); 57 | } 58 | 59 | private template isDuration(T) 60 | { 61 | version (D_BetterC) enum isDuration = false; 62 | else enum isDuration = is(T == Duration); 63 | } 64 | 65 | private template isTraceInfo(T) 66 | { 67 | version (D_BetterC) enum isTraceInfo = false; 68 | else version (linux) enum isTraceInfo = is(T == TraceInfo); 69 | else enum isTraceInfo = false; 70 | } 71 | 72 | /** 73 | * Formats values to with fmt template into provided sink. 74 | * Note: it supports only a basic subset of format type specifiers, main usage is for nogc logging 75 | * and error messages formatting. But more cases can be added as needed. 76 | * 77 | * WARN: %s accepts pointer to some char assuming it's a zero terminated string 78 | * 79 | * Params: 80 | * fmt = The format string, much like in std.format 81 | * sink = The sink where the full string should be written to, see section "Sink Types" 82 | * args = The arguments to fill the format string with 83 | * 84 | * Returns: the length of the formatted string. 85 | */ 86 | size_t nogcFormatTo(string fmt = "%s", S, ARGS...)(ref scope S sink, auto ref ARGS args) 87 | { 88 | // TODO: not pure because of float formatter 89 | alias sfmt = splitFmt!fmt; 90 | static assert (sfmt.numFormatters == ARGS.length, "Expected " ~ sfmt.numFormatters.stringof ~ 91 | " arguments, got " ~ ARGS.length.stringof); 92 | 93 | mixin SinkWriter!S; 94 | 95 | foreach (tok; sfmt.tokens) { 96 | // pragma(msg, "tok: ", tok); 97 | static if (is(typeof(tok) == string)) 98 | { 99 | static if (tok.length > 0) { 100 | write(tok); 101 | } 102 | } 103 | else static if (is(typeof(tok) == ArrFmtSpec)) 104 | { 105 | enum j = tok.idx; 106 | alias Typ = Unqual!(ARGS[j]); 107 | static assert( 108 | __traits(compiles, ForeachType!Typ), "Expected foreach type range instead of " ~ Typ.stringof); 109 | static assert( 110 | !is(S == NullSink) || isArray!Typ || isForwardRange!Typ, 111 | "Don't determine format output length with range argument " ~ Typ.stringof ~ " it'd be consumed."); 112 | static if (!is(S == NullSink) && !isArray!Typ && !isForwardRange!Typ) 113 | pragma(msg, "WARN: Argument of type " ~ Typ.stringof ~ " would be consumed during format"); 114 | 115 | static if (tok.del.length) bool first = true; 116 | static if (!isArray!Typ && isForwardRange!Typ) auto val = args[j].save(); 117 | else auto val = args[j]; 118 | foreach (ref e; val) 119 | { 120 | static if (tok.del.length) { 121 | if (_expect(!first, true)) write(tok.del); 122 | else first = false; 123 | } 124 | advance(s.nogcFormatTo!(tok.fmt)(e)); 125 | } 126 | } 127 | else static if (is(typeof(tok) == FmtSpec)) { 128 | enum j = tok.idx; 129 | enum f = tok.type; 130 | 131 | alias Typ = Unqual!(ARGS[j]); 132 | alias val = args[j]; 133 | 134 | static if (isStdNullable!Typ) { 135 | if (val.isNull) write("null"); 136 | else advance(s.nogcFormatTo!"%s"(val.get)); 137 | } 138 | else static if (f == FMT.STR) { 139 | static if ((isArray!Typ && is(Unqual!(ForeachType!Typ) == char))) 140 | write(val[]); 141 | else static if (isInputRange!Typ && isSomeChar!(Unqual!(ElementEncodingType!Typ))) { 142 | import bc.internal.utf : byUTF; 143 | foreach (c; val.byUTF!char) write(c); 144 | } 145 | else static if (is(Typ == bool)) 146 | write(val ? "true" : "false"); 147 | else static if (is(Typ == enum)) { 148 | auto tmp = enumToStr(val); 149 | if (_expect(tmp is null, false)) advance(s.nogcFormatTo!"%s(%d)"(Typ.stringof, val)); 150 | else write(tmp); 151 | } else static if (isUUID!Typ) advance(s.formatUUID(val)); 152 | else static if (isSysTime!Typ) advance(s.formatSysTime(val)); 153 | else static if (is(Typ == TimeOfDay)) 154 | advance(s.nogcFormatTo!"%02d:%02d:%02d"(val.hour, val.minute, val.second)); 155 | else static if (isDuration!Typ) advance(s.formatDuration(val)); 156 | else static if (isArray!Typ || isInputRange!Typ) { 157 | import std.range : empty; 158 | if (!val.empty) advance(s.nogcFormatTo!"[%(%s%|, %)]"(val)); 159 | else write("[]"); 160 | } 161 | else static if (isPointer!Typ) { 162 | static if (is(typeof(*Typ)) && isSomeChar!(typeof(*Typ))) { 163 | // NOTE: not safe, we can only trust that the provided char pointer is really stringz 164 | size_t i; 165 | while (val[i] != '\0') ++i; 166 | if (i) write(val[0..i]); 167 | } 168 | else advance(s.formatPtr(val)); 169 | } 170 | else static if (is(Typ == char)) write(val); 171 | else static if (isSomeChar!Typ) { 172 | import std.range : only; 173 | import bc.internal.utf : byUTF; 174 | 175 | foreach (c; val.only.byUTF!char) write(c); 176 | } 177 | else static if (is(Typ : ulong)) advance(s.formatDecimal(val)); 178 | else static if (isTuple!Typ) { 179 | write("Tuple("); 180 | foreach (i, _; Typ.Types) 181 | { 182 | static if (Typ.fieldNames[i] == "") enum prefix = (i == 0 ? "" : ", "); 183 | else enum prefix = (i == 0 ? "" : ", ") ~ Typ.fieldNames[i] ~ "="; 184 | write(prefix); 185 | advance(s.nogcFormatTo!"%s"(val[i])); 186 | } 187 | write(")"); 188 | } 189 | else static if (is(Typ : Throwable)) { 190 | auto obj = cast(Object)val; 191 | static if (__traits(compiles, TraceInfo(val))) { 192 | advance(s.nogcFormatTo!"%s@%s(%d): %s\n----------------\n%s"( 193 | typeid(obj).name, val.file, val.line, val.msg, TraceInfo(val))); 194 | } 195 | else 196 | advance(s.nogcFormatTo!"%s@%s(%d): %s"( 197 | typeid(obj).name, val.file, val.line, val.msg)); 198 | } 199 | else static if (isTraceInfo!Typ) { 200 | auto sw = sinkWrap(s); 201 | val.dumpTo(sw); 202 | advance(sw.totalLen); 203 | } 204 | else static if (is(typeof(val[]))) 205 | advance(s.nogcFormatTo!"%s"(val[])); // sliceable values 206 | else static if (is(Typ == struct)) { 207 | static if (__traits(compiles, (v) @nogc {auto sw = sinkWrap(s); v.toString(sw); }(val))) { 208 | // we can use custom defined toString 209 | auto sw = sinkWrap(s); 210 | val.toString(sw); 211 | advance(sw.totalLen); 212 | } else { 213 | static if (hasMember!(Typ, "toString")) 214 | pragma(msg, Typ.stringof ~ " has toString defined, but can't be used with nogcFormatter"); 215 | { 216 | enum Prefix = Typ.stringof ~ "("; 217 | write(Prefix); 218 | } 219 | alias Names = FieldNameTuple!Typ; 220 | foreach(i, field; val.tupleof) { 221 | enum string Name = Names[i]; 222 | enum Prefix = (i == 0 ? "" : ", ") ~ Name ~ "="; 223 | write(Prefix); 224 | advance(s.nogcFormatTo!"%s"(field)); 225 | } 226 | write(")"); 227 | } 228 | } else static if (is(Typ : double)) advance(s.nogcFormatTo!"%g"(val)); 229 | else static assert (false, "Unsupported value type for string format: " ~ Typ.stringof); 230 | } 231 | else static if (f == FMT.CHR) { 232 | static assert (is(Typ : char), "Requested char format, but provided: " ~ Typ.stringof); 233 | write((&val)[0..1]); 234 | } 235 | else static if (f == FMT.DEC) { 236 | static assert (is(Typ : ulong), "Requested decimal format, but provided: " ~ Typ.stringof); 237 | enum fs = formatSpec(f, tok.def); 238 | advance(s.formatDecimal!(fs.width, fs.fill)(val)); 239 | } 240 | else static if (f == FMT.HEX || f == FMT.UHEX) { 241 | static assert (is(Typ : ulong) || isPointer!(Typ), "Requested hex format, but provided: " ~ Typ.stringof); 242 | enum u = f == FMT.HEX ? Upper.yes : Upper.no; 243 | enum fs = formatSpec(f, tok.def); 244 | static if (isPointer!(Typ)) { 245 | import std.stdint : intptr_t; 246 | advance(s.formatHex!(fs.width, fs.fill, u)(cast(intptr_t)val)); 247 | } 248 | else advance(s.formatHex!(fs.width, fs.fill, u)(val)); 249 | } 250 | else static if (f == FMT.PTR) { 251 | static assert (is(Typ : ulong) || isPointer!(Typ), "Requested pointer format, but provided: " ~ Typ.stringof); 252 | advance(s.formatPtr(val)); 253 | } 254 | else static if (f == FMT.FLT) { 255 | static assert (is(Typ : double), "Requested float format, but provided: " ~ Typ.stringof); 256 | advance(s.formatFloat(val)); 257 | } 258 | } 259 | else static assert(false); 260 | } 261 | 262 | return totalLen; 263 | } 264 | 265 | /// 266 | @("combined") 267 | @safe @nogc unittest 268 | { 269 | char[100] buf; 270 | ubyte[3] data = [1, 2, 3]; 271 | immutable ret = nogcFormatTo!"hello %s %s %% world %d %x %p"(buf, data, "moshe", -567, 7, 7); 272 | assert(ret == 53); 273 | assert(buf[0..53] == "hello [1, 2, 3] moshe % world -567 7 0000000000000007"); 274 | } 275 | 276 | /** 277 | * Same as `nogcFormatTo`, but it internally uses static malloc buffer to write formatted string to. 278 | * So be careful that next call replaces internal buffer data and previous result isn't valid anymore. 279 | */ 280 | const(char)[] nogcFormat(string fmt = "%s", ARGS...)(auto ref ARGS args) 281 | { 282 | static StringZ str; 283 | str.clear(); 284 | nogcFormatTo!fmt(str, args); 285 | return cast(const(char)[])str.data; 286 | } 287 | 288 | /// 289 | @("formatters") 290 | @safe unittest 291 | { 292 | import bc.core.memory; 293 | import std.algorithm : filter; 294 | import std.range : chunks; 295 | 296 | assert(nogcFormat!"abcd abcd" == "abcd abcd"); 297 | assert(nogcFormat!"123456789a" == "123456789a"); 298 | version (D_NoBoundsChecks) {} 299 | else version (D_Exceptions) 300 | { 301 | () @trusted 302 | { 303 | import core.exception : RangeError; 304 | import std.exception : assertThrown; 305 | char[5] buf; 306 | assertThrown!RangeError(buf.nogcFormatTo!"123412341234"); 307 | }(); 308 | } 309 | 310 | // literal escape 311 | assert(nogcFormat!"123 %%" == "123 %"); 312 | assert(nogcFormat!"%%%%" == "%%"); 313 | 314 | // %d 315 | assert(nogcFormat!"%d"(1234) == "1234"); 316 | assert(nogcFormat!"%4d"(42) == " 42"); 317 | assert(nogcFormat!"%04d"(42) == "0042"); 318 | assert(nogcFormat!"%04d"(-42) == "-042"); 319 | assert(nogcFormat!"ab%dcd"(1234) == "ab1234cd"); 320 | assert(nogcFormat!"ab%d%d"(1234, 56) == "ab123456"); 321 | 322 | // %x 323 | assert(nogcFormat!"0x%x"(0x1234) == "0x1234"); 324 | 325 | // %p 326 | assert(nogcFormat!("%p")(0x1234) == "0000000000001234"); 327 | 328 | // %s 329 | assert(nogcFormat!"12345%s"("12345") == "1234512345"); 330 | assert(nogcFormat!"12345%s"(12345) == "1234512345"); 331 | enum Floop {XXX, YYY, ZZZ} 332 | assert(nogcFormat!"12345%s"(Floop.YYY) == "12345YYY"); 333 | char[4] str = "foo\0"; 334 | assert(() @trusted { return nogcFormat!"%s"(str.ptr); }() == "foo"); 335 | 336 | version (D_BetterC) {} 337 | else 338 | { 339 | assert(nogcFormat!"%s"( 340 | UUID([138, 179, 6, 14, 44, 186, 79, 35, 183, 76, 181, 45, 179, 189, 251, 70])) 341 | == "8ab3060e-2cba-4f23-b74c-b52db3bdfb46"); 342 | } 343 | 344 | // array format 345 | version (D_BetterC) 346 | { 347 | int[] arr = () @trusted { return (cast(int*)enforceMalloc(int.sizeof*10))[0..10]; }(); 348 | foreach (i; 0..10) arr[i] = i; 349 | scope (exit) () @trusted { pureFree(arr.ptr); }(); 350 | } 351 | else auto arr = [0,1,2,3,4,5,6,7,8,9]; 352 | 353 | assert(nogcFormat!"foo %(%d %)"(arr[1..4]) == "foo 1 2 3"); 354 | assert(nogcFormat!"foo %-(%d %)"(arr[1..4]) == "foo 1 2 3"); 355 | assert(nogcFormat!"foo %(-%d-%|, %)"(arr[1..4]) == "foo -1-, -2-, -3-"); 356 | assert(nogcFormat!"%(0x%02x %)"(arr[1..4]) == "0x01 0x02 0x03"); 357 | assert(nogcFormat!"%(%(%d %)\n%)"(arr[1..$].chunks(3)) == "1 2 3\n4 5 6\n7 8 9"); 358 | 359 | // range format 360 | auto r = arr.filter!(a => a < 5); 361 | assert(nogcFormat!"%s"(r) == "[0, 1, 2, 3, 4]"); 362 | 363 | // Arg num 364 | assert(!__traits(compiles, nogcFormat!"abc"(5))); 365 | assert(!__traits(compiles, nogcFormat!"%d"())); 366 | assert(!__traits(compiles, nogcFormat!"%d a %d"(5))); 367 | 368 | // Format error 369 | assert(!__traits(compiles, nogcFormat!"%"())); 370 | assert(!__traits(compiles, nogcFormat!"abcd%d %"(15))); 371 | assert(!__traits(compiles, nogcFormat!"%$"(1))); 372 | assert(!__traits(compiles, nogcFormat!"%d"("hello"))); 373 | assert(!__traits(compiles, nogcFormat!"%x"("hello"))); 374 | 375 | assert(nogcFormat!"Hello %s"(5) == "Hello 5"); 376 | 377 | struct Foo { int x, y; } 378 | assert(nogcFormat!("Hello %s")(Foo(1, 2)) == "Hello Foo(x=1, y=2)"); 379 | 380 | version (D_BetterC) 381 | { 382 | struct Nullable(T) // can't be instanciated in betterC - fake just for the UT 383 | { 384 | T get() { return T.init; } 385 | bool isNull() { return true; } 386 | void nullify() {} 387 | } 388 | } 389 | else import std.typecons : Nullable; 390 | 391 | struct Msg { Nullable!string foo; } 392 | assert(nogcFormat!"%s"(Msg.init) == "Msg(foo=null)"); 393 | 394 | RCString s = "abcd"; 395 | assert(nogcFormat!"%s"(s) == "abcd"); 396 | } 397 | 398 | /// 399 | @("tuple") 400 | @safe unittest 401 | { 402 | { 403 | alias T = Tuple!(int, "foo", bool); 404 | T t = T(42, true); 405 | assert(nogcFormat(t) == "Tuple(foo=42, true)"); 406 | } 407 | 408 | { 409 | alias T = Tuple!(int, "foo", string, "bar", char, "baz"); 410 | T t = T(42, "bar", 'z'); 411 | assert(nogcFormat(t) == "Tuple(foo=42, bar=bar, baz=z)"); 412 | } 413 | } 414 | 415 | /// 416 | @("custom format") 417 | @safe unittest 418 | { 419 | static struct Custom 420 | { 421 | int foo = 42; 422 | void toString(S)(ref S sink) const 423 | { 424 | sink.put("custom: "); 425 | sink.nogcFormatTo!"foo=%d"(foo); 426 | } 427 | } 428 | 429 | Custom c; 430 | assert(nogcFormat(c) == "custom: foo=42"); 431 | assert(getFormatSize(c) == "custom: foo=42".length); 432 | 433 | char[512] buf; 434 | auto l = buf.nogcFormatTo(c); 435 | assert(buf[0..l] == "custom: foo=42"); 436 | } 437 | 438 | version (D_BetterC) {} 439 | else version (linux) 440 | { 441 | // Only Posix is supported ATM 442 | @("Exception stack trace format") 443 | @safe unittest 444 | { 445 | import std.algorithm : startsWith; 446 | static class TestException : Exception { this(string msg) nothrow { super(msg); } } 447 | static void fn() { throw new TestException("foo"); } 448 | 449 | try fn(); 450 | catch (Exception ex) 451 | { 452 | import std.format : format; 453 | string std = () @trusted { return format!"Now how cool is that!: %s"(ex); }(); 454 | (Exception ex, string std) nothrow @nogc @trusted 455 | { 456 | auto str = nogcFormat!"Now how cool is that!: %s"(ex); 457 | assert(str.startsWith("Now how cool is that!: bc.string.format.__unittest_L")); 458 | // import core.stdc.stdio; printf("%s\nvs\n%s\n", std.ptr, str.ptr); 459 | static if (__VERSION__ >= 2095) 460 | { 461 | // we try to reflect last compiler behavior, previous might differ 462 | assert(str[0..$] == std[0..$]); 463 | } 464 | else 465 | { 466 | int ln = -1; 467 | foreach (i, c; str[]) { 468 | if (c=='\n') { 469 | ln = cast(int)i; 470 | break; 471 | } 472 | } 473 | 474 | assert(ln>0); 475 | assert(str[0..ln] == std[0..ln]); 476 | } 477 | }(ex, std); 478 | } 479 | } 480 | } 481 | 482 | /** 483 | * Gets size needed to hold formatted string result 484 | */ 485 | size_t getFormatSize(string fmt = "%s", ARGS...)(auto ref ARGS args) nothrow @nogc 486 | { 487 | NullSink ns; 488 | return ns.nogcFormatTo!fmt(args); 489 | } 490 | 491 | @("getFormatSize") 492 | @safe unittest 493 | { 494 | assert(getFormatSize!"foo" == 3); 495 | assert(getFormatSize!"foo=%d"(42) == 6); 496 | assert(getFormatSize!"%04d-%02d-%02dT%02d:%02d:%02d.%03d"(2020, 4, 28, 19, 20, 32, 207) == 23); 497 | assert(getFormatSize!"%x"(0x2C38) == 4); 498 | assert(getFormatSize!"%s"(9896) == 4); 499 | } 500 | 501 | /// pseudosink used just for calculation of resulting string length 502 | struct NullSink {} 503 | 504 | private enum FMT: ubyte { 505 | STR, 506 | CHR, 507 | DEC, // also for BOOL 508 | HEX, 509 | UHEX, 510 | PTR, 511 | FLT, 512 | } 513 | 514 | private struct FmtParams 515 | { 516 | bool leftJustify; // Left justify the result in the field. It overrides any 0 flag. 517 | bool signed; // Prefix positive numbers in a signed conversion with a +. It overrides any space flag. 518 | bool prefixHex; // If non-zero, prefix result with 0x (0X). 519 | int width; // pad with characters, if -1, use previous argument as width 520 | char fill = ' '; // character to pad with 521 | int sep; // insert separator each X digits, if -1, use previous argument as X 522 | bool sepChar; // is separator char defined in additional arg? 523 | int prec; // precision, if -1, use previous argument as precision value 524 | } 525 | 526 | private bool isDigit()(immutable char c) { return c >= '0' && c <= '9'; } 527 | 528 | // Parses format specifier in CTFE 529 | // See: https://dlang.org/phobos/std_format.html for details 530 | // Note: Just a subset of the specification is supported ATM. Parser here parses the spec, but 531 | // formatter doesn't use it all. 532 | // 533 | // FormatStringItem: 534 | // '%%' 535 | // '%' Position Flags Width Separator Precision FormatChar 536 | // '%(' FormatString '%)' 537 | // '%-(' FormatString '%)' 538 | // 539 | auto formatSpec()(FMT f, string spec) 540 | { 541 | FmtParams res; int idx; 542 | 543 | if (spec.length) 544 | { 545 | assert(spec.indexOf('$') < 0, "Position specifier not supported"); 546 | 547 | // Flags: 548 | // empty 549 | // '-' Flags 550 | // '+' Flags 551 | // '#' Flags 552 | // '0' Flags 553 | // ' ' Flags 554 | while (idx < spec.length) 555 | { 556 | if (spec[idx] == '-') { 557 | res.leftJustify = true; idx++; continue; 558 | } else if (f.among(FMT.DEC, FMT.FLT) && spec[idx] == '+') { 559 | res.signed = true; idx++; continue; 560 | } else if (f == FMT.HEX && spec[idx] == '#') { 561 | // TODO: 'o' - Add to precision as necessary so that the first digit of the octal formatting is a '0', even if both the argument and the Precision are zero. 562 | res.prefixHex = true; idx++; continue; 563 | } else if (f == FMT.FLT && spec[idx] == '#') { 564 | // TODO: Always insert the decimal point and print trailing zeros. 565 | idx++; continue; 566 | } else if (f.among(FMT.DEC, FMT.FLT, FMT.HEX, FMT.UHEX, FMT.PTR) && spec[idx].among('0', ' ')) { 567 | res.fill = spec[idx++]; continue; 568 | } 569 | break; 570 | } 571 | 572 | if (idx == spec.length) goto done; 573 | 574 | // Width: 575 | // empty 576 | // Integer 577 | // '*' 578 | if (spec[idx] == '*') { res.width = -1; idx++; } 579 | else { 580 | while (idx < spec.length && spec[idx].isDigit) res.width = res.width*10 + (spec[idx++] - '0'); 581 | } 582 | 583 | if (idx == spec.length) goto done; 584 | 585 | // Separator: 586 | // empty 587 | // ',' 588 | // ',' '?' 589 | // ',' '*' '?' 590 | // ',' Integer '?' 591 | // ',' '*' 592 | // ',' Integer 593 | if (spec[idx] == ',') { 594 | // ie: writefln("'%,*?d'", 4, '$', 123456789); 595 | idx++; 596 | if (idx == spec.length) {res.sep = 3; goto done; } 597 | if (spec[idx].isDigit) { 598 | while (idx < spec.length && spec[idx].isDigit) res.sep = res.sep*10 + (spec[idx++] - '0'); 599 | } else if (spec[idx] == '*') { 600 | idx++; res.sep = -1; 601 | } else res.sep = 3; 602 | 603 | if (idx == spec.length) goto done; 604 | if (spec[idx] == '?') { res.sepChar = true; idx++; } 605 | } 606 | if (idx == spec.length) goto done; 607 | 608 | // Precision: 609 | // empty 610 | // '.' 611 | // '.' Integer 612 | // '.*' 613 | if (spec[idx] == '.') { 614 | idx++; 615 | if (idx == spec.length) { res.prec = 6; goto done; } 616 | if (spec[idx].isDigit) { 617 | while (idx < spec.length && spec[idx].isDigit) res.prec = res.prec*10 + (spec[idx++] - '0'); 618 | } else if (spec[idx] == '*') { 619 | idx++; res.prec = -1; 620 | } 621 | } 622 | } 623 | 624 | done: 625 | assert(idx == spec.length, "Parser error"); 626 | return res; 627 | } 628 | 629 | // Used to find end of the format specifier. 630 | // See: https://dlang.org/phobos/std_format.html for grammar and valid characters for fmt spec 631 | // Note: Nested array fmt spec is handled separately so no '(', ')' characters here 632 | private ulong getNextNonDigitFrom()(string fmt) 633 | { 634 | ulong idx; 635 | foreach (c; fmt) { 636 | if ("0123456789+-.,#*?$ ".indexOf(c) < 0) 637 | return idx; 638 | ++idx; 639 | } 640 | return idx; 641 | } 642 | 643 | private long getNestedArrayFmtLen()(string fmt) 644 | { 645 | long idx; int lvl; 646 | while (idx < fmt.length) 647 | { 648 | // detect next level of nested array format spec 649 | if (fmt[idx] == '(' // new nested array can be '%(' or '%-(' 650 | && ( 651 | (idx > 0 && fmt[idx-1] == '%') 652 | || (idx > 1 && fmt[idx-2] == '%' && fmt[idx-1] == '-') 653 | )) lvl++; 654 | // detect end of nested array format spec 655 | if (fmt[idx] == '%' && fmt.length > idx+1 && fmt[idx+1] == ')') { 656 | if (!lvl) return idx+2; 657 | else --lvl; 658 | } 659 | ++idx; 660 | } 661 | return -1; 662 | } 663 | 664 | @("getNestedArrayFmtLen") 665 | unittest 666 | { 667 | static assert(getNestedArrayFmtLen("%d%)foo") == 4); 668 | static assert(getNestedArrayFmtLen("%d%| %)foo") == 7); 669 | static assert(getNestedArrayFmtLen("%(%d%)%)foo") == 8); 670 | } 671 | 672 | // workaround for std.string.indexOf not working in betterC 673 | private ptrdiff_t indexOf()(string fmt, char c) 674 | { 675 | for (ptrdiff_t i = 0; i < fmt.length; ++i) 676 | if (fmt[i] == c) return i; 677 | return -1; 678 | } 679 | 680 | // Phobos version has bug in CTFE, see: https://issues.dlang.org/show_bug.cgi?id=20783 681 | private ptrdiff_t fixedLastIndexOf()(string s, string sub) 682 | { 683 | if (!__ctfe) assert(0); 684 | 685 | LOOP: for (ptrdiff_t i = s.length - sub.length; i >= 0; --i) 686 | { 687 | version (D_BetterC) 688 | { 689 | // workaround for missing symbol used by DMD 690 | for (ptrdiff_t j=0; j 0) 710 | { 711 | enum idx = fmt[lastSubEnd+2..$].fixedLastIndexOf("%|"); // delimiter separator used 712 | static if (idx >= 0) 713 | alias getNestedArrayFmt = AliasSeq!(fmt[0..lastSubEnd+2+idx], fmt[lastSubEnd+idx+4..$]); 714 | else 715 | alias getNestedArrayFmt = AliasSeq!(fmt[0..lastSubEnd+2], fmt[lastSubEnd+2..$]); 716 | } 717 | else 718 | { 719 | enum idx = fmt.fixedLastIndexOf("%|"); // delimiter separator used 720 | static if (idx >= 0) 721 | alias getNestedArrayFmt = AliasSeq!(fmt[0..idx], fmt[idx+2..$]); // we can return delimiter directly 722 | else { 723 | // we need to find end of inner fmt spec first 724 | static assert(fmt.length >= 2, "Invalid nested array element format specifier: " ~ fmt); 725 | enum startIdx = fmt.indexOf('%'); 726 | static assert(startIdx >=0, "No nested array element format specified"); 727 | enum endIdx = startIdx + 1 + getNextNonDigitFrom(fmt[startIdx + 1 .. $]); 728 | enum len = endIdx-startIdx+1; 729 | 730 | static if ((len == 2 && fmt[startIdx+1] == '(') || (len == 3 && fmt[startIdx+1..startIdx+3] == "-(")) 731 | { 732 | // further nested array fmt spec -> split by end of nested highest level 733 | enum nlen = fmt[1] == '(' ? (2 + getNestedArrayFmtLen(fmt[2..$])) : (3 + getNestedArrayFmtLen(fmt[3..$])); 734 | static assert(nlen > 0, "Invalid nested array format specifier: " ~ fmt); 735 | alias getNestedArrayFmt = AliasSeq!(fmt[0..nlen], fmt[nlen..$]); 736 | } 737 | else 738 | // split at the end of element fmt spec 739 | alias getNestedArrayFmt = AliasSeq!(fmt[0..endIdx+1], fmt[endIdx+1..$]); 740 | } 741 | } 742 | } 743 | 744 | @("getNestedArrayFmt") 745 | unittest 746 | { 747 | import std.meta : AliasSeq; 748 | static assert(getNestedArrayFmt!"%d " == AliasSeq!("%d", " ")); 749 | static assert(getNestedArrayFmt!"%d %|, " == AliasSeq!("%d ", ", ")); 750 | static assert(getNestedArrayFmt!"%(%d %|, %)" == AliasSeq!("%(%d %|, %)", "")); 751 | static assert(getNestedArrayFmt!"%(%d %|, %),-" == AliasSeq!("%(%d %|, %)", ",-")); 752 | static assert(getNestedArrayFmt!"foo%(%d %|, %)-%|;" == AliasSeq!("foo%(%d %|, %)-", ";")); 753 | } 754 | 755 | private struct FmtSpec 756 | { 757 | int idx; 758 | FMT type; 759 | string def; 760 | } 761 | 762 | // Nested array format specifier 763 | private struct ArrFmtSpec 764 | { 765 | int idx; 766 | string fmt; // item format 767 | string del; // delimiter 768 | bool esc; // escape strings and characters 769 | } 770 | 771 | /** 772 | * Splits format string based on the same rules as described here: https://dlang.org/phobos/std_format.html 773 | * In addition it supports 'p' as a pointer format specifier to be more compatible with `printf`. 774 | * It supports nested arrays format specifier too. 775 | */ 776 | template splitFmt(string fmt) { 777 | template spec(int j, FMT f, string def) { 778 | enum spec = FmtSpec(j, f, def); 779 | } 780 | template arrSpec(int j, string fmt, string del, bool esc) { 781 | enum arrSpec = ArrFmtSpec(j, fmt, del, esc); 782 | } 783 | 784 | template helper(int from, int j) { 785 | import std.typetuple : TypeTuple; 786 | enum idx = fmt[from .. $].indexOf('%'); 787 | static if (idx < 0) { 788 | enum helper = TypeTuple!(fmt[from .. $]); 789 | } 790 | else { 791 | enum idx1 = idx + from; 792 | static if (idx1 >= fmt.length - 1) { 793 | static assert (false, "Expected formatter after %"); 794 | } else { 795 | enum idx2 = idx1 + getNextNonDigitFrom(fmt[idx1+1 .. $]); 796 | // pragma(msg, "fmt: ", fmt[from .. idx2]); 797 | static if (fmt[idx2+1] == 's') 798 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.STR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 799 | else static if (fmt[idx2+1] == 'c') 800 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.CHR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 801 | else static if (fmt[idx2+1] == 'b') // TODO: should be binary, but use hex for now 802 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.HEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 803 | else static if (fmt[idx2+1].among('d', 'u')) 804 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.DEC, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 805 | else static if (fmt[idx2+1] == 'o') // TODO: should be octal, but use hex for now 806 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.DEC, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 807 | else static if (fmt[idx2+1] == 'x') 808 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.HEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 809 | else static if (fmt[idx2+1] == 'X') 810 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.UHEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 811 | else static if (fmt[idx2+1].among('e', 'E', 'f', 'F', 'g', 'G', 'a', 'A')) // TODO: float number formatters 812 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.FLT, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 813 | else static if (fmt[idx2+1] == 'p') 814 | enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.PTR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1)); 815 | else static if (fmt[idx2+1] == '%') 816 | enum helper = TypeTuple!(fmt[from .. idx1+1], helper!(idx2+2, j)); 817 | else static if (fmt[idx2+1] == '(' || fmt[idx2+1..idx2+3] == "-(") { 818 | // nested array format specifier 819 | enum l = fmt[idx2+1] == '(' 820 | ? getNestedArrayFmtLen(fmt[idx2+2..$]) 821 | : getNestedArrayFmtLen(fmt[idx2+3..$]); 822 | alias naSpec = getNestedArrayFmt!(fmt[idx2+2 .. idx2+2+l-2]); 823 | // pragma(msg, fmt[from .. idx1], "|", naSpec[0], "|", naSpec[1], "|"); 824 | enum helper = TypeTuple!( 825 | fmt[from .. idx1], 826 | arrSpec!(j, naSpec[0], naSpec[1], fmt[idx2+1] != '('), 827 | helper!(idx2+2+l, j+1)); 828 | } 829 | else static assert (false, "Invalid formatter '" ~ fmt[idx2+1] ~ "' in fmt='" ~ fmt ~ "'"); 830 | } 831 | } 832 | } 833 | 834 | template countFormatters(tup...) { 835 | static if (tup.length == 0) 836 | enum countFormatters = 0; 837 | else static if (is(typeof(tup[0]) == FmtSpec) || is(typeof(tup[0]) == ArrFmtSpec)) 838 | enum countFormatters = 1 + countFormatters!(tup[1 .. $]); 839 | else 840 | enum countFormatters = countFormatters!(tup[1 .. $]); 841 | } 842 | 843 | alias tokens = helper!(0, 0); 844 | alias numFormatters = countFormatters!tokens; 845 | } 846 | 847 | /// Returns string of enum member value 848 | string enumToStr(E)(E value) pure @safe nothrow @nogc 849 | { 850 | foreach (i, e; EnumMembers!E) 851 | { 852 | if (value == e) return __traits(allMembers, E)[i]; 853 | } 854 | return null; 855 | } 856 | 857 | size_t formatPtr(S)(auto ref scope S sink, ulong p) 858 | { 859 | pragma(inline); 860 | return formatPtr(sink, () @trusted { return cast(void*)p; }()); 861 | } 862 | 863 | size_t formatPtr(S)(auto ref scope S sink, const void* ptr) 864 | { 865 | pragma(inline); 866 | mixin SinkWriter!S; 867 | if (ptr is null) { write("null"); return 4; } 868 | else { 869 | import std.stdint : intptr_t; 870 | return sink.formatHex!((void*).sizeof*2)(cast(intptr_t)ptr); 871 | } 872 | } 873 | 874 | @("pointer") 875 | @safe unittest 876 | { 877 | char[100] buf; 878 | 879 | () @nogc @safe 880 | { 881 | assert(formatPtr(buf, 0x123) && buf[0..16] == "0000000000000123"); 882 | assert(formatPtr(buf, 0) && buf[0..4] == "null"); 883 | assert(formatPtr(buf, null) && buf[0..4] == "null"); 884 | }(); 885 | } 886 | 887 | alias Upper = Flag!"Upper"; 888 | 889 | pure @safe nothrow @nogc 890 | size_t formatHex(size_t W = 0, char fill = '0', Upper upper = Upper.no, S)(auto ref scope S sink, ulong val) 891 | { 892 | static if (is(S == NullSink)) 893 | { 894 | // just formatted length calculation 895 | import std.algorithm : max; 896 | size_t len = 0; 897 | if (!val) len = 1; 898 | else { while (val) { val >>= 4; len++; } } 899 | return max(W, len); 900 | } 901 | else 902 | { 903 | mixin SinkWriter!S; 904 | 905 | size_t len = 0; 906 | auto v = val; 907 | char[16] buf = void; 908 | 909 | while (v) { v >>= 4; len++; } 910 | static if (W > 0) { 911 | if (W > len) { 912 | buf[0..W-len] = '0'; 913 | len = W; 914 | } 915 | } 916 | 917 | v = val; 918 | if (v == 0) { 919 | static if (W == 0) { buf[0] = '0'; len = 1; } // special case for null 920 | } 921 | else { 922 | auto idx = len; 923 | while (v) { 924 | static if (upper) 925 | buf[--idx] = "0123456789ABCDEF"[v & 0x0f]; 926 | else buf[--idx] = "0123456789abcdef"[v & 0x0f]; 927 | v >>= 4; 928 | } 929 | } 930 | 931 | write(buf[0..len]); 932 | return len; 933 | } 934 | } 935 | 936 | @("hexadecimal") 937 | @safe @nogc unittest 938 | { 939 | char[100] buf; 940 | assert(formatHex(buf, 0x123) && buf[0..3] == "123"); 941 | assert(formatHex!10(buf, 0x123) && buf[0..10] == "0000000123"); 942 | assert(formatHex(buf, 0) && buf[0..1] == "0"); 943 | assert(formatHex!10(buf, 0) && buf[0..10] == "0000000000"); 944 | assert(formatHex!10(buf, 0xa23456789) && buf[0..10] == "0a23456789"); 945 | assert(formatHex!10(buf, 0x1234567890) && buf[0..10] == "1234567890"); 946 | assert(formatHex!(10, '0', Upper.yes)(buf, 0x1234567890a) && buf[0..11] == "1234567890A"); 947 | } 948 | 949 | size_t formatDecimal(size_t W = 0, char fillChar = ' ', S, T)(auto ref scope S sink, T val) 950 | if (is(typeof({ulong v = val;}))) 951 | { 952 | import bc.string.numeric : numDigits; 953 | 954 | static if (is(Unqual!T == bool)) size_t len = 1; 955 | else size_t len = numDigits(val); 956 | 957 | static if (is(S == NullSink)) 958 | { 959 | // just formatted length calculation 960 | import std.algorithm : max; 961 | return max(W, len); 962 | } 963 | else 964 | { 965 | mixin SinkWriter!S; 966 | 967 | ulong v; 968 | char[20] buf = void; // max number of digits for 8bit numbers is 20 969 | size_t idx; 970 | 971 | static if (isSigned!T) 972 | { 973 | import std.ascii : isWhite; 974 | if (_expect(val < 0, false)) 975 | { 976 | if (_expect(val == long.min, false)) 977 | { 978 | // special case for unconvertable value 979 | write("-9223372036854775808"); 980 | return 20; 981 | } 982 | 983 | static if (!isWhite(fillChar)) buf[idx++] = '-'; // to write minus character after padding 984 | v = -long(val); 985 | } 986 | else v = val; 987 | } 988 | else v = val; 989 | 990 | static if (W > 0) { 991 | if (W > len) { 992 | buf[idx .. idx + W - len] = fillChar; 993 | idx += W-len; 994 | len = W; 995 | } 996 | } 997 | 998 | static if (isSigned!T && isWhite(fillChar)) 999 | if (val < 0) buf[idx++] = '-'; 1000 | 1001 | if (v == 0) buf[idx++] = '0'; 1002 | else { 1003 | idx = len; 1004 | while (v) { 1005 | buf[--idx] = "0123456789"[v % 10]; 1006 | v /= 10; 1007 | } 1008 | } 1009 | 1010 | write(buf[0..len]); 1011 | return len; 1012 | } 1013 | } 1014 | 1015 | @("decimal") 1016 | @safe @nogc unittest 1017 | { 1018 | char[100] buf; 1019 | assert(formatDecimal!10(buf, -1234) && buf[0..10] == " -1234"); 1020 | assert(formatDecimal!10(buf, 0) && buf[0..10] == " 0"); 1021 | assert(formatDecimal(buf, -1234) && buf[0..5] == "-1234"); 1022 | assert(formatDecimal(buf, 0) && buf[0..1] == "0"); 1023 | assert(formatDecimal!3(buf, 1234) && buf[0..4] == "1234"); 1024 | assert(formatDecimal!3(buf, -1234) && buf[0..5] == "-1234"); 1025 | assert(formatDecimal!3(buf, 0) && buf[0..3] == " 0"); 1026 | assert(formatDecimal!(3,'0')(buf, 0) && buf[0..3] == "000"); 1027 | assert(formatDecimal!(3,'a')(buf, 0) && buf[0..3] == "aa0"); 1028 | assert(formatDecimal!(10, '0')(buf, -1234) && buf[0..10] == "-000001234"); 1029 | assert(formatDecimal(buf, true) && buf[0..1] == "1"); 1030 | } 1031 | 1032 | size_t formatFloat(S)(auto ref scope S sink, double val) 1033 | { 1034 | import core.stdc.stdio : snprintf; 1035 | import std.algorithm : min; 1036 | char[20] buf = void; 1037 | auto len = () @trusted { return snprintf(&buf[0], 20, "%g", val); }(); 1038 | len = min(len, 19); 1039 | static if (!is(S == NullSink)) 1040 | { 1041 | mixin SinkWriter!S; 1042 | write(buf[0..len]); 1043 | } 1044 | return len; 1045 | } 1046 | 1047 | @("float") 1048 | @safe unittest 1049 | { 1050 | char[100] buf; 1051 | assert(formatFloat(buf, 1.2345) && buf[0..6] == "1.2345"); 1052 | assert(formatFloat(buf, double.init) && buf[0..3] == "nan"); 1053 | assert(formatFloat(buf, double.infinity) && buf[0..3] == "inf"); 1054 | } 1055 | 1056 | size_t formatUUID(S)(auto ref scope S sink, UUID val) 1057 | { 1058 | static if (!is(S == NullSink)) 1059 | { 1060 | import std.meta : AliasSeq; 1061 | 1062 | mixin SinkWriter!S; 1063 | 1064 | alias skipSeq = AliasSeq!(8, 13, 18, 23); 1065 | alias byteSeq = AliasSeq!(0,2,4,6,9,11,14,16,19,21,24,26,28,30,32,34); 1066 | 1067 | char[36] buf = void; 1068 | 1069 | static foreach (pos; skipSeq) 1070 | buf[pos] = '-'; 1071 | 1072 | static foreach (i, pos; byteSeq) 1073 | { 1074 | buf[pos ] = toChar!char(val.data[i] >> 4); 1075 | buf[pos+1] = toChar!char(val.data[i] & 0x0F); 1076 | } 1077 | 1078 | write(buf[0..36]); 1079 | } 1080 | return 36; 1081 | } 1082 | 1083 | version (D_BetterC) {} 1084 | else 1085 | @("UUID") 1086 | @safe unittest 1087 | { 1088 | char[100] buf; 1089 | assert(formatUUID(buf, UUID([138, 179, 6, 14, 44, 186, 79, 35, 183, 76, 181, 45, 179, 189, 251, 70])) == 36); 1090 | assert(buf[0..36] == "8ab3060e-2cba-4f23-b74c-b52db3bdfb46"); 1091 | } 1092 | 1093 | /** 1094 | * Formats SysTime as ISO extended string. 1095 | * Only UTC format supported. 1096 | */ 1097 | size_t formatSysTime(S)(auto ref scope S sink, SysTime val) @trusted 1098 | { 1099 | mixin SinkWriter!S; 1100 | 1101 | // Note: we don't format based on the timezone set in SysTime, but just use UTC here 1102 | enum hnsecsToUnixEpoch = 621_355_968_000_000_000L; 1103 | enum hnsecsFrom1601 = 504_911_232_000_000_000L; 1104 | 1105 | static immutable char[7] invalidTimeBuf = "invalid"; 1106 | 1107 | long time = __traits(getMember, val, "_stdTime"); // access private field 1108 | long hnsecs = time % 10_000_000; 1109 | 1110 | // check for invalid time value 1111 | version (Windows) { 1112 | if (time < hnsecsFrom1601) { write(invalidTimeBuf); return invalidTimeBuf.length; } 1113 | } 1114 | 1115 | static if (is(S == NullSink)) 1116 | { 1117 | // just count required number of characters needed for hnsecs 1118 | int len = 20; // fixed part for date time with just seconds resolution (including 'Z') 1119 | if (hnsecs == 0) return len; // no fract seconds part 1120 | len += 2; // dot and at least one number 1121 | foreach (i; [1_000_000, 100_000, 10_000, 1_000, 100, 10]) 1122 | { 1123 | hnsecs %= i; 1124 | if (hnsecs == 0) break; 1125 | len++; 1126 | } 1127 | return len; 1128 | } 1129 | else 1130 | { 1131 | char[28] buf; // maximal length for UTC extended ISO string 1132 | 1133 | version (Posix) 1134 | { 1135 | import core.sys.posix.sys.types : time_t; 1136 | import core.sys.posix.time : gmtime_r, tm; 1137 | 1138 | time -= hnsecsToUnixEpoch; // convert to unix time but still with hnsecs 1139 | 1140 | // split to hnsecs and time in seconds 1141 | time_t unixTime = time / 10_000_000; 1142 | 1143 | tm timeSplit; 1144 | gmtime_r(&unixTime, &timeSplit); 1145 | 1146 | buf.nogcFormatTo!"%04d-%02d-%02dT%02d:%02d:%02d"( 1147 | timeSplit.tm_year + 1900, 1148 | timeSplit.tm_mon + 1, 1149 | timeSplit.tm_mday, 1150 | timeSplit.tm_hour, 1151 | timeSplit.tm_min, 1152 | timeSplit.tm_sec 1153 | ); 1154 | } 1155 | else version (Windows) 1156 | { 1157 | import core.sys.windows.winbase : FILETIME, FileTimeToSystemTime, SYSTEMTIME; 1158 | import core.sys.windows.winnt : ULARGE_INTEGER; 1159 | 1160 | ULARGE_INTEGER ul; 1161 | ul.QuadPart = cast(ulong)time - hnsecsFrom1601; 1162 | 1163 | FILETIME ft; 1164 | ft.dwHighDateTime = ul.HighPart; 1165 | ft.dwLowDateTime = ul.LowPart; 1166 | 1167 | SYSTEMTIME stime; 1168 | FileTimeToSystemTime(&ft, &stime); 1169 | 1170 | buf.nogcFormatTo!"%04d-%02d-%02dT%02d:%02d:%02d"( 1171 | stime.wYear, 1172 | stime.wMonth, 1173 | stime.wDay, 1174 | stime.wHour, 1175 | stime.wMinute, 1176 | stime.wSecond 1177 | ); 1178 | } 1179 | else static assert(0, "SysTime format not supported for this platform yet"); 1180 | 1181 | if (hnsecs == 0) 1182 | { 1183 | buf[19] = 'Z'; 1184 | write(buf[0..20]); 1185 | return 20; 1186 | } 1187 | 1188 | buf[19] = '.'; 1189 | 1190 | int len = 20; 1191 | foreach (i; [1_000_000, 100_000, 10_000, 1_000, 100, 10, 1]) 1192 | { 1193 | buf[len++] = cast(char)(hnsecs / i + '0'); 1194 | hnsecs %= i; 1195 | if (hnsecs == 0) break; 1196 | } 1197 | buf[len++] = 'Z'; 1198 | write(buf[0..len]); 1199 | return len; 1200 | } 1201 | } 1202 | 1203 | version (D_BetterC) {} 1204 | else 1205 | @("SysTime") 1206 | @safe unittest 1207 | { 1208 | char[100] buf; 1209 | 1210 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1234567Z")) == 28); 1211 | assert(buf[0..28] == "2020-06-08T14:25:30.1234567Z"); 1212 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.123456Z")) == 27); 1213 | assert(buf[0..27] == "2020-06-08T14:25:30.123456Z"); 1214 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.12345Z")) == 26); 1215 | assert(buf[0..26] == "2020-06-08T14:25:30.12345Z"); 1216 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1234Z")) == 25); 1217 | assert(buf[0..25] == "2020-06-08T14:25:30.1234Z"); 1218 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.123Z")) == 24); 1219 | assert(buf[0..24] == "2020-06-08T14:25:30.123Z"); 1220 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.12Z")) == 23); 1221 | assert(buf[0..23] == "2020-06-08T14:25:30.12Z"); 1222 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1Z")) == 22); 1223 | assert(buf[0..22] == "2020-06-08T14:25:30.1Z"); 1224 | assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30Z")) == 20); 1225 | assert(buf[0..20] == "2020-06-08T14:25:30Z"); 1226 | version (Posix) { 1227 | assert(formatSysTime(buf, SysTime.init) == 20); 1228 | assert(buf[0..20] == "0001-01-01T00:00:00Z"); 1229 | } 1230 | else version (Windows) { 1231 | assert(formatSysTime(buf, SysTime.init) == 7); 1232 | assert(buf[0..7] == "invalid"); 1233 | } 1234 | 1235 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1234567Z")) == 28); 1236 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.123456Z")) == 27); 1237 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.12345Z")) == 26); 1238 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1234Z")) == 25); 1239 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.123Z")) == 24); 1240 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.12Z")) == 23); 1241 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1Z")) == 22); 1242 | assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30Z")) == 20); 1243 | } 1244 | 1245 | /** 1246 | * Formats duration. 1247 | * It uses custom formatter that is inspired by std.format output, but a bit shorter. 1248 | * Note: ISO 8601 was considered, but it's not as human readable as used format. 1249 | */ 1250 | size_t formatDuration(S)(auto ref scope S sink, Duration val) 1251 | { 1252 | mixin SinkWriter!S; 1253 | 1254 | enum secondsInDay = 86_400; 1255 | enum secondsInHour = 3_600; 1256 | enum secondsInMinute = 60; 1257 | 1258 | long totalHNS = __traits(getMember, val, "_hnsecs"); // access private member 1259 | if (totalHNS < 0) { write("-"); totalHNS = -totalHNS; } 1260 | 1261 | immutable long fracSecs = totalHNS % 10_000_000; 1262 | long totalSeconds = totalHNS / 10_000_000; 1263 | 1264 | if (totalSeconds) 1265 | { 1266 | immutable long days = totalSeconds / secondsInDay; 1267 | long seconds = totalSeconds % secondsInDay; 1268 | if (days) advance(s.nogcFormatTo!"%d days"(days)); 1269 | if (seconds) 1270 | { 1271 | immutable hours = seconds / secondsInHour; 1272 | seconds %= secondsInHour; 1273 | if (hours) 1274 | advance(days ? s.nogcFormatTo!", %d hrs"(hours) : s.nogcFormatTo!"%d hrs"(hours)); 1275 | 1276 | if (seconds) 1277 | { 1278 | immutable minutes = seconds / secondsInMinute; 1279 | seconds %= secondsInMinute; 1280 | if (minutes) 1281 | advance(days || hours ? s.nogcFormatTo!", %d mins"(minutes) : s.nogcFormatTo!"%d mins"(minutes)); 1282 | 1283 | if (seconds) 1284 | advance(days || hours || minutes ? s.nogcFormatTo!", %d secs"(seconds) : s.nogcFormatTo!"%d secs"(seconds)); 1285 | } 1286 | } 1287 | } 1288 | 1289 | if (fracSecs) 1290 | { 1291 | immutable msecs = fracSecs / 10_000; 1292 | int usecs = fracSecs % 10_000; 1293 | 1294 | if (msecs | usecs) 1295 | { 1296 | advance(totalSeconds ? s.nogcFormatTo!", %d"(msecs) : s.nogcFormatTo!"%d"(msecs)); 1297 | 1298 | if (usecs) 1299 | { 1300 | char[5] buf = void; 1301 | buf[0] = '.'; 1302 | 1303 | int ulen = 1; 1304 | foreach (i; [1_000, 100, 10, 1]) 1305 | { 1306 | buf[ulen++] = cast(char)(usecs / i + '0'); 1307 | usecs %= i; 1308 | if (usecs == 0) break; 1309 | } 1310 | write(buf[0..ulen]); 1311 | } 1312 | 1313 | write(" ms"); 1314 | } 1315 | } 1316 | 1317 | if (!totalLen) write("0 ms"); 1318 | 1319 | return totalLen; 1320 | } 1321 | 1322 | version (D_BetterC) {} 1323 | else 1324 | @("duration") 1325 | @safe unittest 1326 | { 1327 | import core.time; 1328 | char[100] buf; 1329 | 1330 | assert(formatDuration(buf, 1.seconds) == 6); 1331 | assert(buf[0..6] == "1 secs"); 1332 | 1333 | assert(formatDuration(buf, 1.seconds + 15.msecs + 5.hnsecs) == 18); 1334 | assert(buf[0..18] == "1 secs, 15.0005 ms"); 1335 | 1336 | assert(formatDuration(buf, 1.seconds + 1215.msecs + 15.hnsecs) == 19); 1337 | assert(buf[0..19] == "2 secs, 215.0015 ms"); 1338 | 1339 | assert(formatDuration(buf, 5.days) == 6); 1340 | assert(buf[0..6] == "5 days"); 1341 | 1342 | assert(formatDuration(buf, 5.days + 25.hours) == 13); 1343 | assert(buf[0..13] == "6 days, 1 hrs"); 1344 | 1345 | assert(formatDuration(buf, 5.days + 25.hours + 78.minutes) == 22); 1346 | assert(buf[0..22] == "6 days, 2 hrs, 18 mins"); 1347 | 1348 | assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds) == 31); 1349 | assert(buf[0..31] == "6 days, 2 hrs, 19 mins, 42 secs"); 1350 | 1351 | assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs) == 39); 1352 | assert(buf[0..39] == "6 days, 2 hrs, 19 mins, 44 secs, 321 ms"); 1353 | 1354 | assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs + 1987.usecs) == 43); 1355 | assert(buf[0..43] == "6 days, 2 hrs, 19 mins, 44 secs, 322.987 ms"); 1356 | 1357 | assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs + 1987.usecs + 15.hnsecs) == 44); 1358 | assert(buf[0..44] == "6 days, 2 hrs, 19 mins, 44 secs, 322.9885 ms"); 1359 | 1360 | assert(formatDuration(buf, -42.msecs) == 6); 1361 | assert(buf[0..6] == "-42 ms"); 1362 | 1363 | assert(formatDuration(buf, Duration.zero) == 4); 1364 | assert(buf[0..4] == "0 ms"); 1365 | } 1366 | 1367 | @safe pure nothrow @nogc Char toChar(Char)(size_t i) 1368 | { 1369 | pragma(inline); 1370 | if (i <= 9) return cast(Char)('0' + i); 1371 | else return cast(Char)('a' + (i-10)); 1372 | } 1373 | 1374 | /// Output range wrapper for used sinks (so it can be used in toString functions) 1375 | private struct SinkWrap(S) 1376 | { 1377 | private S s; 1378 | 1379 | static if (isArray!S && is(ForeachType!S : char)) 1380 | mixin SinkWriter!(S, false); 1381 | else static if (isPointer!S) 1382 | mixin SinkWriter!(PointerTarget!S, false); 1383 | else static assert(0, "Unsupported sink type: " ~ S.stringof); 1384 | 1385 | this(S sink) @safe pure nothrow @nogc 1386 | { 1387 | this.s = sink; 1388 | } 1389 | } 1390 | 1391 | // helper to create `SinkWrap` that handles various sink types 1392 | private auto sinkWrap(S)(auto ref scope S sink) @trusted // we're only using this internally and don't escape the pointer 1393 | { 1394 | static if (isStaticArray!S && is(ForeachType!S : char)) 1395 | return SinkWrap!(char[])(sink[]); // we need to slice it 1396 | else static if (isArray!S && is(ForeachType!S : char)) 1397 | return SinkWrap!(char[])(sink); 1398 | else static if (is(S == struct)) 1399 | return SinkWrap!(S*)(&sink); // work with a pointer to an original sink (ie `MallocBuffer`) 1400 | else static assert(0, "Unsupported sink type: " ~ S.stringof); 1401 | } 1402 | 1403 | @("sink wrapper") 1404 | unittest 1405 | { 1406 | char[42] buf; 1407 | auto sink = sinkWrap(buf); 1408 | sink.put("foo"); 1409 | assert(sink.totalLen == 3); 1410 | assert(buf[0..3] == "foo"); 1411 | } 1412 | 1413 | // helper functions used in formatters to write formatted string to sink 1414 | private mixin template SinkWriter(S, bool field = true) 1415 | { 1416 | size_t totalLen; 1417 | static if (isArray!S && is(ForeachType!S : char)) 1418 | { 1419 | static if (field) char[] s = sink[]; 1420 | 1421 | @nogc pure nothrow @trusted 1422 | { 1423 | void advance(size_t len) 1424 | { 1425 | s = s[len..$]; 1426 | totalLen += len; 1427 | } 1428 | 1429 | void write(const(char)[] str) { 1430 | s[0..str.length] = str; 1431 | advance(str.length); 1432 | } 1433 | 1434 | void write(char ch) { 1435 | s[0] = ch; 1436 | advance(1); 1437 | } 1438 | } 1439 | } 1440 | else static if (is(S == NullSink)) 1441 | { 1442 | @nogc pure nothrow @safe: 1443 | static if (field) alias s = sink; 1444 | void advance(size_t len) { totalLen += len; } 1445 | void write(const(char)[] str) { advance(str.length); } 1446 | void write(char ch) { advance(1); } 1447 | } 1448 | else 1449 | { 1450 | static if (field) alias s = sink; 1451 | import std.range : rput = put; 1452 | void advance()(size_t len) { totalLen += len; } 1453 | void write()(const(char)[] str) { rput(s, str); advance(str.length); } 1454 | void write()(char ch) { rput(s, ch); advance(1); } 1455 | } 1456 | 1457 | alias put = write; // output range interface 1458 | } 1459 | -------------------------------------------------------------------------------- /source/bc/string/numeric.d: -------------------------------------------------------------------------------- 1 | module bc.string.numeric; 2 | 3 | import bc.core.intrinsics; 4 | import std.traits : isIntegral, isSigned; 5 | 6 | pure @safe nothrow @nogc: 7 | 8 | /// Calculates number of digits of the provided number including sign character for negative numbers. 9 | int numDigits(T)(T number) 10 | if (isIntegral!T) 11 | { 12 | pragma(inline); 13 | 14 | // handle negative numbers 15 | static if (isSigned!T) 16 | { 17 | if (_expect(number == long.min, false)) return 20; // for safety as it can't be converted to positive number 18 | if (number < 0) return 1 + numDigits(-long(number)); 19 | } 20 | 21 | if (__ctfe) 22 | { 23 | // simple ctfe version 24 | import std.traits : Unqual; 25 | if (_expect(!number, false)) return 1; // special case for 0 26 | int digits = 0; 27 | alias UT = Unqual!T; 28 | UT n = number; 29 | while (n) { n /= 10; digits++; } 30 | return digits; 31 | } 32 | else 33 | { 34 | // specialization for 1B numbers 35 | static if (T.sizeof == 1) 36 | { 37 | static immutable ubyte[256] dmap = () { 38 | ubyte[256] ret; 39 | for (int i=0; i < 256; ++i) ret[i] = cast(ubyte)numDigits(i); 40 | return ret; 41 | }(); 42 | return dmap[number]; 43 | } 44 | else 45 | { 46 | if (_expect(number >= 10_000, false)) { 47 | if (number >= 10_000_000) { 48 | if (number >= 100_000_000) { 49 | if (number >= 1_000_000_000) { 50 | static if (T.sizeof <= 4) return 10; 51 | else { 52 | // 8bit number branch 53 | if (number >= 10_000_000_000) { 54 | if (number >= 100_000_000_000) { 55 | if (number >= 1_000_000_000_000) { 56 | if (number >= 10_000_000_000_000) { 57 | if (number >= 100_000_000_000_000) { 58 | if (number >= 1_000_000_000_000_000) { 59 | if (number >= 10_000_000_000_000_000) { 60 | if (number >= 100_000_000_000_000_000) { 61 | if (number >= 1_000_000_000_000_000_000) { 62 | if (number >= 10_000_000_000_000_000_000UL) 63 | return 20; 64 | return 19; 65 | } 66 | return 18; 67 | } 68 | return 17; 69 | } 70 | return 16; 71 | } 72 | return 15; 73 | } 74 | return 14; 75 | } 76 | return 13; 77 | } 78 | return 12; 79 | } 80 | return 11; 81 | } 82 | return 10; 83 | } 84 | } 85 | return 9; 86 | } 87 | return 8; 88 | } 89 | if (number >= 100_000) { 90 | if (number >= 1_000_000) return 7; 91 | return 6; 92 | } 93 | return 5; 94 | } 95 | if (number >= 100) { 96 | if (number >= 1_000) return 4; 97 | return 3; 98 | } 99 | if (number >= 10) return 2; 100 | return 1; 101 | } 102 | } 103 | } 104 | 105 | /// 106 | @("numDigits CT") 107 | @safe unittest 108 | { 109 | // ctfe tests 110 | static assert(numDigits(0) == 1); 111 | static assert(numDigits(11) == 2); 112 | static assert(numDigits(-1) == 2); 113 | 114 | static assert(numDigits(int.min) == 11); 115 | static assert(numDigits(int.max) == 10); 116 | static assert(numDigits(long.min) == 20); 117 | static assert(numDigits(long.max) == 19); 118 | static assert(numDigits(ulong.min) == 1); 119 | static assert(numDigits(ulong.max) == 20); 120 | } 121 | 122 | /// 123 | @("numDigits RT") 124 | @safe unittest 125 | { 126 | // uint.max = 4_294_967_295 -> 10 digits 127 | // ulong.max = 18_446_744_073_709_551_615 -> 20 digits 128 | // long max = 9_223_372_036_854_775_807 -> 19 digits 129 | 130 | // rt tests 131 | assert(numDigits(0) == 1); 132 | assert(numDigits(11) == 2); 133 | assert(numDigits(-1) == 2); 134 | assert(numDigits(-123) == 4); 135 | 136 | assert(numDigits(int.min) == 11); 137 | assert(numDigits(int.max) == 10); 138 | assert(numDigits(long.min) == 20); 139 | assert(numDigits(long.max) == 19); 140 | assert(numDigits(ulong.min) == 1); 141 | assert(numDigits(ulong.max) == 20); 142 | 143 | long n = 10; 144 | foreach (i; 0..20) 145 | { 146 | import std.math : pow; 147 | assert(numDigits(pow(10UL, i)) == i+1); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /source/bc/string/package.d: -------------------------------------------------------------------------------- 1 | module bc.string; 2 | 3 | public import bc.string.ascii; 4 | public import bc.string.conv; 5 | public import bc.string.format; 6 | public import bc.string.numeric; 7 | public import bc.string.string; 8 | 9 | import core.stdc.stdio; 10 | 11 | version (CI_MAIN) 12 | { 13 | // workaround for dub not supporting unittests with betterC 14 | version (D_BetterC) 15 | { 16 | extern(C) int main() 17 | { 18 | version (unittest) 19 | { 20 | import std.meta : AliasSeq; 21 | 22 | alias modules = AliasSeq!(bc.string.ascii, bc.string.conv, bc.string.format, bc.string.numeric, bc.string.string); 23 | static foreach (m; modules) 24 | { 25 | static foreach(u; __traits(getUnitTests, m)) { 26 | static if (__traits(getAttributes, u).length) 27 | printf("unittest %s:%d | '" ~ __traits(getAttributes, u)[0] ~ "'\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]); 28 | else 29 | printf("unittest %s:%d\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]); 30 | u(); 31 | } 32 | } 33 | debug printf("All unit tests have been run successfully.\n"); 34 | return 0; 35 | } 36 | else return rtTest(); 37 | } 38 | 39 | // workaround for linking error: `undefined reference to `_D4core8internal5array8equality__T8__equalsTaTaZQoFNaNbNiNeMxAaMxQeZb'` 40 | bool equals(S1, S2)(S1 a, S2 b) 41 | { 42 | if (a.length != b.length) return false; 43 | import core.stdc.string : strncmp; 44 | return strncmp(a.ptr, b.ptr, a.length) == 0; 45 | } 46 | } 47 | else 48 | { 49 | int main() 50 | { 51 | version (unittest) return 0; // run automagically 52 | else return rtTest(); 53 | } 54 | 55 | bool equals(S1, S2)(S1 a, S2 b) { return a == b; } 56 | } 57 | 58 | int rtTest()() 59 | { 60 | // just a compilation test - same as in the README to be sure it actually works.. 61 | 62 | import bc.core.memory; 63 | import bc.internal.utf : byCodeUnit; 64 | import core.stdc.string : strlen; 65 | import std.algorithm : filter; 66 | import std.range : chunks; 67 | 68 | assert(nogcFormat!"abcd abcd".equals("abcd abcd")); 69 | assert(nogcFormat!"123456789a".equals("123456789a")); 70 | version (D_NoBoundsChecks) {} 71 | else version (D_BetterC) {} 72 | else 73 | { 74 | () @trusted 75 | { 76 | import core.exception : RangeError; 77 | import std.exception : assertThrown; 78 | char[5] buf; 79 | version (assert) assertThrown!RangeError(buf.nogcFormatTo!"123412341234"); 80 | }(); 81 | } 82 | 83 | // literal escape 84 | assert(nogcFormat!"123 %%".equals("123 %")); 85 | assert(nogcFormat!"%%%%".equals("%%")); 86 | 87 | // %d 88 | assert(nogcFormat!"%d"(1234).equals("1234")); 89 | assert(nogcFormat!"%4d"(42).equals(" 42")); 90 | assert(nogcFormat!"%04d"(42).equals("0042")); 91 | assert(nogcFormat!"%04d"(-42).equals("-042")); 92 | assert(nogcFormat!"ab%dcd"(1234).equals("ab1234cd")); 93 | assert(nogcFormat!"ab%d%d"(1234, 56).equals("ab123456")); 94 | 95 | // %x 96 | assert(nogcFormat!"0x%x"(0x1234).equals("0x1234")); 97 | 98 | // %p 99 | assert(nogcFormat!("%p")(0x1234).equals("0000000000001234")); 100 | 101 | // %s 102 | assert(nogcFormat!"12345%s"("12345").equals("1234512345")); 103 | assert(nogcFormat!"12345%s"(12345).equals("1234512345")); 104 | enum Floop {XXX, YYY, ZZZ} 105 | assert(nogcFormat!"12345%s"(Floop.YYY).equals("12345YYY")); 106 | char[4] str = "foo\0"; 107 | assert(nogcFormat!"%s"(&str[0]).equals("foo")); 108 | 109 | version (D_BetterC) {} // can't import std.uuid in betterC 110 | else 111 | { 112 | import std.uuid; 113 | assert(nogcFormat!"%s"( 114 | parseUUID("22390768-cced-325f-8f0f-cfeaa19d0ccd")) 115 | .equals("22390768-cced-325f-8f0f-cfeaa19d0ccd")); 116 | } 117 | 118 | // array format 119 | version (D_BetterC) 120 | { 121 | int[] arr = (cast(int*)enforceMalloc(int.sizeof*10))[0..10]; 122 | foreach (i; 0..10) arr[i] = i; 123 | scope (exit) pureFree(arr.ptr); 124 | } 125 | else auto arr = [0,1,2,3,4,5,6,7,8,9]; 126 | 127 | assert(nogcFormat!"foo %(%d %)"(arr[1..4]).equals("foo 1 2 3")); 128 | assert(nogcFormat!"foo %-(%d %)"(arr[1..4]).equals("foo 1 2 3")); 129 | assert(nogcFormat!"foo %(-%d-%|, %)"(arr[1..4]).equals("foo -1-, -2-, -3-")); 130 | assert(nogcFormat!"%(0x%02x %)"(arr[1..4]).equals("0x01 0x02 0x03")); 131 | assert(nogcFormat!"%(%(%d %)\n%)"(arr[1..$].chunks(3)).equals("1 2 3\n4 5 6\n7 8 9")); 132 | 133 | // range format 134 | auto r = arr.filter!(a => a < 5); 135 | assert(nogcFormat!"%s"(r).equals("[0, 1, 2, 3, 4]")); 136 | 137 | // Arg num 138 | assert(!__traits(compiles, nogcFormat!"abc"(5))); 139 | assert(!__traits(compiles, nogcFormat!"%d"())); 140 | assert(!__traits(compiles, nogcFormat!"%d a %d"(5))); 141 | 142 | // Format error 143 | assert(!__traits(compiles, nogcFormat!"%"())); 144 | assert(!__traits(compiles, nogcFormat!"abcd%d %"(15))); 145 | assert(!__traits(compiles, nogcFormat!"%$"(1))); 146 | assert(!__traits(compiles, nogcFormat!"%d"("hello"))); 147 | assert(!__traits(compiles, nogcFormat!"%x"("hello"))); 148 | 149 | assert(nogcFormat!"Hello %s"(5).equals("Hello 5")); 150 | 151 | struct Foo { int x, y; } 152 | assert(nogcFormat!("Hello %s")(Foo(1, 2)).equals("Hello Foo(x=1, y=2)")); 153 | 154 | version (D_BetterC) 155 | { 156 | struct Nullable(T) // can't be instanciated in betterC - fake just for the UT 157 | { 158 | T get() { return T.init; } 159 | bool isNull() { return true; } 160 | void nullify() {} 161 | } 162 | } 163 | else import std.typecons : Nullable; 164 | struct Msg { Nullable!string foo; } 165 | assert(nogcFormat!"%s"(Msg.init).equals("Msg(foo=null)")); 166 | 167 | RCString s = "abcd"; 168 | assert(nogcFormat!"%s"(s).equals("abcd")); 169 | 170 | version (D_BetterC) {} 171 | else version (linux) 172 | { 173 | import bc.core.system.backtrace : TraceInfo; 174 | StringZ buf; 175 | buf.nogcFormatTo!"where am i: %s"(TraceInfo.current); 176 | printf("%s\n", buf.ptr); 177 | } 178 | 179 | assert(numDigits(0) == 1); 180 | assert(numDigits(11) == 2); 181 | assert(numDigits(-1) == 2); 182 | 183 | assert(numDigits(int.min) == 11); 184 | assert(numDigits(int.max) == 10); 185 | assert(numDigits(long.min) == 20); 186 | assert(numDigits(long.max) == 19); 187 | assert(numDigits(ulong.min) == 1); 188 | assert(numDigits(ulong.max) == 20); 189 | 190 | string str2 = "abc"; 191 | 192 | // Intended usage 193 | assert(strlen(str2.tempCString()) == 3); 194 | 195 | // Correct usage 196 | auto tmp = str2.tempCString(); 197 | assert(strlen(tmp) == 3); // or `tmp.ptr`, or `tmp.buffPtr` 198 | 199 | // Incorrect usage 200 | auto pInvalid1 = str2.tempCString().ptr; 201 | const char* pInvalid2 = str2.tempCString(); 202 | 203 | RCString rcs; 204 | rcs ~= "fo"; 205 | rcs ~= 'o'; 206 | rcs ~= "bar"; 207 | rcs ~= "baz".byCodeUnit.filter!(a => a == 'z'); 208 | assert(rcs.length == "foobarz".length); 209 | assert(rcs.data.equals("foobarz")); 210 | assert(rcs.equals("foobarz")); 211 | assert(rcs.ptr == &rcs.data[0]); 212 | assert((rcs.ptr["foobarz".length]) == 0); 213 | 214 | // construction from write like params 215 | rcs = RCString.from("foo", 42, "bar"); 216 | assert(rcs.equals("foo42bar")); 217 | 218 | auto ss = String("Hello"); 219 | assert(ss[].equals("Hello"), s[]); 220 | ss ~= " String"; 221 | assert(ss[].equals("Hello String"), ss[]); 222 | auto ss2 = ss.clone(); 223 | assert(ss[].equals(ss2[])); 224 | assert(ss.ptr != ss2.ptr); 225 | 226 | auto ss3 = ss.move(); 227 | assert(ss.length == 0); 228 | assert(ss3[].equals("Hello String")); 229 | 230 | enum indented = ` 231 | SELECT foo, bar, baz 232 | FROM foos 233 | WHERE id=ANY($1) AND type_id IN ( 234 | SELECT id FROM bars WHERE owner=$2 235 | )`; 236 | 237 | enum dedented = 238 | "SELECT foo, bar, baz\n" ~ 239 | " FROM foos\n" ~ 240 | " WHERE id=ANY($1) AND type_id IN (\n" ~ 241 | " SELECT id FROM bars WHERE owner=$2\n" ~ 242 | ")"; 243 | 244 | // assert(dedent(indented) == dedented); 245 | 246 | printf("test run completed\n"); 247 | return 0; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /source/bc/string/string.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Some helper functions to work with strings 3 | */ 4 | module bc.string.string; 5 | 6 | import bc.core.intrinsics; 7 | import bc.core.memory : enforceMalloc, enforceRealloc, heapAlloc, heapDealloc; 8 | import std.range : ElementEncodingType, hasLength, isInputRange; 9 | import std.traits : ForeachType, isSomeChar, isSomeString, isStaticArray, Unqual; 10 | // debug import core.stdc.stdio; 11 | 12 | nothrow @nogc: 13 | 14 | alias CString = const(char)[]; 15 | 16 | template isAcceptableString(S) 17 | { 18 | enum isAcceptableString = 19 | (isInputRange!S || isSomeString!S || isStaticArray!S) && 20 | isSomeChar!(ElementEncodingType!S); 21 | } 22 | 23 | /** 24 | * Temporary string buffer. 25 | * It can be used to build temporary \0 ended C strings. 26 | * For lengths < 255, it uses static char array, mallocated buffer otherwise. 27 | * 28 | * NOTE: be careful that pointer becomes invalid as soon as the struct comes out of scope! 29 | * NOTE: inspired by std.internal.cstring.TempCStringBuffer in Phobos library 30 | */ 31 | struct TempCString(C) 32 | { 33 | @trusted pure nothrow @nogc: 34 | 35 | @disable this(); 36 | @disable this(this); 37 | alias ptr this; 38 | 39 | @property inout(C)* bufPtr() inout 40 | { 41 | return _ptr == useStack ? _buf.ptr : _ptr; 42 | } 43 | 44 | @property const(C)* ptr() const { return bufPtr; } 45 | const(C)[] opIndex() const pure { return bufPtr[0 .. _length]; } 46 | 47 | ~this() 48 | { 49 | if (_ptr != useStack) 50 | { 51 | import core.memory : pureFree; 52 | pureFree(_ptr); 53 | } 54 | } 55 | 56 | private: 57 | C* _ptr; 58 | size_t _length; 59 | C[256] _buf; 60 | 61 | enum C* useStack = () @trusted { return cast(C*)size_t.max; }(); 62 | static TempCString initialize() { TempCString res = void; return res; } 63 | } 64 | 65 | /// ditto 66 | auto tempCString(C = char, S)(scope S str) if (isAcceptableString!S) 67 | { 68 | alias CF = Unqual!(ElementEncodingType!S); 69 | auto res = TempCString!C.initialize(); 70 | 71 | static if (isSomeString!S) 72 | { 73 | if (str is null) 74 | { 75 | res._length = 0; 76 | res._ptr = null; 77 | return res; 78 | } 79 | } 80 | 81 | static if (C.sizeof == CF.sizeof && is(typeof(res._buf[0 .. str.length] = str[]))) 82 | { 83 | if (str.length < res._buf.length) 84 | { 85 | res._buf[0..str.length] = str[]; 86 | res._buf[str.length] = 0; 87 | res._ptr = res.useStack; 88 | } 89 | else 90 | { 91 | res._ptr = () @trusted { 92 | auto p = cast(C*)enforceMalloc((str.length + 1) * C.sizeof); 93 | p[0 .. str.length] = str[]; 94 | p[str.length] = 0; 95 | return cast(C*)p; 96 | }(); 97 | } 98 | res._length = str.length; 99 | return res; 100 | } 101 | else 102 | { 103 | static assert(!(isSomeString!S && CF.sizeof == C.sizeof), "Should be using slice assignment."); 104 | C[] p = res._buf; 105 | size_t i; 106 | 107 | size_t strLength; 108 | static if (hasLength!S) strLength = str.length; 109 | 110 | import bc.internal.utf : byUTF; 111 | static if (isSomeString!S) 112 | auto r = cast(const(CF)[])str; // because inout(CF) causes problems with byUTF 113 | else 114 | alias r = str; 115 | 116 | C[] heapBuffer; 117 | foreach (const c; r.byUTF!(Unqual!C)) 118 | { 119 | if (i + 1 == p.length) 120 | { 121 | heapBuffer = trustedRealloc(p, strLength, heapBuffer is null); 122 | p = heapBuffer; 123 | } 124 | p[i++] = c; 125 | } 126 | p[i] = 0; 127 | res._length = i; 128 | res._ptr = (heapBuffer is null ? res.useStack : &heapBuffer[0]); 129 | return res; 130 | } 131 | } 132 | 133 | /// 134 | @("tempCString") 135 | nothrow @nogc @system unittest 136 | { 137 | import core.stdc.string : strlen; 138 | 139 | string str = "abc"; 140 | 141 | // Intended usage 142 | assert(strlen(str.tempCString()) == 3); 143 | 144 | // Correct usage 145 | auto tmp = str.tempCString(); 146 | assert(strlen(tmp) == 3); // or `tmp.ptr`, or `tmp.buffPtr` 147 | 148 | // $(RED WARNING): $(RED Incorrect usage) 149 | auto pInvalid1 = str.tempCString().ptr; 150 | const char* pInvalid2 = str.tempCString(); 151 | } 152 | 153 | @("tempCString - char, wchar, dchar") 154 | nothrow @nogc @trusted unittest 155 | { 156 | import std.algorithm : filter; 157 | import bc.internal.utf : byCodeUnit; 158 | 159 | { 160 | auto tmp = "baz".byCodeUnit.filter!(a => a == 'z').tempCString; 161 | assert(tmp._length == 1); 162 | assert(tmp._buf[0] == 'z'); 163 | assert(tmp._buf[1] == '\0'); 164 | } 165 | 166 | { 167 | auto tmp = "baz".byCodeUnit.filter!(a => a == 'z').tempCString!wchar; 168 | assert(tmp._length == 1); 169 | assert(tmp._buf[0] == 'z'); 170 | assert(tmp._buf[1] == '\0'); 171 | } 172 | 173 | { 174 | auto tmp = "baz".tempCString!dchar; 175 | assert(tmp._buf[0..3] == "baz"d); 176 | } 177 | } 178 | 179 | @("tempCString - static array") 180 | nothrow @nogc @trusted unittest 181 | { 182 | import core.stdc.string : strlen; 183 | 184 | immutable(char)[3] str = "abc"; 185 | assert(strlen(str.tempCString()) == 3); 186 | } 187 | 188 | /** 189 | * Refcounted string implementation. 190 | * 191 | * It uses malloc for string buffer. 192 | * 193 | * Types with `RC` prefix are reference counted, so they can be moved around freely. 194 | * Types without `RC` prefix has disabled copy constructor and can be only moved (passing ownership) or cloned. 195 | * 196 | * There are wariants with `W` and `D` before `String` that corresponds to payloads `wchar` and `dchar` as usual. 197 | * 198 | * Types that ends with `Z` means that they internally manages trailing '\0' and so can be safely used with C interop. 199 | * 200 | * NOTE: Beware of using exposed data pointer stored before some more content is added to RCString as internal buffer can be reallocated / resized if needed. 201 | */ 202 | alias RCString = StringImpl!(char, RC.yes, Zero.no); 203 | 204 | /// ditto 205 | alias RCWString = StringImpl!(wchar, RC.yes, Zero.no); 206 | 207 | /// ditto 208 | alias RCDString = StringImpl!(dchar, RC.yes, Zero.no); 209 | 210 | /// ditto 211 | alias RCStringZ = StringImpl!(char, RC.yes, Zero.yes); 212 | 213 | /// ditto 214 | alias RCWStringZ = StringImpl!(wchar, RC.yes, Zero.yes); 215 | 216 | /// ditto 217 | alias RCDStringZ = StringImpl!(dchar, RC.yes, Zero.yes); 218 | 219 | /** 220 | * String with unique ownership implementation. 221 | * 222 | * Similar to RCString but can be only moved passing it's ownership. 223 | * Furthermore it uses 512B stack allocated buffer for short strings. 224 | */ 225 | alias String = StringImpl!(char, RC.no, Zero.no); 226 | 227 | /// ditto 228 | alias WString = StringImpl!(wchar, RC.no, Zero.no); 229 | 230 | /// ditto 231 | alias DString = StringImpl!(dchar, RC.no, Zero.no); 232 | 233 | /// ditto 234 | alias StringZ = StringImpl!(char, RC.no, Zero.yes); 235 | 236 | /// ditto 237 | alias WStringZ = StringImpl!(wchar, RC.no, Zero.yes); 238 | 239 | /// ditto 240 | alias DStringZ = StringImpl!(dchar, RC.no, Zero.yes); 241 | 242 | private enum RC { no, yes } 243 | private enum Zero { no, yes } 244 | 245 | private struct StringImpl(C, RC rc, Zero zero) 246 | { 247 | @safe nothrow @nogc: 248 | 249 | static if (zero) enum Z = 1; 250 | else enum Z = 0; 251 | 252 | static if (rc) 253 | { 254 | private 255 | { 256 | struct Payload 257 | { 258 | size_t refs; 259 | size_t len; 260 | C[] buf; 261 | 262 | ~this() @trusted pure nothrow @nogc 263 | { 264 | import core.memory : pureFree; 265 | if (buf) pureFree(buf.ptr); 266 | } 267 | } 268 | 269 | Payload* pay; 270 | } 271 | 272 | /// Copy constructor 273 | this(ref return scope inout StringImpl rhs) pure @safe inout 274 | { 275 | pay = rhs.pay; 276 | if (pay) () @trusted { (cast(Payload*)pay).refs++; }(); 277 | } 278 | 279 | /// Destructor 280 | ~this() 281 | { 282 | if (pay && --pay.refs == 0) heapDealloc(pay); 283 | } 284 | } 285 | else 286 | { 287 | private 288 | { 289 | enum STACK_LEN = 512; 290 | size_t len; 291 | C[STACK_LEN] stackBuf; 292 | C[] buf; 293 | bool useStackBuf; 294 | alias pay = typeof(this); // to access fields through pay.xx too 295 | } 296 | 297 | ~this() pure @trusted 298 | { 299 | import core.memory : pureFree; 300 | if (buf) pureFree(buf.ptr); 301 | } 302 | 303 | @disable this(this); 304 | 305 | // constructor used by move 306 | private this(C[] sbuf, C[] buf, size_t len) 307 | { 308 | this.stackBuf[0..sbuf.length] = sbuf[]; 309 | this.buf = buf; 310 | this.len = len; 311 | } 312 | 313 | StringImpl move() scope @trusted 314 | { 315 | import std.algorithm : min; 316 | auto obuf = buf; 317 | auto olen = len; 318 | buf = null; 319 | len = 0; 320 | return StringImpl(stackBuf[0..min(STACK_LEN, olen)], obuf, olen); 321 | } 322 | 323 | /// 324 | StringImpl clone() scope @trusted 325 | { 326 | return StringImpl(this[]); 327 | } 328 | } 329 | 330 | /** 331 | * Constructor for cases when we know prior to the creation total length of the future string. 332 | * It preallocates internal buffer with `initialSize`. 333 | */ 334 | this(size_t initialSize) pure 335 | { 336 | static if (rc) pay = heapAlloc!Payload(1, 0); 337 | immutable len = initialSize + Z; 338 | static if (!rc) { 339 | if (len <= STACK_LEN) return; // we can use stack buffer for that 340 | } 341 | pay.buf = () @trusted { return (cast(C*)enforceMalloc(len * C.sizeof))[0..len]; }(); 342 | } 343 | 344 | this(S)(auto ref scope S str) 345 | { 346 | put(str); 347 | } 348 | 349 | /** 350 | * Creates RCString from the provided arguments formated to string with nogcFormatter 351 | */ 352 | static StringImpl from(ARGS...)(auto ref ARGS args) 353 | { 354 | import bc.string.format : getFormatSize, nogcFormatTo; 355 | 356 | size_t total; 357 | // calculate total size needed so we don't have to reallocate 358 | static foreach (a; args) total += getFormatSize(a); 359 | 360 | // and format arguments to RCString 361 | auto ret = StringImpl(total); 362 | static foreach (a; args) ret.nogcFormatTo(a); 363 | return ret; 364 | } 365 | 366 | alias data this; 367 | 368 | /** 369 | * Access internal string including the reserved block if any. 370 | */ 371 | @property inout(C)[] data() pure inout 372 | { 373 | if (!length) return null; 374 | 375 | static if (!rc) { 376 | if (len + Z <= STACK_LEN) return stackBuf[0..len]; 377 | } 378 | 379 | assert(pay.buf); 380 | return pay.buf[0..pay.len]; 381 | } 382 | 383 | static if (zero) 384 | { 385 | /// Pointer to string data that can be directly used in a C functions expecting '\0' terminal char. 386 | @property inout(C*) ptr() pure inout @trusted 387 | { 388 | if (!length) return null; 389 | static if (!rc) { 390 | if (len + Z <= STACK_LEN) return stackBuf.ptr; 391 | } 392 | return pay.buf.ptr; 393 | } 394 | } 395 | 396 | /// Slicing support for the internal buffer data 397 | @property inout(C)[] opSlice() pure inout 398 | { 399 | return this.data; 400 | } 401 | 402 | /// ditto 403 | @property inout(C)[] opSlice(size_t start, size_t end) pure inout 404 | { 405 | if (start > length || end > length) assert(0, "Index out of bounds"); 406 | if (start > end) assert(0, "Invalid slice indexes"); 407 | return this.data[start .. end]; 408 | } 409 | 410 | /// Indexed access to the buffer data 411 | @property ref C opIndex(size_t idx) pure return 412 | { 413 | if (idx >= length) assert(0, "Index out of bounds"); 414 | return this.data[idx]; 415 | } 416 | 417 | /// opDollar implementation 418 | alias length opDollar; 419 | 420 | /// Managed string length 421 | @property size_t length() pure const 422 | { 423 | static if (rc) 424 | return pay ? pay.len : 0; 425 | else 426 | return len; 427 | } 428 | 429 | /// Returns: capacity that can be used without reallocation 430 | size_t capacity() pure const 431 | { 432 | static if (rc) 433 | return pay ? (pay.buf.length - pay.len - Z) : 0; 434 | else 435 | return (buf ? buf.length : STACK_LEN) - pay.len - Z; 436 | } 437 | 438 | /** 439 | * Reserves space for requested number of characters that also increments string length. 440 | * This can be used for example in cases when we need to fill slice of string with some known length data. 441 | * To return reserved data, use `dropBack`. 442 | */ 443 | void reserve(size_t sz) 444 | { 445 | ensureAvail(sz); 446 | pay.len += sz; 447 | } 448 | 449 | /** 450 | * Drops defined amount of characters from the back. 451 | */ 452 | void dropBack(size_t sz) 453 | { 454 | assert(length >= sz, "Not enough data"); 455 | if (!sz) return; 456 | 457 | static if (!rc) 458 | { 459 | if (len + Z > STACK_LEN && len + Z - sz <= STACK_LEN) 460 | { 461 | // switch from heap buffer back to stack one 462 | len -= sz; 463 | stackBuf[0..len] = buf[0..len]; 464 | static if (zero) stackBuf[len] = 0; 465 | return; 466 | } 467 | } 468 | pay.len -= sz; 469 | static if (zero) pay.buf[pay.len] = 0; 470 | } 471 | 472 | /** 473 | * Clears content of the data, but keeps internal buffer as is so it can be used to build another string. 474 | */ 475 | void clear() pure 476 | { 477 | static if (rc) { 478 | if (pay) pay.len = 0; 479 | } 480 | else len = 0; 481 | } 482 | 483 | alias opOpAssign(string op : "~") = put; 484 | 485 | void opAssign(S)(auto ref scope S str) 486 | if (isAcceptableString!S || is(Unqual!S == C)) 487 | { 488 | clear(); 489 | put(str); 490 | } 491 | 492 | void put(in C val) pure 493 | { 494 | static if (!rc) 495 | { 496 | if (len + 1 + Z <= STACK_LEN) 497 | { 498 | stackBuf[len++] = val; 499 | static if (zero) stackBuf[len] = 0; 500 | return; 501 | } 502 | } 503 | ensureAvail(1); 504 | pay.buf[pay.len++] = val; 505 | static if (zero) pay.buf[pay.len] = 0; 506 | } 507 | 508 | void put(S)(auto ref scope S str) if (isAcceptableString!S) 509 | { 510 | alias CF = Unqual!(ElementEncodingType!S); 511 | 512 | static if (C.sizeof == CF.sizeof && is(typeof(pay.buf[0 .. str.length] = str[]))) 513 | { 514 | static if (!rc) 515 | { 516 | if (len + str.length + Z <= STACK_LEN) 517 | { 518 | stackBuf[len .. len + str.length] = str[]; 519 | len += str.length; 520 | static if (zero) stackBuf[len] = 0; 521 | return; 522 | } 523 | } 524 | 525 | ensureAvail(str.length); 526 | pay.buf[pay.len .. pay.len + str.length] = str[]; 527 | pay.len += str.length; 528 | static if (zero) pay.buf[pay.len] = 0; 529 | } 530 | else 531 | { 532 | // copy range 533 | 534 | // special case when we can determine that it still fits to stack buffer 535 | static if (!rc && hasLength!S && is(C == CF)) 536 | { 537 | if (pay.len + Z <= STACK_LEN) 538 | { 539 | foreach (ch; r.byUTF!(Unqual!C)) 540 | { 541 | stackBuf[pay.len++] = ch; 542 | static if (zero) stackBuf[pay.dlen] = 0; 543 | } 544 | return; 545 | } 546 | } 547 | 548 | static if (!rc) size_t nlen = pay.len; 549 | static if (hasLength!S) { 550 | ensureAvail(str.length); 551 | static if (!rc) nlen += str.length; 552 | } 553 | import bc.internal.utf : byUTF; 554 | static if (isSomeString!S) 555 | auto r = cast(const(CF)[])str; // because inout(CF) causes problems with byUTF 556 | else 557 | alias r = str; 558 | 559 | foreach (ch; r.byUTF!(Unqual!C)) 560 | { 561 | static if (!hasLength!S || !is(C == CF)) 562 | { 563 | ensureAvail(1); 564 | static if (!rc) { 565 | static if (!hasLength!S) nlen++; 566 | else { 567 | if (pay.len == nlen) nlen++; 568 | } 569 | } 570 | } 571 | static if (!rc) 572 | { 573 | if (nlen + Z + 1 <= STACK_LEN) // we can still use stack buffer 574 | { 575 | stackBuf[len++] = ch; 576 | continue; 577 | } 578 | } 579 | pay.buf[pay.len++] = ch; 580 | } 581 | static if (zero) pay.buf[pay.len] = 0; 582 | static if (!rc) assert(nlen == pay.len); 583 | } 584 | } 585 | 586 | private void ensureAvail(size_t sz) pure 587 | { 588 | static if (__VERSION__ >= 2094) pragma(inline, true); 589 | else pragma(inline); 590 | import core.bitop : bsr; 591 | import std.algorithm : max, min; 592 | 593 | static if (rc) 594 | { 595 | if (!pay) 596 | { 597 | // allocate new payload with required size 598 | pay = heapAlloc!Payload(1, 0); 599 | immutable l = max(sz+Z, 64); // allocates at leas 64B 600 | pay.buf = () @trusted { return (cast(C*)enforceMalloc(l * C.sizeof))[0..l]; }(); 601 | return; 602 | } 603 | 604 | if (pay.len + sz + Z <= pay.buf.length) return; // we can fit in what we've already allocated 605 | } 606 | else 607 | { 608 | if (len + sz + Z <= STACK_LEN) return; // still fits to stack buffer 609 | if (buf is null) 610 | { 611 | immutable l = max(len + sz + Z, STACK_LEN + 64); // allocates at leas 64B over 612 | buf = () @trusted { return (cast(C*)enforceMalloc(l * C.sizeof))[0..l]; }(); 613 | buf[0..len] = stackBuf[0..len]; // copy data from stack buffer, we'll use heap allocated one from now 614 | return; 615 | } 616 | if (len + Z <= STACK_LEN) 617 | { 618 | // some buffer is already preallocated, but we're still on stackBuffer and need to move to heap allocated one 619 | assert(buf.length > STACK_LEN); 620 | buf[0..len] = stackBuf[0..len]; // copy current data from the stack 621 | } 622 | 623 | if (len + sz + Z <= buf.length) return; // we can fit in what we've already allocated 624 | } 625 | 626 | // reallocate buffer 627 | // Note: new length calculation taken from std.array.appenderNewCapacity 628 | immutable ulong mult = 100 + (1000UL) / (bsr((pay.len + sz + Z)) + 1); 629 | immutable l = cast(size_t)(((pay.len + sz + Z) * min(mult, 200) + 99) / 100); 630 | // debug printf("realloc %lu -> %lu\n", pay.len, l); 631 | pay.buf = () @trusted { return (cast(C*)enforceRealloc(pay.buf.ptr, l * C.sizeof))[0..l]; }(); 632 | } 633 | } 634 | 635 | auto rcString(C = char, S)(auto ref S str) 636 | { 637 | StringImpl!(C, RC.yes, Zero.no) ret; 638 | ret.put(str); 639 | return ret; 640 | } 641 | 642 | @("RCString") 643 | @system @nogc unittest 644 | { 645 | import bc.internal.utf : byCodeUnit; 646 | import std.algorithm : filter; 647 | 648 | RCStringZ s; 649 | s ~= "fo"; 650 | assert(s.pay.len == 2); 651 | assert(s.pay.buf.length >= 3); 652 | 653 | s ~= 'o'; 654 | assert(s.pay.len == 3); 655 | assert(s.pay.buf.length >= 4); 656 | 657 | s ~= "bar"; 658 | assert(s.pay.len == 6); 659 | assert(s.pay.buf.length >= 7); 660 | assert(s == "foobar"); 661 | 662 | s ~= "baz".byCodeUnit.filter!(a => a == 'z'); 663 | assert(s.length == "foobarz".length); 664 | assert(s.data == "foobarz"); 665 | assert(s == "foobarz"); 666 | assert(s.ptr == &s.data[0]); 667 | assert((s.ptr["foobarz".length]) == 0); 668 | } 669 | 670 | @("RCString.from") 671 | @nogc @safe unittest 672 | { 673 | { 674 | auto str = RCString.from("foo", 42, "bar"); 675 | assert(str == "foo42bar"); 676 | } 677 | 678 | { 679 | auto str = RCWString.from("foo"); 680 | assert(str == "foo"w); 681 | } 682 | } 683 | 684 | version (D_Exceptions) 685 | { 686 | @("RCString with Nullable") 687 | @nogc @safe unittest 688 | { 689 | import std.typecons : Nullable; 690 | Nullable!RCString sn = RCString("foo"); 691 | } 692 | } 693 | 694 | @("rcString") 695 | @nogc @safe unittest 696 | { 697 | auto str = "foo".rcString(); 698 | assert(str == "foo"); 699 | } 700 | 701 | @("String") 702 | @nogc @safe unittest 703 | { 704 | auto s = String("Hello"); 705 | assert(s.capacity == String.stackBuf.length - 5); 706 | assert(s[] == "Hello"); 707 | s ~= " String"; 708 | assert(s[] == "Hello String"); 709 | auto s2 = s.clone(); 710 | assert(s[] == s2[]); 711 | () @trusted { assert(s.ptr != s2.ptr); }(); 712 | 713 | auto s3 = s.move(); 714 | assert(s.buf is null); 715 | assert(s.len == 0); 716 | assert(s3 == "Hello String"); 717 | } 718 | 719 | @("String - put static array") 720 | @nogc @safe unittest 721 | { 722 | String s; 723 | immutable(char)[3] foo = "foo"; 724 | s ~= foo; 725 | assert(s == "foo"); 726 | } 727 | 728 | @("String stack to heap") 729 | @nogc @safe unittest 730 | { 731 | import std.algorithm : each; 732 | import std.range : repeat; 733 | 734 | StringZ s; 735 | 'a'.repeat(s.stackBuf.length-1).each!(c => s.put(c)); 736 | assert(s.length == s.stackBuf.length-1); 737 | assert(s.stackBuf[$-2] == 'a'); 738 | assert(s.stackBuf[$-1] == '\0'); 739 | assert(s.buf is null); 740 | assert(&s.data[0] == &s.stackBuf[0]); 741 | s ~= 'b'; 742 | assert(s.stackBuf[$-1] == '\0'); // doesn't change on stack to heap switch 743 | assert(s.buf !is null); 744 | assert(&s.data[0] == &s.buf[0]); 745 | assert(s.buf[s.stackBuf.length-1] == 'b'); 746 | s ~= "foo"; 747 | 748 | s.clear(); 749 | s ~= 'c'; 750 | assert(&s.data[0] == &s.stackBuf[0]); // back to stack usage 751 | assert(s.buf !is null); // but heap buffer is still there 752 | 'd'.repeat(s.stackBuf.length).each!(c => s.put(c)); 753 | assert(&s.data[0] == &s.buf[0]); 754 | assert(s.length == 1 + s.stackBuf.length); 755 | assert(s.buf[1 + s.stackBuf.length] == '\0'); 756 | } 757 | 758 | @("String reserve") 759 | @nogc @safe unittest 760 | { 761 | String buf; 762 | assert(buf.length == 0); 763 | assert(buf.capacity == buf.stackBuf.length); 764 | buf.reserve(64); 765 | assert(buf.length == 64); 766 | assert(buf.buf is null); 767 | buf[][0..3] = "foo"; 768 | buf.dropBack(61); 769 | assert(buf[] == "foo"); 770 | buf.reserve(buf.stackBuf.length); 771 | assert(buf.buf !is null); 772 | assert(buf.buf[0..3] == "foo"); 773 | buf.buf[0..3] = "bar"; 774 | buf.dropBack(buf.stackBuf.length); 775 | assert(buf.buf !is null); // left allocated for reuse 776 | assert(buf.stackBuf[0..3] == "bar"); // copy from heap 777 | } 778 | 779 | private C[] trustedRealloc(C)(scope C[] buf, size_t strLength, bool bufIsOnStack) 780 | @trusted @nogc pure nothrow 781 | { 782 | pragma(inline, false); // because it's rarely called 783 | 784 | import bc.core.memory : enforceMalloc, enforceRealloc; 785 | 786 | size_t newlen = buf.length * 3 / 2; 787 | 788 | if (bufIsOnStack) 789 | { 790 | if (newlen <= strLength) 791 | newlen = strLength + 1; // +1 for terminating 0 792 | auto ptr = cast(C*) enforceMalloc(newlen * C.sizeof); 793 | ptr[0 .. buf.length] = buf[]; 794 | return ptr[0 .. newlen]; 795 | } 796 | else 797 | { 798 | if (buf.length >= size_t.max / (2 * C.sizeof)) 799 | { 800 | version (D_Exceptions) 801 | { 802 | import core.exception : onOutOfMemoryError; 803 | onOutOfMemoryError(); 804 | } 805 | else assert(0, "Memory allocation failed"); 806 | } 807 | auto ptr = cast(C*) enforceRealloc(buf.ptr, newlen * C.sizeof); 808 | return ptr[0 .. newlen]; 809 | } 810 | } 811 | 812 | /// Strips leading whitespace ('\t', '\n', '\r', ' ') 813 | S stripLeft(S)(S str) 814 | { 815 | pragma(inline, true); 816 | /// All chars except for whitespace ('\t', '\n', '\r', ' ') 817 | enum AllExceptWhitespaceRanges = "\0\10\13\14\16\37\41\377"; 818 | 819 | size_t rpos; 820 | immutable rs = parseToken!(AllExceptWhitespaceRanges, '"')(str, rpos); 821 | if(rs == -1) return str[$..$]; // Only whitespace string, return empty range. 822 | else return str[rpos..$]; 823 | } 824 | 825 | @("stripLeft") 826 | unittest 827 | { 828 | assert(stripLeft("\t\n\r foobar\t\n\r ") == "foobar\t\n\r "); 829 | assert(stripLeft("\t\n\r\t\n\r ") == ""); 830 | } 831 | 832 | bool startsWith(S, char[] chars)(S str) 833 | { 834 | enum validCharMap = buildValidCharMap(chars, true); 835 | return validCharMap[str[0]]; 836 | } 837 | 838 | @("startsWith") 839 | unittest 840 | { 841 | assert(startsWith!(string, ['+', '-'])("-42")); 842 | assert(startsWith!(string, ['+', '-'])("+42")); 843 | assert(!startsWith!(string, ['+', '-'])("42")); 844 | } 845 | 846 | /** 847 | * Alternative implementation of `std.string.outdent` that differs in: 848 | * 849 | * * meant for dedent string literals in CT 850 | * * if first line is not indented, other lines are dedented still (std.string.outdent returns original text in that case) 851 | * * empty lines at the text start are removed 852 | */ 853 | template dedent(alias str) 854 | { 855 | static S getLine(S)(S str) 856 | { 857 | if (!str.length) return null; 858 | for (size_t i = 0; i < str.length; ++i) 859 | { 860 | if (str[i] == '\r') 861 | { 862 | if (i+1 < str.length && str[i+1] == '\n') 863 | return str[0..i+2]; 864 | } 865 | if (str[i] == '\n') return str[0..i+1]; 866 | } 867 | return str; 868 | } 869 | 870 | // strip line whitespace but keep newline characters 871 | static S stripWS(S)(S str) 872 | { 873 | if (!str.length) return null; 874 | for (size_t i = 0; i < str.length; ++i) 875 | { 876 | if (str[i] <= ' ' && str[i] != '\r' && str[i] != '\n') continue; 877 | return str[i..$]; 878 | } 879 | return null; 880 | } 881 | 882 | template shortestIndent(alias str, size_t prev = size_t.max) 883 | { 884 | enum line = getLine(str); 885 | enum stripped = stripWS(line); 886 | static if (line.length == 0) enum shortestIndent = prev; 887 | else static if (line.length == stripped.length) enum shortestIndent = 0; 888 | else 889 | { 890 | enum cur = prev > line.length - stripped.length ? line.length - stripped.length : prev; 891 | enum next = shortestIndent!(str[line.length..$], cur); 892 | enum shortestIndent = cur > next ? next : cur; 893 | } 894 | } 895 | 896 | template dedentNext(alias str, size_t indent) 897 | { 898 | enum ln = getLine(str); 899 | static if (!ln.length) 900 | enum dedentNext = null; 901 | else static if (ln.length < indent) 902 | enum dedentNext = ln ~ dedentNext!(str[ln.length..$], indent); 903 | else 904 | enum dedentNext = ln[indent..$] ~ dedentNext!(str[ln.length..$], indent); 905 | } 906 | 907 | enum line = getLine(str); 908 | enum stripped = stripWS(line); 909 | 910 | static if (!line.length) enum dedent = null; 911 | else static if ( 912 | (stripped.length == 1 && stripped[0] == '\n') 913 | || (stripped.length == 2 && stripped[0] == '\r' && stripped[1] == '\n')) 914 | enum dedent = dedent!(str[line.length..$]); // drop first empty lines 915 | else 916 | { 917 | // ignore no indentation of the first line 918 | enum shortest = shortestIndent!( 919 | str[line.length..$], 920 | stripped.length == line.length ? size_t.max : (line.length - stripped.length)); 921 | 922 | static if (shortest == 0) 923 | enum dedent = str; // no indent used 924 | else 925 | enum dedent = stripped ~ dedentNext!(str[line.length..$], shortest); 926 | } 927 | } 928 | 929 | @("dedent") 930 | unittest 931 | { 932 | // with empty first line 933 | { 934 | enum str1 = ` 935 | DELETE FROM elements.element 936 | WHERE id=ANY($1) AND type_id IN ( 937 | SELECT id FROM elements.element_type WHERE owner=$2 938 | )`; 939 | 940 | enum str2 = 941 | "DELETE FROM elements.element\n" ~ 942 | "WHERE id=ANY($1) AND type_id IN (\n" ~ 943 | " SELECT id FROM elements.element_type WHERE owner=$2\n" ~ 944 | ")"; 945 | 946 | static assert(dedent!str1 == str2); 947 | } 948 | 949 | // with not indented first line 950 | { 951 | enum str1 = `DELETE FROM elements.element 952 | WHERE id=ANY($1) AND type_id IN ( 953 | SELECT id FROM elements.element_type WHERE owner=$2 954 | )`; 955 | 956 | enum str2 = "DELETE FROM elements.element\n" ~ 957 | "WHERE id=ANY($1) AND type_id IN (\n" ~ 958 | " SELECT id FROM elements.element_type WHERE owner=$2\n" ~ 959 | ")"; 960 | 961 | static assert(dedent!str1 == str2); 962 | } 963 | 964 | // test that we didn't touch number of lines 965 | { 966 | static assert(dedent!` 967 | 2 968 | 3 969 | ` == "2\n3\n"); // first line is dropped, last newline is kept 970 | } 971 | 972 | // test we don't dedent when some line is not indented 973 | { 974 | enum str = `aa 975 | bb 976 | cc`; 977 | assert(dedent!str == str); 978 | } 979 | 980 | // test that we don't touch space after last line text 981 | { 982 | assert(dedent!" foo " == "foo "); 983 | assert(dedent!`foo 984 | bar ` == "foo\nbar "); 985 | } 986 | } 987 | 988 | /** 989 | * Builds char map from the provided ranges. 990 | * 991 | * Params: 992 | * ranges = ranges of ascii characters. 993 | * valid = wheteher range characters are valid or not. 994 | * For example: 995 | * buildValidCharMap("\0/:\xff", false) means that only characters 0-9 would have true in the generated map. 996 | * buildValidCharMap("\0/:\xff", true) means that all characters except 0-9 would have true in the generated map. 997 | * 998 | * Returns: generated table 999 | */ 1000 | bool[256] buildValidCharMap(S)(S ranges, bool valid = false) 1001 | { 1002 | assert(ranges.length % 2 == 0, "Uneven ranges"); 1003 | bool[256] res = valid ? false : true; 1004 | 1005 | for (int i=0; i < ranges.length; i+=2) 1006 | for (int j=ranges[i]; j <= ranges[i+1]; ++j) 1007 | res[j] = valid ? true : false; 1008 | return res; 1009 | } 1010 | 1011 | /// 1012 | @("buildValidCharMap") 1013 | @safe unittest 1014 | { 1015 | string ranges = "\0 \"\"(),,//:@[]{{}}\x7f\xff"; 1016 | assert(buildValidCharMap(ranges, false) == 1017 | cast(bool[])[ 1018 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1019 | 0,1,0,1,1,1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0, 1020 | 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1, 1021 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0, 1022 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1023 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1024 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1025 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1026 | ]); 1027 | 1028 | assert(buildValidCharMap(ranges, true) == 1029 | cast(bool[])[ 1030 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1031 | 1,0,1,0,0,0,0,0,1,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1, 1032 | 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0, 1033 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1, 1034 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1035 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1036 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1037 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1038 | ]); 1039 | } 1040 | 1041 | /* 1042 | * Advances index over the token to the next character while checking for valid characters. 1043 | * On success, buffer index is left on the next character. 1044 | * 1045 | * Params: 1046 | * - ranges = ranges of characters to stop on 1047 | * - next = next character/s to stop on (must be present in the provided ranges too) 1048 | * - sseRanges = 1049 | * as SSE optimized path is limited to 8 pairs, here one can provide merged ranges for a fast 1050 | * SSE path that would be precised with `ranges`. Otherwise `ranges` is used for SSE path too. 1051 | * 1052 | * Returns: 1053 | * * 0 on success 1054 | * * -1 when token hasn't been found (ie not enough data in the buffer) 1055 | * * -2 when character from invalid ranges was found but not matching one of next characters (ie invalid token) 1056 | */ 1057 | int parseToken(string ranges, alias next, string sseRanges = null, C)(const(C)[] buffer, ref size_t i) pure 1058 | if (is(C == ubyte) || is(C == char)) 1059 | { 1060 | version (DigitalMars) { 1061 | static if (__VERSION__ >= 2094) pragma(inline, true); // older compilers can't inline this 1062 | } else pragma(inline, true); 1063 | 1064 | immutable charMap = parseTokenCharMap!(ranges)(); 1065 | 1066 | static if (LDC_with_SSE42) 1067 | { 1068 | // CT function to prepare input for SIMD vector enum 1069 | static byte[16] padRanges()(string ranges) 1070 | { 1071 | byte[16] res; 1072 | // res[0..ranges.length] = cast(byte[])ranges[]; - broken on macOS betterC tests 1073 | foreach (i, c; ranges) res[i] = cast(byte)c; 1074 | return res; 1075 | } 1076 | 1077 | static if (sseRanges) alias usedRng = sseRanges; 1078 | else alias usedRng = ranges; 1079 | static assert(usedRng.length <= 16, "Ranges must be at most 16 characters long"); 1080 | static assert(usedRng.length % 2 == 0, "Ranges must have even number of characters"); 1081 | enum rangesSize = usedRng.length; 1082 | enum byte16 rngE = padRanges(usedRng); 1083 | 1084 | if (_expect(buffer.length - i >= 16, true)) 1085 | { 1086 | size_t left = (buffer.length - i) & ~15; // round down to multiple of 16 1087 | byte16 ranges16 = rngE; 1088 | 1089 | do 1090 | { 1091 | byte16 b16 = () @trusted { return cast(byte16)_mm_loadu_si128(cast(__m128i*)&buffer[i]); }(); 1092 | immutable r = _mm_cmpestri( 1093 | ranges16, rangesSize, 1094 | b16, 16, 1095 | _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS 1096 | ); 1097 | 1098 | if (r != 16) 1099 | { 1100 | i += r; 1101 | goto FOUND; 1102 | } 1103 | i += 16; 1104 | left -= 16; 1105 | } 1106 | while (_expect(left != 0, true)); 1107 | } 1108 | } 1109 | else 1110 | { 1111 | // faster unrolled loop to iterate over 8 characters 1112 | loop: while (_expect(buffer.length - i >= 8, true)) 1113 | { 1114 | static foreach (_; 0..8) 1115 | { 1116 | if (_expect(!charMap[buffer[i]], false)) goto FOUND; 1117 | ++i; 1118 | } 1119 | } 1120 | } 1121 | 1122 | // handle the rest 1123 | if (_expect(i >= buffer.length, false)) return -1; 1124 | 1125 | FOUND: 1126 | while (true) 1127 | { 1128 | static if (is(typeof(next) == char)) { 1129 | static assert(!charMap[next], "Next character is not in ranges"); 1130 | if (buffer[i] == next) return 0; 1131 | } else { 1132 | static assert(next.length > 0, "Next character not provided"); 1133 | static foreach (c; next) { 1134 | static assert(!charMap[c], "Next character is not in ranges"); 1135 | if (buffer[i] == c) return 0; 1136 | } 1137 | } 1138 | if (_expect(!charMap[buffer[i]], false)) return -2; 1139 | if (_expect(++i == buffer.length, false)) return -1; 1140 | } 1141 | } 1142 | 1143 | /// 1144 | @("parseToken") 1145 | @safe unittest 1146 | { 1147 | size_t idx; 1148 | string buf = "foo\nbar"; 1149 | auto ret = parseToken!("\0\037\177\377", "\r\n")(buf, idx); 1150 | assert(ret == 0); // no error 1151 | assert(idx == 3); // index of newline character 1152 | 1153 | idx = 0; 1154 | ret = parseToken!("\0\037\177\377", "\r\n")(buf[0..3], idx); 1155 | assert(ret == -1); // not enough data to find next character 1156 | assert(idx == 3); 1157 | 1158 | idx = 0; 1159 | buf = "foo\t\nbar"; 1160 | ret = parseToken!("\0\037\177\377", "\r\n")(buf, idx); 1161 | assert(ret == -2); // invalid character '\t' found in token 1162 | assert(idx == 3); // invalid character on index 3 1163 | } 1164 | 1165 | private immutable(bool[256]) parseTokenCharMap(string invalidRanges)() { 1166 | static immutable charMap = buildValidCharMap(invalidRanges); 1167 | return charMap; 1168 | } 1169 | --------------------------------------------------------------------------------