// 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); }