From 1cc6a04b8bce82fa83b62d919bf8bdf14cad0b92 Mon Sep 17 00:00:00 2001 From: Ralph Amissah Date: Sat, 1 Oct 2016 13:54:14 -0400 Subject: update sdlang, start looking to using dub remote dependencies --- src/sdlang/ast.d | 1275 ++++++++++++++++++++++++-- src/sdlang/exception.d | 168 +++- src/sdlang/lexer.d | 58 +- src/sdlang/libinputvisitor/libInputVisitor.d | 26 +- src/sdlang/package.d | 13 +- src/sdlang/parser.d | 307 ++++--- src/sdlang/symbol.d | 2 +- src/sdlang/taggedalgebraic/taggedalgebraic.d | 1085 ++++++++++++++++++++++ src/sdlang/token.d | 135 ++- src/sdlang/util.d | 126 ++- src/sdp.d | 9 + src/sdp/ao_conf_make_meta_sdlang.d | 2 +- src/sdp/ao_object_setter.d | 34 +- src/sdp/ao_output_debugs.d | 4 +- src/sdp/ao_read_config_files.d | 2 +- src/undead/stream.d | 7 +- 16 files changed, 2934 insertions(+), 319 deletions(-) create mode 100644 src/sdlang/taggedalgebraic/taggedalgebraic.d (limited to 'src') diff --git a/src/sdlang/ast.d b/src/sdlang/ast.d index 7ad1c30..87dd0bd 100644 --- a/src/sdlang/ast.d +++ b/src/sdlang/ast.d @@ -9,13 +9,6 @@ import std.conv; import std.range; import std.string; -version(sdlangUnittest) -version(unittest) -{ - import std.stdio; - import std.exception; -} - import sdlang.exception; import sdlang.token; import sdlang.util; @@ -27,7 +20,7 @@ class Attribute private Tag _parent; /// Get parent tag. To set a parent, attach this Attribute to its intended - /// parent tag by calling 'Tag.add(...)', or by passing it to + /// parent tag by calling `Tag.add(...)`, or by passing it to /// the parent tag's constructor. @property Tag parent() { @@ -35,11 +28,20 @@ class Attribute } private string _namespace; + /++ + This tag's namespace. Empty string if no namespace. + + Note that setting this value is O(n) because internal lookup structures + need to be updated. + + Note also, that setting this may change where this tag is ordered among + its parent's list of tags. + +/ @property string namespace() { return _namespace; } - /// Not particularly efficient, but it works. + ///ditto @property void namespace(string value) { if(_parent && _namespace != value) @@ -61,12 +63,22 @@ class Attribute } private string _name; - /// Not including namespace. Use 'fullName' if you want the namespace included. + /++ + This attribute's name, not including namespace. + + Use `getFullName().toString` if you want the namespace included. + + Note that setting this value is O(n) because internal lookup structures + need to be updated. + + Note also, that setting this may change where this attribute is ordered + among its parent's list of tags. + +/ @property string name() { return _name; } - /// Not the most efficient, but it works. + ///ditto @property void name(string value) { if(_parent && _name != value) @@ -96,9 +108,17 @@ class Attribute _name = value; } + /// This tag's name, including namespace if one exists. + deprecated("Use 'getFullName().toString()'") @property string fullName() { - return _namespace==""? _name : text(_namespace, ":", _name); + return getFullName().toString(); + } + + /// This tag's name, including namespace if one exists. + FullName getFullName() + { + return FullName(_namespace, _name); } this(string namespace, string name, Value value, Location location = Location(0, 0, 0)) @@ -117,7 +137,14 @@ class Attribute this.value = value; } - /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// Copy this Attribute. + /// The clone does $(B $(I not)) have a parent, even if the original does. + Attribute clone() + { + return new Attribute(_namespace, _name, value, location); + } + + /// Removes `this` from its parent, if any. Returns `this` for chaining. /// Inefficient ATM, but it works. Attribute remove() { @@ -190,14 +217,31 @@ class Attribute } } +/// Deep-copy an array of Tag or Attribute. +/// The top-level clones are $(B $(I not)) attached to any parent, even if the originals are. +T[] clone(T)(T[] arr) if(is(T==Tag) || is(T==Attribute)) +{ + T[] newArr; + newArr.length = arr.length; + + foreach(i; 0..arr.length) + newArr[i] = arr[i].clone(); + + return newArr; +} + class Tag { + /// File/Line/Column/Index information for where this tag was located in + /// its original SDLang file. Location location; + + /// Access all this tag's values, as an array of type `sdlang.token.Value`. Value[] values; private Tag _parent; /// Get parent tag. To set a parent, attach this Tag to its intended - /// parent tag by calling 'Tag.add(...)', or by passing it to + /// parent tag by calling `Tag.add(...)`, or by passing it to /// the parent tag's constructor. @property Tag parent() { @@ -205,13 +249,24 @@ class Tag } private string _namespace; + /++ + This tag's namespace. Empty string if no namespace. + + Note that setting this value is O(n) because internal lookup structures + need to be updated. + + Note also, that setting this may change where this tag is ordered among + its parent's list of tags. + +/ @property string namespace() { return _namespace; } - /// Not particularly efficient, but it works. + ///ditto @property void namespace(string value) { + //TODO: Can we do this in-place, without removing/adding and thus + // modyfying the internal order? if(_parent && _namespace != value) { // Remove @@ -231,18 +286,31 @@ class Tag } private string _name; - /// Not including namespace. Use 'fullName' if you want the namespace included. + /++ + This tag's name, not including namespace. + + Use `getFullName().toString` if you want the namespace included. + + Note that setting this value is O(n) because internal lookup structures + need to be updated. + + Note also, that setting this may change where this tag is ordered among + its parent's list of tags. + +/ @property string name() { return _name; } - /// Not the most efficient, but it works. + ///ditto @property void name(string value) { + //TODO: Seriously? Can't we at least do the "*" modification *in-place*? + if(_parent && _name != value) { _parent.updateId++; + // Not the most efficient, but it works. void removeFromGroupedLookup(string ns) { // Remove from _parent._tags[ns] @@ -259,6 +327,7 @@ class Tag _name = value; // Add to new locations in _parent._tags + //TODO: Can we re-insert while preserving the original order? _parent._tags[_namespace][_name] ~= this; _parent._tags["*"][_name] ~= this; } @@ -267,11 +336,18 @@ class Tag } /// This tag's name, including namespace if one exists. + deprecated("Use 'getFullName().toString()'") @property string fullName() { - return _namespace==""? _name : text(_namespace, ":", _name); + return getFullName().toString(); } - + + /// This tag's name, including namespace if one exists. + FullName getFullName() + { + return FullName(_namespace, _name); + } + // Tracks dirtiness. This is incremented every time a change is made which // could invalidate existing ranges. This way, the ranges can detect when // they've been invalidated. @@ -307,6 +383,15 @@ class Tag this.add(children); } + /// Deep-copy this Tag. + /// The clone does $(B $(I not)) have a parent, even if the original does. + Tag clone() + { + auto newTag = new Tag(_namespace, _name, values.dup, allAttributes.clone(), allTags.clone()); + newTag.location = location; + return newTag; + } + private Attribute[] allAttributes; // In same order as specified in SDL file. private Tag[] allTags; // In same order as specified in SDL file. private string[] allNamespaces; // In same order as specified in SDL file. @@ -318,8 +403,8 @@ class Tag private Tag[][string][string] _tags; // tags[namespace or "*"][name][i] /// Adds a Value, Attribute, Tag (or array of such) as a member/child of this Tag. - /// Returns 'this' for chaining. - /// Throws 'SDLangValidationException' if trying to add an Attribute or Tag + /// Returns `this` for chaining. + /// Throws `ValidationException` if trying to add an Attribute or Tag /// that already has a parent. Tag add(Value val) { @@ -342,7 +427,7 @@ class Tag { if(attr._parent) { - throw new SDLangValidationException( + throw new ValidationException( "Attribute is already attached to a parent tag. "~ "Use Attribute.remove() before adding it to another tag." ); @@ -376,7 +461,7 @@ class Tag { if(tag._parent) { - throw new SDLangValidationException( + throw new ValidationException( "Tag is already attached to a parent tag. "~ "Use Tag.remove() before adding it to another tag." ); @@ -405,7 +490,7 @@ class Tag return this; } - /// Removes 'this' from its parent, if any. Returns 'this' for chaining. + /// Removes `this` from its parent, if any. Returns `this` for chaining. /// Inefficient ATM, but it works. Tag remove() { @@ -488,6 +573,7 @@ class Tag frontIndex = 0; if( + tag !is null && namespace in mixin("tag."~membersGrouped) && name in mixin("tag."~membersGrouped~"[namespace]") ) @@ -506,7 +592,7 @@ class Tag @property bool empty() { - return frontIndex == endIndex; + return tag is null || frontIndex == endIndex; } private size_t frontIndex; @@ -517,7 +603,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -530,7 +616,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -565,7 +651,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -573,7 +659,7 @@ class Tag T opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); return mixin("tag."~membersGrouped~"[namespace][name][frontIndex+index]"); } @@ -595,14 +681,20 @@ class Tag this.isMaybe = isMaybe; frontIndex = 0; - if(namespace == "*") - initialEndIndex = mixin("tag."~allMembers~".length"); - else if(namespace in mixin("tag."~memberIndicies)) - initialEndIndex = mixin("tag."~memberIndicies~"[namespace].length"); + if(tag is null) + endIndex = 0; else - initialEndIndex = 0; + { + + if(namespace == "*") + initialEndIndex = mixin("tag."~allMembers~".length"); + else if(namespace in mixin("tag."~memberIndicies)) + initialEndIndex = mixin("tag."~memberIndicies~"[namespace].length"); + else + initialEndIndex = 0; - endIndex = initialEndIndex; + endIndex = initialEndIndex; + } } invariant() @@ -615,7 +707,7 @@ class Tag @property bool empty() { - return frontIndex == endIndex; + return tag is null || frontIndex == endIndex; } private size_t frontIndex; @@ -626,7 +718,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -639,7 +731,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -676,7 +768,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -684,7 +776,7 @@ class Tag T opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(namespace == "*") return mixin("tag."~allMembers~"[ frontIndex+index ]"); @@ -697,7 +789,7 @@ class Tag { if(frontIndex != 0 || endIndex != initialEndIndex) { - throw new SDLangRangeException( + throw new DOMRangeException(tag, "Cannot lookup tags/attributes by name on a subset of a range, "~ "only across the entire tag. "~ "Please make sure you haven't called popFront or popBack on this "~ @@ -706,10 +798,10 @@ class Tag } if(!isMaybe && empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(!isMaybe && name !in this) - throw new SDLangRangeException(`No such `~T.stringof~` named: "`~name~`"`); + throw new DOMRangeException(tag, `No such `~T.stringof~` named: "`~name~`"`); return ThisNamedMemberRange(tag, namespace, name, updateId); } @@ -718,7 +810,7 @@ class Tag { if(frontIndex != 0 || endIndex != initialEndIndex) { - throw new SDLangRangeException( + throw new DOMRangeException(tag, "Cannot lookup tags/attributes by name on a subset of a range, "~ "only across the entire tag. "~ "Please make sure you haven't called popFront or popBack on this "~ @@ -726,6 +818,9 @@ class Tag ); } + if(tag is null) + return false; + return namespace in mixin("tag."~membersGrouped) && name in mixin("tag."~membersGrouped~"[namespace]") && @@ -769,7 +864,7 @@ class Tag void popFront() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); frontIndex++; } @@ -782,7 +877,7 @@ class Tag void popBack() { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); endIndex--; } @@ -818,7 +913,7 @@ class Tag r.endIndex > this.endIndex || r.frontIndex > r.endIndex ) - throw new SDLangRangeException("Slice out of range"); + throw new DOMRangeException(tag, "Slice out of range"); return r; } @@ -826,7 +921,7 @@ class Tag NamespaceAccess opIndex(size_t index) { if(empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); auto namespace = tag.allNamespaces[frontIndex+index]; return NamespaceAccess( @@ -839,10 +934,10 @@ class Tag NamespaceAccess opIndex(string namespace) { if(!isMaybe && empty) - throw new SDLangRangeException("Range is empty"); + throw new DOMRangeException(tag, "Range is empty"); if(!isMaybe && namespace !in this) - throw new SDLangRangeException(`No such namespace: "`~namespace~`"`); + throw new DOMRangeException(tag, `No such namespace: "`~namespace~`"`); return NamespaceAccess( namespace, @@ -866,7 +961,7 @@ class Tag } } - struct NamespaceAccess + static struct NamespaceAccess { string name; AttributeRange attributes; @@ -879,25 +974,66 @@ class Tag static assert(isRandomAccessRange!TagRange); static assert(isRandomAccessRange!NamespaceRange); - /// Access all attributes that don't have a namespace + /++ + Access all attributes that don't have a namespace + + Returns a random access range of `Attribute` objects that supports + numeric-indexing, string-indexing, slicing and length. + + Since SDLang allows multiple attributes with the same name, + string-indexing returns a random access range of all attributes + with the given name. + + The string-indexing does $(B $(I not)) support namespace prefixes. + Use `namespace[string]`.`attributes` or `all`.`attributes` for that. + + See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) + for a high-level overview (and examples) of how to use this. + +/ @property AttributeRange attributes() { return AttributeRange(this, "", false); } - /// Access all direct-child tags that don't have a namespace + /++ + Access all direct-child tags that don't have a namespace. + + Returns a random access range of `Tag` objects that supports + numeric-indexing, string-indexing, slicing and length. + + Since SDLang allows multiple tags with the same name, string-indexing + returns a random access range of all immediate child tags with the + given name. + + The string-indexing does $(B $(I not)) support namespace prefixes. + Use `namespace[string]`.`attributes` or `all`.`attributes` for that. + + See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) + for a high-level overview (and examples) of how to use this. + +/ @property TagRange tags() { return TagRange(this, "", false); } - /// Access all namespaces in this tag, and the attributes/tags within them. + /++ + Access all namespaces in this tag, and the attributes/tags within them. + + Returns a random access range of `NamespaceAccess` elements that supports + numeric-indexing, string-indexing, slicing and length. + + See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) + for a high-level overview (and examples) of how to use this. + +/ @property NamespaceRange namespaces() { return NamespaceRange(this, false); } /// Access all attributes and tags regardless of namespace. + /// + /// See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) + /// for a better understanding (and examples) of how to use this. @property NamespaceAccess all() { // "*" isn't a valid namespace name, so we can use it to indicate "all namespaces" @@ -942,14 +1078,972 @@ class Tag } } - /// Access 'attributes', 'tags', 'namespaces' and 'all' like normal, + /// Access `attributes`, `tags`, `namespaces` and `all` like normal, /// except that looking up a non-existant name/namespace with - /// opIndex(string) results in an empty array instead of a thrown SDLangRangeException. + /// opIndex(string) results in an empty array instead of + /// a thrown `sdlang.exception.DOMRangeException`. + /// + /// See $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/HOWTO.md#tag-and-attribute-api-summary, API Overview) + /// for a more information (and examples) of how to use this. @property MaybeAccess maybe() { return MaybeAccess(this); } + // Internal implementations for the get/expect functions further below: + + private Tag getTagImpl(FullName tagFullName, Tag defaultValue=null, bool useDefaultValue=true) + { + auto tagNS = tagFullName.namespace; + auto tagName = tagFullName.name; + + // Can find namespace? + if(tagNS !in _tags) + { + if(useDefaultValue) + return defaultValue; + else + throw new TagNotFoundException(this, tagFullName, "No tags found in namespace '"~namespace~"'"); + } + + // Can find tag in namespace? + if(tagName !in _tags[tagNS] || _tags[tagNS][tagName].length == 0) + { + if(useDefaultValue) + return defaultValue; + else + throw new TagNotFoundException(this, tagFullName, "Can't find tag '"~tagFullName.toString()~"'"); + } + + // Return last matching tag found + return _tags[tagNS][tagName][$-1]; + } + + private T getValueImpl(T)(T defaultValue, bool useDefaultValue=true) + if(isValueType!T) + { + // Find value + foreach(value; this.values) + { + if(value.type == typeid(T)) + return value.get!T(); + } + + // No value of type T found + if(useDefaultValue) + return defaultValue; + else + { + throw new ValueNotFoundException( + this, + FullName(this.namespace, this.name), + typeid(T), + "No value of type "~T.stringof~" found." + ); + } + } + + private T getAttributeImpl(T)(FullName attrFullName, T defaultValue, bool useDefaultValue=true) + if(isValueType!T) + { + auto attrNS = attrFullName.namespace; + auto attrName = attrFullName.name; + + // Can find namespace and attribute name? + if(attrNS !in this._attributes || attrName !in this._attributes[attrNS]) + { + if(useDefaultValue) + return defaultValue; + else + { + throw new AttributeNotFoundException( + this, this.getFullName(), attrFullName, typeid(T), + "Can't find attribute '"~FullName.combine(attrNS, attrName)~"'" + ); + } + } + + // Find value with chosen type + foreach(attr; this._attributes[attrNS][attrName]) + { + if(attr.value.type == typeid(T)) + return attr.value.get!T(); + } + + // Chosen type not found + if(useDefaultValue) + return defaultValue; + else + { + throw new AttributeNotFoundException( + this, this.getFullName(), attrFullName, typeid(T), + "Can't find attribute '"~FullName.combine(attrNS, attrName)~"' of type "~T.stringof + ); + } + } + + // High-level interfaces for get/expect funtions: + + /++ + Lookup a child tag by name. Returns null if not found. + + Useful if you only expect one, and only one, child tag of a given name. + Only looks for immediate child tags of `this`, doesn't search recursively. + + If you expect multiple tags by the same name and want to get them all, + use `maybe`.`tags[string]` instead. + + The name can optionally include a namespace, as in `"namespace:name"`. + Or, you can search all namespaces using `"*:name"`. Use an empty string + to search for anonymous tags, or `"namespace:"` for anonymous tags inside + a namespace. Wildcard searching is only supported for namespaces, not names. + Use `maybe`.`tags[0]` if you don't care about the name. + + If there are multiple tags by the chosen name, the $(B $(I last tag)) will + always be chosen. That is, this function considers later tags with the + same name to override previous ones. + + If the tag cannot be found, and you provides a default value, the default + value is returned. Otherwise null is returned. If you'd prefer an + exception thrown, use `expectTag` instead. + +/ + Tag getTag(string fullTagName, Tag defaultValue=null) + { + auto parsedName = FullName.parse(fullTagName); + parsedName.ensureNoWildcardName( + "Instead, use 'Tag.maybe.tags[0]', 'Tag.maybe.all.tags[0]' or 'Tag.maybe.namespace[ns].tags[0]'." + ); + return getTagImpl(parsedName, defaultValue); + } + + /// + @("Tag.getTag") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo 1 + foo 2 // getTag considers this to override the first foo + + ns1:foo 3 + ns1:foo 4 // getTag considers this to override the first ns1:foo + ns2:foo 33 + ns2:foo 44 // getTag considers this to override the first ns2:foo + `); + assert( root.getTag("foo" ).values[0].get!int() == 2 ); + assert( root.getTag("ns1:foo").values[0].get!int() == 4 ); + assert( root.getTag("*:foo" ).values[0].get!int() == 44 ); // Search all namespaces + + // Not found + // If you'd prefer an exception, use `expectTag` instead. + assert( root.getTag("doesnt-exist") is null ); + + // Default value + auto foo = root.getTag("foo"); + assert( root.getTag("doesnt-exist", foo) is foo ); + } + + /++ + Lookup a child tag by name. Throws if not found. + + Useful if you only expect one, and only one, child tag of a given name. + Only looks for immediate child tags of `this`, doesn't search recursively. + + If you expect multiple tags by the same name and want to get them all, + use `tags[string]` instead. + + The name can optionally include a namespace, as in `"namespace:name"`. + Or, you can search all namespaces using `"*:name"`. Use an empty string + to search for anonymous tags, or `"namespace:"` for anonymous tags inside + a namespace. Wildcard searching is only supported for namespaces, not names. + Use `tags[0]` if you don't care about the name. + + If there are multiple tags by the chosen name, the $(B $(I last tag)) will + always be chosen. That is, this function considers later tags with the + same name to override previous ones. + + If no such tag is found, an `sdlang.exception.TagNotFoundException` will + be thrown. If you'd rather receive a default value, use `getTag` instead. + +/ + Tag expectTag(string fullTagName) + { + auto parsedName = FullName.parse(fullTagName); + parsedName.ensureNoWildcardName( + "Instead, use 'Tag.tags[0]', 'Tag.all.tags[0]' or 'Tag.namespace[ns].tags[0]'." + ); + return getTagImpl(parsedName, null, false); + } + + /// + @("Tag.expectTag") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo 1 + foo 2 // expectTag considers this to override the first foo + + ns1:foo 3 + ns1:foo 4 // expectTag considers this to override the first ns1:foo + ns2:foo 33 + ns2:foo 44 // expectTag considers this to override the first ns2:foo + `); + assert( root.expectTag("foo" ).values[0].get!int() == 2 ); + assert( root.expectTag("ns1:foo").values[0].get!int() == 4 ); + assert( root.expectTag("*:foo" ).values[0].get!int() == 44 ); // Search all namespaces + + // Not found + // If you'd rather receive a default value than an exception, use `getTag` instead. + assertThrown!TagNotFoundException( root.expectTag("doesnt-exist") ); + } + + /++ + Retrieve a value of type T from `this` tag. Returns a default value if not found. + + Useful if you only expect one value of type T from this tag. Only looks for + values of `this` tag, it does not search child tags. If you wish to search + for a value in a child tag (for example, if this current tag is a root tag), + try `getTagValue`. + + If you want to get more than one value from this tag, use `values` instead. + + If this tag has multiple values, the $(B $(I first)) value matching the + requested type will be returned. Ie, Extra values in the tag are ignored. + + You may provide a default value to be returned in case no value of + the requested type can be found. If you don't provide a default value, + `T.init` will be used. + + If you'd rather an exception be thrown when a value cannot be found, + use `expectValue` instead. + +/ + T getValue(T)(T defaultValue = T.init) if(isValueType!T) + { + return getValueImpl!T(defaultValue, true); + } + + /// + @("Tag.getValue") + unittest + { + import std.exception; + import std.math; + import sdlang.parser; + + auto root = parseSource(` + foo 1 true 2 false + `); + auto foo = root.getTag("foo"); + assert( foo.getValue!int() == 1 ); + assert( foo.getValue!bool() == true ); + + // Value found, default value ignored. + assert( foo.getValue!int(999) == 1 ); + + // No strings found + // If you'd prefer an exception, use `expectValue` instead. + assert( foo.getValue!string("Default") == "Default" ); + assert( foo.getValue!string() is null ); + + // No floats found + assert( foo.getValue!float(99.9).approxEqual(99.9) ); + assert( foo.getValue!float().isNaN() ); + } + + /++ + Retrieve a value of type T from `this` tag. Throws if not found. + + Useful if you only expect one value of type T from this tag. Only looks + for values of `this` tag, it does not search child tags. If you wish to + search for a value in a child tag (for example, if this current tag is a + root tag), try `expectTagValue`. + + If you want to get more than one value from this tag, use `values` instead. + + If this tag has multiple values, the $(B $(I first)) value matching the + requested type will be returned. Ie, Extra values in the tag are ignored. + + An `sdlang.exception.ValueNotFoundException` will be thrown if no value of + the requested type can be found. If you'd rather receive a default value, + use `getValue` instead. + +/ + T expectValue(T)() if(isValueType!T) + { + return getValueImpl!T(T.init, false); + } + + /// + @("Tag.expectValue") + unittest + { + import std.exception; + import std.math; + import sdlang.parser; + + auto root = parseSource(` + foo 1 true 2 false + `); + auto foo = root.getTag("foo"); + assert( foo.expectValue!int() == 1 ); + assert( foo.expectValue!bool() == true ); + + // No strings or floats found + // If you'd rather receive a default value than an exception, use `getValue` instead. + assertThrown!ValueNotFoundException( foo.expectValue!string() ); + assertThrown!ValueNotFoundException( foo.expectValue!float() ); + } + + /++ + Lookup a child tag by name, and retrieve a value of type T from it. + Returns a default value if not found. + + Useful if you only expect one value of type T from a given tag. Only looks + for immediate child tags of `this`, doesn't search recursively. + + This is a shortcut for `getTag().getValue()`, except if the tag isn't found, + then instead of a null reference error, it will return the requested + `defaultValue` (or T.init by default). + +/ + T getTagValue(T)(string fullTagName, T defaultValue = T.init) if(isValueType!T) + { + auto tag = getTag(fullTagName); + if(!tag) + return defaultValue; + + return tag.getValue!T(defaultValue); + } + + /// + @("Tag.getTagValue") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo 1 "a" 2 "b" + foo 3 "c" 4 "d" // getTagValue considers this to override the first foo + + bar "hi" + bar 379 // getTagValue considers this to override the first bar + `); + assert( root.getTagValue!int("foo") == 3 ); + assert( root.getTagValue!string("foo") == "c" ); + + // Value found, default value ignored. + assert( root.getTagValue!int("foo", 999) == 3 ); + + // Tag not found + // If you'd prefer an exception, use `expectTagValue` instead. + assert( root.getTagValue!int("doesnt-exist", 999) == 999 ); + assert( root.getTagValue!int("doesnt-exist") == 0 ); + + // The last "bar" tag doesn't have an int (only the first "bar" tag does) + assert( root.getTagValue!string("bar", "Default") == "Default" ); + assert( root.getTagValue!string("bar") is null ); + + // Using namespaces: + root = parseSource(` + ns1:foo 1 "a" 2 "b" + ns1:foo 3 "c" 4 "d" + ns2:foo 11 "aa" 22 "bb" + ns2:foo 33 "cc" 44 "dd" + + ns1:bar "hi" + ns1:bar 379 // getTagValue considers this to override the first bar + `); + assert( root.getTagValue!int("ns1:foo") == 3 ); + assert( root.getTagValue!int("*:foo" ) == 33 ); // Search all namespaces + + assert( root.getTagValue!string("ns1:foo") == "c" ); + assert( root.getTagValue!string("*:foo" ) == "cc" ); // Search all namespaces + + // The last "bar" tag doesn't have a string (only the first "bar" tag does) + assert( root.getTagValue!string("*:bar", "Default") == "Default" ); + assert( root.getTagValue!string("*:bar") is null ); + } + + /++ + Lookup a child tag by name, and retrieve a value of type T from it. + Throws if not found, + + Useful if you only expect one value of type T from a given tag. Only + looks for immediate child tags of `this`, doesn't search recursively. + + This is a shortcut for `expectTag().expectValue()`. + +/ + T expectTagValue(T)(string fullTagName) if(isValueType!T) + { + return expectTag(fullTagName).expectValue!T(); + } + + /// + @("Tag.expectTagValue") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo 1 "a" 2 "b" + foo 3 "c" 4 "d" // expectTagValue considers this to override the first foo + + bar "hi" + bar 379 // expectTagValue considers this to override the first bar + `); + assert( root.expectTagValue!int("foo") == 3 ); + assert( root.expectTagValue!string("foo") == "c" ); + + // The last "bar" tag doesn't have a string (only the first "bar" tag does) + // If you'd rather receive a default value than an exception, use `getTagValue` instead. + assertThrown!ValueNotFoundException( root.expectTagValue!string("bar") ); + + // Tag not found + assertThrown!TagNotFoundException( root.expectTagValue!int("doesnt-exist") ); + + // Using namespaces: + root = parseSource(` + ns1:foo 1 "a" 2 "b" + ns1:foo 3 "c" 4 "d" + ns2:foo 11 "aa" 22 "bb" + ns2:foo 33 "cc" 44 "dd" + + ns1:bar "hi" + ns1:bar 379 // expectTagValue considers this to override the first bar + `); + assert( root.expectTagValue!int("ns1:foo") == 3 ); + assert( root.expectTagValue!int("*:foo" ) == 33 ); // Search all namespaces + + assert( root.expectTagValue!string("ns1:foo") == "c" ); + assert( root.expectTagValue!string("*:foo" ) == "cc" ); // Search all namespaces + + // The last "bar" tag doesn't have a string (only the first "bar" tag does) + assertThrown!ValueNotFoundException( root.expectTagValue!string("*:bar") ); + + // Namespace not found + assertThrown!TagNotFoundException( root.expectTagValue!int("doesnt-exist:bar") ); + } + + /++ + Lookup an attribute of `this` tag by name, and retrieve a value of type T + from it. Returns a default value if not found. + + Useful if you only expect one attribute of the given name and type. + + Only looks for attributes of `this` tag, it does not search child tags. + If you wish to search for a value in a child tag (for example, if this + current tag is a root tag), try `getTagAttribute`. + + If you expect multiple attributes by the same name and want to get them all, + use `maybe`.`attributes[string]` instead. + + The attribute name can optionally include a namespace, as in + `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. + (Note that unlike tags. attributes can't be anonymous - that's what + values are.) Wildcard searching is only supported for namespaces, not names. + Use `maybe`.`attributes[0]` if you don't care about the name. + + If this tag has multiple attributes, the $(B $(I first)) attribute + matching the requested name and type will be returned. Ie, Extra + attributes in the tag are ignored. + + You may provide a default value to be returned in case no attribute of + the requested name and type can be found. If you don't provide a default + value, `T.init` will be used. + + If you'd rather an exception be thrown when an attribute cannot be found, + use `expectAttribute` instead. + +/ + T getAttribute(T)(string fullAttributeName, T defaultValue = T.init) if(isValueType!T) + { + auto parsedName = FullName.parse(fullAttributeName); + parsedName.ensureNoWildcardName( + "Instead, use 'Attribute.maybe.tags[0]', 'Attribute.maybe.all.tags[0]' or 'Attribute.maybe.namespace[ns].tags[0]'." + ); + return getAttributeImpl!T(parsedName, defaultValue); + } + + /// + @("Tag.getAttribute") + unittest + { + import std.exception; + import std.math; + import sdlang.parser; + + auto root = parseSource(` + foo z=0 X=1 X=true X=2 X=false + `); + auto foo = root.getTag("foo"); + assert( foo.getAttribute!int("X") == 1 ); + assert( foo.getAttribute!bool("X") == true ); + + // Value found, default value ignored. + assert( foo.getAttribute!int("X", 999) == 1 ); + + // Attribute name not found + // If you'd prefer an exception, use `expectValue` instead. + assert( foo.getAttribute!int("doesnt-exist", 999) == 999 ); + assert( foo.getAttribute!int("doesnt-exist") == 0 ); + + // No strings found + assert( foo.getAttribute!string("X", "Default") == "Default" ); + assert( foo.getAttribute!string("X") is null ); + + // No floats found + assert( foo.getAttribute!float("X", 99.9).approxEqual(99.9) ); + assert( foo.getAttribute!float("X").isNaN() ); + + + // Using namespaces: + root = parseSource(` + foo ns1:z=0 ns1:X=1 ns1:X=2 ns2:X=3 ns2:X=4 + `); + foo = root.getTag("foo"); + assert( foo.getAttribute!int("ns2:X") == 3 ); + assert( foo.getAttribute!int("*:X") == 1 ); // Search all namespaces + + // Namespace not found + assert( foo.getAttribute!int("doesnt-exist:X", 999) == 999 ); + + // No attribute X is in the default namespace + assert( foo.getAttribute!int("X", 999) == 999 ); + + // Attribute name not found + assert( foo.getAttribute!int("ns1:doesnt-exist", 999) == 999 ); + } + + /++ + Lookup an attribute of `this` tag by name, and retrieve a value of type T + from it. Throws if not found. + + Useful if you only expect one attribute of the given name and type. + + Only looks for attributes of `this` tag, it does not search child tags. + If you wish to search for a value in a child tag (for example, if this + current tag is a root tag), try `expectTagAttribute`. + + If you expect multiple attributes by the same name and want to get them all, + use `attributes[string]` instead. + + The attribute name can optionally include a namespace, as in + `"namespace:name"`. Or, you can search all namespaces using `"*:name"`. + (Note that unlike tags. attributes can't be anonymous - that's what + values are.) Wildcard searching is only supported for namespaces, not names. + Use `attributes[0]` if you don't care about the name. + + If this tag has multiple attributes, the $(B $(I first)) attribute + matching the requested name and type will be returned. Ie, Extra + attributes in the tag are ignored. + + An `sdlang.exception.AttributeNotFoundException` will be thrown if no + value of the requested type can be found. If you'd rather receive a + default value, use `getAttribute` instead. + +/ + T expectAttribute(T)(string fullAttributeName) if(isValueType!T) + { + auto parsedName = FullName.parse(fullAttributeName); + parsedName.ensureNoWildcardName( + "Instead, use 'Attribute.tags[0]', 'Attribute.all.tags[0]' or 'Attribute.namespace[ns].tags[0]'." + ); + return getAttributeImpl!T(parsedName, T.init, false); + } + + /// + @("Tag.expectAttribute") + unittest + { + import std.exception; + import std.math; + import sdlang.parser; + + auto root = parseSource(` + foo z=0 X=1 X=true X=2 X=false + `); + auto foo = root.getTag("foo"); + assert( foo.expectAttribute!int("X") == 1 ); + assert( foo.expectAttribute!bool("X") == true ); + + // Attribute name not found + // If you'd rather receive a default value than an exception, use `getAttribute` instead. + assertThrown!AttributeNotFoundException( foo.expectAttribute!int("doesnt-exist") ); + + // No strings found + assertThrown!AttributeNotFoundException( foo.expectAttribute!string("X") ); + + // No floats found + assertThrown!AttributeNotFoundException( foo.expectAttribute!float("X") ); + + + // Using namespaces: + root = parseSource(` + foo ns1:z=0 ns1:X=1 ns1:X=2 ns2:X=3 ns2:X=4 + `); + foo = root.getTag("foo"); + assert( foo.expectAttribute!int("ns2:X") == 3 ); + assert( foo.expectAttribute!int("*:X") == 1 ); // Search all namespaces + + // Namespace not found + assertThrown!AttributeNotFoundException( foo.expectAttribute!int("doesnt-exist:X") ); + + // No attribute X is in the default namespace + assertThrown!AttributeNotFoundException( foo.expectAttribute!int("X") ); + + // Attribute name not found + assertThrown!AttributeNotFoundException( foo.expectAttribute!int("ns1:doesnt-exist") ); + } + + /++ + Lookup a child tag and attribute by name, and retrieve a value of type T + from it. Returns a default value if not found. + + Useful if you only expect one attribute of type T from given + the tag and attribute names. Only looks for immediate child tags of + `this`, doesn't search recursively. + + This is a shortcut for `getTag().getAttribute()`, except if the tag isn't + found, then instead of a null reference error, it will return the requested + `defaultValue` (or T.init by default). + +/ + T getTagAttribute(T)(string fullTagName, string fullAttributeName, T defaultValue = T.init) if(isValueType!T) + { + auto tag = getTag(fullTagName); + if(!tag) + return defaultValue; + + return tag.getAttribute!T(fullAttributeName, defaultValue); + } + + /// + @("Tag.getTagAttribute") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo X=1 X="a" X=2 X="b" + foo X=3 X="c" X=4 X="d" // getTagAttribute considers this to override the first foo + + bar X="hi" + bar X=379 // getTagAttribute considers this to override the first bar + `); + assert( root.getTagAttribute!int("foo", "X") == 3 ); + assert( root.getTagAttribute!string("foo", "X") == "c" ); + + // Value found, default value ignored. + assert( root.getTagAttribute!int("foo", "X", 999) == 3 ); + + // Tag not found + // If you'd prefer an exception, use `expectTagAttribute` instead of `getTagAttribute` + assert( root.getTagAttribute!int("doesnt-exist", "X", 999) == 999 ); + assert( root.getTagAttribute!int("doesnt-exist", "X") == 0 ); + assert( root.getTagAttribute!int("foo", "doesnt-exist", 999) == 999 ); + assert( root.getTagAttribute!int("foo", "doesnt-exist") == 0 ); + + // The last "bar" tag doesn't have a string (only the first "bar" tag does) + assert( root.getTagAttribute!string("bar", "X", "Default") == "Default" ); + assert( root.getTagAttribute!string("bar", "X") is null ); + + + // Using namespaces: + root = parseSource(` + ns1:foo X=1 X="a" X=2 X="b" + ns1:foo X=3 X="c" X=4 X="d" + ns2:foo X=11 X="aa" X=22 X="bb" + ns2:foo X=33 X="cc" X=44 X="dd" + + ns1:bar attrNS:X="hi" + ns1:bar attrNS:X=379 // getTagAttribute considers this to override the first bar + `); + assert( root.getTagAttribute!int("ns1:foo", "X") == 3 ); + assert( root.getTagAttribute!int("*:foo", "X") == 33 ); // Search all namespaces + + assert( root.getTagAttribute!string("ns1:foo", "X") == "c" ); + assert( root.getTagAttribute!string("*:foo", "X") == "cc" ); // Search all namespaces + + // bar's attribute X is't in the default namespace + assert( root.getTagAttribute!int("*:bar", "X", 999) == 999 ); + assert( root.getTagAttribute!int("*:bar", "X") == 0 ); + + // The last "bar" tag's "attrNS:X" attribute doesn't have a string (only the first "bar" tag does) + assert( root.getTagAttribute!string("*:bar", "attrNS:X", "Default") == "Default" ); + assert( root.getTagAttribute!string("*:bar", "attrNS:X") is null); + } + + /++ + Lookup a child tag and attribute by name, and retrieve a value of type T + from it. Throws if not found. + + Useful if you only expect one attribute of type T from given + the tag and attribute names. Only looks for immediate child tags of + `this`, doesn't search recursively. + + This is a shortcut for `expectTag().expectAttribute()`. + +/ + T expectTagAttribute(T)(string fullTagName, string fullAttributeName) if(isValueType!T) + { + return expectTag(fullTagName).expectAttribute!T(fullAttributeName); + } + + /// + @("Tag.expectTagAttribute") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo X=1 X="a" X=2 X="b" + foo X=3 X="c" X=4 X="d" // expectTagAttribute considers this to override the first foo + + bar X="hi" + bar X=379 // expectTagAttribute considers this to override the first bar + `); + assert( root.expectTagAttribute!int("foo", "X") == 3 ); + assert( root.expectTagAttribute!string("foo", "X") == "c" ); + + // The last "bar" tag doesn't have an int attribute named "X" (only the first "bar" tag does) + // If you'd rather receive a default value than an exception, use `getAttribute` instead. + assertThrown!AttributeNotFoundException( root.expectTagAttribute!string("bar", "X") ); + + // Tag not found + assertThrown!TagNotFoundException( root.expectTagAttribute!int("doesnt-exist", "X") ); + + // Using namespaces: + root = parseSource(` + ns1:foo X=1 X="a" X=2 X="b" + ns1:foo X=3 X="c" X=4 X="d" + ns2:foo X=11 X="aa" X=22 X="bb" + ns2:foo X=33 X="cc" X=44 X="dd" + + ns1:bar attrNS:X="hi" + ns1:bar attrNS:X=379 // expectTagAttribute considers this to override the first bar + `); + assert( root.expectTagAttribute!int("ns1:foo", "X") == 3 ); + assert( root.expectTagAttribute!int("*:foo", "X") == 33 ); // Search all namespaces + + assert( root.expectTagAttribute!string("ns1:foo", "X") == "c" ); + assert( root.expectTagAttribute!string("*:foo", "X") == "cc" ); // Search all namespaces + + // bar's attribute X is't in the default namespace + assertThrown!AttributeNotFoundException( root.expectTagAttribute!int("*:bar", "X") ); + + // The last "bar" tag's "attrNS:X" attribute doesn't have a string (only the first "bar" tag does) + assertThrown!AttributeNotFoundException( root.expectTagAttribute!string("*:bar", "attrNS:X") ); + + // Tag's namespace not found + assertThrown!TagNotFoundException( root.expectTagAttribute!int("doesnt-exist:bar", "attrNS:X") ); + } + + /++ + Lookup a child tag by name, and retrieve all values from it. + + This just like using `getTag()`.`values`, except if the tag isn't found, + it safely returns null (or an optional array of default values) instead of + a dereferencing null error. + + Note that, unlike `getValue`, this doesn't discriminate by the value's + type. It simply returns all values of a single tag as a `Value[]`. + + If you'd prefer an exception thrown when the tag isn't found, use + `expectTag`.`values` instead. + +/ + Value[] getTagValues(string fullTagName, Value[] defaultValues = null) + { + auto tag = getTag(fullTagName); + if(tag) + return tag.values; + else + return defaultValues; + } + + /// + @("getTagValues") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo 1 "a" 2 "b" + foo 3 "c" 4 "d" // getTagValues considers this to override the first foo + `); + assert( root.getTagValues("foo") == [Value(3), Value("c"), Value(4), Value("d")] ); + + // Tag not found + // If you'd prefer an exception, use `expectTag.values` instead. + assert( root.getTagValues("doesnt-exist") is null ); + assert( root.getTagValues("doesnt-exist", [ Value(999), Value("Not found") ]) == + [ Value(999), Value("Not found") ] ); + } + + /++ + Lookup a child tag by name, and retrieve all attributes in a chosen + (or default) namespace from it. + + This just like using `getTag()`.`attributes` (or + `getTag()`.`namespace[...]`.`attributes`, or `getTag()`.`all`.`attributes`), + except if the tag isn't found, it safely returns an empty range instead + of a dereferencing null error. + + If provided, the `attributeNamespace` parameter can be either the name of + a namespace, or an empty string for the default namespace (the default), + or `"*"` to retreive attributes from all namespaces. + + Note that, unlike `getAttributes`, this doesn't discriminate by the + value's type. It simply returns the usual `attributes` range. + + If you'd prefer an exception thrown when the tag isn't found, use + `expectTag`.`attributes` instead. + +/ + auto getTagAttributes(string fullTagName, string attributeNamespace = null) + { + auto tag = getTag(fullTagName); + if(tag) + { + if(attributeNamespace && attributeNamespace in tag.namespaces) + return tag.namespaces[attributeNamespace].attributes; + else if(attributeNamespace == "*") + return tag.all.attributes; + else + return tag.attributes; + } + + return AttributeRange(null, null, false); + } + + /// + @("getTagAttributes") + unittest + { + import std.exception; + import sdlang.parser; + + auto root = parseSource(` + foo X=1 X=2 + + // getTagAttributes considers this to override the first foo + foo X1=3 X2="c" namespace:bar=7 X3=4 X4="d" + `); + + auto fooAttrs = root.getTagAttributes("foo"); + assert( !fooAttrs.empty ); + assert( fooAttrs.length == 4 ); + assert( fooAttrs[0].name == "X1" && fooAttrs[0].value == Value(3) ); + assert( fooAttrs[1].name == "X2" && fooAttrs[1].value == Value("c") ); + assert( fooAttrs[2].name == "X3" && fooAttrs[2].value == Value(4) ); + assert( fooAttrs[3].name == "X4" && fooAttrs[3].value == Value("d") ); + + fooAttrs = root.getTagAttributes("foo", "namespace"); + assert( !fooAttrs.empty ); + assert( fooAttrs.length == 1 ); + assert( fooAttrs[0].name == "bar" && fooAttrs[0].value == Value(7) ); + + fooAttrs = root.getTagAttributes("foo", "*"); + assert( !fooAttrs.empty ); + assert( fooAttrs.length == 5 ); + assert( fooAttrs[0].name == "X1" && fooAttrs[0].value == Value(3) ); + assert( fooAttrs[1].name == "X2" && fooAttrs[1].value == Value("c") ); + assert( fooAttrs[2].name == "bar" && fooAttrs[2].value == Value(7) ); + assert( fooAttrs[3].name == "X3" && fooAttrs[3].value == Value(4) ); + assert( fooAttrs[4].name == "X4" && fooAttrs[4].value == Value("d") ); + + // Tag not found + // If you'd prefer an exception, use `expectTag.attributes` instead. + assert( root.getTagValues("doesnt-exist").empty ); + } + + @("*: Disallow wildcards for names") + unittest + { + import std.exception; + import std.math; + import sdlang.parser; + + auto root = parseSource(` + foo 1 X=2 + ns:foo 3 ns:X=4 + `); + auto foo = root.getTag("foo"); + auto nsfoo = root.getTag("ns:foo"); + + // Sanity check + assert( foo !is null ); + assert( foo.name == "foo" ); + assert( foo.namespace == "" ); + + assert( nsfoo !is null ); + assert( nsfoo.name == "foo" ); + assert( nsfoo.namespace == "ns" ); + + assert( foo.getValue !int() == 1 ); + assert( foo.expectValue !int() == 1 ); + assert( nsfoo.getValue !int() == 3 ); + assert( nsfoo.expectValue!int() == 3 ); + + assert( root.getTagValue !int("foo") == 1 ); + assert( root.expectTagValue!int("foo") == 1 ); + assert( root.getTagValue !int("ns:foo") == 3 ); + assert( root.expectTagValue!int("ns:foo") == 3 ); + + assert( foo.getAttribute !int("X") == 2 ); + assert( foo.expectAttribute !int("X") == 2 ); + assert( nsfoo.getAttribute !int("ns:X") == 4 ); + assert( nsfoo.expectAttribute!int("ns:X") == 4 ); + + assert( root.getTagAttribute !int("foo", "X") == 2 ); + assert( root.expectTagAttribute!int("foo", "X") == 2 ); + assert( root.getTagAttribute !int("ns:foo", "ns:X") == 4 ); + assert( root.expectTagAttribute!int("ns:foo", "ns:X") == 4 ); + + // No namespace + assertThrown!ArgumentException( root.getTag ("*") ); + assertThrown!ArgumentException( root.expectTag("*") ); + + assertThrown!ArgumentException( root.getTagValue !int("*") ); + assertThrown!ArgumentException( root.expectTagValue!int("*") ); + + assertThrown!ArgumentException( foo.getAttribute !int("*") ); + assertThrown!ArgumentException( foo.expectAttribute !int("*") ); + assertThrown!ArgumentException( root.getTagAttribute !int("*", "X") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("*", "X") ); + assertThrown!ArgumentException( root.getTagAttribute !int("foo", "*") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("foo", "*") ); + + // With namespace + assertThrown!ArgumentException( root.getTag ("ns:*") ); + assertThrown!ArgumentException( root.expectTag("ns:*") ); + + assertThrown!ArgumentException( root.getTagValue !int("ns:*") ); + assertThrown!ArgumentException( root.expectTagValue!int("ns:*") ); + + assertThrown!ArgumentException( nsfoo.getAttribute !int("ns:*") ); + assertThrown!ArgumentException( nsfoo.expectAttribute !int("ns:*") ); + assertThrown!ArgumentException( root.getTagAttribute !int("ns:*", "ns:X") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("ns:*", "ns:X") ); + assertThrown!ArgumentException( root.getTagAttribute !int("ns:foo", "ns:*") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("ns:foo", "ns:*") ); + + // With wildcard namespace + assertThrown!ArgumentException( root.getTag ("*:*") ); + assertThrown!ArgumentException( root.expectTag("*:*") ); + + assertThrown!ArgumentException( root.getTagValue !int("*:*") ); + assertThrown!ArgumentException( root.expectTagValue!int("*:*") ); + + assertThrown!ArgumentException( nsfoo.getAttribute !int("*:*") ); + assertThrown!ArgumentException( nsfoo.expectAttribute !int("*:*") ); + assertThrown!ArgumentException( root.getTagAttribute !int("*:*", "*:X") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("*:*", "*:X") ); + assertThrown!ArgumentException( root.getTagAttribute !int("*:foo", "*:*") ); + assertThrown!ArgumentException( root.expectTagAttribute!int("*:foo", "*:*") ); + } + override bool opEquals(Object o) { auto t = cast(Tag)o; @@ -981,9 +2075,10 @@ class Tag return allTags == t.allTags; } - /// Treats 'this' as the root tag. Note that root tags cannot have + /// Treats `this` as the root tag. Note that root tags cannot have /// values or attributes, and cannot be part of a namespace. - /// If this isn't a valid root tag, 'SDLangValidationException' will be thrown. + /// If this isn't a valid root tag, `sdlang.exception.ValidationException` + /// will be thrown. string toSDLDocument()(string indent="\t", int indentLevel=0) { Appender!string sink; @@ -996,21 +2091,21 @@ class Tag if(isOutputRange!(Sink,char)) { if(values.length > 0) - throw new SDLangValidationException("Root tags cannot have any values, only child tags."); + throw new ValidationException("Root tags cannot have any values, only child tags."); if(allAttributes.length > 0) - throw new SDLangValidationException("Root tags cannot have any attributes, only child tags."); + throw new ValidationException("Root tags cannot have any attributes, only child tags."); if(_namespace != "") - throw new SDLangValidationException("Root tags cannot have a namespace."); + throw new ValidationException("Root tags cannot have a namespace."); foreach(tag; allTags) tag.toSDLString(sink, indent, indentLevel); } - /// Output this entire tag in SDL format. Does *not* treat 'this' as + /// Output this entire tag in SDL format. Does $(B $(I not)) treat `this` as /// a root tag. If you intend this to be the root of a standard SDL - /// document, use 'toSDLDocument' instead. + /// document, use `toSDLDocument` instead. string toSDLString()(string indent="\t", int indentLevel=0) { Appender!string sink; @@ -1023,10 +2118,10 @@ class Tag if(isOutputRange!(Sink,char)) { if(_name == "" && values.length == 0) - throw new SDLangValidationException("Anonymous tags must have at least one value."); + throw new ValidationException("Anonymous tags must have at least one value."); if(_name == "" && _namespace != "") - throw new SDLangValidationException("Anonymous tags cannot have a namespace."); + throw new ValidationException("Anonymous tags cannot have a namespace."); // Indent foreach(i; 0..indentLevel) @@ -1080,7 +2175,7 @@ class Tag sink.put("\n"); } - /// Not the most efficient, but it works. + /// Outputs full information on the tag. string toDebugString() { import std.algorithm : sort; @@ -1129,7 +2224,7 @@ class Tag } } -version(sdlangUnittest) +version(unittest) { private void testRandomAccessRange(R, E)(R range, E[] expected, bool function(E, E) equals=null) { @@ -1269,12 +2364,11 @@ version(sdlangUnittest) } } -version(sdlangUnittest) +@("*: Test sdlang ast") unittest { + import std.exception; import sdlang.parser; - writeln("Unittesting sdlang ast..."); - stdout.flush(); Tag root; root = parseSource(""); @@ -1473,26 +2567,26 @@ unittest testRandomAccessRange(root.all.tags["blue"], [blue3, blue5]); testRandomAccessRange(root.all.tags["orange"], [orange]); - assertThrown!SDLangRangeException(root.tags["foobar"]); - assertThrown!SDLangRangeException(root.all.tags["foobar"]); - assertThrown!SDLangRangeException(root.attributes["foobar"]); - assertThrown!SDLangRangeException(root.all.attributes["foobar"]); + assertThrown!DOMRangeException(root.tags["foobar"]); + assertThrown!DOMRangeException(root.all.tags["foobar"]); + assertThrown!DOMRangeException(root.attributes["foobar"]); + assertThrown!DOMRangeException(root.all.attributes["foobar"]); // DMD Issue #12585 causes a segfault in these two tests when using 2.064 or 2.065, // so work around it. - //assertThrown!SDLangRangeException(root.namespaces["foobar"].tags["foobar"]); - //assertThrown!SDLangRangeException(root.namespaces["foobar"].attributes["foobar"]); + //assertThrown!DOMRangeException(root.namespaces["foobar"].tags["foobar"]); + //assertThrown!DOMRangeException(root.namespaces["foobar"].attributes["foobar"]); bool didCatch = false; try auto x = root.namespaces["foobar"].tags["foobar"]; - catch(SDLangRangeException e) + catch(DOMRangeException e) didCatch = true; assert(didCatch); didCatch = false; try auto x = root.namespaces["foobar"].attributes["foobar"]; - catch(SDLangRangeException e) + catch(DOMRangeException e) didCatch = true; assert(didCatch); @@ -1799,16 +2893,33 @@ unittest assert("" !in people.namespaces); assert("foobar" !in people.namespaces); testRandomAccessRange(people.all.attributes, cast(Attribute[])[]); + + // Test clone() + auto rootClone = root.clone(); + assert(rootClone !is root); + assert(rootClone.parent is null); + assert(rootClone.name == root.name); + assert(rootClone.namespace == root.namespace); + assert(rootClone.location == root.location); + assert(rootClone.values == root.values); + assert(rootClone.toSDLDocument() == root.toSDLDocument()); + + auto peopleClone = people.clone(); + assert(peopleClone !is people); + assert(peopleClone.parent is null); + assert(peopleClone.name == people.name); + assert(peopleClone.namespace == people.namespace); + assert(peopleClone.location == people.location); + assert(peopleClone.values == people.values); + assert(peopleClone.toSDLString() == people.toSDLString()); } // Regression test, issue #11: https://github.com/Abscissa/SDLang-D/issues/11 -version(sdlangUnittest) +@("*: Regression test issue #11") unittest { import sdlang.parser; - writeln("ast: Regression test issue #11..."); - stdout.flush(); - + auto root = parseSource( `// a`); diff --git a/src/sdlang/exception.d b/src/sdlang/exception.d index e87307f..188991e 100644 --- a/src/sdlang/exception.d +++ b/src/sdlang/exception.d @@ -3,40 +3,188 @@ module sdlang.exception; +import std.array; import std.exception; +import std.range; +import std.stdio; import std.string; +import sdlang.ast; import sdlang.util; +/// Abstract parent class of all SDLang-D defined exceptions. abstract class SDLangException : Exception { - this(string msg) { super(msg); } + this(string msg, string file = __FILE__, size_t line = __LINE__) + { + super(msg, file, line); + } } -class SDLangParseException : SDLangException +/// Thrown when a syntax error is encounterd while parsing. +class ParseException : SDLangException { Location location; bool hasLocation; - this(string msg) + this(string msg, string file = __FILE__, size_t line = __LINE__) { hasLocation = false; - super(msg); + super(msg, file, line); } - this(Location location, string msg) + this(Location location, string msg, string file = __FILE__, size_t line = __LINE__) { hasLocation = true; - super("%s: %s".format(location.toString(), msg)); + super("%s: %s".format(location.toString(), msg), file, line); + } +} + +/// Compatibility alias +deprecated("The new name is ParseException") +alias SDLangParseException = ParseException; + +/++ +Thrown when attempting to do something in the DOM that's unsupported, such as: + +$(UL +$(LI Adding the same instance of a tag or attribute to more than one parent.) +$(LI Writing SDLang where: + $(UL + $(LI The root tag has values, attributes or a namespace. ) + $(LI An anonymous tag has a namespace. ) + $(LI An anonymous tag has no values. ) + $(LI A floating point value is infinity or NaN. ) + ) +)) ++/ +class ValidationException : SDLangException +{ + this(string msg, string file = __FILE__, size_t line = __LINE__) + { + super(msg, file, line); + } +} + +/// Compatibility alias +deprecated("The new name is ValidationException") +alias SDLangValidationException = ValidationException; + +/// Thrown when someting is wrong with the provided arguments to a function. +class ArgumentException : SDLangException +{ + this(string msg, string file = __FILE__, size_t line = __LINE__) + { + super(msg, file, line); } } -class SDLangValidationException : SDLangException +/// Thrown by the DOM on empty range and out-of-range conditions. +abstract class DOMException : SDLangException { - this(string msg) { super(msg); } + Tag base; /// The tag searched from + + this(Tag base, string msg, string file = __FILE__, size_t line = __LINE__) + { + this.base = base; + super(msg, file, line); + } + + /// Prefixes a message with file/line information from the tag (if tag exists). + /// Optionally takes output range as a sink. + string customMsg(string msg) + { + if(!base) + return msg; + + Appender!string sink; + this.customMsg(sink, msg); + return sink.data; + } + + ///ditto + void customMsg(Sink)(ref Sink sink, string msg) if(isOutputRange!(Sink,char)) + { + if(base) + { + sink.put(base.location.toString()); + sink.put(": "); + sink.put(msg); + } + else + sink.put(msg); + } + + /// Outputs a message to stderr, prefixed with file/line information + void writeCustomMsg(string msg) + { + stderr.writeln( customMsg(msg) ); + } } -class SDLangRangeException : SDLangException +/// Thrown by the DOM on empty range and out-of-range conditions. +class DOMRangeException : DOMException { - this(string msg) { super(msg); } + this(Tag base, string msg, string file = __FILE__, size_t line = __LINE__) + { + super(base, msg, file, line); + } +} + +/// Compatibility alias +deprecated("The new name is DOMRangeException") +alias SDLangRangeException = DOMRangeException; + +/// Abstract parent class of `TagNotFoundException`, `ValueNotFoundException` +/// and `AttributeNotFoundException`. +/// +/// Thrown by the DOM's `sdlang.ast.Tag.expectTag`, etc. functions if a matching element isn't found. +abstract class DOMNotFoundException : DOMException +{ + FullName tagName; /// The tag searched for + + this(Tag base, FullName tagName, string msg, string file = __FILE__, size_t line = __LINE__) + { + this.tagName = tagName; + super(base, msg, file, line); + } +} + +/// Thrown by the DOM's `sdlang.ast.Tag.expectTag`, etc. functions if a Tag isn't found. +class TagNotFoundException : DOMNotFoundException +{ + this(Tag base, FullName tagName, string msg, string file = __FILE__, size_t line = __LINE__) + { + super(base, tagName, msg, file, line); + } +} + +/// Thrown by the DOM's `sdlang.ast.Tag.expectValue`, etc. functions if a Value isn't found. +class ValueNotFoundException : DOMNotFoundException +{ + /// Expected type for the not-found value. + TypeInfo valueType; + + this(Tag base, FullName tagName, TypeInfo valueType, string msg, string file = __FILE__, size_t line = __LINE__) + { + this.valueType = valueType; + super(base, tagName, msg, file, line); + } +} + +/// Thrown by the DOM's `sdlang.ast.Tag.expectAttribute`, etc. functions if an Attribute isn't found. +class AttributeNotFoundException : DOMNotFoundException +{ + FullName attributeName; /// The attribute searched for + + /// Expected type for the not-found attribute's value. + TypeInfo valueType; + + this(Tag base, FullName tagName, FullName attributeName, TypeInfo valueType, string msg, + string file = __FILE__, size_t line = __LINE__) + { + this.valueType = valueType; + this.attributeName = attributeName; + super(base, tagName, msg, file, line); + } } diff --git a/src/sdlang/lexer.d b/src/sdlang/lexer.d index 91e0a7d..3788188 100644 --- a/src/sdlang/lexer.d +++ b/src/sdlang/lexer.d @@ -5,20 +5,19 @@ module sdlang.lexer; import std.algorithm; import std.array; +static import std.ascii; import std.base64; import std.bigint; import std.conv; import std.datetime; import std.file; -// import std.stream : ByteOrderMarks, BOM; +import std.format; import std.traits; import std.typecons; import std.uni; import std.utf; import std.variant; -import undead.stream : ByteOrderMarks, BOM; - import sdlang.exception; import sdlang.symbol; import sdlang.token; @@ -111,7 +110,7 @@ class Lexer // found after it needs to be saved for the the lexer's next iteration. // // It's a slight kludge, and could instead be implemented as a slightly - // kludgey parser hack, but it's the only situation where SDL's lexing + // kludgey parser hack, but it's the only situation where SDLang's lexing // needs to lookahead more than one character, so this is good enough. private struct LookaheadTokenInfo { @@ -172,9 +171,10 @@ class Lexer error(location, msg); } + //TODO: Take varargs and use output range sink. private void error(Location loc, string msg) { - throw new SDLangParseException(loc, "Error: "~msg); + throw new ParseException(loc, "Error: "~msg); } private Token makeToken(string symbolName)() @@ -442,8 +442,14 @@ class Lexer else { + if(ch == ',') + error("Unexpected comma: SDLang is not a comma-separated format."); + else if(std.ascii.isPrintable(ch)) + error(text("Unexpected: ", ch)); + else + error("Unexpected character code 0x%02X".format(ch)); + advanceChar(ErrorOnEOF.No); - error("Syntax error"); } } @@ -734,8 +740,7 @@ class Lexer //Base64.decode(Base64InputRange(this), OutputBuf()); Base64.decode(tmpBuf, OutputBuf()); - //TODO: Starting with dmd 2.062, this should be a Base64Exception - catch(Exception e) + catch(Base64Exception e) error("Invalid character in base64 binary literal."); advanceChar(ErrorOnEOF.No); // Skip ']' @@ -1455,13 +1460,17 @@ class Lexer } } -version(sdlangUnittest) +version(unittest) { import std.stdio; + version(Have_unit_threaded) import unit_threaded; + else { enum DontTest; } + private auto loc = Location("filename", 0, 0, 0); private auto loc2 = Location("a", 1, 1, 1); + @("lexer: EOL") unittest { assert([Token(symbol!"EOL",loc) ] == [Token(symbol!"EOL",loc) ] ); @@ -1469,18 +1478,19 @@ version(sdlangUnittest) } private int numErrors = 0; + @DontTest private void testLex(string source, Token[] expected, bool test_locations = false, string file=__FILE__, size_t line=__LINE__) { Token[] actual; try actual = lexSource(source, "filename"); - catch(SDLangParseException e) + catch(ParseException e) { numErrors++; stderr.writeln(file, "(", line, "): testLex failed on: ", source); stderr.writeln(" Expected:"); stderr.writeln(" ", expected); - stderr.writeln(" Actual: SDLangParseException thrown:"); + stderr.writeln(" Actual: ParseException thrown:"); stderr.writeln(" ", e.msg); return; } @@ -1524,26 +1534,23 @@ version(sdlangUnittest) Token[] actual; try actual = lexSource(source, "filename"); - catch(SDLangParseException e) + catch(ParseException e) hadException = true; if(!hadException) { numErrors++; stderr.writeln(file, "(", line, "): testLex failed on: ", source); - stderr.writeln(" Expected SDLangParseException"); + stderr.writeln(" Expected ParseException"); stderr.writeln(" Actual:"); stderr.writeln(" ", actual); } } } -version(sdlangUnittest) +@("sdlang lexer") unittest { - writeln("Unittesting sdlang lexer..."); - stdout.flush(); - testLex("", []); testLex(" ", []); testLex("\\\n", []); @@ -1856,7 +1863,7 @@ unittest testLex( "2013/2/22 -34:65-GMT-05:30", [ Token(symbol!"Value",loc,Value(SysTime(DateTime( 2013, 2, 22, 0, 0, 0) - hours(34) - minutes(65) - seconds( 0), new immutable SimpleTimeZone(-hours(5)-minutes(30))))) ]); - // DateTime, with Java SDL's occasionally weird interpretation of some + // DateTime, with Java SDLang's occasionally weird interpretation of some // "not quite ISO" variations of the "GMT with offset" timezone strings. Token testTokenSimpleTimeZone(Duration d) { @@ -2001,23 +2008,17 @@ unittest stderr.writeln(numErrors, " failed test(s)"); } -version(sdlangUnittest) +@("lexer: Regression test issue #8") unittest { - writeln("lexer: Regression test issue #8..."); - stdout.flush(); - testLex(`"\n \n"`, [ Token(symbol!"Value",loc,Value("\n \n"),`"\n \n"`) ]); testLex(`"\t\t"`, [ Token(symbol!"Value",loc,Value("\t\t"),`"\t\t"`) ]); testLex(`"\n\n"`, [ Token(symbol!"Value",loc,Value("\n\n"),`"\n\n"`) ]); } -version(sdlangUnittest) +@("lexer: Regression test issue #11") unittest { - writeln("lexer: Regression test issue #11..."); - stdout.flush(); - void test(string input) { testLex( @@ -2035,12 +2036,9 @@ unittest test("#\na"); } -version(sdlangUnittest) +@("ast: Regression test issue #28") unittest { - writeln("lexer: Regression test issue #28..."); - stdout.flush(); - enum offset = 1; // workaround for an of-by-one error for line numbers testLex("test", [ Token(symbol!"Ident", Location("filename", 0, 0, 0), Value(null), "test") diff --git a/src/sdlang/libinputvisitor/libInputVisitor.d b/src/sdlang/libinputvisitor/libInputVisitor.d index 15c2ce8..f29dc4f 100644 --- a/src/sdlang/libinputvisitor/libInputVisitor.d +++ b/src/sdlang/libinputvisitor/libInputVisitor.d @@ -38,7 +38,24 @@ class InputVisitor(Obj, Elem) : Fiber this(Obj obj) { this.obj = obj; - super(&run); + + version(Windows) // Issue #1 + { + import core.sys.windows.windows : SYSTEM_INFO, GetSystemInfo; + SYSTEM_INFO info; + GetSystemInfo(&info); + auto PAGESIZE = info.dwPageSize; + + super(&run, PAGESIZE * 16); + } + else + super(&run); + } + + this(Obj obj, size_t stackSize) + { + this.obj = obj; + super(&run, stackSize); } private void run() @@ -56,7 +73,7 @@ class InputVisitor(Obj, Elem) : Fiber } // Member 'front' must be a function due to DMD Issue #5403 - private Elem _front; + private Elem _front = Elem.init; // Default initing here avoids "Error: field _front must be initialized in constructor" @property Elem front() { ensureStarted(); @@ -88,4 +105,9 @@ template inputVisitor(Elem) { return new InputVisitor!(Obj, Elem)(obj); } + + @property InputVisitor!(Obj, Elem) inputVisitor(Obj)(Obj obj, size_t stackSize) + { + return new InputVisitor!(Obj, Elem)(obj, stackSize); + } } diff --git a/src/sdlang/package.d b/src/sdlang/package.d index d990e64..dd8df1a 100644 --- a/src/sdlang/package.d +++ b/src/sdlang/package.d @@ -2,7 +2,7 @@ // Written in the D programming language. /++ -$(H2 SDLang-D v0.9.3) +$(H2 SDLang-D v0.10.0) Library for parsing and generating SDL (Simple Declarative Language). @@ -14,15 +14,16 @@ file included with your version of SDLang-D. Links: $(UL + $(LI $(LINK2 http://sdlang.org/, SDLang Language Homepage) ) $(LI $(LINK2 https://github.com/Abscissa/SDLang-D, SDLang-D Homepage) ) $(LI $(LINK2 http://semitwist.com/sdlang-d, SDLang-D API Reference (latest version) ) ) $(LI $(LINK2 http://semitwist.com/sdlang-d-docs, SDLang-D API Reference (earlier versions) ) ) - $(LI $(LINK2 http://sdl.ikayzo.org/display/SDL/Language+Guide, Official SDL Site) [$(LINK2 http://semitwist.com/sdl-mirror/Language+Guide.html, mirror)] ) + $(LI $(LINK2 http://sdl.ikayzo.org/display/SDL/Language+Guide, Old Official SDL Site) [$(LINK2 http://semitwist.com/sdl-mirror/Language+Guide.html, mirror)] ) ) Authors: Nick Sabalausky ("Abscissa") http://semitwist.com/contact Copyright: -Copyright (C) 2012-2015 Nick Sabalausky. +Copyright (C) 2012-2016 Nick Sabalausky. License: $(LINK2 https://github.com/Abscissa/SDLang-D/blob/master/LICENSE.txt, zlib/libpng) +/ @@ -49,10 +50,10 @@ public import sdlang.parser : parseFile, parseSource; public import sdlang.token : Value, Token, DateTimeFrac, DateTimeFracUnknownZone; public import sdlang.util : sdlangVersion, Location; -version(sdlangUnittest) +version(sdlangUsingBuiltinTestRunner) void main() {} -version(sdlangTestApp) +version(sdlangCliApp) { int main(string[] args) { @@ -77,7 +78,7 @@ version(sdlangTestApp) else doToSDL(filename); } - catch(SDLangParseException e) + catch(ParseException e) { stderr.writeln(e.msg); return 1; diff --git a/src/sdlang/parser.d b/src/sdlang/parser.d index ed8084a..c9b8d4f 100644 --- a/src/sdlang/parser.d +++ b/src/sdlang/parser.d @@ -6,6 +6,7 @@ module sdlang.parser; import std.file; import libInputVisitor; +import taggedalgebraic; import sdlang.ast; import sdlang.exception; @@ -21,8 +22,8 @@ Tag parseFile(string filename) return parseSource(source, filename); } -/// Returns root tag. The optional 'filename' parameter can be included -/// so that the SDL document's filename (if any) can be displayed with +/// Returns root tag. The optional `filename` parameter can be included +/// so that the SDLang document's filename (if any) can be displayed with /// any syntax error messages. Tag parseSource(string source, string filename=null) { @@ -36,12 +37,19 @@ Parses an SDL document using StAX/Pull-style. Returns an InputRange with element type ParserEvent. The pullParseFile version reads a file and parses it, while pullParseSource -parses a string passed in. The optional 'filename' parameter in pullParseSource -can be included so that the SDL document's filename (if any) can be displayed +parses a string passed in. The optional `filename` parameter in pullParseSource +can be included so that the SDLang document's filename (if any) can be displayed with any syntax error messages. -Warning! The FileStartEvent and FileEndEvent events *might* be removed later. -See $(LINK https://github.com/Abscissa/SDLang-D/issues/17) +Note: The old FileStartEvent and FileEndEvent events +$(LINK2 https://github.com/Abscissa/SDLang-D/issues/17, were deemed unnessecary) +and removed as of SDLang-D v0.10.0. + +Note: Previously, in SDLang-D v0.9.x, ParserEvent was a +$(LINK2 http://dlang.org/phobos/std_variant.html#.Algebraic, std.variant.Algebraic). +As of SDLang-D v0.10.0, it is now a +$(LINK2 https://github.com/s-ludwig/taggedalgebraic, TaggedAlgebraic), +so usage has changed somewhat. Example: ------------------ @@ -55,85 +63,147 @@ lastTag The ParserEvent sequence emitted for that SDL document would be as follows (indented for readability): ------------------ -FileStartEvent - TagStartEvent (parent) - ValueEvent (12) - AttributeEvent (attr, "q") - TagStartEvent (childA) - ValueEvent (34) - TagEndEvent - TagStartEvent (childB) - ValueEvent (56) - TagEndEvent +TagStartEvent (parent) + ValueEvent (12) + AttributeEvent (attr, "q") + TagStartEvent (childA) + ValueEvent (34) TagEndEvent - TagStartEvent (lastTag) + TagStartEvent (childB) + ValueEvent (56) TagEndEvent -FileEndEvent ------------------- - -Example: +TagEndEvent +TagStartEvent (lastTag) +TagEndEvent ------------------ -foreach(event; pullParseFile("stuff.sdl")) ++/ +auto pullParseFile(string filename) { - import std.stdio; + auto source = cast(string)read(filename); + return parseSource(source, filename); +} - if(event.peek!FileStartEvent()) - writeln("FileStartEvent, starting! "); +///ditto +auto pullParseSource(string source, string filename=null) +{ + auto lexer = new Lexer(source, filename); + auto parser = PullParser(lexer); + return inputVisitor!ParserEvent( parser ); +} - else if(event.peek!FileEndEvent()) - writeln("FileEndEvent, done! "); +/// +@("pullParseFile/pullParseSource example") +unittest +{ + // stuff.sdl + immutable stuffSdl = ` + name "sdlang-d" + description "An SDL (Simple Declarative Language) library for D." + homepage "http://github.com/Abscissa/SDLang-D" + + configuration "library" { + targetType "library" + } + `; + + import std.stdio; - else if(auto e = event.peek!TagStartEvent()) + foreach(event; pullParseSource(stuffSdl)) + final switch(event.kind) + { + case ParserEvent.Kind.tagStart: + auto e = cast(TagStartEvent) event; writeln("TagStartEvent: ", e.namespace, ":", e.name, " @ ", e.location); + break; - else if(event.peek!TagEndEvent()) + case ParserEvent.Kind.tagEnd: + auto e = cast(TagEndEvent) event; writeln("TagEndEvent"); + break; - else if(auto e = event.peek!ValueEvent()) + case ParserEvent.Kind.value: + auto e = cast(ValueEvent) event; writeln("ValueEvent: ", e.value); + break; - else if(auto e = event.peek!AttributeEvent()) + case ParserEvent.Kind.attribute: + auto e = cast(AttributeEvent) event; writeln("AttributeEvent: ", e.namespace, ":", e.name, "=", e.value); - - else // Shouldn't happen - throw new Exception("Received unknown parser event"); -} ------------------- -+/ -auto pullParseFile(string filename) -{ - auto source = cast(string)read(filename); - return parseSource(source, filename); + break; + } } -///ditto -auto pullParseSource(string source, string filename=null) +private union ParserEventUnion { - auto lexer = new Lexer(source, filename); - auto parser = PullParser(lexer); - return inputVisitor!ParserEvent( parser ); + TagStartEvent tagStart; + TagEndEvent tagEnd; + ValueEvent value; + AttributeEvent attribute; } -/// The element of the InputRange returned by pullParseFile and pullParseSource: -alias ParserEvent = std.variant.Algebraic!( - FileStartEvent, - FileEndEvent, - TagStartEvent, - TagEndEvent, - ValueEvent, - AttributeEvent, -); - -/// Event: Start of file -struct FileStartEvent +/++ +The element of the InputRange returned by pullParseFile and pullParseSource. + +This is a tagged union, built from the following: +------- +alias ParserEvent = TaggedAlgebraic!ParserEventUnion; +private union ParserEventUnion { - Location location; + TagStartEvent tagStart; + TagEndEvent tagEnd; + ValueEvent value; + AttributeEvent attribute; } +------- + +Note: The old FileStartEvent and FileEndEvent events +$(LINK2 https://github.com/Abscissa/SDLang-D/issues/17, were deemed unnessecary) +and removed as of SDLang-D v0.10.0. + +Note: Previously, in SDLang-D v0.9.x, ParserEvent was a +$(LINK2 http://dlang.org/phobos/std_variant.html#.Algebraic, std.variant.Algebraic). +As of SDLang-D v0.10.0, it is now a +$(LINK2 https://github.com/s-ludwig/taggedalgebraic, TaggedAlgebraic), +so usage has changed somewhat. ++/ +alias ParserEvent = TaggedAlgebraic!ParserEventUnion; -/// Event: End of file -struct FileEndEvent +/// +@("ParserEvent example") +unittest { - Location location; + // Create + ParserEvent event1 = TagStartEvent(); + ParserEvent event2 = TagEndEvent(); + ParserEvent event3 = ValueEvent(); + ParserEvent event4 = AttributeEvent(); + + // Check type + assert(event1.kind == ParserEvent.Kind.tagStart); + assert(event2.kind == ParserEvent.Kind.tagEnd); + assert(event3.kind == ParserEvent.Kind.value); + assert(event4.kind == ParserEvent.Kind.attribute); + + // Cast to base type + auto e1 = cast(TagStartEvent) event1; + auto e2 = cast(TagEndEvent) event2; + auto e3 = cast(ValueEvent) event3; + auto e4 = cast(AttributeEvent) event4; + //auto noGood = cast(AttributeEvent) event1; // AssertError: event1 is a TagStartEvent, not AttributeEvent. + + // Use as base type. + // In many cases, no casting is even needed. + event1.name = "foo"; + //auto noGood = event3.name; // AssertError: ValueEvent doesn't have a member 'name'. + + // Final switch is supported: + final switch(event1.kind) + { + case ParserEvent.Kind.tagStart: break; + case ParserEvent.Kind.tagEnd: break; + case ParserEvent.Kind.value: break; + case ParserEvent.Kind.attribute: break; + } } /// Event: Start of tag @@ -184,7 +254,7 @@ private struct PullParser private void error(Location loc, string msg) { - throw new SDLangParseException(loc, "Error: "~msg); + throw new ParseException(loc, "Error: "~msg); } private InputVisitor!(PullParser, ParserEvent) v; @@ -207,15 +277,27 @@ private struct PullParser //trace(__FUNCTION__, ": ::= EOF (Lookaheads: Anything)"); auto startLocation = Location(lexer.filename, 0, 0, 0); - emit( FileStartEvent(startLocation) ); parseTags(); auto token = lexer.front; - if(!token.matches!"EOF"()) - error("Expected end-of-file, not " ~ token.symbol.name); - - emit( FileEndEvent(token.location) ); + if(token.matches!":"()) + { + lexer.popFront(); + token = lexer.front; + if(token.matches!"Ident"()) + { + error("Missing namespace. If you don't wish to use a namespace, then say '"~token.data~"', not ':"~token.data~"'"); + assert(0); + } + else + { + error("Missing namespace. If you don't wish to use a namespace, then omit the ':'"); + assert(0); + } + } + else if(!token.matches!"EOF"()) + error("Expected a tag or end-of-file, not " ~ token.symbol.name); } /// ::= (Lookaheads: Ident Value) @@ -241,7 +323,8 @@ private struct PullParser } else if(token.matches!"{"()) { - error("Anonymous tags must have at least one value. They cannot just have children and attributes only."); + error("Found start of child block, but no tag name. If you intended an anonymous "~ + "tag, you must have at least one value before any attributes or child tags."); } else { @@ -274,7 +357,8 @@ private struct PullParser error("Expected tag name or value, not " ~ token.symbol.name); if(lexer.front.matches!"="()) - error("Anonymous tags must have at least one value. They cannot just have attributes and children only."); + error("Found attribute, but no tag name. If you intended an anonymous "~ + "tag, you must have at least one value before any attributes."); parseValues(); parseAttributes(); @@ -477,43 +561,33 @@ private struct DOMParser auto parser = PullParser(lexer); auto eventRange = inputVisitor!ParserEvent( parser ); + foreach(event; eventRange) + final switch(event.kind) { - if(auto e = event.peek!TagStartEvent()) - { - auto newTag = new Tag(currTag, e.namespace, e.name); - newTag.location = e.location; - - currTag = newTag; - } - else if(event.peek!TagEndEvent()) - { - currTag = currTag.parent; + case ParserEvent.Kind.tagStart: + auto newTag = new Tag(currTag, event.namespace, event.name); + newTag.location = event.location; + + currTag = newTag; + break; - if(!currTag) - parser.error("Internal Error: Received an extra TagEndEvent"); - } - else if(auto e = event.peek!ValueEvent()) - { - currTag.add(e.value); - } - else if(auto e = event.peek!AttributeEvent()) - { - auto attr = new Attribute(e.namespace, e.name, e.value, e.location); - currTag.add(attr); - } - else if(event.peek!FileStartEvent()) - { - // Do nothing - } - else if(event.peek!FileEndEvent()) - { - // There shouldn't be another parent. - if(currTag.parent) - parser.error("Internal Error: Unexpected end of file, not enough TagEndEvent"); - } - else - parser.error("Internal Error: Received unknown parser event"); + case ParserEvent.Kind.tagEnd: + currTag = currTag.parent; + + if(!currTag) + parser.error("Internal Error: Received an extra TagEndEvent"); + break; + + case ParserEvent.Kind.value: + currTag.add((cast(ValueEvent)event).value); + break; + + case ParserEvent.Kind.attribute: + auto e = cast(AttributeEvent) event; + auto attr = new Attribute(e.namespace, e.name, e.value, e.location); + currTag.add(attr); + break; } return currTag; @@ -522,30 +596,33 @@ private struct DOMParser // Other parser tests are part of the AST's tests over in the ast module. -// Regression test, issue #16: https://github.com/Abscissa/SDLang-D/issues/16 -version(sdlangUnittest) +// Regression test, issue #13: https://github.com/Abscissa/SDLang-D/issues/13 +// "Incorrectly accepts ":tagname" (blank namespace, tagname prefixed with colon)" +@("parser: Regression test issue #13") unittest { - import std.stdio; - writeln("parser: Regression test issue #16..."); - stdout.flush(); + import std.exception; + assertThrown!ParseException(parseSource(`:test`)); + assertThrown!ParseException(parseSource(`:4`)); +} +// Regression test, issue #16: https://github.com/Abscissa/SDLang-D/issues/16 +@("parser: Regression test issue #16") +unittest +{ // Shouldn't crash foreach(event; pullParseSource(`tag "data"`)) { - event.peek!FileStartEvent(); + if(event.kind == ParserEvent.Kind.tagStart) + auto e = cast(TagStartEvent) event; } } // Regression test, issue #31: https://github.com/Abscissa/SDLang-D/issues/31 // "Escape sequence results in range violation error" -version(sdlangUnittest) +@("parser: Regression test issue #31") unittest { - import std.stdio; - writeln("parser: Regression test issue #31..."); - stdout.flush(); - // Shouldn't get a Range violation parseSource(`test "\"foo\""`); } diff --git a/src/sdlang/symbol.d b/src/sdlang/symbol.d index 14a74a7..ebb2b93 100644 --- a/src/sdlang/symbol.d +++ b/src/sdlang/symbol.d @@ -38,7 +38,7 @@ private Symbol _symbol(string name) /// This only represents terminals. Nonterminal tokens aren't /// constructed since the AST is built directly during parsing. /// -/// You can't create a Symbol directly. Instead, use the 'symbol' +/// You can't create a Symbol directly. Instead, use the `symbol` /// template. struct Symbol { diff --git a/src/sdlang/taggedalgebraic/taggedalgebraic.d b/src/sdlang/taggedalgebraic/taggedalgebraic.d new file mode 100644 index 0000000..ffaac49 --- /dev/null +++ b/src/sdlang/taggedalgebraic/taggedalgebraic.d @@ -0,0 +1,1085 @@ +/** + * Algebraic data type implementation based on a tagged union. + * + * Copyright: Copyright 2015, Sönke Ludwig. + * License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). + * Authors: Sönke Ludwig +*/ +module taggedalgebraic; + +import std.typetuple; + +// TODO: +// - distinguish between @property and non@-property methods. +// - verify that static methods are handled properly + +/** Implements a generic algebraic type using an enum to identify the stored type. + + This struct takes a `union` or `struct` declaration as an input and builds + an algebraic data type from its fields, using an automatically generated + `Kind` enumeration to identify which field of the union is currently used. + Multiple fields with the same value are supported. + + All operators and methods are transparently forwarded to the contained + value. The caller has to make sure that the contained value supports the + requested operation. Failure to do so will result in an assertion failure. + + The return value of forwarded operations is determined as follows: + $(UL + $(LI If the type can be uniquely determined, it is used as the return + value) + $(LI If there are multiple possible return values and all of them match + the unique types defined in the `TaggedAlgebraic`, a + `TaggedAlgebraic` is returned.) + $(LI If there are multiple return values and none of them is a + `Variant`, an `Algebraic` of the set of possible return types is + returned.) + $(LI If any of the possible operations returns a `Variant`, this is used + as the return value.) + ) +*/ +struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) +{ + import std.algorithm : among; + import std.string : format; + import std.traits : FieldTypeTuple, FieldNameTuple, Largest, hasElaborateCopyConstructor, hasElaborateDestructor; + + private alias Union = U; + private alias FieldTypes = FieldTypeTuple!U; + private alias fieldNames = FieldNameTuple!U; + + static assert(FieldTypes.length > 0, "The TaggedAlgebraic's union type must have at least one field."); + static assert(FieldTypes.length == fieldNames.length); + + + private { + void[Largest!FieldTypes.sizeof] m_data = void; + Kind m_kind; + } + + /// A type enum that identifies the type of value currently stored. + alias Kind = TypeEnum!U; + + /// Compatibility alias + deprecated("Use 'Kind' instead.") alias Type = Kind; + + /// The type ID of the currently stored value. + @property Kind kind() const { return m_kind; } + + // Compatibility alias + deprecated("Use 'kind' instead.") + alias typeID = kind; + + // constructors + //pragma(msg, generateConstructors!U()); + mixin(generateConstructors!U); + + this(TaggedAlgebraic other) + { + import std.algorithm : swap; + swap(this, other); + } + + void opAssign(TaggedAlgebraic other) + { + import std.algorithm : swap; + swap(this, other); + } + + // postblit constructor + static if (anySatisfy!(hasElaborateCopyConstructor, FieldTypes)) + { + this(this) + { + switch (m_kind) { + default: break; + foreach (i, tname; fieldNames) { + alias T = typeof(__traits(getMember, U, tname)); + static if (hasElaborateCopyConstructor!T) + { + case __traits(getMember, Kind, tname): + typeid(T).postblit(cast(void*)&trustedGet!tname()); + return; + } + } + } + } + } + + // destructor + static if (anySatisfy!(hasElaborateDestructor, FieldTypes)) + { + ~this() + { + final switch (m_kind) { + foreach (i, tname; fieldNames) { + alias T = typeof(__traits(getMember, U, tname)); + case __traits(getMember, Kind, tname): + static if (hasElaborateDestructor!T) { + .destroy(trustedGet!tname); + } + return; + } + } + } + } + + /// Enables conversion or extraction of the stored value. + T opCast(T)() + { + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(to!T(trustedGet!(fieldNames[i]))))) { + return to!T(trustedGet!(fieldNames[i])); + } else { + assert(false, "Cannot cast a "~(cast(Kind)m_kind).to!string~" value ("~FT.stringof~") to "~T.stringof); + } + } + } + assert(false); // never reached + } + /// ditto + T opCast(T)() const + { + // this method needs to be duplicated because inout doesn't work with to!() + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(to!T(trustedGet!(fieldNames[i]))))) { + return to!T(trustedGet!(fieldNames[i])); + } else { + assert(false, "Cannot cast a "~(cast(Kind)m_kind).to!string~" value ("~FT.stringof~") to "~T.stringof); + } + } + } + assert(false); // never reached + } + + /// Uses `cast(string)`/`to!string` to return a string representation of the enclosed value. + string toString() const { return cast(string)this; } + + // NOTE: "this TA" is used here as the functional equivalent of inout, + // just that it generates one template instantiation per modifier + // combination, so that we can actually decide what to do for each + // case. + + /// Enables the invocation of methods of the stored value. + auto opDispatch(string name, this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.method, name, ARGS)) { return implementOp!(OpKind.method, name)(this, args); } + /// Enables accessing properties/fields of the stored value. + @property auto opDispatch(string name, this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.field, name, ARGS) && !hasOp!(TA, OpKind.method, name, ARGS)) { return implementOp!(OpKind.field, name)(this, args); } + /// Enables equality comparison with the stored value. + auto opEquals(T, this TA)(auto ref T other) if (hasOp!(TA, OpKind.binary, "==", T)) { return implementOp!(OpKind.binary, "==")(this, other); } + /// Enables relational comparisons with the stored value. + auto opCmp(T, this TA)(auto ref T other) if (hasOp!(TA, OpKind.binary, "<", T)) { assert(false, "TODO!"); } + /// Enables the use of unary operators with the stored value. + auto opUnary(string op, this TA)() if (hasOp!(TA, OpKind.unary, op)) { return implementOp!(OpKind.unary, op)(this); } + /// Enables the use of binary operators with the stored value. + auto opBinary(string op, T, this TA)(auto ref T other) if (hasOp!(TA, OpKind.binary, op, T)) { return implementOp!(OpKind.binary, op)(this, other); } + /// Enables the use of binary operators with the stored value. + auto opBinaryRight(string op, T, this TA)(auto ref T other) if (hasOp!(TA, OpKind.binaryRight, op, T)) { return implementOp!(OpKind.binaryRight, op)(this, other); } + /// Enables operator assignments on the stored value. + auto opOpAssign(string op, T, this TA)(auto ref T other) if (hasOp!(TA, OpKind.binary, op~"=", T)) { return implementOp!(OpKind.binary, op~"=")(this, other); } + /// Enables indexing operations on the stored value. + auto opIndex(this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.index, null, ARGS)) { return implementOp!(OpKind.index, null)(this, args); } + /// Enables index assignments on the stored value. + auto opIndexAssign(this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.indexAssign, null, ARGS)) { return implementOp!(OpKind.indexAssign, null)(this, args); } + /// Enables call syntax operations on the stored value. + auto opCall(this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.call, null, ARGS)) { return implementOp!(OpKind.call, null)(this, args); } + + private @trusted @property ref inout(typeof(__traits(getMember, U, f))) trustedGet(string f)() inout { return trustedGet!(inout(typeof(__traits(getMember, U, f)))); } + private @trusted @property ref inout(T) trustedGet(T)() inout { return *cast(inout(T)*)m_data.ptr; } +} + +/// +unittest +{ + import taggedalgebraic; + + struct Foo { + string name; + void bar() {} + } + + union Base { + int i; + string str; + Foo foo; + } + + alias Tagged = TaggedAlgebraic!Base; + + // Instantiate + Tagged taggedInt = 5; + Tagged taggedString = "Hello"; + Tagged taggedFoo = Foo(); + Tagged taggedAny = taggedInt; + taggedAny = taggedString; + taggedAny = taggedFoo; + + // Check type: Tagged.Kind is an enum + assert(taggedInt.kind == Tagged.Kind.i); + assert(taggedString.kind == Tagged.Kind.str); + assert(taggedFoo.kind == Tagged.Kind.foo); + assert(taggedAny.kind == Tagged.Kind.foo); + + // In most cases, can simply use as-is + auto num = 4 + taggedInt; + auto msg = taggedString ~ " World!"; + taggedFoo.bar(); + if (taggedAny.kind == Tagged.Kind.foo) // Make sure to check type first! + taggedAny.bar(); + //taggedString.bar(); // AssertError: Not a Foo! + + // Convert back by casting + auto i = cast(int) taggedInt; + auto str = cast(string) taggedString; + auto foo = cast(Foo) taggedFoo; + if (taggedAny.kind == Tagged.Kind.foo) // Make sure to check type first! + auto foo2 = cast(Foo) taggedAny; + //cast(Foo) taggedString; // AssertError! + + // Kind is an enum, so final switch is supported: + final switch (taggedAny.kind) { + case Tagged.Kind.i: + // It's "int i" + break; + + case Tagged.Kind.str: + // It's "string str" + break; + + case Tagged.Kind.foo: + // It's "Foo foo" + break; + } +} + +/** Operators and methods of the contained type can be used transparently. +*/ +@safe unittest { + static struct S { + int v; + int test() { return v / 2; } + } + + static union Test { + typeof(null) null_; + int integer; + string text; + string[string] dictionary; + S custom; + } + + alias TA = TaggedAlgebraic!Test; + + TA ta; + assert(ta.kind == TA.Kind.null_); + + ta = 12; + assert(ta.kind == TA.Kind.integer); + assert(ta == 12); + assert(cast(int)ta == 12); + assert(cast(long)ta == 12); + assert(cast(short)ta == 12); + + ta += 12; + assert(ta == 24); + assert(ta - 10 == 14); + + ta = ["foo" : "bar"]; + assert(ta.kind == TA.Kind.dictionary); + assert(ta["foo"] == "bar"); + + ta["foo"] = "baz"; + assert(ta["foo"] == "baz"); + + ta = S(8); + assert(ta.test() == 4); +} + +unittest { // std.conv integration + import std.conv : to; + + static struct S { + int v; + int test() { return v / 2; } + } + + static union Test { + typeof(null) null_; + int number; + string text; + } + + alias TA = TaggedAlgebraic!Test; + + TA ta; + assert(ta.kind == TA.Kind.null_); + ta = "34"; + assert(ta == "34"); + assert(to!int(ta) == 34, to!string(to!int(ta))); + assert(to!string(ta) == "34", to!string(ta)); +} + +/** Multiple fields are allowed to have the same type, in which case the type + ID enum is used to disambiguate. +*/ +@safe unittest { + static union Test { + typeof(null) null_; + int count; + int difference; + } + + alias TA = TaggedAlgebraic!Test; + + TA ta; + ta = TA(12, TA.Kind.count); + assert(ta.kind == TA.Kind.count); + assert(ta == 12); + + ta = null; + assert(ta.kind == TA.Kind.null_); +} + +unittest { + // test proper type modifier support + static struct S { + void test() {} + void testI() immutable {} + void testC() const {} + void testS() shared {} + void testSC() shared const {} + } + static union U { + S s; + } + + auto u = TaggedAlgebraic!U(S.init); + const uc = u; + immutable ui = cast(immutable)u; + //const shared usc = cast(shared)u; + //shared us = cast(shared)u; + + static assert( is(typeof(u.test()))); + static assert(!is(typeof(u.testI()))); + static assert( is(typeof(u.testC()))); + static assert(!is(typeof(u.testS()))); + static assert(!is(typeof(u.testSC()))); + + static assert(!is(typeof(uc.test()))); + static assert(!is(typeof(uc.testI()))); + static assert( is(typeof(uc.testC()))); + static assert(!is(typeof(uc.testS()))); + static assert(!is(typeof(uc.testSC()))); + + static assert(!is(typeof(ui.test()))); + static assert( is(typeof(ui.testI()))); + static assert( is(typeof(ui.testC()))); + static assert(!is(typeof(ui.testS()))); + static assert( is(typeof(ui.testSC()))); + + /*static assert(!is(typeof(us.test()))); + static assert(!is(typeof(us.testI()))); + static assert(!is(typeof(us.testC()))); + static assert( is(typeof(us.testS()))); + static assert( is(typeof(us.testSC()))); + + static assert(!is(typeof(usc.test()))); + static assert(!is(typeof(usc.testI()))); + static assert(!is(typeof(usc.testC()))); + static assert(!is(typeof(usc.testS()))); + static assert( is(typeof(usc.testSC())));*/ +} + +unittest { + // test attributes on contained values + import std.typecons : Rebindable, rebindable; + + class C { + void test() {} + void testC() const {} + void testI() immutable {} + } + union U { + Rebindable!(immutable(C)) c; + } + + auto ta = TaggedAlgebraic!U(rebindable(new immutable C)); + static assert(!is(typeof(ta.test()))); + static assert( is(typeof(ta.testC()))); + static assert( is(typeof(ta.testI()))); +} + +version (unittest) { + // test recursive definition using a wrapper dummy struct + // (needed to avoid "no size yet for forward reference" errors) + template ID(What) { alias ID = What; } + private struct _test_Wrapper { + TaggedAlgebraic!_test_U u; + alias u this; + this(ARGS...)(ARGS args) { u = TaggedAlgebraic!_test_U(args); } + } + private union _test_U { + _test_Wrapper[] children; + int value; + } + unittest { + alias TA = _test_Wrapper; + auto ta = TA(null); + ta ~= TA(0); + ta ~= TA(1); + ta ~= TA([TA(2)]); + assert(ta[0] == 0); + assert(ta[1] == 1); + assert(ta[2][0] == 2); + } +} + +unittest { // postblit/destructor test + static struct S { + static int i = 0; + bool initialized = false; + this(bool) { initialized = true; i++; } + this(this) { if (initialized) i++; } + ~this() { if (initialized) i--; } + } + + static struct U { + S s; + int t; + } + alias TA = TaggedAlgebraic!U; + { + assert(S.i == 0); + auto ta = TA(S(true)); + assert(S.i == 1); + { + auto tb = ta; + assert(S.i == 2); + ta = tb; + assert(S.i == 2); + ta = 1; + assert(S.i == 1); + ta = S(true); + assert(S.i == 2); + } + assert(S.i == 1); + } + assert(S.i == 0); + + static struct U2 { + S a; + S b; + } + alias TA2 = TaggedAlgebraic!U2; + { + auto ta2 = TA2(S(true), TA2.Kind.a); + assert(S.i == 1); + } + assert(S.i == 0); +} + +unittest { + static struct S { + union U { + int i; + string s; + U[] a; + } + alias TA = TaggedAlgebraic!U; + TA p; + alias p this; + } + S s = S(S.TA("hello")); + assert(cast(string)s == "hello"); +} + +unittest { // multiple operator choices + union U { + int i; + double d; + } + alias TA = TaggedAlgebraic!U; + TA ta = 12; + static assert(is(typeof(ta + 10) == TA)); // ambiguous, could be int or double + assert((ta + 10).kind == TA.Kind.i); + assert(ta + 10 == 22); + static assert(is(typeof(ta + 10.5) == double)); + assert(ta + 10.5 == 22.5); +} + +unittest { // Binary op between two TaggedAlgebraic values + union U { int i; } + alias TA = TaggedAlgebraic!U; + + TA a = 1, b = 2; + static assert(is(typeof(a + b) == int)); + assert(a + b == 3); +} + +unittest { // Ambiguous binary op between two TaggedAlgebraic values + union U { int i; double d; } + alias TA = TaggedAlgebraic!U; + + TA a = 1, b = 2; + static assert(is(typeof(a + b) == TA)); + assert((a + b).kind == TA.Kind.i); + assert(a + b == 3); +} + +unittest { + struct S { + union U { + @disableIndex string str; + S[] array; + S[string] object; + } + alias TA = TaggedAlgebraic!U; + TA payload; + alias payload this; + } + + S a = S(S.TA("hello")); + S b = S(S.TA(["foo": a])); + S c = S(S.TA([a])); + assert(b["foo"] == a); + assert(b["foo"] == "hello"); + assert(c[0] == a); + assert(c[0] == "hello"); +} + + +/** Tests if the algebraic type stores a value of a certain data type. +*/ +bool hasType(T, U)(in ref TaggedAlgebraic!U ta) +{ + alias Fields = Filter!(fieldMatchesType!(U, T), ta.fieldNames); + static assert(Fields.length > 0, "Type "~T.stringof~" cannot be stored in a "~(TaggedAlgebraic!U).stringof~"."); + + switch (ta.kind) { + default: return false; + foreach (i, fname; Fields) + case __traits(getMember, ta.Kind, fname): + return true; + } + assert(false); // never reached +} + +/// +unittest { + union Fields { + int number; + string text; + } + + TaggedAlgebraic!Fields ta = "test"; + + assert(ta.hasType!string); + assert(!ta.hasType!int); + + ta = 42; + assert(ta.hasType!int); + assert(!ta.hasType!string); +} + +unittest { // issue #1 + union U { + int a; + int b; + } + alias TA = TaggedAlgebraic!U; + + TA ta = TA(0, TA.Kind.b); + static assert(!is(typeof(ta.hasType!double))); + assert(ta.hasType!int); +} + +/** Gets the value stored in an algebraic type based on its data type. +*/ +ref inout(T) get(T, U)(ref inout(TaggedAlgebraic!U) ta) +{ + assert(hasType!(T, U)(ta)); + return ta.trustedGet!T; +} + +/// Convenience type that can be used for union fields that have no value (`void` is not allowed). +struct Void {} + +/// User-defined attibute to disable `opIndex` forwarding for a particular tagged union member. +@property auto disableIndex() { assert(__ctfe, "disableIndex must only be used as an attribute."); return DisableOpAttribute(OpKind.index, null); } + +private struct DisableOpAttribute { + OpKind kind; + string name; +} + + +private template hasOp(TA, OpKind kind, string name, ARGS...) +{ + import std.traits : CopyTypeQualifiers; + alias UQ = CopyTypeQualifiers!(TA, TA.Union); + enum hasOp = TypeTuple!(OpInfo!(UQ, kind, name, ARGS).fields).length > 0; +} + +unittest { + static struct S { + void m(int i) {} + bool opEquals(int i) { return true; } + bool opEquals(S s) { return true; } + } + + static union U { int i; string s; S st; } + alias TA = TaggedAlgebraic!U; + + static assert(hasOp!(TA, OpKind.binary, "+", int)); + static assert(hasOp!(TA, OpKind.binary, "~", string)); + static assert(hasOp!(TA, OpKind.binary, "==", int)); + static assert(hasOp!(TA, OpKind.binary, "==", string)); + static assert(hasOp!(TA, OpKind.binary, "==", int)); + static assert(hasOp!(TA, OpKind.binary, "==", S)); + static assert(hasOp!(TA, OpKind.method, "m", int)); + static assert(hasOp!(TA, OpKind.binary, "+=", int)); + static assert(!hasOp!(TA, OpKind.binary, "~", int)); + static assert(!hasOp!(TA, OpKind.binary, "~", int)); + static assert(!hasOp!(TA, OpKind.method, "m", string)); + static assert(!hasOp!(TA, OpKind.method, "m")); + static assert(!hasOp!(const(TA), OpKind.binary, "+=", int)); + static assert(!hasOp!(const(TA), OpKind.method, "m", int)); +} + +unittest { + struct S { + union U { + string s; + S[] arr; + S[string] obj; + } + alias TA = TaggedAlgebraic!(S.U); + TA payload; + alias payload this; + } + static assert(hasOp!(S.TA, OpKind.index, null, size_t)); + static assert(hasOp!(S.TA, OpKind.index, null, int)); + static assert(hasOp!(S.TA, OpKind.index, null, string)); + static assert(hasOp!(S.TA, OpKind.field, "length")); +} + +unittest { // "in" operator + union U { + string[string] dict; + } + alias TA = TaggedAlgebraic!U; + auto ta = TA(["foo": "bar"]); + assert("foo" in ta); + assert(*("foo" in ta) == "bar"); +} + +private static auto implementOp(OpKind kind, string name, T, ARGS...)(ref T self, auto ref ARGS args) +{ + import std.array : join; + import std.traits : CopyTypeQualifiers; + import std.variant : Algebraic, Variant; + alias UQ = CopyTypeQualifiers!(T, T.Union); + + alias info = OpInfo!(UQ, kind, name, ARGS); + + static assert(hasOp!(T, kind, name, ARGS)); + + static assert(info.fields.length > 0, "Implementing operator that has no valid implementation for any supported type."); + + //pragma(msg, "Fields for "~kind.stringof~" "~name~", "~T.stringof~": "~info.fields.stringof); + //pragma(msg, "Return types for "~kind.stringof~" "~name~", "~T.stringof~": "~info.ReturnTypes.stringof); + //pragma(msg, typeof(T.Union.tupleof)); + //import std.meta : staticMap; pragma(msg, staticMap!(isMatchingUniqueType!(T.Union), info.ReturnTypes)); + + switch (self.m_kind) { + default: assert(false, "Operator "~name~" ("~kind.stringof~") can only be used on values of the following types: "~[info.fields].join(", ")); + foreach (i, f; info.fields) { + alias FT = typeof(__traits(getMember, T.Union, f)); + case __traits(getMember, T.Kind, f): + static if (NoDuplicates!(info.ReturnTypes).length == 1) + return info.perform(self.trustedGet!FT, args); + else static if (allSatisfy!(isMatchingUniqueType!(T.Union), info.ReturnTypes)) + return TaggedAlgebraic!(T.Union)(info.perform(self.trustedGet!FT, args)); + else static if (allSatisfy!(isNoVariant, info.ReturnTypes)) { + alias Alg = Algebraic!(NoDuplicates!(info.ReturnTypes)); + info.ReturnTypes[i] ret = info.perform(self.trustedGet!FT, args); + import std.traits : isInstanceOf; + static if (isInstanceOf!(TaggedAlgebraic, typeof(ret))) return Alg(ret.payload); + else return Alg(ret); + } + else static if (is(FT == Variant)) + return info.perform(self.trustedGet!FT, args); + else + return Variant(info.perform(self.trustedGet!FT, args)); + } + } + + assert(false); // never reached +} + +unittest { // opIndex on recursive TA with closed return value set + static struct S { + union U { + char ch; + string str; + S[] arr; + } + alias TA = TaggedAlgebraic!U; + TA payload; + alias payload this; + + this(T)(T t) { this.payload = t; } + } + S a = S("foo"); + S s = S([a]); + + assert(implementOp!(OpKind.field, "length")(s.payload) == 1); + static assert(is(typeof(implementOp!(OpKind.index, null)(s.payload, 0)) == S.TA)); + assert(implementOp!(OpKind.index, null)(s.payload, 0) == "foo"); +} + +unittest { // opIndex on recursive TA with closed return value set using @disableIndex + static struct S { + union U { + @disableIndex string str; + S[] arr; + } + alias TA = TaggedAlgebraic!U; + TA payload; + alias payload this; + + this(T)(T t) { this.payload = t; } + } + S a = S("foo"); + S s = S([a]); + + assert(implementOp!(OpKind.field, "length")(s.payload) == 1); + static assert(is(typeof(implementOp!(OpKind.index, null)(s.payload, 0)) == S)); + assert(implementOp!(OpKind.index, null)(s.payload, 0) == "foo"); +} + + +private auto performOpRaw(U, OpKind kind, string name, T, ARGS...)(ref T value, /*auto ref*/ ARGS args) +{ + static if (kind == OpKind.binary) return mixin("value "~name~" args[0]"); + else static if (kind == OpKind.binaryRight) return mixin("args[0] "~name~" value"); + else static if (kind == OpKind.unary) return mixin("name "~value); + else static if (kind == OpKind.method) return __traits(getMember, value, name)(args); + else static if (kind == OpKind.field) return __traits(getMember, value, name); + else static if (kind == OpKind.index) return value[args]; + else static if (kind == OpKind.indexAssign) return value[args[1 .. $]] = args[0]; + else static if (kind == OpKind.call) return value(args); + else static assert(false, "Unsupported kind of operator: "~kind.stringof); +} + +unittest { + union U { int i; string s; } + + { int v = 1; assert(performOpRaw!(U, OpKind.binary, "+")(v, 3) == 4); } + { string v = "foo"; assert(performOpRaw!(U, OpKind.binary, "~")(v, "bar") == "foobar"); } +} + + +private auto performOp(U, OpKind kind, string name, T, ARGS...)(ref T value, /*auto ref*/ ARGS args) +{ + import std.traits : isInstanceOf; + static if (ARGS.length > 0 && isInstanceOf!(TaggedAlgebraic, ARGS[0])) { + static if (is(typeof(performOpRaw!(U, kind, name, T, ARGS)(value, args)))) { + return performOpRaw!(U, kind, name, T, ARGS)(value, args); + } else { + alias TA = ARGS[0]; + template MTypesImpl(size_t i) { + static if (i < TA.FieldTypes.length) { + alias FT = TA.FieldTypes[i]; + static if (is(typeof(&performOpRaw!(U, kind, name, T, FT, ARGS[1 .. $])))) + alias MTypesImpl = TypeTuple!(FT, MTypesImpl!(i+1)); + else alias MTypesImpl = TypeTuple!(MTypesImpl!(i+1)); + } else alias MTypesImpl = TypeTuple!(); + } + alias MTypes = NoDuplicates!(MTypesImpl!0); + static assert(MTypes.length > 0, "No type of the TaggedAlgebraic parameter matches any function declaration."); + static if (MTypes.length == 1) { + if (args[0].hasType!(MTypes[0])) + return performOpRaw!(U, kind, name)(value, args[0].get!(MTypes[0]), args[1 .. $]); + } else { + // TODO: allow all return types (fall back to Algebraic or Variant) + foreach (FT; MTypes) { + if (args[0].hasType!FT) + return ARGS[0](performOpRaw!(U, kind, name)(value, args[0].get!FT, args[1 .. $])); + } + } + throw new /*InvalidAgument*/Exception("Algebraic parameter type mismatch"); + } + } else return performOpRaw!(U, kind, name, T, ARGS)(value, args); +} + +unittest { + union U { int i; double d; string s; } + + { int v = 1; assert(performOp!(U, OpKind.binary, "+")(v, 3) == 4); } + { string v = "foo"; assert(performOp!(U, OpKind.binary, "~")(v, "bar") == "foobar"); } + { string v = "foo"; assert(performOp!(U, OpKind.binary, "~")(v, TaggedAlgebraic!U("bar")) == "foobar"); } + { int v = 1; assert(performOp!(U, OpKind.binary, "+")(v, TaggedAlgebraic!U(3)) == 4); } +} + + +private template OpInfo(U, OpKind kind, string name, ARGS...) +{ + import std.traits : CopyTypeQualifiers, FieldTypeTuple, FieldNameTuple, ReturnType; + + private alias FieldTypes = FieldTypeTuple!U; + private alias fieldNames = FieldNameTuple!U; + + private template isOpEnabled(string field) + { + alias attribs = TypeTuple!(__traits(getAttributes, __traits(getMember, U, field))); + template impl(size_t i) { + static if (i < attribs.length) { + static if (is(typeof(attribs[i]) == DisableOpAttribute)) { + static if (kind == attribs[i].kind && name == attribs[i].name) + enum impl = false; + else enum impl = impl!(i+1); + } else enum impl = impl!(i+1); + } else enum impl = true; + } + enum isOpEnabled = impl!0; + } + + template fieldsImpl(size_t i) + { + static if (i < FieldTypes.length) { + static if (isOpEnabled!(fieldNames[i]) && is(typeof(&performOp!(U, kind, name, FieldTypes[i], ARGS)))) { + alias fieldsImpl = TypeTuple!(fieldNames[i], fieldsImpl!(i+1)); + } else alias fieldsImpl = fieldsImpl!(i+1); + } else alias fieldsImpl = TypeTuple!(); + } + alias fields = fieldsImpl!0; + + template ReturnTypesImpl(size_t i) { + static if (i < fields.length) { + alias FT = CopyTypeQualifiers!(U, typeof(__traits(getMember, U, fields[i]))); + alias ReturnTypesImpl = TypeTuple!(ReturnType!(performOp!(U, kind, name, FT, ARGS)), ReturnTypesImpl!(i+1)); + } else alias ReturnTypesImpl = TypeTuple!(); + } + alias ReturnTypes = ReturnTypesImpl!0; + + static auto perform(T)(ref T value, auto ref ARGS args) { return performOp!(U, kind, name)(value, args); } +} + +private template ImplicitUnqual(T) { + import std.traits : Unqual, hasAliasing; + static if (is(T == void)) alias ImplicitUnqual = void; + else { + private static struct S { T t; } + static if (hasAliasing!S) alias ImplicitUnqual = T; + else alias ImplicitUnqual = Unqual!T; + } +} + +private enum OpKind { + binary, + binaryRight, + unary, + method, + field, + index, + indexAssign, + call +} + +private template TypeEnum(U) +{ + import std.array : join; + import std.traits : FieldNameTuple; + mixin("enum TypeEnum { " ~ [FieldNameTuple!U].join(", ") ~ " }"); +} + +private string generateConstructors(U)() +{ + import std.algorithm : map; + import std.array : join; + import std.string : format; + import std.traits : FieldTypeTuple; + + string ret; + + // disable default construction if first type is not a null/Void type + static if (!is(FieldTypeTuple!U[0] == typeof(null)) && !is(FieldTypeTuple!U[0] == Void)) + { + ret ~= q{ + @disable this(); + }; + } + + // normal type constructors + foreach (tname; UniqueTypeFields!U) + ret ~= q{ + this(typeof(U.%s) value) + { + m_data.rawEmplace(value); + m_kind = Kind.%s; + } + + void opAssign(typeof(U.%s) value) + { + if (m_kind != Kind.%s) { + // NOTE: destroy(this) doesn't work for some opDispatch-related reason + static if (is(typeof(&this.__xdtor))) + this.__xdtor(); + m_data.rawEmplace(value); + } else { + trustedGet!"%s" = value; + } + m_kind = Kind.%s; + } + }.format(tname, tname, tname, tname, tname, tname); + + // type constructors with explicit type tag + foreach (tname; AmbiguousTypeFields!U) + ret ~= q{ + this(typeof(U.%s) value, Kind type) + { + assert(type.among!(%s), format("Invalid type ID for type %%s: %%s", typeof(U.%s).stringof, type)); + m_data.rawEmplace(value); + m_kind = type; + } + }.format(tname, [SameTypeFields!(U, tname)].map!(f => "Kind."~f).join(", "), tname); + + return ret; +} + +private template UniqueTypeFields(U) { + import std.traits : FieldTypeTuple, FieldNameTuple; + + alias Types = FieldTypeTuple!U; + + template impl(size_t i) { + static if (i < Types.length) { + enum name = FieldNameTuple!U[i]; + alias T = Types[i]; + static if (staticIndexOf!(T, Types) == i && staticIndexOf!(T, Types[i+1 .. $]) < 0) + alias impl = TypeTuple!(name, impl!(i+1)); + else alias impl = TypeTuple!(impl!(i+1)); + } else alias impl = TypeTuple!(); + } + alias UniqueTypeFields = impl!0; +} + +private template AmbiguousTypeFields(U) { + import std.traits : FieldTypeTuple, FieldNameTuple; + + alias Types = FieldTypeTuple!U; + + template impl(size_t i) { + static if (i < Types.length) { + enum name = FieldNameTuple!U[i]; + alias T = Types[i]; + static if (staticIndexOf!(T, Types) == i && staticIndexOf!(T, Types[i+1 .. $]) >= 0) + alias impl = TypeTuple!(name, impl!(i+1)); + else alias impl = impl!(i+1); + } else alias impl = TypeTuple!(); + } + alias AmbiguousTypeFields = impl!0; +} + +unittest { + union U { + int a; + string b; + int c; + double d; + } + static assert([UniqueTypeFields!U] == ["b", "d"]); + static assert([AmbiguousTypeFields!U] == ["a"]); +} + +private template SameTypeFields(U, string field) { + import std.traits : FieldTypeTuple, FieldNameTuple; + + alias Types = FieldTypeTuple!U; + + alias T = typeof(__traits(getMember, U, field)); + template impl(size_t i) { + static if (i < Types.length) { + enum name = FieldNameTuple!U[i]; + static if (is(Types[i] == T)) + alias impl = TypeTuple!(name, impl!(i+1)); + else alias impl = TypeTuple!(impl!(i+1)); + } else alias impl = TypeTuple!(); + } + alias SameTypeFields = impl!0; +} + +private template MemberType(U) { + template MemberType(string name) { + alias MemberType = typeof(__traits(getMember, U, name)); + } +} + +private template isMatchingType(U) { + import std.traits : FieldTypeTuple; + enum isMatchingType(T) = staticIndexOf!(T, FieldTypeTuple!U) >= 0; +} + +private template isMatchingUniqueType(U) { + import std.traits : staticMap; + alias UniqueTypes = staticMap!(FieldTypeOf!U, UniqueTypeFields!U); + template isMatchingUniqueType(T) { + static if (is(T : TaggedAlgebraic!U)) enum isMatchingUniqueType = true; + else enum isMatchingUniqueType = staticIndexOfImplicit!(T, UniqueTypes) >= 0; + } +} + +private template fieldMatchesType(U, T) +{ + enum fieldMatchesType(string field) = is(typeof(__traits(getMember, U, field)) == T); +} + +private template FieldTypeOf(U) { + template FieldTypeOf(string name) { + alias FieldTypeOf = typeof(__traits(getMember, U, name)); + } +} + +private template staticIndexOfImplicit(T, Types...) { + template impl(size_t i) { + static if (i < Types.length) { + static if (is(T : Types[i])) enum impl = i; + else enum impl = impl!(i+1); + } else enum impl = -1; + } + enum staticIndexOfImplicit = impl!0; +} + +unittest { + static assert(staticIndexOfImplicit!(immutable(char), char) == 0); + static assert(staticIndexOfImplicit!(int, long) == 0); + static assert(staticIndexOfImplicit!(long, int) < 0); + static assert(staticIndexOfImplicit!(int, int, double) == 0); + static assert(staticIndexOfImplicit!(double, int, double) == 1); +} + + +private template isNoVariant(T) { + import std.variant : Variant; + enum isNoVariant = !is(T == Variant); +} + +private void rawEmplace(T)(void[] dst, ref T src) +{ + T* tdst = () @trusted { return cast(T*)dst.ptr; } (); + static if (is(T == class)) { + *tdst = src; + } else { + import std.conv : emplace; + emplace(tdst); + *tdst = src; + } +} diff --git a/src/sdlang/token.d b/src/sdlang/token.d index 908d4a3..0a5b2fd 100644 --- a/src/sdlang/token.d +++ b/src/sdlang/token.d @@ -7,15 +7,18 @@ import std.array; import std.base64; import std.conv; import std.datetime; +import std.meta; import std.range; import std.string; +import std.traits; import std.typetuple; import std.variant; +import sdlang.exception; import sdlang.symbol; import sdlang.util; -/// DateTime doesn't support milliseconds, but SDL's "Date Time" type does. +/// DateTime doesn't support milliseconds, but SDLang's "Date Time" type does. /// So this is needed for any SDL "Date Time" that doesn't include a time zone. struct DateTimeFrac { @@ -30,11 +33,11 @@ struct DateTimeFrac /++ If a "Date Time" literal in the SDL file has a time zone that's not found in your system, you get one of these instead of a SysTime. (Because it's -impossible to indicate "unknown time zone" with 'std.datetime.TimeZone'.) +impossible to indicate "unknown time zone" with `std.datetime.TimeZone`.) -The difference between this and 'DateTimeFrac' is that 'DateTimeFrac' +The difference between this and `DateTimeFrac` is that `DateTimeFrac` indicates that no time zone was specified in the SDL at all, whereas -'DateTimeFracUnknownZone' indicates that a time zone was specified but +`DateTimeFracUnknownZone` indicates that a time zone was specified but data for it could not be found on your system. +/ struct DateTimeFracUnknownZone @@ -61,9 +64,10 @@ struct DateTimeFracUnknownZone } /++ -SDL's datatypes map to D's datatypes as described below. +SDLang's datatypes map to D's datatypes as described below. Most are straightforward, but take special note of the date/time-related types. +--------------------------------------------------------------- Boolean: bool Null: typeof(null) Unicode Character: dchar @@ -81,8 +85,9 @@ Date (with no time at all): Date Date Time (no timezone): DateTimeFrac Date Time (with a known timezone): SysTime Date Time (with an unknown timezone): DateTimeFracUnknownZone +--------------------------------------------------------------- +/ -alias TypeTuple!( +alias ValueTypes = TypeTuple!( bool, string, dchar, int, long, @@ -90,41 +95,24 @@ alias TypeTuple!( Date, DateTimeFrac, SysTime, DateTimeFracUnknownZone, Duration, ubyte[], typeof(null), -) ValueTypes; +); -alias Algebraic!( ValueTypes ) Value; ///ditto +alias Value = Algebraic!( ValueTypes ); ///ditto +enum isValueType(T) = staticIndexOf!(T, ValueTypes) != -1; -template isSDLSink(T) -{ - enum isSink = - isOutputRange!T && - is(ElementType!(T)[] == string); -} +enum isSink(T) = + isOutputRange!T && + is(ElementType!(T)[] == string); -string toSDLString(T)(T value) if( - is( T : Value ) || - is( T : bool ) || - is( T : string ) || - is( T : dchar ) || - is( T : int ) || - is( T : long ) || - is( T : float ) || - is( T : double ) || - is( T : real ) || - is( T : Date ) || - is( T : DateTimeFrac ) || - is( T : SysTime ) || - is( T : DateTimeFracUnknownZone ) || - is( T : Duration ) || - is( T : ubyte[] ) || - is( T : typeof(null) ) -) +string toSDLString(T)(T value) if(is(T==Value) || isValueType!T) { Appender!string sink; toSDLString(value, sink); return sink.data; } +/// Throws SDLangException if value is infinity, -infinity or NaN, because +/// those are not currently supported by the SDLang spec. void toSDLString(Sink)(Value value, ref Sink sink) if(isOutputRange!(Sink,char)) { foreach(T; ValueTypes) @@ -139,6 +127,52 @@ void toSDLString(Sink)(Value value, ref Sink sink) if(isOutputRange!(Sink,char)) throw new Exception("Internal SDLang-D error: Unhandled type of Value. Contains: "~value.toString()); } +@("toSDLString on infinity and NaN") +unittest +{ + import std.exception; + + auto floatInf = float.infinity; + auto floatNegInf = -float.infinity; + auto floatNaN = float.nan; + + auto doubleInf = double.infinity; + auto doubleNegInf = -double.infinity; + auto doubleNaN = double.nan; + + auto realInf = real.infinity; + auto realNegInf = -real.infinity; + auto realNaN = real.nan; + + assertNotThrown( toSDLString(0.0F) ); + assertNotThrown( toSDLString(0.0) ); + assertNotThrown( toSDLString(0.0L) ); + + assertThrown!ValidationException( toSDLString(floatInf) ); + assertThrown!ValidationException( toSDLString(floatNegInf) ); + assertThrown!ValidationException( toSDLString(floatNaN) ); + + assertThrown!ValidationException( toSDLString(doubleInf) ); + assertThrown!ValidationException( toSDLString(doubleNegInf) ); + assertThrown!ValidationException( toSDLString(doubleNaN) ); + + assertThrown!ValidationException( toSDLString(realInf) ); + assertThrown!ValidationException( toSDLString(realNegInf) ); + assertThrown!ValidationException( toSDLString(realNaN) ); + + assertThrown!ValidationException( toSDLString(Value(floatInf)) ); + assertThrown!ValidationException( toSDLString(Value(floatNegInf)) ); + assertThrown!ValidationException( toSDLString(Value(floatNaN)) ); + + assertThrown!ValidationException( toSDLString(Value(doubleInf)) ); + assertThrown!ValidationException( toSDLString(Value(doubleNegInf)) ); + assertThrown!ValidationException( toSDLString(Value(doubleNaN)) ); + + assertThrown!ValidationException( toSDLString(Value(realInf)) ); + assertThrown!ValidationException( toSDLString(Value(realNegInf)) ); + assertThrown!ValidationException( toSDLString(Value(realNaN)) ); +} + void toSDLString(Sink)(typeof(null) value, ref Sink sink) if(isOutputRange!(Sink,char)) { sink.put("null"); @@ -194,18 +228,37 @@ void toSDLString(Sink)(long value, ref Sink sink) if(isOutputRange!(Sink,char)) sink.put( "%sL".format(value) ); } +private void checkUnsupportedFloatingPoint(T)(T value) if(isFloatingPoint!T) +{ + import std.exception; + import std.math; + + enforce!ValidationException( + !isInfinity(value), + "SDLang does not currently support infinity for floating-point types" + ); + + enforce!ValidationException( + !isNaN(value), + "SDLang does not currently support NaN for floating-point types" + ); +} + void toSDLString(Sink)(float value, ref Sink sink) if(isOutputRange!(Sink,char)) { + checkUnsupportedFloatingPoint(value); sink.put( "%.10sF".format(value) ); } void toSDLString(Sink)(double value, ref Sink sink) if(isOutputRange!(Sink,char)) { + checkUnsupportedFloatingPoint(value); sink.put( "%.30sD".format(value) ); } void toSDLString(Sink)(real value, ref Sink sink) if(isOutputRange!(Sink,char)) { + checkUnsupportedFloatingPoint(value); sink.put( "%.30sBD".format(value) ); } @@ -334,7 +387,7 @@ struct Token { Symbol symbol = sdlang.symbol.symbol!"Error"; /// The "type" of this token Location location; - Value value; /// Only valid when 'symbol' is symbol!"Value", otherwise null + Value value; /// Only valid when `symbol` is `symbol!"Value"`, otherwise null string data; /// Original text from source @disable this(); @@ -349,8 +402,8 @@ struct Token /// Tokens with differing symbols are always unequal. /// Tokens with differing values are always unequal. /// Tokens with differing Value types are always unequal. - /// Member 'location' is always ignored for comparison. - /// Member 'data' is ignored for comparison *EXCEPT* when the symbol is Ident. + /// Member `location` is always ignored for comparison. + /// Member `data` is ignored for comparison *EXCEPT* when the symbol is Ident. bool opEquals(Token b) { return opEquals(b); @@ -376,13 +429,9 @@ struct Token } } -version(sdlangUnittest) +@("sdlang token") unittest { - import std.stdio; - writeln("Unittesting sdlang token..."); - stdout.flush(); - auto loc = Location("", 0, 0, 0); auto loc2 = Location("a", 1, 1, 1); @@ -410,13 +459,9 @@ unittest assert(Token(symbol!"Value",loc,Value(cast(float)1.2)) != Token(symbol!"Value",loc, Value(cast(double)1.2))); } -version(sdlangUnittest) +@("sdlang Value.toSDLString()") unittest { - import std.stdio; - writeln("Unittesting sdlang Value.toSDLString()..."); - stdout.flush(); - // Bool and null assert(Value(null ).toSDLString() == "null"); assert(Value(true ).toSDLString() == "true"); diff --git a/src/sdlang/util.d b/src/sdlang/util.d index 329e387..d192ea2 100644 --- a/src/sdlang/util.d +++ b/src/sdlang/util.d @@ -4,10 +4,14 @@ module sdlang.util; import std.algorithm; +import std.array; +import std.conv; import std.datetime; +import std.range; import std.stdio; import std.string; +import sdlang.exception; import sdlang.token; enum sdlangVersion = "0.9.1"; @@ -26,14 +30,14 @@ struct Location int line; /// Zero-indexed int col; /// Zero-indexed, Tab counts as 1 size_t index; /// Index into the source - + this(int line, int col, int index) { this.line = line; this.col = col; this.index = index; } - + this(string file, int line, int col, int index) { this.file = file; @@ -41,12 +45,106 @@ struct Location this.col = col; this.index = index; } - + + /// Convert to string. Optionally takes output range as a sink. + string toString() + { + Appender!string sink; + this.toString(sink); + return sink.data; + } + + ///ditto + void toString(Sink)(ref Sink sink) if(isOutputRange!(Sink,char)) + { + sink.put(file); + sink.put("("); + sink.put(to!string(line+1)); + sink.put(":"); + sink.put(to!string(col+1)); + sink.put(")"); + } +} + +struct FullName +{ + string namespace; + string name; + + /// Convert to string. Optionally takes output range as a sink. string toString() { - return "%s(%s:%s)".format(file, line+1, col+1); + if(namespace == "") + return name; + + Appender!string sink; + this.toString(sink); + return sink.data; + } + + ///ditto + void toString(Sink)(ref Sink sink) if(isOutputRange!(Sink,char)) + { + if(namespace != "") + { + sink.put(namespace); + sink.put(":"); + } + + sink.put(name); + } + + /// + static string combine(string namespace, string name) + { + return FullName(namespace, name).toString(); + } + /// + @("FullName.combine example") + unittest + { + assert(FullName.combine("", "name") == "name"); + assert(FullName.combine("*", "name") == "*:name"); + assert(FullName.combine("namespace", "name") == "namespace:name"); + } + + /// + static FullName parse(string fullName) + { + FullName result; + + auto parts = fullName.findSplit(":"); + if(parts[1] == "") // No colon + { + result.namespace = ""; + result.name = parts[0]; + } + else + { + result.namespace = parts[0]; + result.name = parts[2]; + } + + return result; + } + /// + @("FullName.parse example") + unittest + { + assert(FullName.parse("name") == FullName("", "name")); + assert(FullName.parse("*:name") == FullName("*", "name")); + assert(FullName.parse("namespace:name") == FullName("namespace", "name")); + } + + /// Throws with appropriate message if this.name is "*". + /// Wildcards are only supported for namespaces, not names. + void ensureNoWildcardName(string extaMsg = null) + { + if(name == "*") + throw new ArgumentException(`Wildcards ("*") only allowed for namespaces, not names. `~extaMsg); } } +struct Foo { string foo; } void removeIndex(E)(ref E[] arr, ptrdiff_t index) { @@ -79,6 +177,24 @@ string toString(TypeInfo ti) else if(ti == typeid( Duration )) return "Duration"; else if(ti == typeid( ubyte[] )) return "ubyte[]"; else if(ti == typeid( typeof(null) )) return "null"; - + return "{unknown}"; } + +enum BOM { + UTF8, /// UTF-8 + UTF16LE, /// UTF-16 (little-endian) + UTF16BE, /// UTF-16 (big-endian) + UTF32LE, /// UTF-32 (little-endian) + UTF32BE, /// UTF-32 (big-endian) +} + +enum NBOM = __traits(allMembers, BOM).length; +immutable ubyte[][NBOM] ByteOrderMarks = +[ + [0xEF, 0xBB, 0xBF], //UTF8 + [0xFF, 0xFE], //UTF16LE + [0xFE, 0xFF], //UTF16BE + [0xFF, 0xFE, 0x00, 0x00], //UTF32LE + [0x00, 0x00, 0xFE, 0xFF] //UTF32BE +]; diff --git a/src/sdp.d b/src/sdp.d index f3acc9c..87393d9 100755 --- a/src/sdp.d +++ b/src/sdp.d @@ -42,6 +42,7 @@ private import mixin(import("version.txt")); mixin CompileTimeInfo; mixin RgxInit; +/++ A SiSU document parser writen in D. +/ void main(string[] args) { mixin SiSUregisters; mixin SiSUheaderExtractHub; @@ -248,3 +249,11 @@ void main(string[] args) { } } } +/// sdp sisu document parser +unittest { + /++ + name "sdp" + description "A SiSU document parser writen in D." + homepage "http://sisudoc.org" + +/ +} diff --git a/src/sdp/ao_conf_make_meta_sdlang.d b/src/sdp/ao_conf_make_meta_sdlang.d index 9124697..1cc3498 100644 --- a/src/sdp/ao_conf_make_meta_sdlang.d +++ b/src/sdp/ao_conf_make_meta_sdlang.d @@ -195,7 +195,7 @@ template SiSUheaderExtractSDLang() { try { sdl_root_header = parseSource(src_header); } - catch(SDLangParseException e) { + catch(ParseException e) { stderr.writeln("SDLang problem with this document header:"); stderr.writeln(src_header); // Error messages of the form: diff --git a/src/sdp/ao_object_setter.d b/src/sdp/ao_object_setter.d index cbb4edc..745de4e 100644 --- a/src/sdp/ao_object_setter.d +++ b/src/sdp/ao_object_setter.d @@ -5,35 +5,35 @@ template ObjectSetter() { /+ structs +/ struct HeadingAttrib { - string lev = "9"; - int lev_markup_number = 9; - int lev_collapsed_number = 9; + string lev = "9"; + int lev_markup_number = 9; + int lev_collapsed_number = 9; } struct ParaAttrib { - int indent_start = 0; - int indent_rest = 0; - bool bullet = false; + int indent_start = 0; + int indent_rest = 0; + bool bullet = false; } struct BlockAttrib { - string syntax = ""; + string syntax = ""; } struct Comment { // no .attrib and no .obj_cite_number } struct Node { - int ocn = 0; - int parent_lev = 0; - int parent_ocn = 0; - string node = ""; + int ocn = 0; + int parent_lev = 0; + int parent_ocn = 0; + string node = ""; } struct ObjComposite { // size_t id; - string use = ""; - string of = ""; - string is_a = ""; - string object = ""; - string obj_cite_number = ""; // not used for calculations? output only? else int - string[] anchor_tags = []; + string use = ""; + string of = ""; + string is_a = ""; + string object = ""; + string obj_cite_number = ""; // not used for calculations? output only? else int + string[] anchor_tags = []; HeadingAttrib heading_attrib; ParaAttrib para_attrib; BlockAttrib block_attrib; diff --git a/src/sdp/ao_output_debugs.d b/src/sdp/ao_output_debugs.d index 1bf359a..b5f96fa 100644 --- a/src/sdp/ao_output_debugs.d +++ b/src/sdp/ao_output_debugs.d @@ -383,10 +383,8 @@ template SiSUoutputDebugs() { } } writefln( - "%s%s%s\n%s\n%s%s\n%s%s\n%s%s\n%s:%s", - scr_txt_color["green"], + "%s\n%s\n%s%s\n%s%s\n%s%s\n%s:%s", "-------------------------------", - scr_txt_color["off"], fn_src, "length contents array: ", contents.length, diff --git a/src/sdp/ao_read_config_files.d b/src/sdp/ao_read_config_files.d index 571dbcc..49efe7b 100644 --- a/src/sdp/ao_read_config_files.d +++ b/src/sdp/ao_read_config_files.d @@ -80,7 +80,7 @@ template SiSUconfigSDLang() { try { sdl_root_conf = parseSource(configuration); } - catch(SDLangParseException e) { + catch(ParseException e) { stderr.writeln("SDLang problem with content for ", conf_sdl_filename); // Error messages of the form: // myFile.sdl(5:28): Error: Invalid integer suffix. diff --git a/src/undead/stream.d b/src/undead/stream.d index e31b381..dc81b7f 100644 --- a/src/undead/stream.d +++ b/src/undead/stream.d @@ -1429,7 +1429,7 @@ class Stream : InputStream, OutputStream { if (!seekable) throw new SeekException("Stream is not seekable"); } - + /+ unittest { // unit test for Issue 3363 import std.stdio; immutable fileName = undead.internal.file.deleteme ~ "-issue3363.txt"; @@ -1485,6 +1485,7 @@ class Stream : InputStream, OutputStream { tryFloatRoundtrip(-inf, "", " "); tryFloatRoundtrip(-inf, "%f", " "); } + +/ } /*** @@ -2135,6 +2136,7 @@ class File: Stream { HANDLE handle() { return hFile; } // run a few tests + /+ unittest { import std.internal.cstring : tempCString; @@ -2210,6 +2212,7 @@ class File: Stream { file.close(); remove(stream_file.tempCString()); } + +/ } /*** @@ -2255,6 +2258,7 @@ class BufferedFile: BufferedStream { } // run a few tests same as File + /+ unittest { import std.internal.cstring : tempCString; @@ -2305,6 +2309,7 @@ class BufferedFile: BufferedStream { file.close(); remove(stream_file.tempCString()); } + +/ } -- cgit v1.2.3