diff options
| author | Ralph Amissah <ralph@amissah.com> | 2016-10-01 13:54:14 -0400 | 
|---|---|---|
| committer | Ralph Amissah <ralph@amissah.com> | 2019-04-10 15:14:13 -0400 | 
| commit | 1cc6a04b8bce82fa83b62d919bf8bdf14cad0b92 (patch) | |
| tree | d8c44fa4acb7f588640b2be4117e26bbb864221c /src/sdlang | |
| parent | header, body split a more reliable regex solution (diff) | |
update sdlang, start looking to using dub remote dependenciesdoc-reform_v0.0.6
Diffstat (limited to 'src/sdlang')
| -rw-r--r-- | src/sdlang/ast.d | 1275 | ||||
| -rw-r--r-- | src/sdlang/exception.d | 168 | ||||
| -rw-r--r-- | src/sdlang/lexer.d | 58 | ||||
| -rw-r--r-- | src/sdlang/libinputvisitor/libInputVisitor.d | 26 | ||||
| -rw-r--r-- | src/sdlang/package.d | 13 | ||||
| -rw-r--r-- | src/sdlang/parser.d | 307 | ||||
| -rw-r--r-- | src/sdlang/symbol.d | 2 | ||||
| -rw-r--r-- | src/sdlang/taggedalgebraic/taggedalgebraic.d | 1085 | ||||
| -rw-r--r-- | src/sdlang/token.d | 135 | ||||
| -rw-r--r-- | src/sdlang/util.d | 126 | 
10 files changed, 2899 insertions, 296 deletions
| 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__, ": <Root> ::= <Tags> 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);  	}  	/// <Tags> ::= <Tag> <Tags>  (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 +]; | 
