From 7973da975eac7cf5155a140e4cb4f68d5cbb1eb9 Mon Sep 17 00:00:00 2001 From: Ralph Amissah Date: Sun, 4 Jun 2023 20:24:27 -0400 Subject: make set_depends (dyaml update) --- dub.selections.json | 5 + dub_describe.json | 146 +- src/ext_depends/D-YAML.meta | 2 +- src/ext_depends/D-YAML/.github/workflows/d.yml | 20 +- src/ext_depends/D-YAML/.gitignore | 1 + src/ext_depends/D-YAML/dub.json | 12 + src/ext_depends/D-YAML/source/dyaml/composer.d | 38 +- src/ext_depends/D-YAML/source/dyaml/constructor.d | 4 +- src/ext_depends/D-YAML/source/dyaml/emitter.d | 17 +- src/ext_depends/D-YAML/source/dyaml/escapes.d | 16 +- src/ext_depends/D-YAML/source/dyaml/exception.d | 19 +- src/ext_depends/D-YAML/source/dyaml/loader.d | 14 +- src/ext_depends/D-YAML/source/dyaml/node.d | 263 +- src/ext_depends/D-YAML/source/dyaml/resolver.d | 10 +- src/ext_depends/D-YAML/source/dyaml/scanner.d | 52 +- src/ext_depends/D-YAML/source/dyaml/serializer.d | 2 +- src/ext_depends/D-YAML/source/dyaml/stdsumtype.d | 2627 ++++++++++++++++++++ src/ext_depends/D-YAML/source/dyaml/test/tokens.d | 2 +- .../D-YAML/test/data/emojianchor.canonical | 5 + src/ext_depends/D-YAML/test/data/emojianchor.data | 2 + .../D-YAML/test/data/invalid-anchor-1.loader-error | 1 - .../D-YAML/test/data/invalid-anchor-2.loader-error | 8 - .../D-YAML/test/data/invalid-anchor.loader-error | 8 + 23 files changed, 3083 insertions(+), 191 deletions(-) create mode 100644 dub.selections.json create mode 100644 src/ext_depends/D-YAML/source/dyaml/stdsumtype.d create mode 100644 src/ext_depends/D-YAML/test/data/emojianchor.canonical create mode 100644 src/ext_depends/D-YAML/test/data/emojianchor.data delete mode 100644 src/ext_depends/D-YAML/test/data/invalid-anchor-1.loader-error delete mode 100644 src/ext_depends/D-YAML/test/data/invalid-anchor-2.loader-error create mode 100644 src/ext_depends/D-YAML/test/data/invalid-anchor.loader-error diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..322586b --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,5 @@ +{ + "fileVersion": 1, + "versions": { + } +} diff --git a/dub_describe.json b/dub_describe.json index 97e9f4a..19bde01 100644 --- a/dub_describe.json +++ b/dub_describe.json @@ -15,12 +15,12 @@ "path": "./", "name": "spine", "version": "0.12.0", - "description": "a sisu like document parser", + "description": "an object-centric sisu-like document parser", "homepage": "https://sisudoc.org", "authors": [ "Ralph Amissah" ], - "copyright": "Copyright © 2015 - 2022 Ralph Amissah", + "copyright": "Copyright © 2015 - 2023 Ralph Amissah", "license": "AGPL-3.0+", "dependencies": [ "spine:d2sqlite3", @@ -38,6 +38,7 @@ "dflags": [], "lflags": [], "libs": [], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [], @@ -54,6 +55,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "buildRequirements": [ "allowWarnings" ], @@ -123,6 +133,14 @@ "role": "source", "path": "src/doc_reform/io_out/rgx.d" }, + { + "role": "source", + "path": "src/doc_reform/io_out/rgx_latex.d" + }, + { + "role": "source", + "path": "src/doc_reform/io_out/rgx_xhtml.d" + }, { "role": "source", "path": "src/doc_reform/io_out/source_pod.d" @@ -207,6 +225,14 @@ "role": "source", "path": "src/doc_reform/meta/rgx.d" }, + { + "role": "source", + "path": "src/doc_reform/meta/rgx_files.d" + }, + { + "role": "source", + "path": "src/doc_reform/meta/rgx_yaml_tags.d" + }, { "role": "source", "path": "src/doc_reform/share/defaults.d" @@ -235,6 +261,10 @@ "role": "import_", "path": "src/ext_depends/D-YAML/examples/tojson/source/app.d" }, + { + "role": "import_", + "path": "src/ext_depends/D-YAML/examples/tokens/source/app.d" + }, { "role": "import_", "path": "src/ext_depends/D-YAML/examples/yaml_bench/yaml_bench.d" @@ -479,6 +509,7 @@ "libs": [ "sqlite3" ], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [], @@ -495,6 +526,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "buildRequirements": [], "options": [], "files": [ @@ -568,6 +608,7 @@ "dflags": [], "lflags": [], "libs": [], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [], @@ -588,6 +629,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "buildRequirements": [], "options": [], "files": [ @@ -760,6 +810,7 @@ "dflags": [], "lflags": [], "libs": [], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [], @@ -776,6 +827,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "buildRequirements": [], "options": [], "files": [ @@ -823,12 +883,7 @@ "targetName": "spine", "workingDirectory": "", "mainSourceFile": "", - "dflags": [ - "-O2", - "-inline", - "-boundscheck=on", - "-color=on" - ], + "dflags": [], "lflags": [], "libs": [ "sqlite3" @@ -855,6 +910,8 @@ "./src/doc_reform/io_out/package.d", "./src/doc_reform/io_out/paths_output.d", "./src/doc_reform/io_out/rgx.d", + "./src/doc_reform/io_out/rgx_latex.d", + "./src/doc_reform/io_out/rgx_xhtml.d", "./src/doc_reform/io_out/source_pod.d", "./src/doc_reform/io_out/sqlite.d", "./src/doc_reform/io_out/xmls.d", @@ -876,9 +933,12 @@ "./src/doc_reform/meta/metadoc_show_summary.d", "./src/doc_reform/meta/package.d", "./src/doc_reform/meta/rgx.d", + "./src/doc_reform/meta/rgx_files.d", + "./src/doc_reform/meta/rgx_yaml_tags.d", "./src/doc_reform/share/defaults.d", "./src/doc_reform/spine.d" ], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [ @@ -901,6 +961,15 @@ "./views" ], "importFiles": [ + "./src/ext_depends/D-YAML/examples/constructor/main.d", + "./src/ext_depends/D-YAML/examples/getting_started/main.d", + "./src/ext_depends/D-YAML/examples/representer/main.d", + "./src/ext_depends/D-YAML/examples/resolver/main.d", + "./src/ext_depends/D-YAML/examples/tojson/source/app.d", + "./src/ext_depends/D-YAML/examples/tokens/source/app.d", + "./src/ext_depends/D-YAML/examples/yaml_bench/yaml_bench.d", + "./src/ext_depends/D-YAML/examples/yaml_gen/yaml_gen.d", + "./src/ext_depends/D-YAML/examples/yaml_stats/yaml_stats.d", "./src/ext_depends/D-YAML/source/dyaml/composer.d", "./src/ext_depends/D-YAML/source/dyaml/constructor.d", "./src/ext_depends/D-YAML/source/dyaml/dumper.d", @@ -934,6 +1003,7 @@ "./src/ext_depends/D-YAML/source/dyaml/test/resolver.d", "./src/ext_depends/D-YAML/source/dyaml/test/tokens.d", "./src/ext_depends/D-YAML/source/dyaml/token.d", + "./src/ext_depends/D-YAML/testsuite/source/app.d", "./src/ext_depends/d2sqlite3/source/d2sqlite3/database.d", "./src/ext_depends/d2sqlite3/source/d2sqlite3/internal/memory.d", "./src/ext_depends/d2sqlite3/source/d2sqlite3/internal/util.d", @@ -960,6 +1030,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "requirements": {}, "options": {} }, @@ -986,12 +1065,7 @@ "targetName": "spine_d2sqlite3", "workingDirectory": "", "mainSourceFile": "", - "dflags": [ - "-O2", - "-inline", - "-boundscheck=on", - "-color=on" - ], + "dflags": [], "lflags": [], "libs": [ "sqlite3" @@ -1008,6 +1082,7 @@ "./src/ext_depends/d2sqlite3/source/d2sqlite3/statement.d", "./src/ext_depends/d2sqlite3/source/tests.d" ], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [ @@ -1033,6 +1108,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "requirements": {}, "options": {} }, @@ -1051,12 +1135,7 @@ "targetName": "spine_dyaml", "workingDirectory": "", "mainSourceFile": "", - "dflags": [ - "-O2", - "-inline", - "-boundscheck=on", - "-color=on" - ], + "dflags": [], "lflags": [], "libs": [], "linkerFiles": [], @@ -1096,6 +1175,7 @@ "./src/ext_depends/D-YAML/source/dyaml/token.d", "./src/ext_depends/tinyendian/source/tinyendian.d" ], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [ @@ -1125,6 +1205,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "requirements": {}, "options": {} }, @@ -1143,12 +1232,7 @@ "targetName": "spine_imageformats", "workingDirectory": "", "mainSourceFile": "", - "dflags": [ - "-O2", - "-inline", - "-boundscheck=on", - "-color=on" - ], + "dflags": [], "lflags": [], "libs": [], "linkerFiles": [], @@ -1159,6 +1243,7 @@ "./src/ext_depends/imageformats/imageformats/png.d", "./src/ext_depends/imageformats/imageformats/tga.d" ], + "injectSourceFiles": [], "copyFiles": [], "extraDependencyFiles": [], "versions": [ @@ -1184,6 +1269,15 @@ "postBuildCommands": [], "preRunCommands": [], "postRunCommands": [], + "environments": {}, + "buildEnvironments": {}, + "runEnvironments": {}, + "preGenerateEnvironments": {}, + "postGenerateEnvironments": {}, + "preBuildEnvironments": {}, + "postBuildEnvironments": {}, + "preRunEnvironments": {}, + "postRunEnvironments": {}, "requirements": {}, "options": {} }, diff --git a/src/ext_depends/D-YAML.meta b/src/ext_depends/D-YAML.meta index a0cc85c..c9a2d17 100644 --- a/src/ext_depends/D-YAML.meta +++ b/src/ext_depends/D-YAML.meta @@ -1,3 +1,3 @@ -D-YAML e157571e +D-YAML 2c915b3f https://github.com/dlang-community/D-YAML Boost Software License 1.0 (BSL-1.0) diff --git a/src/ext_depends/D-YAML/.github/workflows/d.yml b/src/ext_depends/D-YAML/.github/workflows/d.yml index 08f583f..b14a069 100644 --- a/src/ext_depends/D-YAML/.github/workflows/d.yml +++ b/src/ext_depends/D-YAML/.github/workflows/d.yml @@ -16,20 +16,23 @@ jobs: - dmd-beta runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7 + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - uses: dlang-community/setup-dlang@v1 with: compiler: ${{ matrix.dc }} - name: 'Test' run: | + dub test -c unittest-dip1000 dub test --build=unittest-cov bash <(curl -s https://codecov.io/bash) examples: runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v2 - - uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7 + - uses: actions/checkout@v3 + - uses: dlang-community/setup-dlang@v1 with: compiler: dmd-latest - name: 'Build Examples' @@ -41,13 +44,14 @@ jobs: dub build dyaml:resolver dub build dyaml:testsuite dub build dyaml:tojson + dub build dyaml:tokens dub build dyaml:yaml_gen dub build dyaml:yaml_stats ninja: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7 + - uses: actions/checkout@v3 + - uses: dlang-community/setup-dlang@v1 with: compiler: dmd-latest - name: 'Install dependencies' @@ -63,8 +67,8 @@ jobs: runs-on: ubuntu-latest needs: build steps: - - uses: actions/checkout@v2 - - uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7 + - uses: actions/checkout@v3 + - uses: dlang-community/setup-dlang@v1 with: compiler: dmd-latest - name: 'Run YAML test suite' diff --git a/src/ext_depends/D-YAML/.gitignore b/src/ext_depends/D-YAML/.gitignore index 1a1aec4..ce69e30 100644 --- a/src/ext_depends/D-YAML/.gitignore +++ b/src/ext_depends/D-YAML/.gitignore @@ -13,6 +13,7 @@ /examples/getting_started/main.obj /cdc.obj /unittest +*.pdb # Backups # ########### diff --git a/src/ext_depends/D-YAML/dub.json b/src/ext_depends/D-YAML/dub.json index 07ee17e..601f902 100644 --- a/src/ext_depends/D-YAML/dub.json +++ b/src/ext_depends/D-YAML/dub.json @@ -11,12 +11,24 @@ }, "homepage": "https://github.com/dlang-community/D-YAML", "copyright": "Copyright © 2011-2018, Ferdinand Majerech", + "configurations": [ + { "name": "library" }, + { "name": "unittest" }, + { + "name": "unittest-dip1000", + "dflags": [ "-preview=dip1000" ], + "dependencies": { + "tinyendian": { "version": "*", "dflags" : [ "-preview=dip1000" ] }, + } + } + ], "subPackages": [ "examples/constructor", "examples/getting_started", "examples/representer", "examples/resolver", "examples/tojson", + "examples/tokens", "examples/yaml_bench", "examples/yaml_gen", "examples/yaml_stats", diff --git a/src/ext_depends/D-YAML/source/dyaml/composer.d b/src/ext_depends/D-YAML/source/dyaml/composer.d index c000b02..e7b083a 100644 --- a/src/ext_depends/D-YAML/source/dyaml/composer.d +++ b/src/ext_depends/D-YAML/source/dyaml/composer.d @@ -16,6 +16,7 @@ import std.algorithm; import std.array; import std.conv; import std.exception; +import std.format; import std.range; import std.typecons; @@ -357,12 +358,18 @@ struct Composer merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1], pairAppenderLevel + 1, nodeAppenderLevel)); } - auto numUnique = pairAppender.data.dup - .sort!((x,y) => x.key > y.key) - .uniq!((x,y) => x.key == y.key) - .walkLength; - enforce(numUnique == pairAppender.data.length, - new ComposerException("Duplicate key found in mapping", parser_.front.startMark)); + + auto sorted = pairAppender.data.dup.sort!((x,y) => x.key > y.key); + if (sorted.length) { + foreach (index, const ref value; sorted[0 .. $ - 1].enumerate) + if (value.key == sorted[index + 1].key) { + const message = () @trusted { + return format("Key '%s' appears multiple times in mapping (first: %s)", + value.key.get!string, value.key.startMark); + }(); + throw new ComposerException(message, sorted[index + 1].key.startMark); + } + } Node node = constructNode(startEvent.startMark, parser_.front.endMark, tag, pairAppender.data.dup); @@ -373,3 +380,22 @@ struct Composer return node; } } + +// Provide good error message on multiple keys (which JSON supports) +@safe unittest +{ + import dyaml.loader : Loader; + + const str = `{ + "comment": "This is a common technique", + "name": "foobar", + "comment": "To write down comments pre-JSON5" +}`; + + try + auto node = Loader.fromString(str).load(); + catch (ComposerException exc) + assert(exc.message() == + "Key 'comment' appears multiple times in mapping " ~ + "(first: file ,line 2,column 5)\nfile ,line 4,column 5"); +} diff --git a/src/ext_depends/D-YAML/source/dyaml/constructor.d b/src/ext_depends/D-YAML/source/dyaml/constructor.d index bc1d75c..4cd1546 100644 --- a/src/ext_depends/D-YAML/source/dyaml/constructor.d +++ b/src/ext_depends/D-YAML/source/dyaml/constructor.d @@ -481,7 +481,7 @@ Node.Pair[] constructOrderedMap(const Node[] nodes) @safe //Detect duplicates. //TODO this should be replaced by something with deterministic memory allocation. - auto keys = redBlackTree!Node(); + auto keys = new RedBlackTree!Node(); foreach(ref pair; pairs) { enforce(!(pair.key in keys), @@ -600,7 +600,7 @@ Node.Pair[] constructMap(Node.Pair[] pairs) @safe { //Detect duplicates. //TODO this should be replaced by something with deterministic memory allocation. - auto keys = redBlackTree!Node(); + auto keys = new RedBlackTree!Node(); foreach(ref pair; pairs) { enforce(!(pair.key in keys), diff --git a/src/ext_depends/D-YAML/source/dyaml/emitter.d b/src/ext_depends/D-YAML/source/dyaml/emitter.d index 5cf6a92..5aafc0e 100644 --- a/src/ext_depends/D-YAML/source/dyaml/emitter.d +++ b/src/ext_depends/D-YAML/source/dyaml/emitter.d @@ -29,6 +29,7 @@ import dyaml.event; import dyaml.exception; import dyaml.linebreak; import dyaml.queue; +import dyaml.scanner; import dyaml.style; import dyaml.tagdirective; @@ -77,7 +78,7 @@ struct Emitter(Range, CharType) if (isOutputRange!(Range, CharType)) Range stream_; /// Type used for upcoming emitter steps - alias EmitterFunction = void function(typeof(this)*) @safe; + alias EmitterFunction = void function(scope typeof(this)*) @safe; ///Stack of states. Appender!(EmitterFunction[]) states_; @@ -732,14 +733,14 @@ struct Emitter(Range, CharType) if (isOutputRange!(Range, CharType)) //} auto writer = ScalarWriter!(Range, CharType)(&this, analysis_.scalar, context_ != Context.mappingSimpleKey); - with(writer) final switch(style_) + final switch(style_) { case ScalarStyle.invalid: assert(false); - case ScalarStyle.doubleQuoted: writeDoubleQuoted(); break; - case ScalarStyle.singleQuoted: writeSingleQuoted(); break; - case ScalarStyle.folded: writeFolded(); break; - case ScalarStyle.literal: writeLiteral(); break; - case ScalarStyle.plain: writePlain(); break; + case ScalarStyle.doubleQuoted: writer.writeDoubleQuoted(); break; + case ScalarStyle.singleQuoted: writer.writeSingleQuoted(); break; + case ScalarStyle.folded: writer.writeFolded(); break; + case ScalarStyle.literal: writer.writeLiteral(); break; + case ScalarStyle.plain: writer.writePlain(); break; } analysis_.flags.isNull = true; style_ = ScalarStyle.invalid; @@ -949,7 +950,7 @@ struct Emitter(Range, CharType) if (isOutputRange!(Range, CharType)) ///Prepare anchor for output. static string prepareAnchor(const string anchor) @safe in(anchor != "", "Anchor must not be empty") - in(anchor.all!(c => isAlphaNum(c) || c.among!('-', '_')), "Anchor contains invalid characters") + in(anchor.all!isNSAnchorName, "Anchor contains invalid characters") { return anchor; } diff --git a/src/ext_depends/D-YAML/source/dyaml/escapes.d b/src/ext_depends/D-YAML/source/dyaml/escapes.d index 32080a2..36fd744 100644 --- a/src/ext_depends/D-YAML/source/dyaml/escapes.d +++ b/src/ext_depends/D-YAML/source/dyaml/escapes.d @@ -11,7 +11,7 @@ package: import std.meta : AliasSeq; alias escapes = AliasSeq!('0', 'a', 'b', 't', '\t', 'n', 'v', 'f', 'r', 'e', ' ', - '\"', '\\', 'N', '_', 'L', 'P'); + '/', '\"', '\\', 'N', '_', 'L', 'P'); /// YAML hex codes specifying the length of the hex number. alias escapeHexCodeList = AliasSeq!('x', 'u', 'U'); @@ -31,6 +31,7 @@ dchar fromEscape(dchar escape) @safe pure nothrow @nogc case 'f': return '\x0C'; case 'r': return '\x0D'; case 'e': return '\x1B'; + case '/': return '/'; case ' ': return '\x20'; case '\"': return '\"'; case '\\': return '\\'; @@ -90,3 +91,16 @@ uint escapeHexLength(dchar hexCode) @safe pure nothrow @nogc } } +// Issue #302: Support optional escaping of forward slashes in string +// for JSON compatibility +@safe unittest +{ + import dyaml.loader : Loader; + + const str = `{ + "forward/slashes": "can\/be\/optionally\/escaped" +}`; + + auto node = Loader.fromString(str).load(); + assert(node["forward/slashes"] == "can/be/optionally/escaped"); +} diff --git a/src/ext_depends/D-YAML/source/dyaml/exception.d b/src/ext_depends/D-YAML/source/dyaml/exception.d index 15e9c61..145e9c3 100644 --- a/src/ext_depends/D-YAML/source/dyaml/exception.d +++ b/src/ext_depends/D-YAML/source/dyaml/exception.d @@ -19,7 +19,7 @@ class YAMLException : Exception { /// Construct a YAMLException with specified message and position where it was thrown. public this(string msg, string file = __FILE__, size_t line = __LINE__) - @safe pure nothrow + @safe pure nothrow @nogc { super(msg, file, line); } @@ -65,8 +65,14 @@ struct Mark return column_; } + /// Duplicate a mark + Mark dup () const scope @safe pure nothrow + { + return Mark(this.name_.idup, this.line_, this.column_); + } + /// Get a string representation of the mark. - string toString() @safe pure nothrow const + string toString() const scope @safe pure nothrow { // Line/column numbers start at zero internally, make them start at 1. static string clamped(ushort v) @safe pure nothrow @@ -84,23 +90,24 @@ abstract class MarkedYAMLException : YAMLException Mark mark; // Construct a MarkedYAMLException with specified context and problem. - this(string context, const Mark contextMark, string problem, const Mark problemMark, + this(string context, scope const Mark contextMark, + string problem, scope const Mark problemMark, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow { const msg = context ~ '\n' ~ (contextMark != problemMark ? contextMark.toString() ~ '\n' : "") ~ problem ~ '\n' ~ problemMark.toString() ~ '\n'; super(msg, file, line); - mark = problemMark; + mark = problemMark.dup; } // Construct a MarkedYAMLException with specified problem. - this(string problem, const Mark problemMark, + this(string problem, scope const Mark problemMark, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow { super(problem ~ '\n' ~ problemMark.toString(), file, line); - mark = problemMark; + mark = problemMark.dup; } /// Construct a MarkedYAMLException from a struct storing constructor parameters. diff --git a/src/ext_depends/D-YAML/source/dyaml/loader.d b/src/ext_depends/D-YAML/source/dyaml/loader.d index 09c19db..6638dfc 100644 --- a/src/ext_depends/D-YAML/source/dyaml/loader.d +++ b/src/ext_depends/D-YAML/source/dyaml/loader.d @@ -82,8 +82,10 @@ struct Loader /** Construct a Loader to load YAML from a string. * - * Params: data = String to load YAML from. The char[] version $(B will) - * overwrite its input during parsing as D:YAML reuses memory. + * Params: + * data = String to load YAML from. The char[] version $(B will) + * overwrite its input during parsing as D:YAML reuses memory. + * filename = The filename to give to the Loader, defaults to `""` * * Returns: Loader loading YAML from given string. * @@ -91,14 +93,14 @@ struct Loader * * YAMLException if data could not be read (e.g. a decoding error) */ - static Loader fromString(char[] data) @safe + static Loader fromString(char[] data, string filename = "") @safe { - return Loader(cast(ubyte[])data); + return Loader(cast(ubyte[])data, filename); } /// Ditto - static Loader fromString(string data) @safe + static Loader fromString(string data, string filename = "") @safe { - return fromString(data.dup); + return fromString(data.dup, filename); } /// Load a char[]. @safe unittest diff --git a/src/ext_depends/D-YAML/source/dyaml/node.d b/src/ext_depends/D-YAML/source/dyaml/node.d index e96bcec..4c3c5eb 100644 --- a/src/ext_depends/D-YAML/source/dyaml/node.d +++ b/src/ext_depends/D-YAML/source/dyaml/node.d @@ -14,13 +14,17 @@ import std.array; import std.conv; import std.datetime; import std.exception; +import std.format; import std.math; import std.meta : AliasSeq; import std.range; import std.string; import std.traits; import std.typecons; -import std.variant; + +// FIXME: Switch back to upstream's when v2.101 is the oldest +// supported version (recommended: after v2.111 release). +import dyaml.stdsumtype; import dyaml.event; import dyaml.exception; @@ -34,8 +38,9 @@ class NodeException : MarkedYAMLException // // Params: msg = Error message. // start = Start position of the node. - this(string msg, Mark start, string file = __FILE__, size_t line = __LINE__) - @safe + this(string msg, const scope Mark start, + string file = __FILE__, size_t line = __LINE__) + @safe pure nothrow { super(msg, start, file, line); } @@ -57,6 +62,9 @@ struct YAMLNull string toString() const pure @safe nothrow {return "null";} } +/// Invalid YAML type, used internally by SumType +private struct YAMLInvalid {} + // Merge YAML type, used to support "tag:yaml.org,2002:merge". package struct YAMLMerge{} @@ -79,18 +87,28 @@ private struct Pair } /// Equality test with another Pair. - bool opEquals(const ref Pair rhs) const @safe + bool opEquals(const ref Pair rhs) const scope @safe { return key == rhs.key && value == rhs.value; } // Comparison with another Pair. - int opCmp(ref const(Pair) rhs) const @safe + int opCmp(const scope ref Pair rhs) const scope @safe { const keyCmp = key.opCmp(rhs.key); return keyCmp != 0 ? keyCmp : value.opCmp(rhs.value); } + + /// + public void toString (scope void delegate(scope const(char)[]) @safe sink) + const scope @safe + { + // formattedWrite does not accept `scope` parameters + () @trusted { + formattedWrite(sink, "%s: %s", this.key, this.value); + }(); + } } enum NodeType @@ -121,15 +139,16 @@ struct Node package: // YAML value type. - alias Value = Algebraic!(YAMLNull, YAMLMerge, bool, long, real, ubyte[], SysTime, string, - Node.Pair[], Node[]); + alias Value = SumType!( + YAMLInvalid, YAMLNull, YAMLMerge, + bool, long, real, ubyte[], SysTime, string, + Node.Pair[], Node[]); // Can Value hold this type naturally? enum allowed(T) = isIntegral!T || - isFloatingPoint!T || - isSomeString!T || - is(Unqual!T == bool) || - Value.allowed!T; + isFloatingPoint!T || + isSomeString!T || + is(typeof({ Value i = T.init; })); // Stored value. Value value_; @@ -391,19 +410,19 @@ struct Node } /// Is this node valid (initialized)? - @property bool isValid() const @safe pure nothrow + @property bool isValid() const scope @safe pure nothrow @nogc { - return value_.hasValue; + return value_.match!((const YAMLInvalid _) => false, _ => true); } /// Return tag of the node. - @property string tag() const @safe nothrow + @property string tag() const return scope @safe pure nothrow @nogc { return tag_; } /// Return the start position of the node. - @property Mark startMark() const @safe pure nothrow + @property Mark startMark() const return scope @safe pure nothrow @nogc { return startMark_; } @@ -422,11 +441,11 @@ struct Node * * Returns: true if equal, false otherwise. */ - bool opEquals(const Node rhs) const @safe + bool opEquals(const scope Node rhs) const scope @safe { return opCmp(rhs) == 0; } - bool opEquals(T)(const auto ref T rhs) const + bool opEquals(T)(const scope auto ref T rhs) const @safe { try { @@ -497,10 +516,11 @@ struct Node * Throws: NodeException if unable to convert to specified type, or if * the value is out of range of requested type. */ - inout(T) get(T, Flag!"stringConversion" stringConversion = Yes.stringConversion)() inout - if (allowed!(Unqual!T) || hasNodeConstructor!(inout(Unqual!T)) || (!hasIndirections!(Unqual!T) && hasNodeConstructor!(Unqual!T))) + inout(T) get(T, Flag!"stringConversion" stringConversion = Yes.stringConversion)() inout @safe return scope { - if(isType!(Unqual!T)){return getValue!T;} + static assert (allowed!(Unqual!T) || + hasNodeConstructor!(inout(Unqual!T)) || + (!hasIndirections!(Unqual!T) && hasNodeConstructor!(Unqual!T))); static if(!allowed!(Unqual!T)) { @@ -530,6 +550,8 @@ struct Node static assert(0, "Unhandled user type"); } } else { + static if (canBeType!T) + if (isType!(Unqual!T)) { return getValue!T; } // If we're getting from a mapping and we're not getting Node.Pair[], // we're getting the default value. @@ -549,9 +571,9 @@ struct Node // Try to convert to string. try { - return coerceValue!T(); + return coerceValue!T().dup; } - catch(VariantException e) + catch (MatchException e) { throw new NodeException("Unable to convert node value to string", startMark_); } @@ -646,9 +668,11 @@ struct Node this.z = z; } - this(Node node) @safe + this(scope const Node node) @safe { - auto parts = node.as!string().split(":"); + // `std.array.split` is not marked as taking a `scope` range, + // but we don't escape a reference. + scope parts = () @trusted { return node.as!string().split(":"); }(); x = parts[0].to!int; y = parts[1].to!int; z = parts[2].to!int; @@ -770,9 +794,11 @@ struct Node this.z = z; } - this(Node node) @safe inout + this(scope const Node node) @safe inout { - auto parts = node.as!string().split(":"); + // `std.array.split` is not marked as taking a `scope` range, + // but we don't escape a reference. + scope parts = () @trusted { return node.as!string().split(":"); }(); x = parts[0].to!int; y = parts[1].to!int; z = parts[2].to!int; @@ -921,7 +947,7 @@ struct Node * non-integral index is used with a sequence or the node is * not a collection. */ - ref inout(Node) opIndex(T)(T index) inout @safe + ref inout(Node) opIndex(T)(T index) inout return scope @safe { final switch (nodeID) { @@ -1362,7 +1388,7 @@ struct Node string[int] test; foreach (pair; n.mapping) - test[pair.key.as!int] = pair.value.as!string; + test[pair.key.as!int] = pair.value.as!string.idup; assert(test[1] == "foo"); assert(test[2] == "bar"); @@ -1665,7 +1691,7 @@ struct Node Pair(k3, Node(cast(real)1.0)), Pair(k4, Node("yarly"))]); - foreach(string key, Node value; nmap2) + foreach(scope string key, scope Node value; nmap2) { switch(key) { @@ -1957,12 +1983,16 @@ struct Node } /// Compare with another _node. - int opCmp(const ref Node rhs) const @safe - { - // Compare tags - if equal or both null, we need to compare further. - const tagCmp = (tag_ is null) ? (rhs.tag_ is null) ? 0 : -1 - : (rhs.tag_ is null) ? 1 : std.algorithm.comparison.cmp(tag_, rhs.tag_); - if(tagCmp != 0){return tagCmp;} + int opCmp(const scope ref Node rhs) const scope @safe + { + const bool hasNullTag = this.tag_ is null; + // Only one of them is null: we can order nodes + if ((hasNullTag) ^ (rhs.tag is null)) + return hasNullTag ? -1 : 1; + // Either both `null` or both have a value + if (!hasNullTag) + if (int result = std.algorithm.comparison.cmp(tag_, rhs.tag_)) + return result; static int cmp(T1, T2)(T1 a, T2 b) { @@ -1972,15 +2002,14 @@ struct Node } // Compare validity: if both valid, we have to compare further. - const v1 = isValid; - const v2 = rhs.isValid; - if(!v1){return v2 ? -1 : 0;} - if(!v2){return 1;} - - const typeCmp = cmp(type, rhs.type); - if(typeCmp != 0){return typeCmp;} + if (!this.isValid()) + return rhs.isValid() ? -1 : 0; + if (!rhs.isValid()) + return 1; + if (const typeCmp = cmp(type, rhs.type)) + return typeCmp; - static int compareCollections(T)(const ref Node lhs, const ref Node rhs) + static int compareCollections(T)(const scope ref Node lhs, const scope ref Node rhs) { const c1 = lhs.getValue!T; const c2 = rhs.getValue!T; @@ -2070,7 +2099,7 @@ struct Node // Compute hash of the node. hash_t toHash() nothrow const @trusted { - const valueHash = value_.toHash(); + const valueHash = value_.match!(v => hashOf(v)); return tag_ is null ? valueHash : tag_.hashOf(valueHash); } @@ -2081,57 +2110,25 @@ struct Node } /// Get type of the node value. - @property NodeType type() const @safe nothrow - { - if (value_.type is typeid(bool)) - { - return NodeType.boolean; - } - else if (value_.type is typeid(long)) - { - return NodeType.integer; - } - else if (value_.type is typeid(Node[])) - { - return NodeType.sequence; - } - else if (value_.type is typeid(ubyte[])) - { - return NodeType.binary; - } - else if (value_.type is typeid(string)) - { - return NodeType.string; - } - else if (value_.type is typeid(Node.Pair[])) - { - return NodeType.mapping; - } - else if (value_.type is typeid(SysTime)) - { - return NodeType.timestamp; - } - else if (value_.type is typeid(YAMLNull)) - { - return NodeType.null_; - } - else if (value_.type is typeid(YAMLMerge)) - { - return NodeType.merge; - } - else if (value_.type is typeid(real)) - { - return NodeType.decimal; - } - else if (!value_.hasValue) - { - return NodeType.invalid; - } - else assert(0, text(value_.type)); + @property NodeType type() const scope @safe pure nothrow @nogc + { + return this.value_.match!( + (const bool _) => NodeType.boolean, + (const long _) => NodeType.integer, + (const Node[] _) => NodeType.sequence, + (const ubyte[] _) => NodeType.binary, + (const string _) => NodeType.string, + (const Node.Pair[] _) => NodeType.mapping, + (const SysTime _) => NodeType.timestamp, + (const YAMLNull _) => NodeType.null_, + (const YAMLMerge _) => NodeType.merge, + (const real _) => NodeType.decimal, + (const YAMLInvalid _) => NodeType.invalid, + ); } /// Get the kind of node this is. - @property NodeID nodeID() const @safe nothrow + @property NodeID nodeID() const scope @safe pure nothrow @nogc { final switch (type) { @@ -2159,7 +2156,7 @@ struct Node // Params: level = Level of the node in the tree. // // Returns: String representing the node tree. - @property string debugString(uint level = 0) const @safe + @property string debugString(uint level = 0) const scope @safe { string indent; foreach(i; 0 .. level){indent ~= " ";} @@ -2192,7 +2189,7 @@ struct Node public: - @property string nodeTypeString() const @safe nothrow + @property string nodeTypeString() const scope @safe pure nothrow @nogc { final switch (nodeID) { @@ -2325,9 +2322,16 @@ struct Node // This only works for default YAML types, not for user defined types. @property bool isType(T)() const { - return value_.type is typeid(Unqual!T); + return value_.match!( + (const T _) => true, + _ => false, + ); } + /// Check at compile time if a type is stored natively + enum canBeType (T) = is(typeof({ value_.match!((const T _) => true, _ => false); })); + + // Implementation of contains() and containsKey(). bool contains_(T, Flag!"key" key, string func)(T rhs) const { @@ -2403,7 +2407,8 @@ struct Node // Get index of pair with key (or value, if key is false) matching index. // Cannot be inferred @safe due to https://issues.dlang.org/show_bug.cgi?id=16528 - sizediff_t findPair(T, Flag!"key" key = Yes.key)(const ref T index) const @safe + sizediff_t findPair(T, Flag!"key" key = Yes.key)(const scope ref T index) + const scope @safe { const pairs = getValue!(Pair[])(); const(Node)* node; @@ -2427,7 +2432,7 @@ struct Node } // Check if index is integral and in range. - void checkSequenceIndex(T)(T index) const + void checkSequenceIndex(T)(T index) const scope @safe { assert(nodeID == NodeID.sequence, "checkSequenceIndex() called on a " ~ nodeTypeString ~ " node"); @@ -2444,14 +2449,42 @@ struct Node } } // Safe wrapper for getting a value out of the variant. - inout(T) getValue(T)() @trusted inout + inout(T) getValue(T)() @safe return scope inout { - return value_.get!T; + alias RType = typeof(return); + return value_.tryMatch!((RType r) => r); } // Safe wrapper for coercing a value out of the variant. - inout(T) coerceValue(T)() @trusted inout - { - return (cast(Value)value_).coerce!T; + inout(T) coerceValue(T)() @trusted scope return inout + { + alias RType = typeof(return); + static if (is(typeof({ RType rt = T.init; T t = RType.init; }))) + alias TType = T; + else // `inout` matters (indirection) + alias TType = RType; + + // `inout(Node[]).to!string` apparently is not safe: + // struct SumTypeBug { + // import std.conv; + // Node[] data; + // + // string bug () inout @safe + // { + // return this.data.to!string; + // } + // } + // Doesn't compile with DMD v2.100.0 + return this.value_.tryMatch!( + (inout bool v) @safe => v.to!TType, + (inout long v) @safe => v.to!TType, + (inout Node[] v) @trusted => v.to!TType, + (inout ubyte[] v) @safe => v.to!TType, + (inout string v) @safe => v.to!TType, + (inout Node.Pair[] v) @trusted => v.to!TType, + (inout SysTime v) @trusted => v.to!TType, + (inout real v) @safe => v.to!TType, + (inout YAMLNull v) @safe => null.to!TType, + ); } // Safe wrapper for setting a value for the variant. void setValue(T)(T value) @trusted @@ -2469,6 +2502,25 @@ struct Node value_ = tmpNode.value_; } } + + /// + public void toString (DGT) (scope DGT sink) + const scope @safe + { + this.value_.match!( + (const bool v) => formattedWrite(sink, v ? "true" : "false"), + (const long v) => formattedWrite(sink, "%s", v), + (const Node[] v) => formattedWrite(sink, "[%(%s, %)]", v), + (const ubyte[] v) => formattedWrite(sink, "%s", v), + (const string v) => formattedWrite(sink, `"%s"`, v), + (const Node.Pair[] v) => formattedWrite(sink, "{%(%s, %)}", v), + (const SysTime v) => formattedWrite(sink, "%s", v), + (const YAMLNull v) => formattedWrite(sink, "%s", v), + (const YAMLMerge v) => formattedWrite(sink, "%s", v), + (const real v) => formattedWrite(sink, "%s", v), + (const YAMLInvalid v) => formattedWrite(sink, "%s", v), + ); + } } package: @@ -2481,7 +2533,10 @@ package: // toMerge = Pairs to merge. void merge(ref Appender!(Node.Pair[]) pairs, Node.Pair[] toMerge) @safe { - bool eq(ref Node.Pair a, ref Node.Pair b){return a.key == b.key;} + bool eq(ref Node.Pair a, ref Node.Pair b) @safe + { + return a.key == b.key; + } foreach(ref pair; toMerge) if(!canFind!eq(pairs.data, pair)) { @@ -2528,7 +2583,7 @@ enum castableToNode(T) = (is(T == struct) || is(T == class)) && is(typeof(T.opCa { foreach(value; node["bars"].sequence) { - bars ~= value.as!string; + bars ~= value.as!string.idup; } } } diff --git a/src/ext_depends/D-YAML/source/dyaml/resolver.d b/src/ext_depends/D-YAML/source/dyaml/resolver.d index f57cbbe..16d8419 100644 --- a/src/ext_depends/D-YAML/source/dyaml/resolver.d +++ b/src/ext_depends/D-YAML/source/dyaml/resolver.d @@ -163,7 +163,7 @@ struct Resolver * * Returns: Resolved tag. */ - string resolve(const NodeID kind, const string tag, const string value, + string resolve(const NodeID kind, const string tag, scope string value, const bool implicit) @safe { import std.array : empty, front; @@ -189,7 +189,13 @@ struct Resolver //If regexp matches, return tag. foreach(resolver; resolvers) { - if(!(match(value, resolver[1]).empty)) + // source/dyaml/resolver.d(192,35): Error: scope variable `__tmpfordtorXXX` + // assigned to non-scope parameter `this` calling + // `std.regex.RegexMatch!string.RegexMatch.~this` + bool isEmpty = () @trusted { + return match(value, resolver[1]).empty; + }(); + if(!isEmpty) { return resolver[0]; } diff --git a/src/ext_depends/D-YAML/source/dyaml/scanner.d b/src/ext_depends/D-YAML/source/dyaml/scanner.d index 3f0f394..77c3e38 100644 --- a/src/ext_depends/D-YAML/source/dyaml/scanner.d +++ b/src/ext_depends/D-YAML/source/dyaml/scanner.d @@ -72,6 +72,8 @@ alias isBChar = among!('\n', '\r', '\u0085', '\u2028', '\u2029'); alias isFlowScalarBreakSpace = among!(' ', '\t', '\0', '\n', '\r', '\u0085', '\u2028', '\u2029', '\'', '"', '\\'); +alias isNSAnchorName = c => !c.isWhiteSpace && !c.among!('[', ']', '{', '}', ',', '\uFEFF'); + /// Marked exception thrown at scanner errors. /// /// See_Also: MarkedYAMLException @@ -763,6 +765,25 @@ struct Scanner reader_.sliceBuilder.write(reader_.get(length)); } + /// Scan a string. + /// + /// Assumes that the caller is building a slice in Reader, and puts the scanned + /// characters into that slice. + void scanAnchorAliasToSlice(const Mark startMark) @safe + { + size_t length; + dchar c = reader_.peek(); + while (c.isNSAnchorName) + { + c = reader_.peek(++length); + } + + enforce(length > 0, new ScannerException("While scanning an anchor or alias", + startMark, expected("a printable character besides '[', ']', '{', '}' and ','", c), reader_.mark)); + + reader_.sliceBuilder.write(reader_.get(length)); + } + /// Scan and throw away all characters until next line break. void scanToNextBreak() @safe { @@ -988,20 +1009,14 @@ struct Scanner Token scanAnchor(const TokenID id) @safe { const startMark = reader_.mark; - const dchar i = reader_.get(); + reader_.forward(); // The */& character was only peeked, so we drop it now reader_.sliceBuilder.begin(); - if(i == '*') { scanAlphaNumericToSlice!"an alias"(startMark); } - else { scanAlphaNumericToSlice!"an anchor"(startMark); } + scanAnchorAliasToSlice(startMark); // On error, value is discarded as we return immediately char[] value = reader_.sliceBuilder.finish(); - enum anchorCtx = "While scanning an anchor"; - enum aliasCtx = "While scanning an alias"; - enforce(reader_.peek().isWhiteSpace || - reader_.peekByte().among!('?', ':', ',', ']', '}', '%', '@'), - new ScannerException(i == '*' ? aliasCtx : anchorCtx, startMark, - expected("alphanumeric, '-' or '_'", reader_.peek()), reader_.mark)); + assert(!reader_.peek().isNSAnchorName, "Anchor/alias name not fully scanned"); if(id == TokenID.alias_) { @@ -1212,7 +1227,10 @@ struct Scanner // is not Strip) else { - reader_.sliceBuilder.write(lineBreak); + if (lineBreak != '\0') + { + reader_.sliceBuilder.write(lineBreak); + } } } @@ -1792,3 +1810,17 @@ struct Scanner return '\0'; } } + +// Issue 309 - https://github.com/dlang-community/D-YAML/issues/309 +@safe unittest +{ + enum str = q"EOS +exp: | + foobar +EOS".chomp; + + auto r = new Reader(cast(ubyte[])str.dup); + auto s = Scanner(r); + auto elems = s.map!"a.value".filter!"a.length > 0".array; + assert(elems[1] == "foobar"); +} diff --git a/src/ext_depends/D-YAML/source/dyaml/serializer.d b/src/ext_depends/D-YAML/source/dyaml/serializer.d index 4100cf3..cbaef63 100644 --- a/src/ext_depends/D-YAML/source/dyaml/serializer.d +++ b/src/ext_depends/D-YAML/source/dyaml/serializer.d @@ -236,7 +236,7 @@ struct Serializer const bool isDetected = node.tag_ == detectedTag; emitter.emit(scalarEvent(Mark(), Mark(), aliased, node.tag_, - isDetected, value, node.scalarStyle)); + isDetected, value.idup, node.scalarStyle)); return; case NodeID.invalid: assert(0); diff --git a/src/ext_depends/D-YAML/source/dyaml/stdsumtype.d b/src/ext_depends/D-YAML/source/dyaml/stdsumtype.d new file mode 100644 index 0000000..3dac4dd --- /dev/null +++ b/src/ext_depends/D-YAML/source/dyaml/stdsumtype.d @@ -0,0 +1,2627 @@ +/++ + This module was copied from Phobos at commit 87c6e7e35 (2022-07-06). + This is necessary to include https://github.com/dlang/phobos/pull/8501 + which is a fix needed for DIP1000 compatibility. A couple minor changes + where also required to deal with `package(std)` imports. + +[SumType] is a generic discriminated union implementation that uses +design-by-introspection to generate safe and efficient code. Its features +include: + +* [Pattern matching.][match] +* Support for self-referential types. +* Full attribute correctness (`pure`, `@safe`, `@nogc`, and `nothrow` are + inferred whenever possible). +* A type-safe and memory-safe API compatible with DIP 1000 (`scope`). +* No dependency on runtime type information (`TypeInfo`). +* Compatibility with BetterC. + +License: Boost License 1.0 +Authors: Paul Backus +Source: $(PHOBOSSRC std/sumtype.d) ++/ +module dyaml.stdsumtype; + +/// $(DIVID basic-usage,$(H3 Basic usage)) +version (D_BetterC) {} else +@safe unittest +{ + import std.math.operations : isClose; + + struct Fahrenheit { double degrees; } + struct Celsius { double degrees; } + struct Kelvin { double degrees; } + + alias Temperature = SumType!(Fahrenheit, Celsius, Kelvin); + + // Construct from any of the member types. + Temperature t1 = Fahrenheit(98.6); + Temperature t2 = Celsius(100); + Temperature t3 = Kelvin(273); + + // Use pattern matching to access the value. + Fahrenheit toFahrenheit(Temperature t) + { + return Fahrenheit( + t.match!( + (Fahrenheit f) => f.degrees, + (Celsius c) => c.degrees * 9.0/5 + 32, + (Kelvin k) => k.degrees * 9.0/5 - 459.4 + ) + ); + } + + assert(toFahrenheit(t1).degrees.isClose(98.6)); + assert(toFahrenheit(t2).degrees.isClose(212)); + assert(toFahrenheit(t3).degrees.isClose(32)); + + // Use ref to modify the value in place. + void freeze(ref Temperature t) + { + t.match!( + (ref Fahrenheit f) => f.degrees = 32, + (ref Celsius c) => c.degrees = 0, + (ref Kelvin k) => k.degrees = 273 + ); + } + + freeze(t1); + assert(toFahrenheit(t1).degrees.isClose(32)); + + // Use a catch-all handler to give a default result. + bool isFahrenheit(Temperature t) + { + return t.match!( + (Fahrenheit f) => true, + _ => false + ); + } + + assert(isFahrenheit(t1)); + assert(!isFahrenheit(t2)); + assert(!isFahrenheit(t3)); +} + +/** $(DIVID introspection-based-matching, $(H3 Introspection-based matching)) + * + * In the `length` and `horiz` functions below, the handlers for `match` do not + * specify the types of their arguments. Instead, matching is done based on how + * the argument is used in the body of the handler: any type with `x` and `y` + * properties will be matched by the `rect` handlers, and any type with `r` and + * `theta` properties will be matched by the `polar` handlers. + */ +version (D_BetterC) {} else +@safe unittest +{ + import std.math.operations : isClose; + import std.math.trigonometry : cos; + import std.math.constants : PI; + import std.math.algebraic : sqrt; + + struct Rectangular { double x, y; } + struct Polar { double r, theta; } + alias Vector = SumType!(Rectangular, Polar); + + double length(Vector v) + { + return v.match!( + rect => sqrt(rect.x^^2 + rect.y^^2), + polar => polar.r + ); + } + + double horiz(Vector v) + { + return v.match!( + rect => rect.x, + polar => polar.r * cos(polar.theta) + ); + } + + Vector u = Rectangular(1, 1); + Vector v = Polar(1, PI/4); + + assert(length(u).isClose(sqrt(2.0))); + assert(length(v).isClose(1)); + assert(horiz(u).isClose(1)); + assert(horiz(v).isClose(sqrt(0.5))); +} + +/** $(DIVID arithmetic-expression-evaluator, $(H3 Arithmetic expression evaluator)) + * + * This example makes use of the special placeholder type `This` to define a + * [recursive data type](https://en.wikipedia.org/wiki/Recursive_data_type): an + * [abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) for + * representing simple arithmetic expressions. + */ +version (D_BetterC) {} else +@system unittest +{ + import std.functional : partial; + import std.traits : EnumMembers; + import std.typecons : Tuple; + + enum Op : string + { + Plus = "+", + Minus = "-", + Times = "*", + Div = "/" + } + + // An expression is either + // - a number, + // - a variable, or + // - a binary operation combining two sub-expressions. + alias Expr = SumType!( + double, + string, + Tuple!(Op, "op", This*, "lhs", This*, "rhs") + ); + + // Shorthand for Tuple!(Op, "op", Expr*, "lhs", Expr*, "rhs"), + // the Tuple type above with Expr substituted for This. + alias BinOp = Expr.Types[2]; + + // Factory function for number expressions + Expr* num(double value) + { + return new Expr(value); + } + + // Factory function for variable expressions + Expr* var(string name) + { + return new Expr(name); + } + + // Factory function for binary operation expressions + Expr* binOp(Op op, Expr* lhs, Expr* rhs) + { + return new Expr(BinOp(op, lhs, rhs)); + } + + // Convenience wrappers for creating BinOp expressions + alias sum = partial!(binOp, Op.Plus); + alias diff = partial!(binOp, Op.Minus); + alias prod = partial!(binOp, Op.Times); + alias quot = partial!(binOp, Op.Div); + + // Evaluate expr, looking up variables in env + double eval(Expr expr, double[string] env) + { + return expr.match!( + (double num) => num, + (string var) => env[var], + (BinOp bop) + { + double lhs = eval(*bop.lhs, env); + double rhs = eval(*bop.rhs, env); + final switch (bop.op) + { + static foreach (op; EnumMembers!Op) + { + case op: + return mixin("lhs" ~ op ~ "rhs"); + } + } + } + ); + } + + // Return a "pretty-printed" representation of expr + string pprint(Expr expr) + { + import std.format : format; + + return expr.match!( + (double num) => "%g".format(num), + (string var) => var, + (BinOp bop) => "(%s %s %s)".format( + pprint(*bop.lhs), + cast(string) bop.op, + pprint(*bop.rhs) + ) + ); + } + + Expr* myExpr = sum(var("a"), prod(num(2), var("b"))); + double[string] myEnv = ["a":3, "b":4, "c":7]; + + assert(eval(*myExpr, myEnv) == 11); + assert(pprint(*myExpr) == "(a + (2 * b))"); +} + +import std.format.spec : FormatSpec, singleSpec; +import std.meta : AliasSeq, Filter, IndexOf = staticIndexOf, Map = staticMap; +import std.meta : NoDuplicates; +import std.meta : anySatisfy, allSatisfy; +import std.traits : hasElaborateCopyConstructor, hasElaborateDestructor; +import std.traits : isAssignable, isCopyable, isStaticArray, isRvalueAssignable; +import std.traits : ConstOf, ImmutableOf, InoutOf, TemplateArgsOf; + +// FIXME: std.sumtype : `std.traits : DeducedParameterType` and `std.conv : toCtString` +// are `package(std)` but trivial, hence copied below +import std.traits : CommonType, /*DeducatedParameterType*/ Unqual; +private template DeducedParameterType(T) +{ + static if (is(T == U*, U) || is(T == U[], U)) + alias DeducedParameterType = Unqual!T; + else + alias DeducedParameterType = T; +} + +import std.typecons : ReplaceTypeUnless; +import std.typecons : Flag; +//import std.conv : toCtString; +private enum toCtString(ulong n) = n.stringof[0 .. $ - "LU".length]; + +/// Placeholder used to refer to the enclosing [SumType]. +struct This {} + +// True if a variable of type T can appear on the lhs of an assignment +private enum isAssignableTo(T) = + isAssignable!T || (!isCopyable!T && isRvalueAssignable!T); + +// toHash is required by the language spec to be nothrow and @safe +private enum isHashable(T) = __traits(compiles, + () nothrow @safe { hashOf(T.init); } +); + +private enum hasPostblit(T) = __traits(hasPostblit, T); + +private enum isInout(T) = is(T == inout); + +/** + * A [tagged union](https://en.wikipedia.org/wiki/Tagged_union) that can hold a + * single value from any of a specified set of types. + * + * The value in a `SumType` can be operated on using [pattern matching][match]. + * + * To avoid ambiguity, duplicate types are not allowed (but see the + * ["basic usage" example](#basic-usage) for a workaround). + * + * The special type `This` can be used as a placeholder to create + * self-referential types, just like with `Algebraic`. See the + * ["Arithmetic expression evaluator" example](#arithmetic-expression-evaluator) for + * usage. + * + * A `SumType` is initialized by default to hold the `.init` value of its + * first member type, just like a regular union. The version identifier + * `SumTypeNoDefaultCtor` can be used to disable this behavior. + * + * See_Also: $(REF Algebraic, std,variant) + */ +struct SumType(Types...) +if (is(NoDuplicates!Types == Types) && Types.length > 0) +{ + /// The types a `SumType` can hold. + alias Types = AliasSeq!( + ReplaceTypeUnless!(isSumTypeInstance, This, typeof(this), TemplateArgsOf!SumType) + ); + +private: + + enum bool canHoldTag(T) = Types.length <= T.max; + alias unsignedInts = AliasSeq!(ubyte, ushort, uint, ulong); + + alias Tag = Filter!(canHoldTag, unsignedInts)[0]; + + union Storage + { + // Workaround for https://issues.dlang.org/show_bug.cgi?id=20068 + template memberName(T) + if (IndexOf!(T, Types) >= 0) + { + enum tid = IndexOf!(T, Types); + mixin("enum memberName = `values_", toCtString!tid, "`;"); + } + + static foreach (T; Types) + { + mixin("T ", memberName!T, ";"); + } + } + + Storage storage; + Tag tag; + + /* Accesses the value stored in a SumType. + * + * This method is memory-safe, provided that: + * + * 1. A SumType's tag is always accurate. + * 2. A SumType cannot be assigned to in @safe code if that assignment + * could cause unsafe aliasing. + * + * All code that accesses a SumType's tag or storage directly, including + * @safe code in this module, must be manually checked to ensure that it + * does not violate either of the above requirements. + */ + @trusted + ref inout(T) get(T)() inout + if (IndexOf!(T, Types) >= 0) + { + enum tid = IndexOf!(T, Types); + assert(tag == tid, + "This `" ~ SumType.stringof ~ + "` does not contain a(n) `" ~ T.stringof ~ "`" + ); + return __traits(getMember, storage, Storage.memberName!T); + } + +public: + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21399 + version (StdDdoc) + { + // Dummy type to stand in for loop variable + private struct T; + + /// Constructs a `SumType` holding a specific value. + this(T value); + + /// ditto + this(const(T) value) const; + + /// ditto + this(immutable(T) value) immutable; + + /// ditto + this(Value)(Value value) inout + if (is(Value == DeducedParameterType!(inout(T)))); + } + + static foreach (tid, T; Types) + { + /// Constructs a `SumType` holding a specific value. + this(T value) + { + import core.lifetime : forward; + + static if (isCopyable!T) + { + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21542 + __traits(getMember, storage, Storage.memberName!T) = __ctfe ? value : forward!value; + } + else + { + __traits(getMember, storage, Storage.memberName!T) = forward!value; + } + + tag = tid; + } + + static if (isCopyable!(const(T))) + { + static if (IndexOf!(const(T), Map!(ConstOf, Types)) == tid) + { + /// ditto + this(const(T) value) const + { + __traits(getMember, storage, Storage.memberName!T) = value; + tag = tid; + } + } + } + else + { + @disable this(const(T) value) const; + } + + static if (isCopyable!(immutable(T))) + { + static if (IndexOf!(immutable(T), Map!(ImmutableOf, Types)) == tid) + { + /// ditto + this(immutable(T) value) immutable + { + __traits(getMember, storage, Storage.memberName!T) = value; + tag = tid; + } + } + } + else + { + @disable this(immutable(T) value) immutable; + } + + static if (isCopyable!(inout(T))) + { + static if (IndexOf!(inout(T), Map!(InoutOf, Types)) == tid) + { + /// ditto + this(Value)(Value value) inout + if (is(Value == DeducedParameterType!(inout(T)))) + { + __traits(getMember, storage, Storage.memberName!T) = value; + tag = tid; + } + } + } + else + { + @disable this(Value)(Value value) inout + if (is(Value == DeducedParameterType!(inout(T)))); + } + } + + static if (anySatisfy!(hasElaborateCopyConstructor, Types)) + { + static if + ( + allSatisfy!(isCopyable, Map!(InoutOf, Types)) + && !anySatisfy!(hasPostblit, Map!(InoutOf, Types)) + && allSatisfy!(isInout, Map!(InoutOf, Types)) + ) + { + /// Constructs a `SumType` that's a copy of another `SumType`. + this(ref inout(SumType) other) inout + { + storage = other.match!((ref value) { + alias OtherTypes = Map!(InoutOf, Types); + enum tid = IndexOf!(typeof(value), OtherTypes); + alias T = Types[tid]; + + mixin("inout(Storage) newStorage = { ", + Storage.memberName!T, ": value", + " };"); + + return newStorage; + }); + + tag = other.tag; + } + } + else + { + static if (allSatisfy!(isCopyable, Types)) + { + /// ditto + this(ref SumType other) + { + storage = other.match!((ref value) { + alias T = typeof(value); + + mixin("Storage newStorage = { ", + Storage.memberName!T, ": value", + " };"); + + return newStorage; + }); + + tag = other.tag; + } + } + else + { + @disable this(ref SumType other); + } + + static if (allSatisfy!(isCopyable, Map!(ConstOf, Types))) + { + /// ditto + this(ref const(SumType) other) const + { + storage = other.match!((ref value) { + alias OtherTypes = Map!(ConstOf, Types); + enum tid = IndexOf!(typeof(value), OtherTypes); + alias T = Types[tid]; + + mixin("const(Storage) newStorage = { ", + Storage.memberName!T, ": value", + " };"); + + return newStorage; + }); + + tag = other.tag; + } + } + else + { + @disable this(ref const(SumType) other) const; + } + + static if (allSatisfy!(isCopyable, Map!(ImmutableOf, Types))) + { + /// ditto + this(ref immutable(SumType) other) immutable + { + storage = other.match!((ref value) { + alias OtherTypes = Map!(ImmutableOf, Types); + enum tid = IndexOf!(typeof(value), OtherTypes); + alias T = Types[tid]; + + mixin("immutable(Storage) newStorage = { ", + Storage.memberName!T, ": value", + " };"); + + return newStorage; + }); + + tag = other.tag; + } + } + else + { + @disable this(ref immutable(SumType) other) immutable; + } + } + } + + version (SumTypeNoDefaultCtor) + { + @disable this(); + } + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21399 + version (StdDdoc) + { + // Dummy type to stand in for loop variable + private struct T; + + /** + * Assigns a value to a `SumType`. + * + * If any of the `SumType`'s members other than the one being assigned + * to contain pointers or references, it is possible for the assignment + * to cause memory corruption (see the + * ["Memory corruption" example](#memory-corruption) below for an + * illustration of how). Therefore, such assignments are considered + * `@system`. + * + * An individual assignment can be `@trusted` if the caller can + * guarantee that there are no outstanding references to any `SumType` + * members that contain pointers or references at the time the + * assignment occurs. + * + * Examples: + * + * $(DIVID memory-corruption, $(H3 Memory corruption)) + * + * This example shows how assignment to a `SumType` can be used to + * cause memory corruption in `@system` code. In `@safe` code, the + * assignment `s = 123` would not be allowed. + * + * --- + * SumType!(int*, int) s = new int; + * s.tryMatch!( + * (ref int* p) { + * s = 123; // overwrites `p` + * return *p; // undefined behavior + * } + * ); + * --- + */ + ref SumType opAssign(T rhs); + } + + static foreach (tid, T; Types) + { + static if (isAssignableTo!T) + { + /** + * Assigns a value to a `SumType`. + * + * If any of the `SumType`'s members other than the one being assigned + * to contain pointers or references, it is possible for the assignment + * to cause memory corruption (see the + * ["Memory corruption" example](#memory-corruption) below for an + * illustration of how). Therefore, such assignments are considered + * `@system`. + * + * An individual assignment can be `@trusted` if the caller can + * guarantee that there are no outstanding references to any `SumType` + * members that contain pointers or references at the time the + * assignment occurs. + * + * Examples: + * + * $(DIVID memory-corruption, $(H3 Memory corruption)) + * + * This example shows how assignment to a `SumType` can be used to + * cause memory corruption in `@system` code. In `@safe` code, the + * assignment `s = 123` would not be allowed. + * + * --- + * SumType!(int*, int) s = new int; + * s.tryMatch!( + * (ref int* p) { + * s = 123; // overwrites `p` + * return *p; // undefined behavior + * } + * ); + * --- + */ + ref SumType opAssign(T rhs) + { + import core.lifetime : forward; + import std.traits : hasIndirections, hasNested; + import std.meta : AliasSeq, Or = templateOr; + + alias OtherTypes = + AliasSeq!(Types[0 .. tid], Types[tid + 1 .. $]); + enum unsafeToOverwrite = + anySatisfy!(Or!(hasIndirections, hasNested), OtherTypes); + + static if (unsafeToOverwrite) + { + cast(void) () @system {}(); + } + + this.match!destroyIfOwner; + + static if (isCopyable!T) + { + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21542 + mixin("Storage newStorage = { ", + Storage.memberName!T, ": __ctfe ? rhs : forward!rhs", + " };"); + } + else + { + mixin("Storage newStorage = { ", + Storage.memberName!T, ": forward!rhs", + " };"); + } + + storage = newStorage; + tag = tid; + + return this; + } + } + } + + static if (allSatisfy!(isAssignableTo, Types)) + { + static if (allSatisfy!(isCopyable, Types)) + { + /** + * Copies the value from another `SumType` into this one. + * + * See the value-assignment overload for details on `@safe`ty. + * + * Copy assignment is `@disable`d if any of `Types` is non-copyable. + */ + ref SumType opAssign(ref SumType rhs) + { + rhs.match!((ref value) { this = value; }); + return this; + } + } + else + { + @disable ref SumType opAssign(ref SumType rhs); + } + + /** + * Moves the value from another `SumType` into this one. + * + * See the value-assignment overload for details on `@safe`ty. + */ + ref SumType opAssign(SumType rhs) + { + import core.lifetime : move; + + rhs.match!((ref value) { + static if (isCopyable!(typeof(value))) + { + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21542 + this = __ctfe ? value : move(value); + } + else + { + this = move(value); + } + }); + return this; + } + } + + /** + * Compares two `SumType`s for equality. + * + * Two `SumType`s are equal if they are the same kind of `SumType`, they + * contain values of the same type, and those values are equal. + */ + bool opEquals(this This, Rhs)(auto ref Rhs rhs) + if (!is(CommonType!(This, Rhs) == void)) + { + static if (is(This == Rhs)) + { + return AliasSeq!(this, rhs).match!((ref value, ref rhsValue) { + static if (is(typeof(value) == typeof(rhsValue))) + { + return value == rhsValue; + } + else + { + return false; + } + }); + } + else + { + alias CommonSumType = CommonType!(This, Rhs); + return cast(CommonSumType) this == cast(CommonSumType) rhs; + } + } + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=19407 + static if (__traits(compiles, anySatisfy!(hasElaborateDestructor, Types))) + { + // If possible, include the destructor only when it's needed + private enum includeDtor = anySatisfy!(hasElaborateDestructor, Types); + } + else + { + // If we can't tell, always include it, even when it does nothing + private enum includeDtor = true; + } + + static if (includeDtor) + { + /// Calls the destructor of the `SumType`'s current value. + ~this() + { + this.match!destroyIfOwner; + } + } + + invariant + { + this.match!((ref value) { + static if (is(typeof(value) == class)) + { + if (value !is null) + { + assert(value); + } + } + else static if (is(typeof(value) == struct)) + { + assert(&value); + } + }); + } + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21400 + version (StdDdoc) + { + /** + * Returns a string representation of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + */ + string toString(this This)(); + + /** + * Handles formatted writing of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + * + * Params: + * sink = Output range to write to. + * fmt = Format specifier to use. + * + * See_Also: $(REF formatValue, std,format) + */ + void toString(this This, Sink, Char)(ref Sink sink, const ref FormatSpec!Char fmt); + } + + version (D_BetterC) {} else + /** + * Returns a string representation of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + */ + string toString(this This)() + { + import std.conv : to; + + return this.match!(to!string); + } + + version (D_BetterC) {} else + /** + * Handles formatted writing of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + * + * Params: + * sink = Output range to write to. + * fmt = Format specifier to use. + * + * See_Also: $(REF formatValue, std,format) + */ + void toString(this This, Sink, Char)(ref Sink sink, const ref FormatSpec!Char fmt) + { + import std.format.write : formatValue; + + this.match!((ref value) { + formatValue(sink, value, fmt); + }); + } + + static if (allSatisfy!(isHashable, Map!(ConstOf, Types))) + { + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21400 + version (StdDdoc) + { + /** + * Returns the hash of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + */ + size_t toHash() const; + } + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=20095 + version (D_BetterC) {} else + /** + * Returns the hash of the `SumType`'s current value. + * + * Not available when compiled with `-betterC`. + */ + size_t toHash() const + { + return this.match!hashOf; + } + } +} + +// Construction +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); +} + +// Assignment +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + x = 3.14; +} + +// Self assignment +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + y = x; +} + +// Equality +@safe unittest +{ + alias MySum = SumType!(int, float); + + assert(MySum(123) == MySum(123)); + assert(MySum(123) != MySum(456)); + assert(MySum(123) != MySum(123.0)); + assert(MySum(123) != MySum(456.0)); + +} + +// Equality of differently-qualified SumTypes +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + alias SumA = SumType!(int, float); + alias SumB = SumType!(const(int[]), int[]); + alias SumC = SumType!(int[], const(int[])); + + int[] ma = [1, 2, 3]; + const(int[]) ca = [1, 2, 3]; + + assert(const(SumA)(123) == SumA(123)); + assert(const(SumB)(ma[]) == SumB(ca[])); + assert(const(SumC)(ma[]) == SumC(ca[])); +} + +// Imported types +@safe unittest +{ + import std.typecons : Tuple; + + alias MySum = SumType!(Tuple!(int, int)); +} + +// const and immutable types +@safe unittest +{ + alias MySum = SumType!(const(int[]), immutable(float[])); +} + +// Recursive types +@safe unittest +{ + alias MySum = SumType!(This*); + assert(is(MySum.Types[0] == MySum*)); +} + +// Allowed types +@safe unittest +{ + import std.meta : AliasSeq; + + alias MySum = SumType!(int, float, This*); + + assert(is(MySum.Types == AliasSeq!(int, float, MySum*))); +} + +// Types with destructors and postblits +@system unittest +{ + int copies; + + static struct Test + { + bool initialized = false; + int* copiesPtr; + + this(this) { (*copiesPtr)++; } + ~this() { if (initialized) (*copiesPtr)--; } + } + + alias MySum = SumType!(int, Test); + + Test t = Test(true, &copies); + + { + MySum x = t; + assert(copies == 1); + } + assert(copies == 0); + + { + MySum x = 456; + assert(copies == 0); + } + assert(copies == 0); + + { + MySum x = t; + assert(copies == 1); + x = 456; + assert(copies == 0); + } + + { + MySum x = 456; + assert(copies == 0); + x = t; + assert(copies == 1); + } + + { + MySum x = t; + MySum y = x; + assert(copies == 2); + } + + { + MySum x = t; + MySum y; + y = x; + assert(copies == 2); + } +} + +// Doesn't destroy reference types +// Disabled in BetterC due to use of classes +version (D_BetterC) {} else +@system unittest +{ + bool destroyed; + + class C + { + ~this() + { + destroyed = true; + } + } + + struct S + { + ~this() {} + } + + alias MySum = SumType!(S, C); + + C c = new C(); + { + MySum x = c; + destroyed = false; + } + assert(!destroyed); + + { + MySum x = c; + destroyed = false; + x = S(); + assert(!destroyed); + } +} + +// Types with @disable this() +@safe unittest +{ + static struct NoInit + { + @disable this(); + } + + alias MySum = SumType!(NoInit, int); + + assert(!__traits(compiles, MySum())); + auto _ = MySum(42); +} + +// const SumTypes +version (D_BetterC) {} else // not @nogc, https://issues.dlang.org/show_bug.cgi?id=22117 +@safe unittest +{ + auto _ = const(SumType!(int[]))([1, 2, 3]); +} + +// Equality of const SumTypes +@safe unittest +{ + alias MySum = SumType!int; + + auto _ = const(MySum)(123) == const(MySum)(456); +} + +// Compares reference types using value equality +@safe unittest +{ + import std.array : staticArray; + + static struct Field {} + static struct Struct { Field[] fields; } + alias MySum = SumType!Struct; + + static arr1 = staticArray([Field()]); + static arr2 = staticArray([Field()]); + + auto a = MySum(Struct(arr1[])); + auto b = MySum(Struct(arr2[])); + + assert(a == b); +} + +// toString +// Disabled in BetterC due to use of std.conv.text +version (D_BetterC) {} else +@safe unittest +{ + import std.conv : text; + + static struct Int { int i; } + static struct Double { double d; } + alias Sum = SumType!(Int, Double); + + assert(Sum(Int(42)).text == Int(42).text, Sum(Int(42)).text); + assert(Sum(Double(33.3)).text == Double(33.3).text, Sum(Double(33.3)).text); + assert((const(Sum)(Int(42))).text == (const(Int)(42)).text, (const(Sum)(Int(42))).text); +} + +// string formatting +// Disabled in BetterC due to use of std.format.format +version (D_BetterC) {} else +@safe unittest +{ + import std.format : format; + + SumType!int x = 123; + + assert(format!"%s"(x) == format!"%s"(123)); + assert(format!"%x"(x) == format!"%x"(123)); +} + +// string formatting of qualified SumTypes +// Disabled in BetterC due to use of std.format.format and dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + import std.format : format; + + int[] a = [1, 2, 3]; + const(SumType!(int[])) x = a; + + assert(format!"%(%d, %)"(x) == format!"%(%s, %)"(a)); +} + +// Github issue #16 +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + alias Node = SumType!(This[], string); + + // override inference of @system attribute for cyclic functions + assert((() @trusted => + Node([Node([Node("x")])]) + == + Node([Node([Node("x")])]) + )()); +} + +// Github issue #16 with const +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + alias Node = SumType!(const(This)[], string); + + // override inference of @system attribute for cyclic functions + assert((() @trusted => + Node([Node([Node("x")])]) + == + Node([Node([Node("x")])]) + )()); +} + +// Stale pointers +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@system unittest +{ + alias MySum = SumType!(ubyte, void*[2]); + + MySum x = [null, cast(void*) 0x12345678]; + void** p = &x.get!(void*[2])[1]; + x = ubyte(123); + + assert(*p != cast(void*) 0x12345678); +} + +// Exception-safe assignment +// Disabled in BetterC due to use of exceptions +version (D_BetterC) {} else +@safe unittest +{ + static struct A + { + int value = 123; + } + + static struct B + { + int value = 456; + this(this) { throw new Exception("oops"); } + } + + alias MySum = SumType!(A, B); + + MySum x; + try + { + x = B(); + } + catch (Exception e) {} + + assert( + (x.tag == 0 && x.get!A.value == 123) || + (x.tag == 1 && x.get!B.value == 456) + ); +} + +// Types with @disable this(this) +@safe unittest +{ + import core.lifetime : move; + + static struct NoCopy + { + @disable this(this); + } + + alias MySum = SumType!NoCopy; + + NoCopy lval = NoCopy(); + + MySum x = NoCopy(); + MySum y = NoCopy(); + + + assert(!__traits(compiles, SumType!NoCopy(lval))); + + y = NoCopy(); + y = move(x); + assert(!__traits(compiles, y = lval)); + assert(!__traits(compiles, y = x)); + + bool b = x == y; +} + +// Github issue #22 +// Disabled in BetterC due to use of std.typecons.Nullable +version (D_BetterC) {} else +@safe unittest +{ + import std.typecons; + + static struct A + { + SumType!(Nullable!int) a = Nullable!int.init; + } +} + +// Static arrays of structs with postblits +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + static struct S + { + int n; + this(this) { n++; } + } + + SumType!(S[1]) x = [S(0)]; + SumType!(S[1]) y = x; + + auto xval = x.get!(S[1])[0].n; + auto yval = y.get!(S[1])[0].n; + + assert(xval != yval); +} + +// Replacement does not happen inside SumType +// Disabled in BetterC due to use of associative arrays +version (D_BetterC) {} else +@safe unittest +{ + import std.typecons : Tuple, ReplaceTypeUnless; + alias A = Tuple!(This*,SumType!(This*))[SumType!(This*,string)[This]]; + alias TR = ReplaceTypeUnless!(isSumTypeInstance, This, int, A); + static assert(is(TR == Tuple!(int*,SumType!(This*))[SumType!(This*, string)[int]])); +} + +// Supports nested self-referential SumTypes +@safe unittest +{ + import std.typecons : Tuple, Flag; + alias Nat = SumType!(Flag!"0", Tuple!(This*)); + alias Inner = SumType!Nat; + alias Outer = SumType!(Nat*, Tuple!(This*, This*)); +} + +// Self-referential SumTypes inside Algebraic +// Disabled in BetterC due to use of std.variant.Algebraic +version (D_BetterC) {} else +@safe unittest +{ + import std.variant : Algebraic; + + alias T = Algebraic!(SumType!(This*)); + + assert(is(T.AllowedTypes[0].Types[0] == T.AllowedTypes[0]*)); +} + +// Doesn't call @system postblits in @safe code +@safe unittest +{ + static struct SystemCopy { @system this(this) {} } + SystemCopy original; + + assert(!__traits(compiles, () @safe + { + SumType!SystemCopy copy = original; + })); + + assert(!__traits(compiles, () @safe + { + SumType!SystemCopy copy; copy = original; + })); +} + +// Doesn't overwrite pointers in @safe code +@safe unittest +{ + alias MySum = SumType!(int*, int); + + MySum x; + + assert(!__traits(compiles, () @safe + { + x = 123; + })); + + assert(!__traits(compiles, () @safe + { + x = MySum(123); + })); +} + +// Types with invariants +// Disabled in BetterC due to use of exceptions +version (D_BetterC) {} else +version (D_Invariants) +@system unittest +{ + import std.exception : assertThrown; + import core.exception : AssertError; + + struct S + { + int i; + invariant { assert(i >= 0); } + } + + class C + { + int i; + invariant { assert(i >= 0); } + } + + SumType!S x; + x.match!((ref v) { v.i = -1; }); + assertThrown!AssertError(assert(&x)); + + SumType!C y = new C(); + y.match!((ref v) { v.i = -1; }); + assertThrown!AssertError(assert(&y)); +} + +// Calls value postblit on self-assignment +@safe unittest +{ + static struct S + { + int n; + this(this) { n++; } + } + + SumType!S x = S(); + SumType!S y; + y = x; + + auto xval = x.get!S.n; + auto yval = y.get!S.n; + + assert(xval != yval); +} + +// Github issue #29 +@safe unittest +{ + alias A = SumType!string; + + @safe A createA(string arg) + { + return A(arg); + } + + @safe void test() + { + A a = createA(""); + } +} + +// SumTypes as associative array keys +// Disabled in BetterC due to use of associative arrays +version (D_BetterC) {} else +@safe unittest +{ + int[SumType!(int, string)] aa; +} + +// toString with non-copyable types +// Disabled in BetterC due to use of std.conv.to (in toString) +version (D_BetterC) {} else +@safe unittest +{ + struct NoCopy + { + @disable this(this); + } + + SumType!NoCopy x; + + auto _ = x.toString(); +} + +// Can use the result of assignment +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum a = MySum(123); + MySum b = MySum(3.14); + + assert((a = b) == b); + assert((a = MySum(123)) == MySum(123)); + assert((a = 3.14) == MySum(3.14)); + assert(((a = b) = MySum(123)) == MySum(123)); +} + +// Types with copy constructors +@safe unittest +{ + static struct S + { + int n; + + this(ref return scope inout S other) inout + { + n = other.n + 1; + } + } + + SumType!S x = S(); + SumType!S y = x; + + auto xval = x.get!S.n; + auto yval = y.get!S.n; + + assert(xval != yval); +} + +// Copyable by generated copy constructors +@safe unittest +{ + static struct Inner + { + ref this(ref inout Inner other) {} + } + + static struct Outer + { + SumType!Inner inner; + } + + Outer x; + Outer y = x; +} + +// Types with qualified copy constructors +@safe unittest +{ + static struct ConstCopy + { + int n; + this(inout int n) inout { this.n = n; } + this(ref const typeof(this) other) const { this.n = other.n; } + } + + static struct ImmutableCopy + { + int n; + this(inout int n) inout { this.n = n; } + this(ref immutable typeof(this) other) immutable { this.n = other.n; } + } + + const SumType!ConstCopy x = const(ConstCopy)(1); + immutable SumType!ImmutableCopy y = immutable(ImmutableCopy)(1); +} + +// Types with disabled opEquals +@safe unittest +{ + static struct S + { + @disable bool opEquals(const S rhs) const; + } + + auto _ = SumType!S(S()); +} + +// Types with non-const opEquals +@safe unittest +{ + static struct S + { + int i; + bool opEquals(S rhs) { return i == rhs.i; } + } + + auto _ = SumType!S(S(123)); +} + +// Incomparability of different SumTypes +@safe unittest +{ + SumType!(int, string) x = 123; + SumType!(string, int) y = 123; + + assert(!__traits(compiles, x != y)); +} + +// Self-reference in return/parameter type of function pointer member +// Disabled in BetterC due to use of delegates +version (D_BetterC) {} else +@safe unittest +{ + alias T = SumType!(int, This delegate(This)); +} + +// Construction and assignment from implicitly-convertible lvalue +@safe unittest +{ + alias MySum = SumType!bool; + + const(bool) b = true; + + MySum x = b; + MySum y; y = b; +} + +// @safe assignment to the only pointer type in a SumType +@safe unittest +{ + SumType!(string, int) sm = 123; + sm = "this should be @safe"; +} + +// Immutable member type with copy constructor +// https://issues.dlang.org/show_bug.cgi?id=22572 +@safe unittest +{ + static struct CopyConstruct + { + this(ref inout CopyConstruct other) inout {} + } + + static immutable struct Value + { + CopyConstruct c; + } + + SumType!Value s; +} + +// Construction of inout-qualified SumTypes +// https://issues.dlang.org/show_bug.cgi?id=22901 +@safe unittest +{ + static inout(SumType!(int[])) example(inout(int[]) arr) + { + return inout(SumType!(int[]))(arr); + } +} + +// Assignment of struct with overloaded opAssign in CTFE +// https://issues.dlang.org/show_bug.cgi?id=23182 +@safe unittest +{ + static struct HasOpAssign + { + void opAssign(HasOpAssign rhs) {} + } + + static SumType!HasOpAssign test() + { + SumType!HasOpAssign s; + // Test both overloads + s = HasOpAssign(); + s = SumType!HasOpAssign(); + return s; + } + + // Force CTFE + enum result = test(); +} + +/// True if `T` is an instance of the `SumType` template, otherwise false. +private enum bool isSumTypeInstance(T) = is(T == SumType!Args, Args...); + +@safe unittest +{ + static struct Wrapper + { + SumType!int s; + alias s this; + } + + assert(isSumTypeInstance!(SumType!int)); + assert(!isSumTypeInstance!Wrapper); +} + +/// True if `T` is a [SumType] or implicitly converts to one, otherwise false. +enum bool isSumType(T) = is(T : SumType!Args, Args...); + +/// +@safe unittest +{ + static struct ConvertsToSumType + { + SumType!int payload; + alias payload this; + } + + static struct ContainsSumType + { + SumType!int payload; + } + + assert(isSumType!(SumType!int)); + assert(isSumType!ConvertsToSumType); + assert(!isSumType!ContainsSumType); +} + +/** + * Calls a type-appropriate function with the value held in a [SumType]. + * + * For each possible type the [SumType] can hold, the given handlers are + * checked, in order, to see whether they accept a single argument of that type. + * The first one that does is chosen as the match for that type. (Note that the + * first match may not always be the most exact match. + * See ["Avoiding unintentional matches"](#avoiding-unintentional-matches) for + * one common pitfall.) + * + * Every type must have a matching handler, and every handler must match at + * least one type. This is enforced at compile time. + * + * Handlers may be functions, delegates, or objects with `opCall` overloads. If + * a function with more than one overload is given as a handler, all of the + * overloads are considered as potential matches. + * + * Templated handlers are also accepted, and will match any type for which they + * can be [implicitly instantiated](https://dlang.org/glossary.html#ifti). See + * ["Introspection-based matching"](#introspection-based-matching) for an + * example of templated handler usage. + * + * If multiple [SumType]s are passed to match, their values are passed to the + * handlers as separate arguments, and matching is done for each possible + * combination of value types. See ["Multiple dispatch"](#multiple-dispatch) for + * an example. + * + * Returns: + * The value returned from the handler that matches the currently-held type. + * + * See_Also: $(REF visit, std,variant) + */ +template match(handlers...) +{ + import std.typecons : Yes; + + /** + * The actual `match` function. + * + * Params: + * args = One or more [SumType] objects. + */ + auto ref match(SumTypes...)(auto ref SumTypes args) + if (allSatisfy!(isSumType, SumTypes) && args.length > 0) + { + return matchImpl!(Yes.exhaustive, handlers)(args); + } +} + +/** $(DIVID avoiding-unintentional-matches, $(H3 Avoiding unintentional matches)) + * + * Sometimes, implicit conversions may cause a handler to match more types than + * intended. The example below shows two solutions to this problem. + */ +@safe unittest +{ + alias Number = SumType!(double, int); + + Number x; + + // Problem: because int implicitly converts to double, the double + // handler is used for both types, and the int handler never matches. + assert(!__traits(compiles, + x.match!( + (double d) => "got double", + (int n) => "got int" + ) + )); + + // Solution 1: put the handler for the "more specialized" type (in this + // case, int) before the handler for the type it converts to. + assert(__traits(compiles, + x.match!( + (int n) => "got int", + (double d) => "got double" + ) + )); + + // Solution 2: use a template that only accepts the exact type it's + // supposed to match, instead of any type that implicitly converts to it. + alias exactly(T, alias fun) = function (arg) + { + static assert(is(typeof(arg) == T)); + return fun(arg); + }; + + // Now, even if we put the double handler first, it will only be used for + // doubles, not ints. + assert(__traits(compiles, + x.match!( + exactly!(double, d => "got double"), + exactly!(int, n => "got int") + ) + )); +} + +/** $(DIVID multiple-dispatch, $(H3 Multiple dispatch)) + * + * Pattern matching can be performed on multiple `SumType`s at once by passing + * handlers with multiple arguments. This usually leads to more concise code + * than using nested calls to `match`, as show below. + */ +@safe unittest +{ + struct Point2D { double x, y; } + struct Point3D { double x, y, z; } + + alias Point = SumType!(Point2D, Point3D); + + version (none) + { + // This function works, but the code is ugly and repetitive. + // It uses three separate calls to match! + @safe pure nothrow @nogc + bool sameDimensions(Point p1, Point p2) + { + return p1.match!( + (Point2D _) => p2.match!( + (Point2D _) => true, + _ => false + ), + (Point3D _) => p2.match!( + (Point3D _) => true, + _ => false + ) + ); + } + } + + // This version is much nicer. + @safe pure nothrow @nogc + bool sameDimensions(Point p1, Point p2) + { + alias doMatch = match!( + (Point2D _1, Point2D _2) => true, + (Point3D _1, Point3D _2) => true, + (_1, _2) => false + ); + + return doMatch(p1, p2); + } + + Point a = Point2D(1, 2); + Point b = Point2D(3, 4); + Point c = Point3D(5, 6, 7); + Point d = Point3D(8, 9, 0); + + assert( sameDimensions(a, b)); + assert( sameDimensions(c, d)); + assert(!sameDimensions(a, c)); + assert(!sameDimensions(d, b)); +} + +/** + * Attempts to call a type-appropriate function with the value held in a + * [SumType], and throws on failure. + * + * Matches are chosen using the same rules as [match], but are not required to + * be exhaustive—in other words, a type (or combination of types) is allowed to + * have no matching handler. If a type without a handler is encountered at + * runtime, a [MatchException] is thrown. + * + * Not available when compiled with `-betterC`. + * + * Returns: + * The value returned from the handler that matches the currently-held type, + * if a handler was given for that type. + * + * Throws: + * [MatchException], if the currently-held type has no matching handler. + * + * See_Also: $(REF tryVisit, std,variant) + */ +version (D_Exceptions) +template tryMatch(handlers...) +{ + import std.typecons : No; + + /** + * The actual `tryMatch` function. + * + * Params: + * args = One or more [SumType] objects. + */ + auto ref tryMatch(SumTypes...)(auto ref SumTypes args) + if (allSatisfy!(isSumType, SumTypes) && args.length > 0) + { + return matchImpl!(No.exhaustive, handlers)(args); + } +} + +/** + * Thrown by [tryMatch] when an unhandled type is encountered. + * + * Not available when compiled with `-betterC`. + */ +version (D_Exceptions) +class MatchException : Exception +{ + /// + pure @safe @nogc nothrow + this(string msg, string file = __FILE__, size_t line = __LINE__) + { + super(msg, file, line); + } +} + +/** + * True if `handler` is a potential match for `Ts`, otherwise false. + * + * See the documentation for [match] for a full explanation of how matches are + * chosen. + */ +template canMatch(alias handler, Ts...) +if (Ts.length > 0) +{ + enum canMatch = is(typeof((ref Ts args) => handler(args))); +} + +/// +@safe unittest +{ + alias handleInt = (int i) => "got an int"; + + assert( canMatch!(handleInt, int)); + assert(!canMatch!(handleInt, string)); +} + +// Includes all overloads of the given handler +@safe unittest +{ + static struct OverloadSet + { + static void fun(int n) {} + static void fun(double d) {} + } + + assert(canMatch!(OverloadSet.fun, int)); + assert(canMatch!(OverloadSet.fun, double)); +} + +// Like aliasSeqOf!(iota(n)), but works in BetterC +private template Iota(size_t n) +{ + static if (n == 0) + { + alias Iota = AliasSeq!(); + } + else + { + alias Iota = AliasSeq!(Iota!(n - 1), n - 1); + } +} + +@safe unittest +{ + assert(is(Iota!0 == AliasSeq!())); + assert(Iota!1 == AliasSeq!(0)); + assert(Iota!3 == AliasSeq!(0, 1, 2)); +} + +/* The number that the dim-th argument's tag is multiplied by when + * converting TagTuples to and from case indices ("caseIds"). + * + * Named by analogy to the stride that the dim-th index into a + * multidimensional static array is multiplied by to calculate the + * offset of a specific element. + */ +private size_t stride(size_t dim, lengths...)() +{ + import core.checkedint : mulu; + + size_t result = 1; + bool overflow = false; + + static foreach (i; 0 .. dim) + { + result = mulu(result, lengths[i], overflow); + } + + /* The largest number matchImpl uses, numCases, is calculated with + * stride!(SumTypes.length), so as long as this overflow check + * passes, we don't need to check for overflow anywhere else. + */ + assert(!overflow, "Integer overflow"); + return result; +} + +private template matchImpl(Flag!"exhaustive" exhaustive, handlers...) +{ + auto ref matchImpl(SumTypes...)(auto ref SumTypes args) + if (allSatisfy!(isSumType, SumTypes) && args.length > 0) + { + alias stride(size_t i) = .stride!(i, Map!(typeCount, SumTypes)); + alias TagTuple = .TagTuple!(SumTypes); + + /* + * A list of arguments to be passed to a handler needed for the case + * labeled with `caseId`. + */ + template handlerArgs(size_t caseId) + { + enum tags = TagTuple.fromCaseId(caseId); + enum argsFrom(size_t i : tags.length) = ""; + enum argsFrom(size_t i) = "args[" ~ toCtString!i ~ "].get!(SumTypes[" ~ toCtString!i ~ "]" ~ + ".Types[" ~ toCtString!(tags[i]) ~ "])(), " ~ argsFrom!(i + 1); + enum handlerArgs = argsFrom!0; + } + + /* An AliasSeq of the types of the member values in the argument list + * returned by `handlerArgs!caseId`. + * + * Note that these are the actual (that is, qualified) types of the + * member values, which may not be the same as the types listed in + * the arguments' `.Types` properties. + */ + template valueTypes(size_t caseId) + { + enum tags = TagTuple.fromCaseId(caseId); + + template getType(size_t i) + { + enum tid = tags[i]; + alias T = SumTypes[i].Types[tid]; + alias getType = typeof(args[i].get!T()); + } + + alias valueTypes = Map!(getType, Iota!(tags.length)); + } + + /* The total number of cases is + * + * Π SumTypes[i].Types.length for 0 ≤ i < SumTypes.length + * + * Or, equivalently, + * + * ubyte[SumTypes[0].Types.length]...[SumTypes[$-1].Types.length].sizeof + * + * Conveniently, this is equal to stride!(SumTypes.length), so we can + * use that function to compute it. + */ + enum numCases = stride!(SumTypes.length); + + /* Guaranteed to never be a valid handler index, since + * handlers.length <= size_t.max. + */ + enum noMatch = size_t.max; + + // An array that maps caseIds to handler indices ("hids"). + enum matches = () + { + size_t[numCases] matches; + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=19561 + foreach (ref match; matches) + { + match = noMatch; + } + + static foreach (caseId; 0 .. numCases) + { + static foreach (hid, handler; handlers) + { + static if (canMatch!(handler, valueTypes!caseId)) + { + if (matches[caseId] == noMatch) + { + matches[caseId] = hid; + } + } + } + } + + return matches; + }(); + + import std.algorithm.searching : canFind; + + // Check for unreachable handlers + static foreach (hid, handler; handlers) + { + static assert(matches[].canFind(hid), + "`handlers[" ~ toCtString!hid ~ "]` " ~ + "of type `" ~ ( __traits(isTemplate, handler) + ? "template" + : typeof(handler).stringof + ) ~ "` " ~ + "never matches" + ); + } + + // Workaround for https://issues.dlang.org/show_bug.cgi?id=19993 + enum handlerName(size_t hid) = "handler" ~ toCtString!hid; + + static foreach (size_t hid, handler; handlers) + { + mixin("alias ", handlerName!hid, " = handler;"); + } + + immutable argsId = TagTuple(args).toCaseId; + + final switch (argsId) + { + static foreach (caseId; 0 .. numCases) + { + case caseId: + static if (matches[caseId] != noMatch) + { + return mixin(handlerName!(matches[caseId]), "(", handlerArgs!caseId, ")"); + } + else + { + static if (exhaustive) + { + static assert(false, + "No matching handler for types `" ~ valueTypes!caseId.stringof ~ "`"); + } + else + { + throw new MatchException( + "No matching handler for types `" ~ valueTypes!caseId.stringof ~ "`"); + } + } + } + } + + assert(false, "unreachable"); + } +} + +private enum typeCount(SumType) = SumType.Types.length; + +/* A TagTuple represents a single possible set of tags that `args` + * could have at runtime. + * + * Because D does not allow a struct to be the controlling expression + * of a switch statement, we cannot dispatch on the TagTuple directly. + * Instead, we must map each TagTuple to a unique integer and generate + * a case label for each of those integers. + * + * This mapping is implemented in `fromCaseId` and `toCaseId`. It uses + * the same technique that's used to map index tuples to memory offsets + * in a multidimensional static array. + * + * For example, when `args` consists of two SumTypes with two member + * types each, the TagTuples corresponding to each case label are: + * + * case 0: TagTuple([0, 0]) + * case 1: TagTuple([1, 0]) + * case 2: TagTuple([0, 1]) + * case 3: TagTuple([1, 1]) + * + * When there is only one argument, the caseId is equal to that + * argument's tag. + */ +private struct TagTuple(SumTypes...) +{ + size_t[SumTypes.length] tags; + alias tags this; + + alias stride(size_t i) = .stride!(i, Map!(typeCount, SumTypes)); + + invariant + { + static foreach (i; 0 .. tags.length) + { + assert(tags[i] < SumTypes[i].Types.length, "Invalid tag"); + } + } + + this(ref const(SumTypes) args) + { + static foreach (i; 0 .. tags.length) + { + tags[i] = args[i].tag; + } + } + + static TagTuple fromCaseId(size_t caseId) + { + TagTuple result; + + // Most-significant to least-significant + static foreach_reverse (i; 0 .. result.length) + { + result[i] = caseId / stride!i; + caseId %= stride!i; + } + + return result; + } + + size_t toCaseId() + { + size_t result; + + static foreach (i; 0 .. tags.length) + { + result += tags[i] * stride!i; + } + + return result; + } +} + +// Matching +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + + assert(x.match!((int v) => true, (float v) => false)); + assert(y.match!((int v) => false, (float v) => true)); +} + +// Missing handlers +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + + assert(!__traits(compiles, x.match!((int x) => true))); + assert(!__traits(compiles, x.match!())); +} + +// Handlers with qualified parameters +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + alias MySum = SumType!(int[], float[]); + + MySum x = MySum([1, 2, 3]); + MySum y = MySum([1.0, 2.0, 3.0]); + + assert(x.match!((const(int[]) v) => true, (const(float[]) v) => false)); + assert(y.match!((const(int[]) v) => false, (const(float[]) v) => true)); +} + +// Handlers for qualified types +// Disabled in BetterC due to use of dynamic arrays +version (D_BetterC) {} else +@safe unittest +{ + alias MySum = SumType!(immutable(int[]), immutable(float[])); + + MySum x = MySum([1, 2, 3]); + + assert(x.match!((immutable(int[]) v) => true, (immutable(float[]) v) => false)); + assert(x.match!((const(int[]) v) => true, (const(float[]) v) => false)); + // Tail-qualified parameters + assert(x.match!((immutable(int)[] v) => true, (immutable(float)[] v) => false)); + assert(x.match!((const(int)[] v) => true, (const(float)[] v) => false)); + // Generic parameters + assert(x.match!((immutable v) => true)); + assert(x.match!((const v) => true)); + // Unqualified parameters + assert(!__traits(compiles, + x.match!((int[] v) => true, (float[] v) => false) + )); +} + +// Delegate handlers +// Disabled in BetterC due to use of closures +version (D_BetterC) {} else +@safe unittest +{ + alias MySum = SumType!(int, float); + + int answer = 42; + MySum x = MySum(42); + MySum y = MySum(3.14); + + assert(x.match!((int v) => v == answer, (float v) => v == answer)); + assert(!y.match!((int v) => v == answer, (float v) => v == answer)); +} + +version (unittest) +{ + version (D_BetterC) + { + // std.math.isClose depends on core.runtime.math, so use a + // libc-based version for testing with -betterC + @safe pure @nogc nothrow + private bool isClose(double lhs, double rhs) + { + import core.stdc.math : fabs; + + return fabs(lhs - rhs) < 1e-5; + } + } + else + { + import std.math.operations : isClose; + } +} + +// Generic handler +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + + assert(x.match!(v => v*2) == 84); + assert(y.match!(v => v*2).isClose(6.28)); +} + +// Fallback to generic handler +// Disabled in BetterC due to use of std.conv.to +version (D_BetterC) {} else +@safe unittest +{ + import std.conv : to; + + alias MySum = SumType!(int, float, string); + + MySum x = MySum(42); + MySum y = MySum("42"); + + assert(x.match!((string v) => v.to!int, v => v*2) == 84); + assert(y.match!((string v) => v.to!int, v => v*2) == 42); +} + +// Multiple non-overlapping generic handlers +@safe unittest +{ + import std.array : staticArray; + + alias MySum = SumType!(int, float, int[], char[]); + + static ints = staticArray([1, 2, 3]); + static chars = staticArray(['a', 'b', 'c']); + + MySum x = MySum(42); + MySum y = MySum(3.14); + MySum z = MySum(ints[]); + MySum w = MySum(chars[]); + + assert(x.match!(v => v*2, v => v.length) == 84); + assert(y.match!(v => v*2, v => v.length).isClose(6.28)); + assert(w.match!(v => v*2, v => v.length) == 3); + assert(z.match!(v => v*2, v => v.length) == 3); +} + +// Structural matching +@safe unittest +{ + static struct S1 { int x; } + static struct S2 { int y; } + alias MySum = SumType!(S1, S2); + + MySum a = MySum(S1(0)); + MySum b = MySum(S2(0)); + + assert(a.match!(s1 => s1.x + 1, s2 => s2.y - 1) == 1); + assert(b.match!(s1 => s1.x + 1, s2 => s2.y - 1) == -1); +} + +// Separate opCall handlers +@safe unittest +{ + static struct IntHandler + { + bool opCall(int arg) + { + return true; + } + } + + static struct FloatHandler + { + bool opCall(float arg) + { + return false; + } + } + + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + + assert(x.match!(IntHandler.init, FloatHandler.init)); + assert(!y.match!(IntHandler.init, FloatHandler.init)); +} + +// Compound opCall handler +@safe unittest +{ + static struct CompoundHandler + { + bool opCall(int arg) + { + return true; + } + + bool opCall(float arg) + { + return false; + } + } + + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + + assert(x.match!(CompoundHandler.init)); + assert(!y.match!(CompoundHandler.init)); +} + +// Ordered matching +@safe unittest +{ + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + + assert(x.match!((int v) => true, v => false)); +} + +// Non-exhaustive matching +version (D_Exceptions) +@system unittest +{ + import std.exception : assertThrown, assertNotThrown; + + alias MySum = SumType!(int, float); + + MySum x = MySum(42); + MySum y = MySum(3.14); + + assertNotThrown!MatchException(x.tryMatch!((int n) => true)); + assertThrown!MatchException(y.tryMatch!((int n) => true)); +} + +// Non-exhaustive matching in @safe code +version (D_Exceptions) +@safe unittest +{ + SumType!(int, float) x; + + auto _ = x.tryMatch!( + (int n) => n + 1, + ); +} + +// Handlers with ref parameters +@safe unittest +{ + alias Value = SumType!(long, double); + + auto value = Value(3.14); + + value.match!( + (long) {}, + (ref double d) { d *= 2; } + ); + + assert(value.get!double.isClose(6.28)); +} + +// Unreachable handlers +@safe unittest +{ + alias MySum = SumType!(int, string); + + MySum s; + + assert(!__traits(compiles, + s.match!( + (int _) => 0, + (string _) => 1, + (double _) => 2 + ) + )); + + assert(!__traits(compiles, + s.match!( + _ => 0, + (int _) => 1 + ) + )); +} + +// Unsafe handlers +@system unittest +{ + SumType!int x; + alias unsafeHandler = (int x) @system { return; }; + + assert(!__traits(compiles, () @safe + { + x.match!unsafeHandler; + })); + + auto test() @system + { + return x.match!unsafeHandler; + } +} + +// Overloaded handlers +@safe unittest +{ + static struct OverloadSet + { + static string fun(int i) { return "int"; } + static string fun(double d) { return "double"; } + } + + alias MySum = SumType!(int, double); + + MySum a = 42; + MySum b = 3.14; + + assert(a.match!(OverloadSet.fun) == "int"); + assert(b.match!(OverloadSet.fun) == "double"); +} + +// Overload sets that include SumType arguments +@safe unittest +{ + alias Inner = SumType!(int, double); + alias Outer = SumType!(Inner, string); + + static struct OverloadSet + { + @safe: + static string fun(int i) { return "int"; } + static string fun(double d) { return "double"; } + static string fun(string s) { return "string"; } + static string fun(Inner i) { return i.match!fun; } + static string fun(Outer o) { return o.match!fun; } + } + + Outer a = Inner(42); + Outer b = Inner(3.14); + Outer c = "foo"; + + assert(OverloadSet.fun(a) == "int"); + assert(OverloadSet.fun(b) == "double"); + assert(OverloadSet.fun(c) == "string"); +} + +// Overload sets with ref arguments +@safe unittest +{ + static struct OverloadSet + { + static void fun(ref int i) { i = 42; } + static void fun(ref double d) { d = 3.14; } + } + + alias MySum = SumType!(int, double); + + MySum x = 0; + MySum y = 0.0; + + x.match!(OverloadSet.fun); + y.match!(OverloadSet.fun); + + assert(x.match!((value) => is(typeof(value) == int) && value == 42)); + assert(y.match!((value) => is(typeof(value) == double) && value == 3.14)); +} + +// Overload sets with templates +@safe unittest +{ + import std.traits : isNumeric; + + static struct OverloadSet + { + static string fun(string arg) + { + return "string"; + } + + static string fun(T)(T arg) + if (isNumeric!T) + { + return "numeric"; + } + } + + alias MySum = SumType!(int, string); + + MySum x = 123; + MySum y = "hello"; + + assert(x.match!(OverloadSet.fun) == "numeric"); + assert(y.match!(OverloadSet.fun) == "string"); +} + +// Github issue #24 +@safe unittest +{ + void test() @nogc + { + int acc = 0; + SumType!int(1).match!((int x) => acc += x); + } +} + +// Github issue #31 +@safe unittest +{ + void test() @nogc + { + int acc = 0; + + SumType!(int, string)(1).match!( + (int x) => acc += x, + (string _) => 0, + ); + } +} + +// Types that `alias this` a SumType +@safe unittest +{ + static struct A {} + static struct B {} + static struct D { SumType!(A, B) value; alias value this; } + + auto _ = D().match!(_ => true); +} + +// Multiple dispatch +@safe unittest +{ + alias MySum = SumType!(int, string); + + static int fun(MySum x, MySum y) + { + import std.meta : Args = AliasSeq; + + return Args!(x, y).match!( + (int xv, int yv) => 0, + (string xv, int yv) => 1, + (int xv, string yv) => 2, + (string xv, string yv) => 3 + ); + } + + assert(fun(MySum(0), MySum(0)) == 0); + assert(fun(MySum(""), MySum(0)) == 1); + assert(fun(MySum(0), MySum("")) == 2); + assert(fun(MySum(""), MySum("")) == 3); +} + +// inout SumTypes +@safe unittest +{ + inout(int[]) fun(inout(SumType!(int[])) x) + { + return x.match!((inout(int[]) a) => a); + } +} + +private void destroyIfOwner(T)(ref T value) +{ + static if (hasElaborateDestructor!T) + { + destroy(value); + } +} diff --git a/src/ext_depends/D-YAML/source/dyaml/test/tokens.d b/src/ext_depends/D-YAML/source/dyaml/test/tokens.d index c099647..d3dce6e 100644 --- a/src/ext_depends/D-YAML/source/dyaml/test/tokens.d +++ b/src/ext_depends/D-YAML/source/dyaml/test/tokens.d @@ -21,7 +21,7 @@ module dyaml.test.tokens; static auto scanTestCommon(string filename) @safe { ubyte[] yamlData = cast(ubyte[])readText(filename).dup; - return Scanner(new Reader(yamlData)); + return Scanner(new Reader(yamlData, filename)); } /** diff --git a/src/ext_depends/D-YAML/test/data/emojianchor.canonical b/src/ext_depends/D-YAML/test/data/emojianchor.canonical new file mode 100644 index 0000000..8a71040 --- /dev/null +++ b/src/ext_depends/D-YAML/test/data/emojianchor.canonical @@ -0,0 +1,5 @@ +%YAML 1.1 +--- +!!seq [ + !!str "unicode anchor" +] diff --git a/src/ext_depends/D-YAML/test/data/emojianchor.data b/src/ext_depends/D-YAML/test/data/emojianchor.data new file mode 100644 index 0000000..72c1c37 --- /dev/null +++ b/src/ext_depends/D-YAML/test/data/emojianchor.data @@ -0,0 +1,2 @@ +--- +- &😁 unicode anchor diff --git a/src/ext_depends/D-YAML/test/data/invalid-anchor-1.loader-error b/src/ext_depends/D-YAML/test/data/invalid-anchor-1.loader-error deleted file mode 100644 index fcf7d0f..0000000 --- a/src/ext_depends/D-YAML/test/data/invalid-anchor-1.loader-error +++ /dev/null @@ -1 +0,0 @@ ---- &? foo # we allow only ascii and numeric characters in anchor names. diff --git a/src/ext_depends/D-YAML/test/data/invalid-anchor-2.loader-error b/src/ext_depends/D-YAML/test/data/invalid-anchor-2.loader-error deleted file mode 100644 index bfc4ff0..0000000 --- a/src/ext_depends/D-YAML/test/data/invalid-anchor-2.loader-error +++ /dev/null @@ -1,8 +0,0 @@ ---- -- [ - &correct foo, - *correct, - *correct] # still correct -- *correct: still correct -- &correct-or-not[foo, bar] - diff --git a/src/ext_depends/D-YAML/test/data/invalid-anchor.loader-error b/src/ext_depends/D-YAML/test/data/invalid-anchor.loader-error new file mode 100644 index 0000000..bfc4ff0 --- /dev/null +++ b/src/ext_depends/D-YAML/test/data/invalid-anchor.loader-error @@ -0,0 +1,8 @@ +--- +- [ + &correct foo, + *correct, + *correct] # still correct +- *correct: still correct +- &correct-or-not[foo, bar] + -- cgit v1.2.3