Skip to content

Commit

Permalink
Improve generic type parsing and referencing (#178)
Browse files Browse the repository at this point in the history
* Fix issue with error message.

* Improve generic type parsing, so that the same type can be registered with different Arity.
Properly parse constructor invocation of a generic type.

* Added test with named generics with arity.

* Fix CloseCurlyBracketExpected format.
  • Loading branch information
metoule authored Oct 30, 2021
1 parent 7e172a1 commit 859f107
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 50 deletions.
118 changes: 71 additions & 47 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,8 @@ private Parameter ParseLambdaParameter()
ValidateToken(TokenId.Identifier);
var name = _token.text;

if (_arguments.TryGetKnownType(name, out var type))
if (TryParseKnownType(name, out var type))
{
type = ParseFullType(type);

ValidateToken(TokenId.Identifier);
name = _token.text;
}
Expand Down Expand Up @@ -454,7 +452,7 @@ private Expression ParseTypeTesting()
NextToken();

Type knownType;
if (!_arguments.TryGetKnownType(_token.text, out knownType))
if (!TryParseKnownType(_token.text, out knownType))
throw CreateParseException(op.pos, ErrorMessages.TypeIdentifierExpected);

if (typeOperator == ParserConstants.KeywordIs)
Expand Down Expand Up @@ -963,7 +961,7 @@ private Expression ParseIdentifier()
return parameterExpression;
}

if (_arguments.TryGetKnownType(_token.text, out Type knownType))
if (TryParseKnownType(_token.text, out Type knownType))
{
return ParseTypeKeyword(knownType);
}
Expand Down Expand Up @@ -1016,13 +1014,7 @@ private Expression ParseDefaultOperator()
ValidateToken(TokenId.OpenParen, ErrorMessages.OpenParenExpected);
NextToken();
ValidateToken(TokenId.Identifier);

var name = _token.text;
if (_arguments.TryGetKnownType(name, out var type))
type = ParseFullType(type);
else
throw new UnknownIdentifierException(_token.text, _token.pos);

var type = ParseKnownType();
ValidateToken(TokenId.CloseParen, ErrorMessages.CloseParenOrCommaExpected);
NextToken();

Expand Down Expand Up @@ -1075,12 +1067,7 @@ private Expression ParseNew()
NextToken();
ValidateToken(TokenId.Identifier, ErrorMessages.IdentifierExpected);

Type newType;
if (!_arguments.TryGetKnownType(_token.text, out newType))
throw new UnknownIdentifierException(_token.text, _token.pos);

NextToken();

var newType = ParseKnownType();
var args = new Expression[0];
if (_token.id == TokenId.OpenParen)
args = ParseArgumentList();
Expand Down Expand Up @@ -1202,27 +1189,72 @@ private bool PrepareDelegateInvoke(Type type, ref Expression[] args)
return true;
}

// we found a known type identifier, check if it has some modifiers
private Type ParseFullType(Type type)
private Type ParseKnownType()
{
var name = _token.text;
var errorPos = _token.pos;
if (!TryParseKnownType(name, out var type))
throw new UnknownIdentifierException(name, errorPos);

return type;
}

private bool TryParseKnownType(string name, out Type type)
{
// if the type is unknown, we need to restart parsing
var originalPos = _token.pos;
_arguments.TryGetKnownType(name, out type);

NextToken();
if (_token.id == TokenId.LessThan)
{
var typeArguments = ParseTypeArgumentList();
var rank = typeArguments.Count;

// if no type was registered with the simple name, try the full generic name
if (type == null && !_arguments.TryGetKnownType(name + $"`{rank}", out type))
return false;

if (rank != type.GetGenericArguments().Length)
throw new ArgumentException($"The number of generic arguments provided doesn't equal the arity of the generic type definition.");

// there are actual type arguments: instantiate the proper generic type
if (typeArguments.All(_ => _ != null))
type = type.MakeGenericType(typeArguments.ToArray());

NextToken();
}

type = ParseTypeModifiers(type);
if (type == null)
{
// type name couldn't be parsed: restore position
SetTextPos(originalPos);
NextToken();

return false;
}

return true;
}

// we found a known type identifier, check if it has some modifiers
private Type ParseTypeModifiers(Type type)
{
var errorPos = _token.pos;
if (_token.id == TokenId.Question)
{
if (!type.IsValueType || IsNullableType(type))
throw CreateParseException(errorPos, ErrorMessages.TypeHasNoNullableForm, GetTypeName(type));
type = typeof(Nullable<>).MakeGenericType(type);
type = ParseFullType(type);

NextToken();
type = ParseTypeModifiers(type);
}
else if (_token.id == TokenId.OpenBracket)
{
type = ParseArrayRankSpecifier(type);
}
else if (_token.id == TokenId.LessThan)
{
type = ParseTypeArgumentList(type);
type = ParseFullType(type);
}

return type;
}
Expand Down Expand Up @@ -1258,42 +1290,39 @@ private Type ParseArrayRankSpecifier(Type type)
return type;
}

private Type ParseTypeArgumentList(Type type)
private List<Type> ParseTypeArgumentList()
{
ValidateToken(TokenId.LessThan);
NextToken();

List<Type> args;
if (_token.id == TokenId.Identifier)
type = ParseTypeArguments(type);
args = ParseTypeArguments();
else
type = ParseUnboundType(type);
{
var arity = ParseUnboundTypeArity();
args = new List<Type>(new Type[arity]);
}

ValidateToken(TokenId.GreaterThan, ErrorMessages.CloseTypeArgumentListExpected);
return type;
return args;
}

private Type ParseTypeArguments(Type type)
private List<Type> ParseTypeArguments()
{
var genericArguments = new List<Type>();
while (true)
{
ValidateToken(TokenId.Identifier);
var name = _token.text;
if (_arguments.TryGetKnownType(name, out var genericArgument))
genericArgument = ParseFullType(genericArgument);
else
throw new UnknownIdentifierException(_token.text, _token.pos);

genericArguments.Add(genericArgument);
genericArguments.Add(ParseKnownType());
if (_token.id != TokenId.Comma) break;
NextToken();
}

type = type.MakeGenericType(genericArguments.ToArray());
return type;
return genericArguments;
}

private Type ParseUnboundType(Type type)
private int ParseUnboundTypeArity()
{
var rank = 1;
while (_token.id == TokenId.Comma)
Expand All @@ -1302,16 +1331,11 @@ private Type ParseUnboundType(Type type)
NextToken();
}

if (rank != type.GetGenericArguments().Length)
throw new ArgumentException($"The number of generic arguments provided doesn't equal the arity of the generic type definition.");

return type;
return rank;
}

private Expression ParseTypeKeyword(Type type)
{
type = ParseFullType(type);

//if (token.id == TokenId.OpenParen)
//{
// return ParseTypeConstructor(type, errorPos);
Expand Down
4 changes: 2 additions & 2 deletions src/DynamicExpresso.Core/Resources/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@
<value>Ambiguous invocation of delegate (multiple overloads found)</value>
</data>
<data name="CloseCurlyBracketExpected" xml:space="preserve">
<value>'}' expected</value>
<value>'}}' expected</value>
</data>
<data name="OpenCurlyBracketExpected" xml:space="preserve">
<value>'{' expected</value>
<value>'{{' expected</value>
</data>
<data name="EqualExpected" xml:space="preserve">
<value>'=' expected</value>
Expand Down
39 changes: 38 additions & 1 deletion test/DynamicExpresso.UnitTest/ConstructorTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using DynamicExpresso.Exceptions;
using NUnit.Framework;

Expand Down Expand Up @@ -79,6 +79,43 @@ public void Object_initializer()
Assert.AreEqual(new MyClass() { StrProp = "test", IntField = 5 }, target.Eval("new MyClass() { StrProp = \"test\", IntField = 5 }"));
}

[Test]
public void Constructor_invocation_generics()
{
var target = new Interpreter();
target.Reference(typeof(Tuple<>));
target.Reference(typeof(Tuple<,>));
target.Reference(typeof(Tuple<,,>));
Assert.AreEqual(1, target.Eval("new Tuple<int>(1).Item1"));
Assert.AreEqual("My str item", target.Eval("new Tuple<int, string>(5, \"My str item\").Item2"));
Assert.AreEqual(3, target.Eval("new Tuple<int, int, int>(1, 2, 3).Item3"));
}

[Test]
public void Constructor_invocation_named_generics_with_arity()
{
var target = new Interpreter();
target.Reference(typeof(Tuple<>), "Toto`1");
target.Reference(typeof(Tuple<,>), "Toto`2");
target.Reference(typeof(Tuple<,,>), "Toto`3");
Assert.AreEqual(1, target.Eval("new Toto<int>(1).Item1"));
Assert.AreEqual("My str item", target.Eval("new Toto<int, string>(5, \"My str item\").Item2"));
Assert.AreEqual(3, target.Eval("new Toto<int, int, int>(1, 2, 3).Item3"));
}

[Test]
public void Constructor_invocation_named_generics()
{
var target = new Interpreter();
target.Reference(typeof(Tuple<,>), "Tuple");
Assert.AreEqual("My str item", target.Eval("new Tuple<int, string>(5, \"My str item\").Item2"));

target.Reference(typeof(Tuple<>), "Tuple1");
target.Reference(typeof(Tuple<,,>), "Tuple3");
Assert.AreEqual(1, target.Eval("new Tuple1<int>(1).Item1"));
Assert.AreEqual(3, target.Eval("new Tuple3<int, int, int>(1, 2, 3).Item3"));
}

[Test]
public void Object_initializer_syntax_error()
{
Expand Down
29 changes: 29 additions & 0 deletions test/DynamicExpresso.UnitTest/OperatorsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ public void Typeof_Operator_Generics()
Assert.AreEqual(typeof(Dictionary<,>), target.Eval("typeof(Dictionary<,>)"));
}

[Test]
public void Typeof_Operator_Generics_Arity()
{
var target = new Interpreter();
target.Reference(typeof(Tuple<>));
target.Reference(typeof(Tuple<,>));
target.Reference(typeof(Tuple<,,>));
Assert.AreEqual(typeof(Tuple<>), target.Eval("typeof(Tuple<>)"));
Assert.AreEqual(typeof(Tuple<,>), target.Eval("typeof(Tuple<,>)"));
Assert.AreEqual(typeof(Tuple<,,>), target.Eval("typeof(Tuple<,,>)"));
}

[Test]
public void Is_Operator()
{
Expand All @@ -120,6 +132,23 @@ public void Is_Operator()
Assert.AreEqual(b is int, target.Eval("b is int"));
}

[Test]
public void Is_Operator_Generics()
{
object a = Tuple.Create(1);
object b = Tuple.Create(1, 2);
var target = new Interpreter()
.SetVariable("a", a, typeof(object))
.SetVariable("b", b, typeof(object));

target.Reference(typeof(Tuple<>));
target.Reference(typeof(Tuple<,>));

Assert.AreEqual(true, target.Eval("a is Tuple<int>"));
Assert.AreEqual(typeof(bool), target.Parse("a is Tuple<int>").ReturnType);
Assert.AreEqual(true, target.Eval("b is Tuple<int,int>"));
}

[Test]
public void As_Operator()
{
Expand Down

0 comments on commit 859f107

Please sign in to comment.