Skip to content

Commit

Permalink
js translations: support Linq methods on ImmutableArray
Browse files Browse the repository at this point in the history
ImmutableArray is a struct so it has custom Linq method overloads
to avoid allocations.
These are preferred by the compiler over IEnumerable<T>,
so our Linq support was broken.

Also added .AsEnumerable and few other conversion methods
to help users work around similar issues.
  • Loading branch information
exyi committed Oct 17, 2023
1 parent 3c6ee78 commit 06f82c4
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public void AddDefaultMethodTranslators()
AddMethodTranslator(() => default(ICollection)!.Count, lengthMethod);
AddMethodTranslator(() => default(ICollection<Generic.T>)!.Count, lengthMethod);
AddMethodTranslator(() => default(IReadOnlyCollection<Generic.T>)!.Count, lengthMethod);
AddMethodTranslator(() => default(ImmutableArray<Generic.T>)!.Length, lengthMethod);
AddMethodTranslator(() => "".Length, lengthMethod);
AddMethodTranslator(() => Enums.GetNames<Generic.Enum>(), new EnumGetNamesMethodTranslator());
var identityTranslator = new GenericMethodCompiler(a => a[1]);
Expand Down Expand Up @@ -538,27 +539,43 @@ bool IsDelegateReturnTypeEnum(Type type)
string GetDelegateReturnTypeHash(Type type)
=> type.GetGenericArguments().Last().GetTypeHash();

AddMethodTranslator(() => Enumerable.All(Enumerable.Empty<Generic.T>(), _ => false), new GenericMethodCompiler(args => args[1].Member("every").Invoke(args[2])));
AddMethodTranslator(() => Enumerable.Any(Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1].Member("some").Invoke(returnTrueFunc.Clone())));
AddMethodTranslator(() => Enumerable.Any(Enumerable.Empty<Generic.T>(), _ => false), new GenericMethodCompiler(args => args[1].Member("some").Invoke(args[2])));
var all = new GenericMethodCompiler(args => args[1].Member("every").Invoke(args[2]));
AddMethodTranslator(() => Enumerable.All(Enumerable.Empty<Generic.T>(), _ => false), all);
AddMethodTranslator(() => ImmutableArrayExtensions.All(default(ImmutableArray<Generic.T>), _ => false), all);
var any = new GenericMethodCompiler(args => args[1].Member("length").Binary(BinaryOperatorType.Greater, new JsLiteral(0)));
AddMethodTranslator(() => Enumerable.Any(Enumerable.Empty<Generic.T>()), any);
AddMethodTranslator(() => ImmutableArrayExtensions.Any(default(ImmutableArray<Generic.T>)), any);
var anyPred = new GenericMethodCompiler(args => args[1].Member("some").Invoke(args[2]));
AddMethodTranslator(() => Enumerable.Any(Enumerable.Empty<Generic.T>(), _ => false), anyPred);
AddMethodTranslator(() => ImmutableArrayExtensions.Any(default(ImmutableArray<Generic.T>), _ => false), anyPred);
AddMethodTranslator(() => Enumerable.Concat(Enumerable.Empty<Generic.T>(), Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1].Member("concat").Invoke(args[2])));
AddMethodTranslator(() => Enumerable.Count(Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1].Member("length")));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Distinct(), new GenericMethodCompiler(args => new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("distinct").Invoke(args[1]),
check: (method, target, arguments) => EnsureIsComparableInJavascript(method, ReflectionUtils.GetEnumerableType(arguments.First().Type).NotNull())));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ElementAt(0),
new GenericMethodCompiler((args, method) => BuildIndexer(args[1], args[2], method)));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ElementAtOrDefault(0),
new GenericMethodCompiler((args, method) => BuildIndexer(args[1], args[2], method)));
AddMethodTranslator(() => ImmutableArrayExtensions.ElementAt(default(ImmutableArray<Generic.T>), 0),
new GenericMethodCompiler((args, method) => BuildIndexer(args[1], args[2], method)));
AddMethodTranslator(() => ImmutableArrayExtensions.ElementAtOrDefault(default(ImmutableArray<Generic.T>), 0),
new GenericMethodCompiler((args, method) => BuildIndexer(args[1], args[2], method)));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().FirstOrDefault(), new GenericMethodCompiler((args, m) =>
args[1].Indexer(0)
.WithAnnotation(new VMPropertyInfoAnnotation(m.ReturnType)).WithAnnotation(MayBeNullAnnotation.Instance)));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().FirstOrDefault(_ => true), new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("firstOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance)));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().LastOrDefault(), new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("lastOrDefault").Invoke(args[1], returnTrueFunc.Clone()).WithAnnotation(MayBeNullAnnotation.Instance)));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().LastOrDefault(_ => false), new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("lastOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance)));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().FirstOrDefault(), new GenericMethodCompiler((args, m) => BuildIndexer(args[1], new JsLiteral(0), m)));
AddMethodTranslator(() => ImmutableArrayExtensions.FirstOrDefault(default(ImmutableArray<Generic.T>)), new GenericMethodCompiler((args, m) => BuildIndexer(args[1], new JsLiteral(0), m)));
var firstOrDefaultPred = new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("firstOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().FirstOrDefault(_ => true), firstOrDefaultPred);
AddMethodTranslator(() => ImmutableArrayExtensions.FirstOrDefault(default(ImmutableArray<Generic.T>), _ => true), firstOrDefaultPred);

var lastOrDefault = new GenericMethodCompiler(args => args[1].Member("at").Invoke(new JsLiteral(-1)).WithAnnotation(MayBeNullAnnotation.Instance));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().LastOrDefault(), lastOrDefault);
AddMethodTranslator(() => ImmutableArrayExtensions.LastOrDefault(default(ImmutableArray<Generic.T>)), lastOrDefault);
var lastOrDefaultPred = new GenericMethodCompiler(args =>
new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("lastOrDefault").Invoke(args[1], args[2]).WithAnnotation(MayBeNullAnnotation.Instance));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().LastOrDefault(_ => false), lastOrDefaultPred);
AddMethodTranslator(() => ImmutableArrayExtensions.LastOrDefault(default(ImmutableArray<Generic.T>), _ => false), lastOrDefaultPred);

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().OrderBy(_ => Generic.Enum.Something), new GenericMethodCompiler((jArgs, dArgs) => new JsIdentifierExpression("dotvvm").Member("translations").Member("array").Member("orderBy")
.Invoke(jArgs[1], jArgs[2], new JsLiteral((IsDelegateReturnTypeEnum(dArgs.Last().Type)) ? GetDelegateReturnTypeHash(dArgs.Last().Type) : null)),
Expand All @@ -567,17 +584,30 @@ string GetDelegateReturnTypeHash(Type type)
.Invoke(jArgs[1], jArgs[2], new JsLiteral((IsDelegateReturnTypeEnum(dArgs.Last().Type)) ? GetDelegateReturnTypeHash(dArgs.Last().Type) : null)),
check: (method, _, arguments) => EnsureIsComparableInJavascript(method, arguments.Last().Type.GetGenericArguments().Last())));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Select(_ => Generic.Enum.Something),
translator: new GenericMethodCompiler(args => args[1].Member("map").Invoke(args[2])));
var select = new GenericMethodCompiler(args => args[1].Member("map").Invoke(args[2]));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Select(_ => Generic.Enum.Something), select);
AddMethodTranslator(() => ImmutableArrayExtensions.Select(default(ImmutableArray<Generic.T>), _ => Generic.Enum.Something), select);
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Skip(0), new GenericMethodCompiler(args => args[1].Member("slice").Invoke(args[2])));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Take(0), new GenericMethodCompiler(args =>
args[1].Member("slice").Invoke(new JsLiteral(0), args[2])));

var where = new GenericMethodCompiler(args => args[1].Member("filter").Invoke(args[2]));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Where(_ => true), where);
AddMethodTranslator(() => ImmutableArrayExtensions.Where(default(ImmutableArray<Generic.T>), _ => true), where);

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ToArray(), new GenericMethodCompiler(args => args[1]));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ToList(), new GenericMethodCompiler(args => args[1]));
AddMethodTranslator(() => Enumerable.Empty<Generic.T>().ToHashSet(), new GenericMethodCompiler(args => args[1]));
AddMethodTranslator(() => Enumerable.AsEnumerable(Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1]));

AddMethodTranslator(() => ImmutableArray.ToImmutableArray(Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1]));
AddMethodTranslator(() => ImmutableList.ToImmutableList(Enumerable.Empty<Generic.T>()), new GenericMethodCompiler(args => args[1]));
AddMethodTranslator(() => ImmutableArrayExtensions.ToArray(ImmutableArray<Generic.T>.Empty), new GenericMethodCompiler(args => args[1]));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>().Where(_ => true), new GenericMethodCompiler(args => args[1].Member("filter").Invoke(args[2])));

AddMethodTranslator(() => Enumerable.Empty<Generic.T>(), new GenericMethodCompiler(args => new JsArrayExpression()));
AddMethodTranslator(() => Array.Empty<Generic.T>(), new GenericMethodCompiler(args => new JsArrayExpression()));
AddDefaultNumericEnumerableTranslations();
}

Expand Down
41 changes: 31 additions & 10 deletions src/Tests/Binding/JavascriptCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,10 @@ public void JsTranslator_DictionaryRemove()
[TestMethod]
[DataRow("Enumerable.Where(LongArray, (long item) => item % 2 == 0)", DisplayName = "Regular call of Enumerable.Where")]
[DataRow("LongArray.Where((long item) => item % 2 == 0)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().Where((long item) => item % 2 == 0)", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableWhere(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().filter((item)=>ko.unwrap(item)%2==0)", result);
}

Expand All @@ -579,18 +580,21 @@ public void JsTranslator_NestedEnumerableMethods()
[TestMethod]
[DataRow("Enumerable.Select(LongArray, (long item) => -item)", DisplayName = "Regular call of Enumerable.Select")]
[DataRow("LongArray.Select((long item) => -item)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().Select((long item) => -item)", DisplayName = "Immutable array - extension method")]
[DataRow("LongArray.ToImmutableList().Select((long item) => -item)", DisplayName = "Immutable list - extension method")]
public void JsTranslator_EnumerableSelect(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().map((item)=>-ko.unwrap(item))", result);
}

[TestMethod]
[DataRow("Enumerable.Concat(LongArray, LongArray)", DisplayName = "Regular call of Enumerable.Concat")]
[DataRow("LongArray.Concat(LongArray)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().Concat(LongArray.ToImmutableArray())", DisplayName = "Immutable arrays")]
public void JsTranslator_EnumerableConcat(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().concat(LongArray())", result);
}

Expand Down Expand Up @@ -636,18 +640,20 @@ public void JsTranslator_ListAddRange()
[TestMethod]
[DataRow("Enumerable.All(LongArray, (long item) => item > 0)", DisplayName = "Regular call of Enumerable.All")]
[DataRow("LongArray.All((long item) => item > 0)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().All((long item) => item > 0)", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableAll(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().every((item)=>ko.unwrap(item)>0)", result);
}

[TestMethod]
[DataRow("Enumerable.Any(LongArray, (long item) => item > 0)", DisplayName = "Regular call of Enumerable.Any")]
[DataRow("LongArray.Any((long item) => item > 0)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().Any((long item) => item > 0)", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableAny(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().some((item)=>ko.unwrap(item)>0)", result);
}

Expand All @@ -665,21 +671,35 @@ public void JsTranslator_ICollectionClear()
Assert.AreEqual("dotvvm.translations.array.clear(TestViewModel2().Collection)", result);
}

[TestMethod]
[DataRow("Enumerable.Empty<int>()")]
[DataRow("Array.Empty<int>()")]
[DataRow("ImmutableArray<int>.Empty")]
[DataRow("ImmutableList<int>.Empty")]
public void JsTranslator_EnumerableEmpty(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("[]", result);
}


[TestMethod]
[DataRow("Enumerable.FirstOrDefault(LongArray)", DisplayName = "Regular call of Enumerable.FirstOrDefault")]
[DataRow("LongArray.FirstOrDefault()", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().FirstOrDefault()", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableFirstOrDefault(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray()[0]", result);
}

[TestMethod]
[DataRow("Enumerable.FirstOrDefault(LongArray, (long item) => item > 0)", DisplayName = "Regular call of Enumerable.FirstOrDefault")]
[DataRow("LongArray.FirstOrDefault((long item) => item > 0)", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().FirstOrDefault((long item) => item > 0)", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableFirstOrDefaultParametrized(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("dotvvm.translations.array.firstOrDefault(LongArray(),(item)=>ko.unwrap(item)>0)", result);
}

Expand Down Expand Up @@ -718,18 +738,19 @@ public void JsTranslator_ICollectionContains()
[TestMethod]
[DataRow("Enumerable.LastOrDefault(LongArray)", DisplayName = "Regular call of Enumerable.LastOrDefault")]
[DataRow("LongArray.LastOrDefault()", DisplayName = "Syntax sugar - extension method")]
[DataRow("LongArray.ToImmutableArray().LastOrDefault()", DisplayName = "Immutable array - extension method")]
public void JsTranslator_EnumerableLastOrDefault(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("dotvvm.translations.array.lastOrDefault(LongArray(),()=>true)", result);
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("LongArray().at(-1)", result);
}

[TestMethod]
[DataRow("Enumerable.LastOrDefault(LongArray, (long item) => item > 0)", DisplayName = "Regular call of Enumerable.LastOrDefault")]
[DataRow("LongArray.LastOrDefault((long item) => item > 0)", DisplayName = "Syntax sugar - extension method")]
public void JsTranslator_EnumerableLastOrDefaultParametrized(string binding)
{
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq") }, new[] { typeof(TestViewModel) });
var result = CompileBinding(binding, new[] { new NamespaceImport("System.Linq"), new NamespaceImport("System.Collections.Immutable") }, new[] { typeof(TestViewModel) });
Assert.AreEqual("dotvvm.translations.array.lastOrDefault(LongArray(),(item)=>ko.unwrap(item)>0)", result);
}

Expand Down

0 comments on commit 06f82c4

Please sign in to comment.