diff options
Diffstat (limited to 'src/sdlang/ast.d')
| -rw-r--r-- | src/sdlang/ast.d | 1834 | 
1 files changed, 1834 insertions, 0 deletions
| diff --git a/src/sdlang/ast.d b/src/sdlang/ast.d new file mode 100644 index 0000000..7ad1c30 --- /dev/null +++ b/src/sdlang/ast.d @@ -0,0 +1,1834 @@ +// SDLang-D +// Written in the D programming language. + +module sdlang.ast; + +import std.algorithm; +import std.array; +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; + +class Attribute +{ +	Value    value; +	Location location; +	 +	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 +	/// the parent tag's constructor. +	@property Tag parent() +	{ +		return _parent; +	} + +	private string _namespace; +	@property string namespace() +	{ +		return _namespace; +	} +	/// Not particularly efficient, but it works. +	@property void namespace(string value) +	{ +		if(_parent && _namespace != value) +		{ +			// Remove +			auto saveParent = _parent; +			if(_parent) +				this.remove(); + +			// Change namespace +			_namespace = value; + +			// Re-add +			if(saveParent) +				saveParent.add(this); +		} +		else +			_namespace = value; +	} +	 +	private string _name; +	/// Not including namespace. Use 'fullName' if you want the namespace included. +	@property string name() +	{ +		return _name; +	} +	/// Not the most efficient, but it works. +	@property void name(string value) +	{ +		if(_parent && _name != value) +		{ +			_parent.updateId++; +			 +			void removeFromGroupedLookup(string ns) +			{ +				// Remove from _parent._attributes[ns] +				auto sameNameAttrs = _parent._attributes[ns][_name]; +				auto targetIndex = sameNameAttrs.countUntil(this); +				_parent._attributes[ns][_name].removeIndex(targetIndex); +			} +			 +			// Remove from _parent._tags +			removeFromGroupedLookup(_namespace); +			removeFromGroupedLookup("*"); + +			// Change name +			_name = value; +			 +			// Add to new locations in _parent._attributes +			_parent._attributes[_namespace][_name] ~= this; +			_parent._attributes["*"][_name] ~= this; +		} +		else +			_name = value; +	} + +	@property string fullName() +	{ +		return _namespace==""? _name : text(_namespace, ":", _name); +	} + +	this(string namespace, string name, Value value, Location location = Location(0, 0, 0)) +	{ +		this._namespace = namespace; +		this._name      = name; +		this.location   = location; +		this.value      = value; +	} +	 +	this(string name, Value value, Location location = Location(0, 0, 0)) +	{ +		this._namespace = ""; +		this._name      = name; +		this.location   = location; +		this.value      = value; +	} +	 +	/// Removes 'this' from its parent, if any. Returns 'this' for chaining. +	/// Inefficient ATM, but it works. +	Attribute remove() +	{ +		if(!_parent) +			return this; +		 +		void removeFromGroupedLookup(string ns) +		{ +			// Remove from _parent._attributes[ns] +			auto sameNameAttrs = _parent._attributes[ns][_name]; +			auto targetIndex = sameNameAttrs.countUntil(this); +			_parent._attributes[ns][_name].removeIndex(targetIndex); +		} +		 +		// Remove from _parent._attributes +		removeFromGroupedLookup(_namespace); +		removeFromGroupedLookup("*"); + +		// Remove from _parent.allAttributes +		auto allAttrsIndex = _parent.allAttributes.countUntil(this); +		_parent.allAttributes.removeIndex(allAttrsIndex); + +		// Remove from _parent.attributeIndicies +		auto sameNamespaceAttrs = _parent.attributeIndicies[_namespace]; +		auto attrIndiciesIndex = sameNamespaceAttrs.countUntil(allAttrsIndex); +		_parent.attributeIndicies[_namespace].removeIndex(attrIndiciesIndex); +		 +		// Fixup other indicies +		foreach(ns, ref nsAttrIndicies; _parent.attributeIndicies) +		foreach(k, ref v; nsAttrIndicies) +		if(v > allAttrsIndex) +			v--; +		 +		_parent.removeNamespaceIfEmpty(_namespace); +		_parent.updateId++; +		_parent = null; +		return this; +	} + +	override bool opEquals(Object o) +	{ +		auto a = cast(Attribute)o; +		if(!a) +			return false; + +		return +			_namespace == a._namespace && +			_name      == a._name      && +			value      == a.value; +	} +	 +	string toSDLString()() +	{ +		Appender!string sink; +		this.toSDLString(sink); +		return sink.data; +	} + +	void toSDLString(Sink)(ref Sink sink) if(isOutputRange!(Sink,char)) +	{ +		if(_namespace != "") +		{ +			sink.put(_namespace); +			sink.put(':'); +		} + +		sink.put(_name); +		sink.put('='); +		value.toSDLString(sink); +	} +} + +class Tag +{ +	Location location; +	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 +	/// the parent tag's constructor. +	@property Tag parent() +	{ +		return _parent; +	} + +	private string _namespace; +	@property string namespace() +	{ +		return _namespace; +	} +	/// Not particularly efficient, but it works. +	@property void namespace(string value) +	{ +		if(_parent && _namespace != value) +		{ +			// Remove +			auto saveParent = _parent; +			if(_parent) +				this.remove(); + +			// Change namespace +			_namespace = value; + +			// Re-add +			if(saveParent) +				saveParent.add(this); +		} +		else +			_namespace = value; +	} +	 +	private string _name; +	/// Not including namespace. Use 'fullName' if you want the namespace included. +	@property string name() +	{ +		return _name; +	} +	/// Not the most efficient, but it works. +	@property void name(string value) +	{ +		if(_parent && _name != value) +		{ +			_parent.updateId++; +			 +			void removeFromGroupedLookup(string ns) +			{ +				// Remove from _parent._tags[ns] +				auto sameNameTags = _parent._tags[ns][_name]; +				auto targetIndex = sameNameTags.countUntil(this); +				_parent._tags[ns][_name].removeIndex(targetIndex); +			} +			 +			// Remove from _parent._tags +			removeFromGroupedLookup(_namespace); +			removeFromGroupedLookup("*"); +			 +			// Change name +			_name = value; +			 +			// Add to new locations in _parent._tags +			_parent._tags[_namespace][_name] ~= this; +			_parent._tags["*"][_name] ~= this; +		} +		else +			_name = value; +	} +	 +	/// This tag's name, including namespace if one exists. +	@property string fullName() +	{ +		return _namespace==""? _name : text(_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. +	private size_t updateId=0; +	 +	this(Tag parent = null) +	{ +		if(parent) +			parent.add(this); +	} + +	this( +		string namespace, string name, +		Value[] values=null, Attribute[] attributes=null, Tag[] children=null +	) +	{ +		this(null, namespace, name, values, attributes, children); +	} + +	this( +		Tag parent, string namespace, string name, +		Value[] values=null, Attribute[] attributes=null, Tag[] children=null +	) +	{ +		this._namespace = namespace; +		this._name      = name; + +		if(parent) +			parent.add(this); +		 +		this.values = values; +		this.add(attributes); +		this.add(children); +	} + +	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. + +	private size_t[][string] attributeIndicies; // allAttributes[ attributes[namespace][i] ] +	private size_t[][string] tagIndicies;       // allTags[ tags[namespace][i] ] + +	private Attribute[][string][string] _attributes; // attributes[namespace or "*"][name][i] +	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 +	/// that already has a parent. +	Tag add(Value val) +	{ +		values ~= val; +		updateId++; +		return this; +	} +	 +	///ditto +	Tag add(Value[] vals) +	{ +		foreach(val; vals) +			add(val); + +		return this; +	} +	 +	///ditto +	Tag add(Attribute attr) +	{ +		if(attr._parent) +		{ +			throw new SDLangValidationException( +				"Attribute is already attached to a parent tag. "~ +				"Use Attribute.remove() before adding it to another tag." +			); +		} +		 +		if(!allNamespaces.canFind(attr._namespace)) +			allNamespaces ~= attr._namespace; + +		attr._parent = this; +		 +		allAttributes ~= attr; +		attributeIndicies[attr._namespace] ~= allAttributes.length-1; +		_attributes[attr._namespace][attr._name] ~= attr; +		_attributes["*"]            [attr._name] ~= attr; + +		updateId++; +		return this; +	} +	 +	///ditto +	Tag add(Attribute[] attrs) +	{ +		foreach(attr; attrs) +			add(attr); + +		return this; +	} +	 +	///ditto +	Tag add(Tag tag) +	{ +		if(tag._parent) +		{ +			throw new SDLangValidationException( +				"Tag is already attached to a parent tag. "~ +				"Use Tag.remove() before adding it to another tag." +			); +		} + +		if(!allNamespaces.canFind(tag._namespace)) +			allNamespaces ~= tag._namespace; +		 +		tag._parent = this; + +		allTags ~= tag; +		tagIndicies[tag._namespace] ~= allTags.length-1; +		_tags[tag._namespace][tag._name] ~= tag; +		_tags["*"]           [tag._name] ~= tag; +		 +		updateId++; +		return this; +	} +	 +	///ditto +	Tag add(Tag[] tags) +	{ +		foreach(tag; tags) +			add(tag); + +		return this; +	} +	 +	/// Removes 'this' from its parent, if any. Returns 'this' for chaining. +	/// Inefficient ATM, but it works. +	Tag remove() +	{ +		if(!_parent) +			return this; +		 +		void removeFromGroupedLookup(string ns) +		{ +			// Remove from _parent._tags[ns] +			auto sameNameTags = _parent._tags[ns][_name]; +			auto targetIndex = sameNameTags.countUntil(this); +			_parent._tags[ns][_name].removeIndex(targetIndex); +		} +		 +		// Remove from _parent._tags +		removeFromGroupedLookup(_namespace); +		removeFromGroupedLookup("*"); + +		// Remove from _parent.allTags +		auto allTagsIndex = _parent.allTags.countUntil(this); +		_parent.allTags.removeIndex(allTagsIndex); + +		// Remove from _parent.tagIndicies +		auto sameNamespaceTags = _parent.tagIndicies[_namespace]; +		auto tagIndiciesIndex = sameNamespaceTags.countUntil(allTagsIndex); +		_parent.tagIndicies[_namespace].removeIndex(tagIndiciesIndex); +		 +		// Fixup other indicies +		foreach(ns, ref nsTagIndicies; _parent.tagIndicies) +		foreach(k, ref v; nsTagIndicies) +		if(v > allTagsIndex) +			v--; +		 +		_parent.removeNamespaceIfEmpty(_namespace); +		_parent.updateId++; +		_parent = null; +		return this; +	} +	 +	private void removeNamespaceIfEmpty(string namespace) +	{ +		// If namespace has no attributes, remove it from attributeIndicies/_attributes +		if(namespace in attributeIndicies && attributeIndicies[namespace].length == 0) +		{ +			attributeIndicies.remove(namespace); +			_attributes.remove(namespace); +		} + +		// If namespace has no tags, remove it from tagIndicies/_tags +		if(namespace in tagIndicies && tagIndicies[namespace].length == 0) +		{ +			tagIndicies.remove(namespace); +			_tags.remove(namespace); +		} +		 +		// If namespace is now empty, remove it from allNamespaces +		if( +			namespace !in tagIndicies && +			namespace !in attributeIndicies +		) +		{ +			auto allNamespacesIndex = allNamespaces.length - allNamespaces.find(namespace).length; +			allNamespaces = allNamespaces[0..allNamespacesIndex] ~ allNamespaces[allNamespacesIndex+1..$]; +		} +	} +	 +	struct NamedMemberRange(T, string membersGrouped) +	{ +		private Tag tag; +		private string namespace; // "*" indicates "all namespaces" (ok since it's not a valid namespace name) +		private string name; +		private size_t updateId;  // Tag's updateId when this range was created. + +		this(Tag tag, string namespace, string name, size_t updateId) +		{ +			this.tag       = tag; +			this.namespace = namespace; +			this.name      = name; +			this.updateId  = updateId; +			frontIndex = 0; + +			if( +				namespace in mixin("tag."~membersGrouped) && +				name in mixin("tag."~membersGrouped~"[namespace]") +			) +				endIndex = mixin("tag."~membersGrouped~"[namespace][name].length"); +			else +				endIndex = 0; +		} +		 +		invariant() +		{ +			assert( +				this.updateId == tag.updateId, +				"This range has been invalidated by a change to the tag." +			); +		} + +		@property bool empty() +		{ +			return frontIndex == endIndex; +		} +		 +		private size_t frontIndex; +		@property T front() +		{ +			return this[0]; +		} +		void popFront() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			frontIndex++; +		} + +		private size_t endIndex; // One past the last element +		@property T back() +		{ +			return this[$-1]; +		} +		void popBack() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			endIndex--; +		} +		 +		alias length opDollar; +		@property size_t length() +		{ +			return endIndex - frontIndex; +		} +		 +		@property typeof(this) save() +		{ +			auto r = typeof(this)(this.tag, this.namespace, this.name, this.updateId); +			r.frontIndex = this.frontIndex; +			r.endIndex   = this.endIndex; +			return r; +		} +		 +		typeof(this) opSlice() +		{ +			return save(); +		} +		 +		typeof(this) opSlice(size_t start, size_t end) +		{ +			auto r = save(); +			r.frontIndex = this.frontIndex + start; +			r.endIndex   = this.frontIndex + end; +			 +			if( +				r.frontIndex > this.endIndex || +				r.endIndex > this.endIndex || +				r.frontIndex > r.endIndex +			) +				throw new SDLangRangeException("Slice out of range"); +			 +			return r; +		} + +		T opIndex(size_t index) +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			return mixin("tag."~membersGrouped~"[namespace][name][frontIndex+index]"); +		} +	} + +	struct MemberRange(T, string allMembers, string memberIndicies, string membersGrouped) +	{ +		private Tag tag; +		private string namespace; // "*" indicates "all namespaces" (ok since it's not a valid namespace name) +		private bool isMaybe; +		private size_t updateId;  // Tag's updateId when this range was created. +		private size_t initialEndIndex; + +		this(Tag tag, string namespace, bool isMaybe) +		{ +			this.tag       = tag; +			this.namespace = namespace; +			this.updateId  = tag.updateId; +			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"); +			else +				initialEndIndex = 0; +			 +			endIndex = initialEndIndex; +		} +		 +		invariant() +		{ +			assert( +				this.updateId == tag.updateId, +				"This range has been invalidated by a change to the tag." +			); +		} + +		@property bool empty() +		{ +			return frontIndex == endIndex; +		} +		 +		private size_t frontIndex; +		@property T front() +		{ +			return this[0]; +		} +		void popFront() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			frontIndex++; +		} + +		private size_t endIndex; // One past the last element +		@property T back() +		{ +			return this[$-1]; +		} +		void popBack() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			endIndex--; +		} +		 +		alias length opDollar; +		@property size_t length() +		{ +			return endIndex - frontIndex; +		} +		 +		@property typeof(this) save() +		{ +			auto r = typeof(this)(this.tag, this.namespace, this.isMaybe); +			r.frontIndex      = this.frontIndex; +			r.endIndex        = this.endIndex; +			r.initialEndIndex = this.initialEndIndex; +			r.updateId        = this.updateId; +			return r; +		} +		 +		typeof(this) opSlice() +		{ +			return save(); +		} +		 +		typeof(this) opSlice(size_t start, size_t end) +		{ +			auto r = save(); +			r.frontIndex = this.frontIndex + start; +			r.endIndex   = this.frontIndex + end; +			 +			if( +				r.frontIndex > this.endIndex || +				r.endIndex > this.endIndex || +				r.frontIndex > r.endIndex +			) +				throw new SDLangRangeException("Slice out of range"); +			 +			return r; +		} +		 +		T opIndex(size_t index) +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			if(namespace == "*") +				return mixin("tag."~allMembers~"[ frontIndex+index ]"); +			else +				return mixin("tag."~allMembers~"[ tag."~memberIndicies~"[namespace][frontIndex+index] ]"); +		} +		 +		alias NamedMemberRange!(T,membersGrouped) ThisNamedMemberRange; +		ThisNamedMemberRange opIndex(string name) +		{ +			if(frontIndex != 0 || endIndex != initialEndIndex) +			{ +				throw new SDLangRangeException( +					"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 "~ +					"range and that you aren't using a slice of the range." +				); +			} +			 +			if(!isMaybe && empty) +				throw new SDLangRangeException("Range is empty"); +			 +			if(!isMaybe && name !in this) +				throw new SDLangRangeException(`No such `~T.stringof~` named: "`~name~`"`); + +			return ThisNamedMemberRange(tag, namespace, name, updateId); +		} + +		bool opBinaryRight(string op)(string name) if(op=="in") +		{ +			if(frontIndex != 0 || endIndex != initialEndIndex) +			{ +				throw new SDLangRangeException( +					"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 "~ +					"range and that you aren't using a slice of the range." +				); +			} +			 +			return +				namespace in mixin("tag."~membersGrouped) && +				name in mixin("tag."~membersGrouped~"[namespace]") &&  +				mixin("tag."~membersGrouped~"[namespace][name].length") > 0; +		} +	} + +	struct NamespaceRange +	{ +		private Tag tag; +		private bool isMaybe; +		private size_t updateId;  // Tag's updateId when this range was created. + +		this(Tag tag, bool isMaybe) +		{ +			this.tag      = tag; +			this.isMaybe  = isMaybe; +			this.updateId = tag.updateId; +			frontIndex = 0; +			endIndex = tag.allNamespaces.length; +		} + +		invariant() +		{ +			assert( +				this.updateId == tag.updateId, +				"This range has been invalidated by a change to the tag." +			); +		} +		 +		@property bool empty() +		{ +			return frontIndex == endIndex; +		} +		 +		private size_t frontIndex; +		@property NamespaceAccess front() +		{ +			return this[0]; +		} +		void popFront() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); +			 +			frontIndex++; +		} + +		private size_t endIndex; // One past the last element +		@property NamespaceAccess back() +		{ +			return this[$-1]; +		} +		void popBack() +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); +			 +			endIndex--; +		} +		 +		alias length opDollar; +		@property size_t length() +		{ +			return endIndex - frontIndex; +		} +		 +		@property NamespaceRange save() +		{ +			auto r = NamespaceRange(this.tag, this.isMaybe); +			r.frontIndex = this.frontIndex; +			r.endIndex   = this.endIndex; +			r.updateId   = this.updateId; +			return r; +		} +		 +		typeof(this) opSlice() +		{ +			return save(); +		} +		 +		typeof(this) opSlice(size_t start, size_t end) +		{ +			auto r = save(); +			r.frontIndex = this.frontIndex + start; +			r.endIndex   = this.frontIndex + end; +			 +			if( +				r.frontIndex > this.endIndex || +				r.endIndex > this.endIndex || +				r.frontIndex > r.endIndex +			) +				throw new SDLangRangeException("Slice out of range"); +			 +			return r; +		} +		 +		NamespaceAccess opIndex(size_t index) +		{ +			if(empty) +				throw new SDLangRangeException("Range is empty"); + +			auto namespace = tag.allNamespaces[frontIndex+index]; +			return NamespaceAccess( +				namespace, +				AttributeRange(tag, namespace, isMaybe), +				TagRange(tag, namespace, isMaybe) +			); +		} +		 +		NamespaceAccess opIndex(string namespace) +		{ +			if(!isMaybe && empty) +				throw new SDLangRangeException("Range is empty"); +			 +			if(!isMaybe && namespace !in this) +				throw new SDLangRangeException(`No such namespace: "`~namespace~`"`); +			 +			return NamespaceAccess( +				namespace, +				AttributeRange(tag, namespace, isMaybe), +				TagRange(tag, namespace, isMaybe) +			); +		} +		 +		/// Inefficient when range is a slice or has used popFront/popBack, but it works. +		bool opBinaryRight(string op)(string namespace) if(op=="in") +		{ +			if(frontIndex == 0 && endIndex == tag.allNamespaces.length) +			{ +				return +					namespace in tag.attributeIndicies || +					namespace in tag.tagIndicies; +			} +			else +				// Slower fallback method +				return tag.allNamespaces[frontIndex..endIndex].canFind(namespace); +		} +	} + +	struct NamespaceAccess +	{ +		string name; +		AttributeRange attributes; +		TagRange tags; +	} + +	alias MemberRange!(Attribute, "allAttributes", "attributeIndicies", "_attributes") AttributeRange; +	alias MemberRange!(Tag,       "allTags",       "tagIndicies",       "_tags"      ) TagRange; +	static assert(isRandomAccessRange!AttributeRange); +	static assert(isRandomAccessRange!TagRange); +	static assert(isRandomAccessRange!NamespaceRange); + +	/// Access all attributes that don't have a namespace +	@property AttributeRange attributes() +	{ +		return AttributeRange(this, "", false); +	} + +	/// Access all direct-child tags that don't have a namespace +	@property TagRange tags() +	{ +		return TagRange(this, "", false); +	} +	 +	/// Access all namespaces in this tag, and the attributes/tags within them. +	@property NamespaceRange namespaces() +	{ +		return NamespaceRange(this, false); +	} + +	/// Access all attributes and tags regardless of namespace. +	@property NamespaceAccess all() +	{ +		// "*" isn't a valid namespace name, so we can use it to indicate "all namespaces" +		return NamespaceAccess( +			"*", +			AttributeRange(this, "*", false), +			TagRange(this, "*", false) +		); +	} + +	struct MaybeAccess +	{ +		Tag tag; + +		/// Access all attributes that don't have a namespace +		@property AttributeRange attributes() +		{ +			return AttributeRange(tag, "", true); +		} + +		/// Access all direct-child tags that don't have a namespace +		@property TagRange tags() +		{ +			return TagRange(tag, "", true); +		} +		 +		/// Access all namespaces in this tag, and the attributes/tags within them. +		@property NamespaceRange namespaces() +		{ +			return NamespaceRange(tag, true); +		} + +		/// Access all attributes and tags regardless of namespace. +		@property NamespaceAccess all() +		{ +			// "*" isn't a valid namespace name, so we can use it to indicate "all namespaces" +			return NamespaceAccess( +				"*", +				AttributeRange(tag, "*", true), +				TagRange(tag, "*", true) +			); +		} +	} +	 +	/// 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. +	@property MaybeAccess maybe() +	{ +		return MaybeAccess(this); +	} +	 +	override bool opEquals(Object o) +	{ +		auto t = cast(Tag)o; +		if(!t) +			return false; +		 +		if(_namespace != t._namespace || _name != t._name) +			return false; + +		if( +			values        .length != t.values       .length || +			allAttributes .length != t.allAttributes.length || +			allNamespaces .length != t.allNamespaces.length || +			allTags       .length != t.allTags      .length +		) +			return false; +		 +		if(values != t.values) +			return false; + +		if(allNamespaces != t.allNamespaces) +			return false; + +		if(allAttributes != t.allAttributes) +			return false; +		 +		// Ok because cycles are not allowed +		//TODO: Actually check for or prevent cycles. +		return allTags == t.allTags; +	} +	 +	/// 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. +	string toSDLDocument()(string indent="\t", int indentLevel=0) +	{ +		Appender!string sink; +		toSDLDocument(sink, indent, indentLevel); +		return sink.data; +	} +	 +	///ditto +	void toSDLDocument(Sink)(ref Sink sink, string indent="\t", int indentLevel=0) +		if(isOutputRange!(Sink,char)) +	{ +		if(values.length > 0) +			throw new SDLangValidationException("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."); + +		if(_namespace != "") +			throw new SDLangValidationException("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 +	/// a root tag. If you intend this to be the root of a standard SDL +	/// document, use 'toSDLDocument' instead. +	string toSDLString()(string indent="\t", int indentLevel=0) +	{ +		Appender!string sink; +		toSDLString(sink, indent, indentLevel); +		return sink.data; +	} +	 +	///ditto +	void toSDLString(Sink)(ref Sink sink, string indent="\t", int indentLevel=0) +		if(isOutputRange!(Sink,char)) +	{ +		if(_name == "" && values.length == 0) +			throw new SDLangValidationException("Anonymous tags must have at least one value."); +		 +		if(_name == "" && _namespace != "") +			throw new SDLangValidationException("Anonymous tags cannot have a namespace."); +	 +		// Indent +		foreach(i; 0..indentLevel) +			sink.put(indent); +		 +		// Name +		if(_namespace != "") +		{ +			sink.put(_namespace); +			sink.put(':'); +		} +		sink.put(_name); +		 +		// Values +		foreach(i, v; values) +		{ +			// Omit the first space for anonymous tags +			if(_name != "" || i > 0) +				sink.put(' '); +			 +			v.toSDLString(sink); +		} +		 +		// Attributes +		foreach(attr; allAttributes) +		{ +			sink.put(' '); +			attr.toSDLString(sink); +		} +		 +		// Child tags +		bool foundChild=false; +		foreach(tag; allTags) +		{ +			if(!foundChild) +			{ +				sink.put(" {\n"); +				foundChild = true; +			} + +			tag.toSDLString(sink, indent, indentLevel+1); +		} +		if(foundChild) +		{ +			foreach(i; 0..indentLevel) +				sink.put(indent); + +			sink.put("}\n"); +		} +		else +			sink.put("\n"); +	} + +	/// Not the most efficient, but it works. +	string toDebugString() +	{ +		import std.algorithm : sort; + +		Appender!string buf; +		 +		buf.put("\n"); +		buf.put("Tag "); +		if(_namespace != "") +		{ +			buf.put("["); +			buf.put(_namespace); +			buf.put("]"); +		} +		buf.put("'%s':\n".format(_name)); + +		// Values +		foreach(val; values) +			buf.put("    (%s): %s\n".format(.toString(val.type), val)); + +		// Attributes +		foreach(attrNamespace; _attributes.keys.sort()) +		if(attrNamespace != "*") +		foreach(attrName; _attributes[attrNamespace].keys.sort()) +		foreach(attr; _attributes[attrNamespace][attrName]) +		{ +			string namespaceStr; +			if(attr._namespace != "") +				namespaceStr = "["~attr._namespace~"]"; +			 +			buf.put( +				"    %s%s(%s): %s\n".format( +					namespaceStr, attr._name, .toString(attr.value.type), attr.value +				) +			); +		} +		 +		// Children +		foreach(tagNamespace; _tags.keys.sort()) +		if(tagNamespace != "*") +		foreach(tagName; _tags[tagNamespace].keys.sort()) +		foreach(tag; _tags[tagNamespace][tagName]) +			buf.put( tag.toDebugString().replace("\n", "\n    ") ); +		 +		return buf.data; +	} +} + +version(sdlangUnittest) +{ +	private void testRandomAccessRange(R, E)(R range, E[] expected, bool function(E, E) equals=null) +	{ +		static assert(isRandomAccessRange!R); +		static assert(is(ElementType!R == E)); +		static assert(hasLength!R); +		static assert(!isInfinite!R); + +		assert(range.length == expected.length); +		if(range.length == 0) +		{ +			assert(range.empty); +			return; +		} +		 +		static bool defaultEquals(E e1, E e2) +		{ +			return e1 == e2; +		} +		if(equals is null) +			equals = &defaultEquals; +		 +		assert(equals(range.front, expected[0])); +		assert(equals(range.front, expected[0]));  // Ensure consistent result from '.front' +		assert(equals(range.front, expected[0]));  // Ensure consistent result from '.front' + +		assert(equals(range.back, expected[$-1])); +		assert(equals(range.back, expected[$-1]));  // Ensure consistent result from '.back' +		assert(equals(range.back, expected[$-1]));  // Ensure consistent result from '.back' +		 +		// Forward iteration +		auto original = range.save; +		auto r2 = range.save; +		foreach(i; 0..expected.length) +		{ +			//trace("Forward iteration: ", i); +			 +			// Test length/empty +			assert(range.length == expected.length - i); +			assert(range.length == r2.length); +			assert(!range.empty); +			assert(!r2.empty); +			 +			// Test front +			assert(equals(range.front, expected[i])); +			assert(equals(range.front, r2.front)); + +			// Test back +			assert(equals(range.back, expected[$-1])); +			assert(equals(range.back, r2.back)); + +			// Test opIndex(0) +			assert(equals(range[0], expected[i])); +			assert(equals(range[0], r2[0])); + +			// Test opIndex($-1) +			assert(equals(range[$-1], expected[$-1])); +			assert(equals(range[$-1], r2[$-1])); + +			// Test popFront +			range.popFront(); +			assert(range.length == r2.length - 1); +			r2.popFront(); +			assert(range.length == r2.length); +		} +		assert(range.empty); +		assert(r2.empty); +		assert(original.length == expected.length); +		 +		// Backwards iteration +		range = original.save; +		r2    = original.save; +		foreach(i; iota(0, expected.length).retro()) +		{ +			//trace("Backwards iteration: ", i); + +			// Test length/empty +			assert(range.length == i+1); +			assert(range.length == r2.length); +			assert(!range.empty); +			assert(!r2.empty); +			 +			// Test front +			assert(equals(range.front, expected[0])); +			assert(equals(range.front, r2.front)); + +			// Test back +			assert(equals(range.back, expected[i])); +			assert(equals(range.back, r2.back)); + +			// Test opIndex(0) +			assert(equals(range[0], expected[0])); +			assert(equals(range[0], r2[0])); + +			// Test opIndex($-1) +			assert(equals(range[$-1], expected[i])); +			assert(equals(range[$-1], r2[$-1])); + +			// Test popBack +			range.popBack(); +			assert(range.length == r2.length - 1); +			r2.popBack(); +			assert(range.length == r2.length); +		} +		assert(range.empty); +		assert(r2.empty); +		assert(original.length == expected.length); +		 +		// Random access +		range = original.save; +		r2    = original.save; +		foreach(i; 0..expected.length) +		{ +			//trace("Random access: ", i); + +			// Test length/empty +			assert(range.length == expected.length); +			assert(range.length == r2.length); +			assert(!range.empty); +			assert(!r2.empty); +			 +			// Test front +			assert(equals(range.front, expected[0])); +			assert(equals(range.front, r2.front)); + +			// Test back +			assert(equals(range.back, expected[$-1])); +			assert(equals(range.back, r2.back)); + +			// Test opIndex(i) +			assert(equals(range[i], expected[i])); +			assert(equals(range[i], r2[i])); +		} +		assert(!range.empty); +		assert(!r2.empty); +		assert(original.length == expected.length); +	} +} + +version(sdlangUnittest) +unittest +{ +	import sdlang.parser; +	writeln("Unittesting sdlang ast..."); +	stdout.flush(); +	 +	Tag root; +	root = parseSource(""); +	testRandomAccessRange(root.attributes, cast(          Attribute[])[]); +	testRandomAccessRange(root.tags,       cast(                Tag[])[]); +	testRandomAccessRange(root.namespaces, cast(Tag.NamespaceAccess[])[]); +	 +	root = parseSource(` +		blue 3 "Lee" isThree=true +		blue 5 "Chan" 12345 isThree=false +		stuff:orange 1 2 3 2 1 +		stuff:square points=4 dimensions=2 points="Still four" +		stuff:triangle data:points=3 data:dimensions=2 +		nothing +		namespaces small:A=1 med:A=2 big:A=3 small:B=10 big:B=30 +		 +		people visitor:a=1 b=2 { +			chiyo "Small" "Flies?" nemesis="Car" score=100 +			yukari +			visitor:sana +			tomo +			visitor:hayama +		} +	`); + +	auto blue3 = new Tag( +		null, "", "blue", +		[ Value(3), Value("Lee") ], +		[ new Attribute("isThree", Value(true)) ], +		null +	); +	auto blue5 = new Tag( +		null, "", "blue", +		[ Value(5), Value("Chan"), Value(12345) ], +		[ new Attribute("isThree", Value(false)) ], +		null +	); +	auto orange = new Tag( +		null, "stuff", "orange", +		[ Value(1), Value(2), Value(3), Value(2), Value(1) ], +		null, +		null +	); +	auto square = new Tag( +		null, "stuff", "square", +		null, +		[ +			new Attribute("points", Value(4)), +			new Attribute("dimensions", Value(2)), +			new Attribute("points", Value("Still four")), +		], +		null +	); +	auto triangle = new Tag( +		null, "stuff", "triangle", +		null, +		[ +			new Attribute("data", "points", Value(3)), +			new Attribute("data", "dimensions", Value(2)), +		], +		null +	); +	auto nothing = new Tag( +		null, "", "nothing", +		null, null, null +	); +	auto namespaces = new Tag( +		null, "", "namespaces", +		null, +		[ +			new Attribute("small", "A", Value(1)), +			new Attribute("med",   "A", Value(2)), +			new Attribute("big",   "A", Value(3)), +			new Attribute("small", "B", Value(10)), +			new Attribute("big",   "B", Value(30)), +		], +		null +	); +	auto chiyo = new Tag( +		null, "", "chiyo", +		[ Value("Small"), Value("Flies?") ], +		[ +			new Attribute("nemesis", Value("Car")), +			new Attribute("score", Value(100)), +		], +		null +	); +	auto chiyo_ = new Tag( +		null, "", "chiyo_", +		[ Value("Small"), Value("Flies?") ], +		[ +			new Attribute("nemesis", Value("Car")), +			new Attribute("score", Value(100)), +		], +		null +	); +	auto yukari = new Tag( +		null, "", "yukari", +		null, null, null +	); +	auto sana = new Tag( +		null, "visitor", "sana", +		null, null, null +	); +	auto sana_ = new Tag( +		null, "visitor", "sana_", +		null, null, null +	); +	auto sanaVisitor_ = new Tag( +		null, "visitor_", "sana_", +		null, null, null +	); +	auto tomo = new Tag( +		null, "", "tomo", +		null, null, null +	); +	auto hayama = new Tag( +		null, "visitor", "hayama", +		null, null, null +	); +	auto people = new Tag( +		null, "", "people", +		null, +		[ +			new Attribute("visitor", "a", Value(1)), +			new Attribute("b", Value(2)), +		], +		[chiyo, yukari, sana, tomo, hayama] +	); +	 +	assert(blue3      .opEquals( blue3      )); +	assert(blue5      .opEquals( blue5      )); +	assert(orange     .opEquals( orange     )); +	assert(square     .opEquals( square     )); +	assert(triangle   .opEquals( triangle   )); +	assert(nothing    .opEquals( nothing    )); +	assert(namespaces .opEquals( namespaces )); +	assert(people     .opEquals( people     )); +	assert(chiyo      .opEquals( chiyo      )); +	assert(yukari     .opEquals( yukari     )); +	assert(sana       .opEquals( sana       )); +	assert(tomo       .opEquals( tomo       )); +	assert(hayama     .opEquals( hayama     )); +	 +	assert(!blue3.opEquals(orange)); +	assert(!blue3.opEquals(people)); +	assert(!blue3.opEquals(sana)); +	assert(!blue3.opEquals(blue5)); +	assert(!blue5.opEquals(blue3)); +	 +	alias Tag.NamespaceAccess NSA; +	static bool namespaceEquals(NSA n1, NSA n2) +	{ +		return n1.name == n2.name; +	} +	 +	testRandomAccessRange(root.attributes, cast(Attribute[])[]); +	testRandomAccessRange(root.tags,       [blue3, blue5, nothing, namespaces, people]); +	testRandomAccessRange(root.namespaces, [NSA(""), NSA("stuff")], &namespaceEquals); +	testRandomAccessRange(root.namespaces[0].tags, [blue3, blue5, nothing, namespaces, people]); +	testRandomAccessRange(root.namespaces[1].tags, [orange, square, triangle]); +	assert(""        in root.namespaces); +	assert("stuff"   in root.namespaces); +	assert("foobar" !in root.namespaces); +	testRandomAccessRange(root.namespaces[     ""].tags, [blue3, blue5, nothing, namespaces, people]); +	testRandomAccessRange(root.namespaces["stuff"].tags, [orange, square, triangle]); +	testRandomAccessRange(root.all.attributes, cast(Attribute[])[]); +	testRandomAccessRange(root.all.tags,       [blue3, blue5, orange, square, triangle, nothing, namespaces, people]); +	testRandomAccessRange(root.all.tags[],     [blue3, blue5, orange, square, triangle, nothing, namespaces, people]); +	testRandomAccessRange(root.all.tags[3..6], [square, triangle, nothing]); +	assert("blue"    in root.tags); +	assert("nothing" in root.tags); +	assert("people"  in root.tags); +	assert("orange" !in root.tags); +	assert("square" !in root.tags); +	assert("foobar" !in root.tags); +	assert("blue"    in root.all.tags); +	assert("nothing" in root.all.tags); +	assert("people"  in root.all.tags); +	assert("orange"  in root.all.tags); +	assert("square"  in root.all.tags); +	assert("foobar" !in root.all.tags); +	assert("orange"  in root.namespaces["stuff"].tags); +	assert("square"  in root.namespaces["stuff"].tags); +	assert("square"  in root.namespaces["stuff"].tags); +	assert("foobar" !in root.attributes); +	assert("foobar" !in root.all.attributes); +	assert("foobar" !in root.namespaces["stuff"].attributes); +	assert("blue"   !in root.attributes); +	assert("blue"   !in root.all.attributes); +	assert("blue"   !in root.namespaces["stuff"].attributes); +	testRandomAccessRange(root.tags["nothing"],                    [nothing]); +	testRandomAccessRange(root.tags["blue"],                       [blue3, blue5]); +	testRandomAccessRange(root.namespaces["stuff"].tags["orange"], [orange]); +	testRandomAccessRange(root.all.tags["nothing"],                [nothing]); +	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"]); +	 +	// 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"]); +	bool didCatch = false; +	try +		auto x = root.namespaces["foobar"].tags["foobar"]; +	catch(SDLangRangeException e) +		didCatch = true; +	assert(didCatch); +	 +	didCatch = false; +	try +		auto x = root.namespaces["foobar"].attributes["foobar"]; +	catch(SDLangRangeException e) +		didCatch = true; +	assert(didCatch); + +	testRandomAccessRange(root.maybe.tags["nothing"],                    [nothing]); +	testRandomAccessRange(root.maybe.tags["blue"],                       [blue3, blue5]); +	testRandomAccessRange(root.maybe.namespaces["stuff"].tags["orange"], [orange]); +	testRandomAccessRange(root.maybe.all.tags["nothing"],                [nothing]); +	testRandomAccessRange(root.maybe.all.tags["blue"],                   [blue3, blue5]); +	testRandomAccessRange(root.maybe.all.tags["blue"][],                 [blue3, blue5]); +	testRandomAccessRange(root.maybe.all.tags["blue"][0..1],             [blue3]); +	testRandomAccessRange(root.maybe.all.tags["blue"][1..2],             [blue5]); +	testRandomAccessRange(root.maybe.all.tags["orange"],                 [orange]); +	testRandomAccessRange(root.maybe.tags["foobar"],                      cast(Tag[])[]); +	testRandomAccessRange(root.maybe.all.tags["foobar"],                  cast(Tag[])[]); +	testRandomAccessRange(root.maybe.namespaces["foobar"].tags["foobar"], cast(Tag[])[]); +	testRandomAccessRange(root.maybe.attributes["foobar"],                      cast(Attribute[])[]); +	testRandomAccessRange(root.maybe.all.attributes["foobar"],                  cast(Attribute[])[]); +	testRandomAccessRange(root.maybe.namespaces["foobar"].attributes["foobar"], cast(Attribute[])[]); + +	testRandomAccessRange(blue3.attributes,     [ new Attribute("isThree", Value(true)) ]); +	testRandomAccessRange(blue3.tags,           cast(Tag[])[]); +	testRandomAccessRange(blue3.namespaces,     [NSA("")], &namespaceEquals); +	testRandomAccessRange(blue3.all.attributes, [ new Attribute("isThree", Value(true)) ]); +	testRandomAccessRange(blue3.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(blue5.attributes,     [ new Attribute("isThree", Value(false)) ]); +	testRandomAccessRange(blue5.tags,           cast(Tag[])[]); +	testRandomAccessRange(blue5.namespaces,     [NSA("")], &namespaceEquals); +	testRandomAccessRange(blue5.all.attributes, [ new Attribute("isThree", Value(false)) ]); +	testRandomAccessRange(blue5.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(orange.attributes,     cast(Attribute[])[]); +	testRandomAccessRange(orange.tags,           cast(Tag[])[]); +	testRandomAccessRange(orange.namespaces,     cast(NSA[])[], &namespaceEquals); +	testRandomAccessRange(orange.all.attributes, cast(Attribute[])[]); +	testRandomAccessRange(orange.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(square.attributes, [ +		new Attribute("points", Value(4)), +		new Attribute("dimensions", Value(2)), +		new Attribute("points", Value("Still four")), +	]); +	testRandomAccessRange(square.tags,       cast(Tag[])[]); +	testRandomAccessRange(square.namespaces, [NSA("")], &namespaceEquals); +	testRandomAccessRange(square.all.attributes, [ +		new Attribute("points", Value(4)), +		new Attribute("dimensions", Value(2)), +		new Attribute("points", Value("Still four")), +	]); +	testRandomAccessRange(square.all.tags, cast(Tag[])[]); +	 +	testRandomAccessRange(triangle.attributes, cast(Attribute[])[]); +	testRandomAccessRange(triangle.tags,       cast(Tag[])[]); +	testRandomAccessRange(triangle.namespaces, [NSA("data")], &namespaceEquals); +	testRandomAccessRange(triangle.namespaces[0].attributes, [ +		new Attribute("data", "points", Value(3)), +		new Attribute("data", "dimensions", Value(2)), +	]); +	assert("data"    in triangle.namespaces); +	assert("foobar" !in triangle.namespaces); +	testRandomAccessRange(triangle.namespaces["data"].attributes, [ +		new Attribute("data", "points", Value(3)), +		new Attribute("data", "dimensions", Value(2)), +	]); +	testRandomAccessRange(triangle.all.attributes, [ +		new Attribute("data", "points", Value(3)), +		new Attribute("data", "dimensions", Value(2)), +	]); +	testRandomAccessRange(triangle.all.tags, cast(Tag[])[]); +	 +	testRandomAccessRange(nothing.attributes,     cast(Attribute[])[]); +	testRandomAccessRange(nothing.tags,           cast(Tag[])[]); +	testRandomAccessRange(nothing.namespaces,     cast(NSA[])[], &namespaceEquals); +	testRandomAccessRange(nothing.all.attributes, cast(Attribute[])[]); +	testRandomAccessRange(nothing.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(namespaces.attributes,   cast(Attribute[])[]); +	testRandomAccessRange(namespaces.tags,         cast(Tag[])[]); +	testRandomAccessRange(namespaces.namespaces,   [NSA("small"), NSA("med"), NSA("big")], &namespaceEquals); +	testRandomAccessRange(namespaces.namespaces[], [NSA("small"), NSA("med"), NSA("big")], &namespaceEquals); +	testRandomAccessRange(namespaces.namespaces[1..2], [NSA("med")], &namespaceEquals); +	testRandomAccessRange(namespaces.namespaces[0].attributes, [ +		new Attribute("small", "A", Value(1)), +		new Attribute("small", "B", Value(10)), +	]); +	testRandomAccessRange(namespaces.namespaces[1].attributes, [ +		new Attribute("med", "A", Value(2)), +	]); +	testRandomAccessRange(namespaces.namespaces[2].attributes, [ +		new Attribute("big", "A", Value(3)), +		new Attribute("big", "B", Value(30)), +	]); +	testRandomAccessRange(namespaces.namespaces[1..2][0].attributes, [ +		new Attribute("med", "A", Value(2)), +	]); +	assert("small"   in namespaces.namespaces); +	assert("med"     in namespaces.namespaces); +	assert("big"     in namespaces.namespaces); +	assert("foobar" !in namespaces.namespaces); +	assert("small"  !in namespaces.namespaces[1..2]); +	assert("med"     in namespaces.namespaces[1..2]); +	assert("big"    !in namespaces.namespaces[1..2]); +	assert("foobar" !in namespaces.namespaces[1..2]); +	testRandomAccessRange(namespaces.namespaces["small"].attributes, [ +		new Attribute("small", "A", Value(1)), +		new Attribute("small", "B", Value(10)), +	]); +	testRandomAccessRange(namespaces.namespaces["med"].attributes, [ +		new Attribute("med", "A", Value(2)), +	]); +	testRandomAccessRange(namespaces.namespaces["big"].attributes, [ +		new Attribute("big", "A", Value(3)), +		new Attribute("big", "B", Value(30)), +	]); +	testRandomAccessRange(namespaces.all.attributes, [ +		new Attribute("small", "A", Value(1)), +		new Attribute("med",   "A", Value(2)), +		new Attribute("big",   "A", Value(3)), +		new Attribute("small", "B", Value(10)), +		new Attribute("big",   "B", Value(30)), +	]); +	testRandomAccessRange(namespaces.all.attributes[], [ +		new Attribute("small", "A", Value(1)), +		new Attribute("med",   "A", Value(2)), +		new Attribute("big",   "A", Value(3)), +		new Attribute("small", "B", Value(10)), +		new Attribute("big",   "B", Value(30)), +	]); +	testRandomAccessRange(namespaces.all.attributes[2..4], [ +		new Attribute("big",   "A", Value(3)), +		new Attribute("small", "B", Value(10)), +	]); +	testRandomAccessRange(namespaces.all.tags, cast(Tag[])[]); +	assert("A"      !in namespaces.attributes); +	assert("B"      !in namespaces.attributes); +	assert("foobar" !in namespaces.attributes); +	assert("A"       in namespaces.all.attributes); +	assert("B"       in namespaces.all.attributes); +	assert("foobar" !in namespaces.all.attributes); +	assert("A"       in namespaces.namespaces["small"].attributes); +	assert("B"       in namespaces.namespaces["small"].attributes); +	assert("foobar" !in namespaces.namespaces["small"].attributes); +	assert("A"       in namespaces.namespaces["med"].attributes); +	assert("B"      !in namespaces.namespaces["med"].attributes); +	assert("foobar" !in namespaces.namespaces["med"].attributes); +	assert("A"       in namespaces.namespaces["big"].attributes); +	assert("B"       in namespaces.namespaces["big"].attributes); +	assert("foobar" !in namespaces.namespaces["big"].attributes); +	assert("foobar" !in namespaces.tags); +	assert("foobar" !in namespaces.all.tags); +	assert("foobar" !in namespaces.namespaces["small"].tags); +	assert("A"      !in namespaces.tags); +	assert("A"      !in namespaces.all.tags); +	assert("A"      !in namespaces.namespaces["small"].tags); +	testRandomAccessRange(namespaces.namespaces["small"].attributes["A"], [ +		new Attribute("small", "A", Value(1)), +	]); +	testRandomAccessRange(namespaces.namespaces["med"].attributes["A"], [ +		new Attribute("med", "A", Value(2)), +	]); +	testRandomAccessRange(namespaces.namespaces["big"].attributes["A"], [ +		new Attribute("big", "A", Value(3)), +	]); +	testRandomAccessRange(namespaces.all.attributes["A"], [ +		new Attribute("small", "A", Value(1)), +		new Attribute("med",   "A", Value(2)), +		new Attribute("big",   "A", Value(3)), +	]); +	testRandomAccessRange(namespaces.all.attributes["B"], [ +		new Attribute("small", "B", Value(10)), +		new Attribute("big",   "B", Value(30)), +	]); + +	testRandomAccessRange(chiyo.attributes, [ +		new Attribute("nemesis", Value("Car")), +		new Attribute("score", Value(100)), +	]); +	testRandomAccessRange(chiyo.tags,       cast(Tag[])[]); +	testRandomAccessRange(chiyo.namespaces, [NSA("")], &namespaceEquals); +	testRandomAccessRange(chiyo.all.attributes, [ +		new Attribute("nemesis", Value("Car")), +		new Attribute("score", Value(100)), +	]); +	testRandomAccessRange(chiyo.all.tags, cast(Tag[])[]); +	 +	testRandomAccessRange(yukari.attributes,     cast(Attribute[])[]); +	testRandomAccessRange(yukari.tags,           cast(Tag[])[]); +	testRandomAccessRange(yukari.namespaces,     cast(NSA[])[], &namespaceEquals); +	testRandomAccessRange(yukari.all.attributes, cast(Attribute[])[]); +	testRandomAccessRange(yukari.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(sana.attributes,     cast(Attribute[])[]); +	testRandomAccessRange(sana.tags,           cast(Tag[])[]); +	testRandomAccessRange(sana.namespaces,     cast(NSA[])[], &namespaceEquals); +	testRandomAccessRange(sana.all.attributes, cast(Attribute[])[]); +	testRandomAccessRange(sana.all.tags,       cast(Tag[])[]); +	 +	testRandomAccessRange(people.attributes,         [new Attribute("b", Value(2))]); +	testRandomAccessRange(people.tags,               [chiyo, yukari, tomo]); +	testRandomAccessRange(people.namespaces,         [NSA("visitor"), NSA("")], &namespaceEquals); +	testRandomAccessRange(people.namespaces[0].attributes, [new Attribute("visitor", "a", Value(1))]); +	testRandomAccessRange(people.namespaces[1].attributes, [new Attribute("b", Value(2))]); +	testRandomAccessRange(people.namespaces[0].tags,       [sana, hayama]); +	testRandomAccessRange(people.namespaces[1].tags,       [chiyo, yukari, tomo]); +	assert("visitor" in people.namespaces); +	assert(""        in people.namespaces); +	assert("foobar" !in people.namespaces); +	testRandomAccessRange(people.namespaces["visitor"].attributes, [new Attribute("visitor", "a", Value(1))]); +	testRandomAccessRange(people.namespaces[       ""].attributes, [new Attribute("b", Value(2))]); +	testRandomAccessRange(people.namespaces["visitor"].tags,       [sana, hayama]); +	testRandomAccessRange(people.namespaces[       ""].tags,       [chiyo, yukari, tomo]); +	testRandomAccessRange(people.all.attributes, [ +		new Attribute("visitor", "a", Value(1)), +		new Attribute("b", Value(2)), +	]); +	testRandomAccessRange(people.all.tags, [chiyo, yukari, sana, tomo, hayama]); +	 +	people.attributes["b"][0].name = "b_"; +	people.namespaces["visitor"].attributes["a"][0].name = "a_"; +	people.tags["chiyo"][0].name = "chiyo_"; +	people.namespaces["visitor"].tags["sana"][0].name = "sana_"; + +	assert("b_"     in people.attributes); +	assert("a_"     in people.namespaces["visitor"].attributes); +	assert("chiyo_" in people.tags); +	assert("sana_"  in people.namespaces["visitor"].tags); + +	assert(people.attributes["b_"][0]                       == new Attribute("b_", Value(2))); +	assert(people.namespaces["visitor"].attributes["a_"][0] == new Attribute("visitor", "a_", Value(1))); +	assert(people.tags["chiyo_"][0]                         == chiyo_); +	assert(people.namespaces["visitor"].tags["sana_"][0]    == sana_); + +	assert("b"     !in people.attributes); +	assert("a"     !in people.namespaces["visitor"].attributes); +	assert("chiyo" !in people.tags); +	assert("sana"  !in people.namespaces["visitor"].tags); + +	assert(people.maybe.attributes["b"].length                       == 0); +	assert(people.maybe.namespaces["visitor"].attributes["a"].length == 0); +	assert(people.maybe.tags["chiyo"].length                         == 0); +	assert(people.maybe.namespaces["visitor"].tags["sana"].length    == 0); + +	people.tags["tomo"][0].remove(); +	people.namespaces["visitor"].tags["hayama"][0].remove(); +	people.tags["chiyo_"][0].remove(); +	testRandomAccessRange(people.tags,               [yukari]); +	testRandomAccessRange(people.namespaces,         [NSA("visitor"), NSA("")], &namespaceEquals); +	testRandomAccessRange(people.namespaces[0].tags, [sana_]); +	testRandomAccessRange(people.namespaces[1].tags, [yukari]); +	assert("visitor" in people.namespaces); +	assert(""        in people.namespaces); +	assert("foobar" !in people.namespaces); +	testRandomAccessRange(people.namespaces["visitor"].tags, [sana_]); +	testRandomAccessRange(people.namespaces[       ""].tags, [yukari]); +	testRandomAccessRange(people.all.tags, [yukari, sana_]); +	 +	people.attributes["b_"][0].namespace = "_"; +	people.namespaces["visitor"].attributes["a_"][0].namespace = "visitor_"; +	assert("_"         in people.namespaces); +	assert("visitor_"  in people.namespaces); +	assert(""          in people.namespaces); +	assert("visitor"   in people.namespaces); +	people.namespaces["visitor"].tags["sana_"][0].namespace = "visitor_"; +	assert("_"         in people.namespaces); +	assert("visitor_"  in people.namespaces); +	assert(""          in people.namespaces); +	assert("visitor"  !in people.namespaces); + +	assert(people.namespaces["_"       ].attributes["b_"][0] == new Attribute("_", "b_", Value(2))); +	assert(people.namespaces["visitor_"].attributes["a_"][0] == new Attribute("visitor_", "a_", Value(1))); +	assert(people.namespaces["visitor_"].tags["sana_"][0]    == sanaVisitor_); +	 +	people.tags["yukari"][0].remove(); +	people.namespaces["visitor_"].tags["sana_"][0].remove(); +	people.namespaces["visitor_"].attributes["a_"][0].namespace = "visitor"; +	people.namespaces["_"].attributes["b_"][0].namespace = ""; +	testRandomAccessRange(people.tags,               cast(Tag[])[]); +	testRandomAccessRange(people.namespaces,         [NSA("visitor"), NSA("")], &namespaceEquals); +	testRandomAccessRange(people.namespaces[0].tags, cast(Tag[])[]); +	testRandomAccessRange(people.namespaces[1].tags, cast(Tag[])[]); +	assert("visitor" in people.namespaces); +	assert(""        in people.namespaces); +	assert("foobar" !in people.namespaces); +	testRandomAccessRange(people.namespaces["visitor"].tags, cast(Tag[])[]); +	testRandomAccessRange(people.namespaces[       ""].tags, cast(Tag[])[]); +	testRandomAccessRange(people.all.tags, cast(Tag[])[]); +	 +	people.namespaces["visitor"].attributes["a_"][0].remove(); +	testRandomAccessRange(people.attributes,               [new Attribute("b_", Value(2))]); +	testRandomAccessRange(people.namespaces,               [NSA("")], &namespaceEquals); +	testRandomAccessRange(people.namespaces[0].attributes, [new Attribute("b_", Value(2))]); +	assert("visitor" !in people.namespaces); +	assert(""         in people.namespaces); +	assert("foobar"  !in people.namespaces); +	testRandomAccessRange(people.namespaces[""].attributes, [new Attribute("b_", Value(2))]); +	testRandomAccessRange(people.all.attributes, [ +		new Attribute("b_", Value(2)), +	]); +	 +	people.attributes["b_"][0].remove(); +	testRandomAccessRange(people.attributes, cast(Attribute[])[]); +	testRandomAccessRange(people.namespaces, cast(NSA[])[], &namespaceEquals); +	assert("visitor" !in people.namespaces); +	assert(""        !in people.namespaces); +	assert("foobar"  !in people.namespaces); +	testRandomAccessRange(people.all.attributes, cast(Attribute[])[]); +} + +// Regression test, issue #11: https://github.com/Abscissa/SDLang-D/issues/11 +version(sdlangUnittest) +unittest +{ +	import sdlang.parser; +	writeln("ast: Regression test issue #11..."); +	stdout.flush(); +	 +	auto root = parseSource( +`// +a`); + +	assert("a" in root.tags); + +	root = parseSource( +`// +parent { +	child +} +`); + +	auto child = new Tag( +		null, "", "child", +		null, null, null +	); +	 +	assert("parent" in root.tags); +	assert("child" !in root.tags); +	testRandomAccessRange(root.tags["parent"][0].tags, [child]); +	assert("child" in root.tags["parent"][0].tags); +} | 
