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