diff --git a/README.md b/README.md
index deb8083..ae2bff2 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ Aliases with the same dimensions share a type. For example, `Energy` and `Torque
### Current scope
-Arithmetic discovery currently supports multiplication and division with known quantity operands, scalar factors, and nested expressions. Roots, powers, dimensionless results and inference through unresolved intermediate local variables are not yet supported by discovery.
+Arithmetic discovery currently supports multiplication and division with known quantity operands, scalar factors, nested expressions, and intermediate `var` locals inferred through their initialisers. Roots, powers, dimensionless results and inference through unresolved explicit type names are not yet supported by discovery.
The library is under active development; the API and supported expression patterns are still evolving.
diff --git a/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj b/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
index 663e063..36e211d 100644
--- a/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
+++ b/src/DimensionalQuantities.Roslyn.Tests/DimensionalQuantities.Roslyn.Tests.csproj
@@ -17,8 +17,4 @@
-
-
-
-
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Discovery/ArithmeticDiscoveryTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Discovery/ArithmeticDiscoveryTests.cs
index e5c8de4..604f8da 100644
--- a/src/DimensionalQuantities.Roslyn.Tests/Discovery/ArithmeticDiscoveryTests.cs
+++ b/src/DimensionalQuantities.Roslyn.Tests/Discovery/ArithmeticDiscoveryTests.cs
@@ -1,9 +1,4 @@
-using System.Collections.Immutable;
-using DimensionalQuantities.Dimensions;
using DimensionalQuantities.Roslyn.Discovery;
-using DimensionalQuantities.Roslyn.Generators;
-using DimensionalQuantities.TypeSystem;
-using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Xunit;
@@ -12,181 +7,19 @@ namespace DimensionalQuantities.Roslyn.Tests.Discovery;
public sealed class ArithmeticDiscoveryTests
{
- private static readonly CSharpParseOptions _parseOptions = new(LanguageVersion.CSharp14);
-
- [Theory]
- [InlineData("a * b", true)]
- [InlineData("a / b", false)]
- public void DiscoveryReadsQuantityOperandsFromConsumerSymbols(string expression, bool multiply)
- {
- string source = $$"""
- public static class Consumer
- {
- public static object Test(Length a, Time b) => {{expression}};
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
- ArithmeticOperation operation = Assert.Single(result);
- ArithmeticOperationKind expectedKind = multiply ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
- Dimension expectedLeft = new(Length: 1);
- Dimension expectedRight = new(Time: 1);
- Dimension expectedResult = new(Length: 1, Time: multiply ? 1 : -1);
- ArithmeticOperation expected = new(expectedKind, expectedLeft, expectedRight, expectedResult);
-
- Assert.Equal(expected, operation);
- }
-
- [Fact]
- public void DiscoveryFindsLeftNestedOperationsBottomUp()
- {
- const string source = """
- public static class Consumer
- {
- public static object Test(Current a, Current b, Resistance r) => (a * b) * r;
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
- Dimension current = new(ElectricCurrent: 1);
- Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
- Dimension currentSquared = new(ElectricCurrent: 2);
- Dimension power = new(Length: 2, Mass: 1, Time: -3);
- ArithmeticOperation[] expected =
- [
- new(ArithmeticOperationKind.Multiply, current, current, currentSquared),
- new(ArithmeticOperationKind.Multiply, currentSquared, resistance, power)
- ];
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void DiscoveryFindsRightNestedOperationsBottomUp()
- {
- const string source = """
- public static class Consumer
- {
- public static object Test(Current a, Current b, Resistance r) => a * (b * r);
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
- Dimension current = new(ElectricCurrent: 1);
- Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
- Dimension currentTimesResistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -1);
- Dimension power = new(Length: 2, Mass: 1, Time: -3);
- ArithmeticOperation[] expected =
- [
- new(ArithmeticOperationKind.Multiply, current, resistance, currentTimesResistance),
- new(ArithmeticOperationKind.Multiply, current, currentTimesResistance, power)
- ];
-
- Assert.Equal(expected, result);
- }
-
[Theory]
- [InlineData("a * missing")]
- [InlineData("a * 1m")]
- [InlineData("a * \"text\"")]
- [InlineData("a + b")]
- [InlineData("a / a")]
- public void DiscoverySkipsUnsupportedOrDimensionlessExpressions(string expression)
- {
- string source = $$"""
- public static class Consumer
- {
- public static object Test(Length a, Time b) => {{expression}};
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
-
- Assert.Empty(result);
- }
-
- [Fact]
- public void ScalarDivisionCreatesInverseDimensionForNestedQuantityArithmetic()
- {
- const string source = """
- public static class Consumer
- {
- public static object Test(Length length, Time time) => (2d / length) * time;
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
- Dimension length = new(Length: 1);
- Dimension inverseLength = new(Length: -1);
- Dimension time = new(Time: 1);
- Dimension timePerLength = new(Length: -1, Time: 1);
- ArithmeticOperation[] expected =
- [
- new(ArithmeticOperationKind.ScalarDivide, default, length, inverseLength),
- new(ArithmeticOperationKind.Multiply, inverseLength, time, timePerLength)
- ];
+ [InlineData("a * b", new[] { "a * b" })]
+ [InlineData("(a * b) / c", new[] { "(a * b) / c" })]
+ [InlineData("a * (b / c)", new[] { "a * (b / c)" })]
+ [InlineData("Use(a * b) * c", new[] { "Use(a * b) * c", "a * b" })]
+ [InlineData("a * b + c / d", new[] { "a * b", "c / d" })]
+ [InlineData("a + b", new string[] { })]
+ public void SelectsOutermostArithmeticThroughParentheses(string source, string[] expected)
+ {
+ ExpressionSyntax expression = SyntaxFactory.ParseExpression(source);
+ string[] result = expression.DescendantNodesAndSelf().OfType()
+ .Where(ArithmeticDiscovery.IsRoot).Select(binary => binary.ToString()).ToArray();
Assert.Equal(expected, result);
}
-
- [Theory]
- [InlineData("(2 + 3) * a * b")]
- [InlineData("(2 * 3) * a * b")]
- [InlineData("(a * b) / 2")]
- public void ScalarFactorsPreserveQuantityDimensionsInChains(string expression)
- {
- string source = $$"""
- public static class Consumer
- {
- public static object Test(Length a, Time b) => {{expression}};
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
- Dimension length = new(Length: 1);
- Dimension time = new(Time: 1);
- Dimension lengthTime = new(Length: 1, Time: 1);
- ArithmeticOperation expected = new(ArithmeticOperationKind.Multiply, length, time, lengthTime);
-
- Assert.Equal(expected, Assert.Single(result));
- }
-
- [Fact]
- public void DiscoverySkipsDimensionlessQuantityOperand()
- {
- const string source = """
- using DimensionalQuantities.TypeSystem;
- public static class Consumer
- {
- public static object Test(Length length,
- Quantity> scalar)
- => length * scalar;
- }
- """;
- CSharpCompilation compilation = Compile(source);
- ImmutableArray result = Analyse(compilation);
-
- Assert.Empty(result);
- }
-
- private static ImmutableArray Analyse(CSharpCompilation compilation)
- {
- SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs");
- ArrowExpressionClauseSyntax expressionBody = tree.GetRoot().DescendantNodes().OfType().Single();
- SemanticModel semanticModel = compilation.GetSemanticModel(tree);
- return ArithmeticDiscovery.Analyse(expressionBody.Expression, semanticModel, TestContext.Current.CancellationToken);
- }
-
- private static CSharpCompilation Compile(string source)
- {
- string[] assemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator);
- IEnumerable references = assemblies.Select(path => MetadataReference.CreateFromFile(path))
- .Append(MetadataReference.CreateFromFile(typeof(Quantity<>).Assembly.Location));
- QuantityGenerationPlan plan = QuantityDiscovery.Discover([]);
- SyntaxTree knownQuantities = CSharpSyntaxTree.ParseText(QuantitySourceEmitter.Emit(plan.KnownQuantities), _parseOptions,
- path: "KnownQuantities.g.cs");
- return CSharpCompilation.Create("DiscoveryConsumer",
- [CSharpSyntaxTree.ParseText(source, _parseOptions, path: "Consumer.cs"), knownQuantities], references,
- new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
- }
}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs
new file mode 100644
index 0000000..6a5de18
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn.Tests/Discovery/QuantityExpressionAnalyserTests.cs
@@ -0,0 +1,365 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Dimensions;
+using DimensionalQuantities.Roslyn.Discovery;
+using DimensionalQuantities.Roslyn.Generators;
+using DimensionalQuantities.TypeSystem;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Xunit;
+
+namespace DimensionalQuantities.Roslyn.Tests.Discovery;
+
+public sealed class QuantityExpressionAnalyserTests
+{
+ private static readonly CSharpParseOptions _parseOptions = new(LanguageVersion.CSharp14);
+
+ [Theory]
+ [InlineData("a * b", true)]
+ [InlineData("a / b", false)]
+ public void DiscoveryReadsQuantityOperandsFromConsumerSymbols(string expression, bool multiply)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static object Test(Length a, Time b) => {{expression}};
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+ ArithmeticOperation operation = Assert.Single(result);
+ ArithmeticOperationKind expectedKind = multiply ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
+ Dimension expectedLeft = new(Length: 1);
+ Dimension expectedRight = new(Time: 1);
+ Dimension expectedResult = new(Length: 1, Time: multiply ? 1 : -1);
+ ArithmeticOperation expected = new(expectedKind, expectedLeft, expectedRight, expectedResult);
+
+ Assert.Equal(expected, operation);
+ }
+
+ [Fact]
+ public void DiscoveryFindsLeftNestedOperationsBottomUp()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Current a, Current b, Resistance r) => (a * b) * r;
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ Dimension currentSquared = new(ElectricCurrent: 2);
+ Dimension power = new(Length: 2, Mass: 1, Time: -3);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, currentSquared),
+ new(ArithmeticOperationKind.Multiply, currentSquared, resistance, power)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void DiscoveryFindsRightNestedOperationsBottomUp()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Current a, Current b, Resistance r) => a * (b * r);
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ Dimension currentTimesResistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -1);
+ Dimension power = new(Length: 2, Mass: 1, Time: -3);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, resistance, currentTimesResistance),
+ new(ArithmeticOperationKind.Multiply, current, currentTimesResistance, power)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("a * missing")]
+ [InlineData("a * 1m")]
+ [InlineData("a * \"text\"")]
+ [InlineData("a + b")]
+ [InlineData("a / a")]
+ public void DiscoverySkipsUnsupportedOrDimensionlessExpressions(string expression)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static object Test(Length a, Time b) => {{expression}};
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ScalarDivisionCreatesInverseDimensionForNestedQuantityArithmetic()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Length length, Time time) => (2d / length) * time;
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+ Dimension length = new(Length: 1);
+ Dimension inverseLength = new(Length: -1);
+ Dimension time = new(Time: 1);
+ Dimension timePerLength = new(Length: -1, Time: 1);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.ScalarDivide, default, length, inverseLength),
+ new(ArithmeticOperationKind.Multiply, inverseLength, time, timePerLength)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("(2 + 3) * a * b")]
+ [InlineData("(2 * 3) * a * b")]
+ [InlineData("(a * b) / 2")]
+ public void ScalarFactorsPreserveQuantityDimensionsInChains(string expression)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static object Test(Length a, Time b) => {{expression}};
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+ Dimension length = new(Length: 1);
+ Dimension time = new(Time: 1);
+ Dimension lengthTime = new(Length: 1, Time: 1);
+ ArithmeticOperation expected = new(ArithmeticOperationKind.Multiply, length, time, lengthTime);
+
+ Assert.Equal(expected, Assert.Single(result));
+ }
+
+ [Fact]
+ public void DiscoverySkipsDimensionlessQuantityOperand()
+ {
+ const string source = """
+ using DimensionalQuantities.TypeSystem;
+ public static class Consumer
+ {
+ public static object Test(Length length,
+ Quantity> scalar)
+ => length * scalar;
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ ImmutableArray result = Analyse(compilation);
+
+ Assert.Empty(result);
+ }
+
+ [Theory]
+ [InlineData("var squared = current * current;", "squared * resistance")]
+ [InlineData("var squared = current * current; var copy = squared;", "copy * resistance")]
+ [InlineData("var squared = (2 * current) * current / 2;", "squared * resistance")]
+ public void FollowsLocalInitialisersAndCopies(string declarations, string expression)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static object Test(Current current, Resistance resistance)
+ {
+ {{declarations}}
+ return {{expression}};
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension squared = new(ElectricCurrent: 2);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ Dimension power = new(Length: 2, Mass: 1, Time: -3);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, squared),
+ new(ArithmeticOperationKind.Multiply, squared, resistance, power)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void FollowsSeveralDependentLocalInitialisers()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Current current, Resistance resistance, Time time)
+ {
+ var squared = current * current;
+ var heating = squared * resistance;
+ return heating * time;
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension squared = new(ElectricCurrent: 2);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ Dimension power = new(Length: 2, Mass: 1, Time: -3);
+ Dimension time = new(Time: 1);
+ Dimension energy = new(Length: 2, Mass: 1, Time: -2);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, squared),
+ new(ArithmeticOperationKind.Multiply, squared, resistance, power),
+ new(ArithmeticOperationKind.Multiply, power, time, energy)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void RepeatedLocalReferencesShareTheirInferredOperations()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Current current)
+ {
+ var squared = current * current;
+ return squared * squared;
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension squared = new(ElectricCurrent: 2);
+ Dimension fourth = new(ElectricCurrent: 4);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, squared),
+ new(ArithmeticOperationKind.Multiply, squared, squared, fourth)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("var value = ;")]
+ [InlineData("var value;")]
+ [InlineData("var value = missing;")]
+ [InlineData("var value = value;")]
+ [InlineData("var value = other; var other = value;")]
+ [InlineData("var value = other * current; var other = value * current;")]
+ [InlineData("object value = current * current;")]
+ [InlineData("Missing value = current * current;")]
+ [InlineData("var value = current / current;")]
+ public void DoesNotInferUnsupportedOrCyclicLocals(string declaration)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static object Test(Current current, Resistance resistance)
+ {
+ {{declaration}}
+ return value * resistance;
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+
+ Assert.Empty(result);
+ }
+
+ [Theory]
+ [InlineData("using var = System.Object;")]
+ [InlineData("public class var { }")]
+ public void DoesNotTreatATypedDeclarationNamedVarAsImplicitTyping(string typeDeclaration)
+ {
+ string source = $$"""
+ {{typeDeclaration}}
+ public static class Consumer
+ {
+ public static object Test(Current current, Resistance resistance)
+ {
+ var value = current * current;
+ return value * resistance;
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void UsesTheDeclarationTypeRatherThanLaterAssignments()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Test(Current current, Resistance resistance, Length length)
+ {
+ var value = current * current;
+ value = length * length;
+ return value * resistance;
+ }
+ }
+ """;
+ ImmutableArray result = AnalyseReturn(source);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension squared = new(ElectricCurrent: 2);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ Dimension power = new(Length: 2, Mass: 1, Time: -3);
+ ArithmeticOperation[] expected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, squared),
+ new(ArithmeticOperationKind.Multiply, squared, resistance, power)
+ ];
+
+ Assert.Equal(expected, result);
+ }
+
+ private static ImmutableArray Analyse(CSharpCompilation compilation)
+ {
+ SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs");
+ ArrowExpressionClauseSyntax expressionBody = tree.GetRoot().DescendantNodes().OfType().Single();
+ SemanticModel semanticModel = compilation.GetSemanticModel(tree);
+ QuantityExpressionAnalyser analyser = new(semanticModel);
+ return analyser.Analyse(expressionBody.Expression, TestContext.Current.CancellationToken);
+ }
+
+ private static CSharpCompilation Compile(string source)
+ {
+ string[] assemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator);
+ IEnumerable references = assemblies.Select(path => MetadataReference.CreateFromFile(path))
+ .Append(MetadataReference.CreateFromFile(typeof(Quantity<>).Assembly.Location));
+ QuantityGenerationPlan plan = QuantityDiscovery.Discover([]);
+ SyntaxTree knownQuantities = CSharpSyntaxTree.ParseText(QuantitySourceEmitter.Emit(plan.KnownQuantities), _parseOptions,
+ path: "KnownQuantities.g.cs");
+ return CSharpCompilation.Create("DiscoveryConsumer",
+ [CSharpSyntaxTree.ParseText(source, _parseOptions, path: "Consumer.cs"), knownQuantities], references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+
+ private static ImmutableArray AnalyseReturn(string source)
+ {
+ CSharpCompilation compilation = Compile(source);
+ SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs");
+ ReturnStatementSyntax statement = tree.GetRoot().DescendantNodes().OfType().Single();
+ QuantityExpressionAnalyser analyser = new(compilation.GetSemanticModel(tree));
+ return analyser.Analyse(statement.Expression!, TestContext.Current.CancellationToken);
+ }
+}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Arithmetic.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Arithmetic.cs
deleted file mode 100644
index bf4f3db..0000000
--- a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Arithmetic.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Xunit;
-
-namespace DimensionalQuantities.Roslyn.Tests.Generators;
-
-public sealed partial class QuantityGeneratorTests
-{
- [Theory]
- [InlineData("Length.FromMetres(2) * Length.FromMetres(3)", "Area", 6)]
- [InlineData("Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 6)]
- [InlineData("Current.FromAmps(2) * Current.FromAmps(3) * Resistance.FromOhms(4)", "Power", 24)]
- [InlineData("Current.FromAmps(2) * (Current.FromAmps(3) * Resistance.FromOhms(4))", "Power", 24)]
- [InlineData("Voltage.FromVolts(12) / Current.FromAmps(3)", "Resistance", 4)]
- [InlineData("Current.FromAmps(3) * Time.FromSeconds(2)", "TimeCurrent", 6)]
- [InlineData("(Current.FromAmps(2) * Time.FromSeconds(3)) / Time.FromSeconds(3)", "Current", 2)]
- [InlineData("(Voltage.FromVolts(4) * Current.FromAmps(3)) / Current.FromAmps(2)", "Voltage", 6)]
- [InlineData("Current.FromAmps(3) * 2d", "Current", 6)]
- [InlineData("2 * Current.FromAmps(3)", "Current", 6)]
- [InlineData("Current.FromAmps(6) / 2f", "Current", 3)]
- [InlineData("2d / Time.FromSeconds(4)", "Frequency", 0.5)]
- [InlineData("2f / Current.FromAmps(4)", "InverseCurrent", 0.5)]
- [InlineData("(2 / Current.FromAmps(4)) * Time.FromSeconds(3)", "TimePerCurrent", 1.5)]
- [InlineData("(Current.FromAmps(2) * 2) * Current.FromAmps(3)", "CurrentSquared", 12)]
- [InlineData("2 * (Current.FromAmps(2) * Current.FromAmps(3))", "CurrentSquared", 12)]
- [InlineData("(Current.FromAmps(2) * Current.FromAmps(3)) / 2", "CurrentSquared", 3)]
- [InlineData("(2 + 3) * Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 30)]
- [InlineData("(2 * 3) * Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 36)]
- public void QuantityArithmeticProducesTheExpectedTypeAndSiMagnitude(string arithmetic, string resultType, double expected)
- {
- string expression = $"(({resultType})({arithmetic})).AsSiBaseUnit()";
- double result = Evaluate(expression);
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void QuantityArithmeticAcceptsTypedMethodParametersAndReturns()
- {
- const string source = """
- public static class Consumer
- {
- public static Area Multiply(Length left, Length right) => left * right;
- public static Voltage Divide(Power power, Current current) => power / current;
- public static InverseCurrent Invert(double numerator, Current current) => numerator / current;
- }
- """;
-
- Compile(source);
- }
-
- [Fact]
- public void RepeatedConsumerOperationIsEmittedOnce()
- {
- const string source = """
- public static class Consumer
- {
- public static Area First(Length left, Length right) => left * right;
- public static Area Second(Length left, Length right) => left * right;
- }
- """;
- CSharpCompilation compilation = Compile(source);
- INamedTypeSymbol extensions = compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")!;
- Assert.Single(extensions.GetMembers().OfType(), method => method.MetadataName == "op_Multiply");
- }
-
-
- [Fact]
- public void ArithmeticReadsFieldAndMethodReturnTypes()
- {
- const string source = """
- public static class Consumer
- {
- public static Current Input = Current.FromAmps(2);
- public static Resistance GetResistance() => Resistance.FromOhms(4);
- public static Power Calculate() => Input * Input * GetResistance();
- }
- """;
- Compile(source);
- }
-
- [Fact]
- public void DimensionPreservingScalarArithmeticUsesCoreOperatorsOnly()
- {
- const string source = """
- public static class Consumer
- {
- public static Current Scale(Current original, double factor) => factor * original / 2;
- }
- """;
- CSharpCompilation compilation = Compile(source);
-
- Assert.Null(compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions"));
- }
-}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Incremental.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Incremental.cs
deleted file mode 100644
index 9d2baa8..0000000
--- a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.Incremental.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using System.Collections.Immutable;
-using DimensionalQuantities.Dimensions;
-using DimensionalQuantities.Roslyn.Discovery;
-using DimensionalQuantities.Roslyn.Generators;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Xunit;
-
-namespace DimensionalQuantities.Roslyn.Tests.Generators;
-
-public sealed partial class QuantityGeneratorTests
-{
- [Fact]
- public void GeneratorRefreshesArithmeticAfterAddingEditingAndRemovingConsumerTrees()
- {
- const string firstSource = """
- public static class Consumer
- {
- public static CurrentSquared Value() => Current.FromAmps(2) * Current.FromAmps(3);
- }
- """;
- const string addedSource = """
- public static class Added
- {
- public static LengthTime Value() => Length.FromMetres(2) * Time.FromSeconds(3);
- }
- """;
- const string editedSource = """
- public static class Consumer
- {
- public static Resistance Value() => Voltage.FromVolts(12) / Current.FromAmps(3);
- }
- """;
- CSharpCompilation initialInput = CreateInputCompilation(firstSource);
- SyntaxTree initialTree = Assert.Single(initialInput.SyntaxTrees);
- SyntaxTree addedTree = CSharpSyntaxTree.ParseText(addedSource, _parseOptions, path: "Added.cs", cancellationToken: TestContext.Current.CancellationToken);
- SyntaxTree editedTree = CSharpSyntaxTree.ParseText(editedSource, _parseOptions, path: "Consumer.cs", cancellationToken: TestContext.Current.CancellationToken);
- GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
-
- driver = driver.RunGeneratorsAndUpdateCompilation(initialInput, out Compilation firstOutput, out ImmutableArray firstDiagnostics,
- TestContext.Current.CancellationToken);
- Assert.Multiple(
- () => Assert.Empty(firstDiagnostics),
- () => Assert.Empty(firstOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
- () => Assert.NotNull(firstOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")));
-
- CSharpCompilation addedInput = initialInput.AddSyntaxTrees(addedTree);
- driver = driver.RunGeneratorsAndUpdateCompilation(addedInput, out Compilation addedOutput, out ImmutableArray addedDiagnostics,
- TestContext.Current.CancellationToken);
- Assert.Multiple(
- () => Assert.Empty(addedDiagnostics),
- () => Assert.Empty(addedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
- () => Assert.NotNull(addedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
- () => Assert.NotNull(addedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")));
-
- CSharpCompilation editedInput = addedInput.ReplaceSyntaxTree(initialTree, editedTree);
- driver = driver.RunGeneratorsAndUpdateCompilation(editedInput, out Compilation editedOutput, out ImmutableArray editedDiagnostics,
- TestContext.Current.CancellationToken);
- Dimension length = new(Length: 1);
- Dimension time = new(Time: 1);
- Dimension lengthTime = new(Length: 1, Time: 1);
- Dimension voltage = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -1);
- Dimension current = new(ElectricCurrent: 1);
- Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
- ArithmeticOperation addedOperation = new(ArithmeticOperationKind.Multiply, length, time, lengthTime);
- ArithmeticOperation editedOperation = new(ArithmeticOperationKind.Divide, voltage, current, resistance);
- ArithmeticOperation[] editedOperators = ReadOperators(editedOutput);
- Assert.Multiple(
- () => Assert.Empty(editedDiagnostics),
- () => Assert.Empty(editedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
- () => Assert.Null(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
- () => Assert.NotNull(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")),
- () => Assert.Equal(2, editedOperators.Length),
- () => Assert.Contains(addedOperation, editedOperators),
- () => Assert.Contains(editedOperation, editedOperators));
-
- CSharpCompilation removedInput = editedInput.RemoveSyntaxTrees(addedTree);
- driver = driver.RunGeneratorsAndUpdateCompilation(removedInput, out Compilation removedOutput, out ImmutableArray removedDiagnostics,
- TestContext.Current.CancellationToken);
- ArithmeticOperation[] removedOperators = ReadOperators(removedOutput);
- Assert.Multiple(
- () => Assert.Empty(removedDiagnostics),
- () => Assert.Empty(removedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
- () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
- () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")),
- () => Assert.Equal(editedOperation, Assert.Single(removedOperators)));
-
- CSharpCompilation emptyInput = removedInput.RemoveSyntaxTrees(editedTree);
- driver.RunGeneratorsAndUpdateCompilation(emptyInput, out Compilation emptyOutput, out ImmutableArray emptyDiagnostics,
- TestContext.Current.CancellationToken);
- Assert.Multiple(
- () => Assert.Empty(emptyDiagnostics),
- () => Assert.Empty(emptyOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
- () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")),
- () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
- () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")));
- }
-
- [Fact]
- public void GeneratorIgnoresUnrelatedIncompleteAndDimensionlessArithmetic()
- {
- const string source = """
- public static class Consumer
- {
- public static int Number(int left, int right) => left * right;
- public static object Incomplete(Length length) => length * Missing;
- public static object Dimensionless(Length length) => length / length;
- }
- """;
- CSharpCompilation input = CreateInputCompilation(source);
- GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
- driver.RunGeneratorsAndUpdateCompilation(input, out Compilation output, out ImmutableArray generatorDiagnostics,
- TestContext.Current.CancellationToken);
-
- Assert.Multiple(
- () => Assert.Empty(generatorDiagnostics),
- () => Assert.Null(output.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")));
- }
-
- private static ArithmeticOperation[] ReadOperators(Compilation compilation)
- {
- INamedTypeSymbol extensions = compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")!;
- QuantityTypeReader reader = new(compilation);
- return extensions.GetMembers().OfType()
- .Where(method => method.MetadataName is "op_Multiply" or "op_Division")
- .Select(method =>
- {
- if (!reader.TryRead(method.Parameters[0].Type, out Dimension left, TestContext.Current.CancellationToken)
- || !reader.TryRead(method.Parameters[1].Type, out Dimension right, TestContext.Current.CancellationToken)
- || !reader.TryRead(method.ReturnType, out Dimension result, TestContext.Current.CancellationToken))
- {
- throw new InvalidOperationException("Generated operator has an unreadable quantity signature.");
- }
-
- ArithmeticOperationKind kind = method.MetadataName == "op_Multiply" ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
- return new ArithmeticOperation(kind, left, right, result);
- }).ToArray();
- }
-}
diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs
index 8598a48..8ea7e15 100644
--- a/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs
+++ b/src/DimensionalQuantities.Roslyn.Tests/Generators/QuantityGeneratorTests.cs
@@ -1,6 +1,8 @@
using System.Collections.Immutable;
using System.Reflection;
using System.Text;
+using DimensionalQuantities.Dimensions;
+using DimensionalQuantities.Roslyn.Discovery;
using DimensionalQuantities.Roslyn.Generators;
using DimensionalQuantities.TypeSystem;
using Microsoft.CodeAnalysis;
@@ -12,7 +14,7 @@
namespace DimensionalQuantities.Roslyn.Tests.Generators;
-public sealed partial class QuantityGeneratorTests
+public sealed class QuantityGeneratorTests
{
private static readonly CSharpParseOptions _parseOptions = new(LanguageVersion.CSharp14);
@@ -82,6 +84,342 @@ public static class Consumer
Compile(source);
}
+ [Theory]
+ [InlineData("Length.FromMetres(2) * Length.FromMetres(3)", "Area", 6)]
+ [InlineData("Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 6)]
+ [InlineData("Current.FromAmps(2) * Current.FromAmps(3) * Resistance.FromOhms(4)", "Power", 24)]
+ [InlineData("Current.FromAmps(2) * (Current.FromAmps(3) * Resistance.FromOhms(4))", "Power", 24)]
+ [InlineData("Voltage.FromVolts(12) / Current.FromAmps(3)", "Resistance", 4)]
+ [InlineData("Current.FromAmps(3) * Time.FromSeconds(2)", "TimeCurrent", 6)]
+ [InlineData("(Current.FromAmps(2) * Time.FromSeconds(3)) / Time.FromSeconds(3)", "Current", 2)]
+ [InlineData("(Voltage.FromVolts(4) * Current.FromAmps(3)) / Current.FromAmps(2)", "Voltage", 6)]
+ [InlineData("Current.FromAmps(3) * 2d", "Current", 6)]
+ [InlineData("2 * Current.FromAmps(3)", "Current", 6)]
+ [InlineData("Current.FromAmps(6) / 2f", "Current", 3)]
+ [InlineData("2d / Time.FromSeconds(4)", "Frequency", 0.5)]
+ [InlineData("2f / Current.FromAmps(4)", "InverseCurrent", 0.5)]
+ [InlineData("(2 / Current.FromAmps(4)) * Time.FromSeconds(3)", "TimePerCurrent", 1.5)]
+ [InlineData("(Current.FromAmps(2) * 2) * Current.FromAmps(3)", "CurrentSquared", 12)]
+ [InlineData("2 * (Current.FromAmps(2) * Current.FromAmps(3))", "CurrentSquared", 12)]
+ [InlineData("(Current.FromAmps(2) * Current.FromAmps(3)) / 2", "CurrentSquared", 3)]
+ [InlineData("(2 + 3) * Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 30)]
+ [InlineData("(2 * 3) * Current.FromAmps(2) * Current.FromAmps(3)", "CurrentSquared", 36)]
+ public void QuantityArithmeticProducesTheExpectedTypeAndSiMagnitude(string arithmetic, string resultType, double expected)
+ {
+ string expression = $"(({resultType})({arithmetic})).AsSiBaseUnit()";
+ double result = Evaluate(expression);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void QuantityArithmeticAcceptsTypedMethodParametersAndReturns()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static Area Multiply(Length left, Length right) => left * right;
+ public static Voltage Divide(Power power, Current current) => power / current;
+ public static InverseCurrent Invert(double numerator, Current current) => numerator / current;
+ }
+ """;
+
+ Compile(source);
+ }
+
+ [Fact]
+ public void RepeatedConsumerOperationIsEmittedOnce()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static Area First(Length left, Length right) => left * right;
+ public static Area Second(Length left, Length right) => left * right;
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+ INamedTypeSymbol extensions = compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")!;
+ Assert.Single(extensions.GetMembers().OfType(), method => method.MetadataName == "op_Multiply");
+ }
+
+
+ [Fact]
+ public void ArithmeticReadsFieldAndMethodReturnTypes()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static Current Input = Current.FromAmps(2);
+ public static Resistance GetResistance() => Resistance.FromOhms(4);
+ public static Power Calculate() => Input * Input * GetResistance();
+ }
+ """;
+ Compile(source);
+ }
+
+ [Fact]
+ public void DimensionPreservingScalarArithmeticUsesCoreOperatorsOnly()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static Current Scale(Current original, double factor) => factor * original / 2;
+ }
+ """;
+ CSharpCompilation compilation = Compile(source);
+
+ Assert.Null(compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions"));
+ }
+
+ [Fact]
+ public void GeneratorRefreshesArithmeticAfterAddingEditingAndRemovingConsumerTrees()
+ {
+ const string firstSource = """
+ public static class Consumer
+ {
+ public static CurrentSquared Value() => Current.FromAmps(2) * Current.FromAmps(3);
+ }
+ """;
+ const string addedSource = """
+ public static class Added
+ {
+ public static LengthTime Value() => Length.FromMetres(2) * Time.FromSeconds(3);
+ }
+ """;
+ const string editedSource = """
+ public static class Consumer
+ {
+ public static Resistance Value() => Voltage.FromVolts(12) / Current.FromAmps(3);
+ }
+ """;
+ CSharpCompilation initialInput = CreateInputCompilation(firstSource);
+ SyntaxTree initialTree = Assert.Single(initialInput.SyntaxTrees);
+ SyntaxTree addedTree = CSharpSyntaxTree.ParseText(addedSource, _parseOptions, path: "Added.cs", cancellationToken: TestContext.Current.CancellationToken);
+ SyntaxTree editedTree = CSharpSyntaxTree.ParseText(editedSource, _parseOptions, path: "Consumer.cs", cancellationToken: TestContext.Current.CancellationToken);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
+
+ driver = driver.RunGeneratorsAndUpdateCompilation(initialInput, out Compilation firstOutput, out ImmutableArray firstDiagnostics,
+ TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(firstDiagnostics),
+ () => Assert.Empty(firstOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.NotNull(firstOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")));
+
+ CSharpCompilation addedInput = initialInput.AddSyntaxTrees(addedTree);
+ driver = driver.RunGeneratorsAndUpdateCompilation(addedInput, out Compilation addedOutput, out ImmutableArray addedDiagnostics,
+ TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(addedDiagnostics),
+ () => Assert.Empty(addedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.NotNull(addedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
+ () => Assert.NotNull(addedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")));
+
+ CSharpCompilation editedInput = addedInput.ReplaceSyntaxTree(initialTree, editedTree);
+ driver = driver.RunGeneratorsAndUpdateCompilation(editedInput, out Compilation editedOutput, out ImmutableArray editedDiagnostics,
+ TestContext.Current.CancellationToken);
+ Dimension length = new(Length: 1);
+ Dimension time = new(Time: 1);
+ Dimension lengthTime = new(Length: 1, Time: 1);
+ Dimension voltage = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -1);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension resistance = new(Length: 2, Mass: 1, Time: -3, ElectricCurrent: -2);
+ ArithmeticOperation addedOperation = new(ArithmeticOperationKind.Multiply, length, time, lengthTime);
+ ArithmeticOperation editedOperation = new(ArithmeticOperationKind.Divide, voltage, current, resistance);
+ ArithmeticOperation[] editedOperators = ReadOperators(editedOutput);
+ Assert.Multiple(
+ () => Assert.Empty(editedDiagnostics),
+ () => Assert.Empty(editedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Null(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
+ () => Assert.NotNull(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")),
+ () => Assert.Equal(2, editedOperators.Length),
+ () => Assert.Contains(addedOperation, editedOperators),
+ () => Assert.Contains(editedOperation, editedOperators));
+
+ CSharpCompilation removedInput = editedInput.RemoveSyntaxTrees(addedTree);
+ driver = driver.RunGeneratorsAndUpdateCompilation(removedInput, out Compilation removedOutput, out ImmutableArray removedDiagnostics,
+ TestContext.Current.CancellationToken);
+ ArithmeticOperation[] removedOperators = ReadOperators(removedOutput);
+ Assert.Multiple(
+ () => Assert.Empty(removedDiagnostics),
+ () => Assert.Empty(removedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
+ () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")),
+ () => Assert.Equal(editedOperation, Assert.Single(removedOperators)));
+
+ CSharpCompilation emptyInput = removedInput.RemoveSyntaxTrees(editedTree);
+ driver.RunGeneratorsAndUpdateCompilation(emptyInput, out Compilation emptyOutput, out ImmutableArray emptyDiagnostics,
+ TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(emptyDiagnostics),
+ () => Assert.Empty(emptyOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")),
+ () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
+ () => Assert.Null(emptyOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthTimeExtensions")));
+ }
+
+ [Fact]
+ public void GeneratorIgnoresUnrelatedIncompleteAndDimensionlessArithmetic()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static int Number(int left, int right) => left * right;
+ public static object Incomplete(Length length) => length * Missing;
+ public static object Dimensionless(Length length) => length / length;
+ }
+ """;
+ CSharpCompilation input = CreateInputCompilation(source);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
+ driver.RunGeneratorsAndUpdateCompilation(input, out Compilation output, out ImmutableArray generatorDiagnostics,
+ TestContext.Current.CancellationToken);
+
+ Assert.Multiple(
+ () => Assert.Empty(generatorDiagnostics),
+ () => Assert.Null(output.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")));
+ }
+
+ [Theory]
+ [InlineData("var squared = current * current; var heating = squared * resistance;", "((Power)heating).AsWatts()", 36)]
+ [InlineData("var squared = current * current; var copy = squared; var heating = copy * resistance; var energy = heating * time;", "((Energy)energy).AsJoules()", 72)]
+ [InlineData("var squared = (2 * current) * current / 2; var heating = squared * resistance;", "((Power)heating).AsWatts()", 36)]
+ [InlineData("var squared = current * current; var fourth = squared * squared;", "fourth.AsSiBaseUnit()", 81)]
+ public void ArithmeticThroughLocalInitialisersCompilesAndExecutes(string declarations, string expression, double expected)
+ {
+ string source = $$"""
+ public static class Consumer
+ {
+ public static double Value()
+ {
+ Current current = Current.FromAmps(3);
+ Resistance resistance = Resistance.FromOhms(4);
+ Time time = Time.FromSeconds(2);
+ {{declarations}}
+ return {{expression}};
+ }
+ }
+ """;
+ double result = EvaluateSource(source);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void IdenticallyNamedLocalsInSeparateScopesRetainTheirOwnDimensions()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static double Value()
+ {
+ Current current = Current.FromAmps(3);
+ Resistance resistance = Resistance.FromOhms(4);
+ Time time = Time.FromSeconds(2);
+ double first;
+ double second;
+ {
+ var intermediate = current * current;
+ var result = intermediate * resistance;
+ first = result.AsWatts();
+ }
+ {
+ var intermediate = time * time;
+ var result = intermediate * current;
+ second = result.AsSiBaseUnit();
+ }
+ return first + second;
+ }
+ }
+ """;
+ double result = EvaluateSource(source);
+ double expected = 48;
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void LocalInitialiserEditsRefreshDependentOperatorsAndRemoveObsoleteOutput()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static double Value(Current current, Length length, Time time)
+ {
+ var product = current * current;
+ var result = product * time;
+ return result.AsSiBaseUnit();
+ }
+ }
+ """;
+ string editedSource = source.Replace("current * current", "length * length", StringComparison.Ordinal);
+ const string removedSource = "public static class Consumer { public static double Value() => 0; }";
+ CSharpCompilation input = CreateInputCompilation(source);
+ SyntaxTree originalTree = Assert.Single(input.SyntaxTrees);
+ SyntaxTree editedTree = CSharpSyntaxTree.ParseText(editedSource, _parseOptions, path: "Consumer.cs", cancellationToken: TestContext.Current.CancellationToken);
+ SyntaxTree removedTree = CSharpSyntaxTree.ParseText(removedSource, _parseOptions, path: "Consumer.cs", cancellationToken: TestContext.Current.CancellationToken);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
+ Dimension current = new(ElectricCurrent: 1);
+ Dimension squaredCurrent = new(ElectricCurrent: 2);
+ Dimension length = new(Length: 1);
+ Dimension area = new(Length: 2);
+ Dimension time = new(Time: 1);
+ ArithmeticOperation[] originalExpected =
+ [
+ new(ArithmeticOperationKind.Multiply, current, current, squaredCurrent),
+ new(ArithmeticOperationKind.Multiply, squaredCurrent, time, new(Time: 1, ElectricCurrent: 2))
+ ];
+ ArithmeticOperation[] editedExpected =
+ [
+ new(ArithmeticOperationKind.Multiply, length, length, area),
+ new(ArithmeticOperationKind.Multiply, area, time, new(Length: 2, Time: 1))
+ ];
+
+ driver = driver.RunGeneratorsAndUpdateCompilation(input, out Compilation originalOutput, out ImmutableArray originalDiagnostics, TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(originalDiagnostics),
+ () => Assert.Empty(originalOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Equal(originalExpected, ReadOperators(originalOutput)));
+
+ CSharpCompilation editedInput = input.ReplaceSyntaxTree(originalTree, editedTree);
+ driver = driver.RunGeneratorsAndUpdateCompilation(editedInput, out Compilation editedOutput, out ImmutableArray editedDiagnostics, TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(editedDiagnostics),
+ () => Assert.Empty(editedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Equal(editedExpected, ReadOperators(editedOutput)),
+ () => Assert.Null(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.CurrentSquaredExtensions")),
+ () => Assert.Null(editedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.TimeCurrentSquaredExtensions")));
+
+ CSharpCompilation removedInput = editedInput.ReplaceSyntaxTree(editedTree, removedTree);
+ driver.RunGeneratorsAndUpdateCompilation(removedInput, out Compilation removedOutput, out ImmutableArray removedDiagnostics, TestContext.Current.CancellationToken);
+ Assert.Multiple(
+ () => Assert.Empty(removedDiagnostics),
+ () => Assert.Empty(removedOutput.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)),
+ () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")),
+ () => Assert.Null(removedOutput.GetTypeByMetadataName("DimensionalQuantities.Generated.LengthSquaredTimeExtensions")));
+ }
+
+ [Fact]
+ public void CyclicLocalInitialisersDoNotCrashTheGenerator()
+ {
+ const string source = """
+ public static class Consumer
+ {
+ public static object Value(Current current)
+ {
+ var first = second * current;
+ var second = first * current;
+ return second;
+ }
+ }
+ """;
+ CSharpCompilation input = CreateInputCompilation(source);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
+ driver.RunGeneratorsAndUpdateCompilation(input, out Compilation output, out ImmutableArray diagnostics, TestContext.Current.CancellationToken);
+
+ Assert.Multiple(
+ () => Assert.Empty(diagnostics),
+ () => Assert.Null(output.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")));
+ }
+
private static T Evaluate(string expression)
{
string source = $$"""
@@ -90,6 +428,11 @@ public static class Consumer
public static global::{{typeof(T).FullName}} Value() => {{expression}};
}
""";
+ return EvaluateSource(source);
+ }
+
+ private static T EvaluateSource(string source)
+ {
CSharpCompilation compilation = Compile(source);
using MemoryStream assemblyBytes = new();
EmitResult emitResult = compilation.Emit(assemblyBytes, cancellationToken: TestContext.Current.CancellationToken);
@@ -155,4 +498,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
});
}
}
+
+ private static ArithmeticOperation[] ReadOperators(Compilation compilation)
+ {
+ INamedTypeSymbol extensions = compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")!;
+ QuantityTypeReader reader = new(compilation);
+ return extensions.GetMembers().OfType()
+ .Where(method => method.MetadataName is "op_Multiply" or "op_Division")
+ .Select(method =>
+ {
+ if (!reader.TryRead(method.Parameters[0].Type, out Dimension left, TestContext.Current.CancellationToken)
+ || !reader.TryRead(method.Parameters[1].Type, out Dimension right, TestContext.Current.CancellationToken)
+ || !reader.TryRead(method.ReturnType, out Dimension result, TestContext.Current.CancellationToken))
+ {
+ throw new InvalidOperationException("Generated operator has an unreadable quantity signature.");
+ }
+
+ ArithmeticOperationKind kind = method.MetadataName == "op_Multiply" ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
+ return new ArithmeticOperation(kind, left, right, result);
+ }).ToArray();
+ }
}
diff --git a/src/DimensionalQuantities.Roslyn/Discovery/ArithmeticDiscovery.cs b/src/DimensionalQuantities.Roslyn/Discovery/ArithmeticDiscovery.cs
index b69f29f..55eae2c 100644
--- a/src/DimensionalQuantities.Roslyn/Discovery/ArithmeticDiscovery.cs
+++ b/src/DimensionalQuantities.Roslyn/Discovery/ArithmeticDiscovery.cs
@@ -1,9 +1,7 @@
using System.Collections.Immutable;
-using DimensionalQuantities.Dimensions;
-using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Operations;
+using Microsoft.CodeAnalysis;
namespace DimensionalQuantities.Roslyn.Discovery;
@@ -27,98 +25,9 @@ public static bool IsRoot(BinaryExpressionSyntax binary)
public static ImmutableArray Analyse(ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken)
{
- QuantityTypeReader typeReader = new(semanticModel.Compilation);
- ImmutableArray.Builder operations = ImmutableArray.CreateBuilder();
- AnalyseExpression(expression, semanticModel, typeReader, operations, cancellationToken);
- return operations.ToImmutable();
- }
-
- private static Operand AnalyseExpression(ExpressionSyntax expression, SemanticModel semanticModel, QuantityTypeReader typeReader,
- ImmutableArray.Builder operations, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- if (expression is ParenthesizedExpressionSyntax parenthesized)
- {
- return AnalyseExpression(parenthesized.Expression, semanticModel, typeReader, operations, cancellationToken);
- }
-
- if (expression is BinaryExpressionSyntax binary && IsSupported(binary))
- {
- Operand left = AnalyseExpression(binary.Left, semanticModel, typeReader, operations, cancellationToken);
- Operand right = AnalyseExpression(binary.Right, semanticModel, typeReader, operations, cancellationToken);
- return Combine(binary, left, right, operations);
- }
-
- return ClassifyLeaf(expression, semanticModel, typeReader, cancellationToken);
- }
-
- private static Operand Combine(BinaryExpressionSyntax binary, Operand left, Operand right, ImmutableArray.Builder operations)
- {
- if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Scalar)
- {
- return new(OperandKind.Scalar);
- }
-
- if (left.Kind == OperandKind.Quantity && right.Kind == OperandKind.Scalar)
- {
- return left;
- }
-
- if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Quantity)
- {
- if (binary.IsKind(SyntaxKind.MultiplyExpression))
- {
- return right;
- }
-
- Dimension inverse = right.Dimension.Pow(-1);
- operations.Add(new(ArithmeticOperationKind.ScalarDivide, default, right.Dimension, inverse));
- return new(OperandKind.Quantity, inverse);
- }
-
- if (left.Kind == OperandKind.Quantity && right.Kind == OperandKind.Quantity)
- {
- ArithmeticOperationKind kind = binary.IsKind(SyntaxKind.MultiplyExpression) ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
- Dimension result = kind == ArithmeticOperationKind.Multiply ? left.Dimension * right.Dimension : left.Dimension / right.Dimension;
- if (result != default)
- {
- operations.Add(new(kind, left.Dimension, right.Dimension, result));
- return new(OperandKind.Quantity, result);
- }
- }
-
- return default;
- }
-
- private static Operand ClassifyLeaf(ExpressionSyntax expression, SemanticModel semanticModel, QuantityTypeReader typeReader, CancellationToken cancellationToken)
- {
- ITypeSymbol? type = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
- if (typeReader.TryRead(type, out Dimension dimension, cancellationToken))
- {
- return dimension == default ? default : new(OperandKind.Quantity, dimension);
- }
-
- ITypeSymbol doubleType = semanticModel.Compilation.GetSpecialType(SpecialType.System_Double);
- if (type is not null)
- {
- CommonConversion conversion = semanticModel.Compilation.ClassifyCommonConversion(type, doubleType);
- if (conversion.IsImplicit && (conversion.IsIdentity || conversion.IsNumeric) && !conversion.IsUserDefined)
- {
- return new(OperandKind.Scalar);
- }
- }
-
- return default;
+ QuantityExpressionAnalyser analyser = new(semanticModel);
+ return analyser.Analyse(expression, cancellationToken);
}
private static bool IsSupported(BinaryExpressionSyntax binary) => binary.IsKind(SyntaxKind.MultiplyExpression) || binary.IsKind(SyntaxKind.DivideExpression);
-
- private enum OperandKind
- {
- Unsupported,
- Scalar,
- Quantity
- }
-
- private readonly record struct Operand(OperandKind Kind, Dimension Dimension = default);
}
diff --git a/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs b/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs
new file mode 100644
index 0000000..d020aa9
--- /dev/null
+++ b/src/DimensionalQuantities.Roslyn/Discovery/QuantityExpressionAnalyser.cs
@@ -0,0 +1,169 @@
+using System.Collections.Immutable;
+using DimensionalQuantities.Dimensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace DimensionalQuantities.Roslyn.Discovery;
+
+internal sealed class QuantityExpressionAnalyser
+{
+ private readonly SemanticModel _semanticModel;
+
+ public QuantityExpressionAnalyser(SemanticModel semanticModel)
+ {
+ _semanticModel = semanticModel;
+ }
+
+ public ImmutableArray Analyse(ExpressionSyntax expression, CancellationToken cancellationToken)
+ {
+ Analysis analysis = new(_semanticModel, cancellationToken);
+ return analysis.Analyse(expression);
+ }
+
+ private sealed class Analysis
+ {
+ private readonly SemanticModel _semanticModel;
+ private readonly QuantityTypeReader _typeReader;
+ private readonly CancellationToken _cancellationToken;
+ private readonly ImmutableArray.Builder _operations = ImmutableArray.CreateBuilder();
+ private readonly Dictionary _locals = new(SymbolEqualityComparer.Default);
+ private readonly HashSet _inProgress = new(SymbolEqualityComparer.Default);
+
+ public Analysis(SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ _semanticModel = semanticModel;
+ _typeReader = new(semanticModel.Compilation);
+ _cancellationToken = cancellationToken;
+ }
+
+ public ImmutableArray Analyse(ExpressionSyntax expression)
+ {
+ AnalyseExpression(expression);
+ return _operations.ToImmutable();
+ }
+
+ private Operand AnalyseExpression(ExpressionSyntax expression)
+ {
+ _cancellationToken.ThrowIfCancellationRequested();
+ if (expression is ParenthesizedExpressionSyntax parenthesized)
+ {
+ return AnalyseExpression(parenthesized.Expression);
+ }
+
+ if (expression is BinaryExpressionSyntax binary && IsSupported(binary))
+ {
+ Operand left = AnalyseExpression(binary.Left);
+ Operand right = AnalyseExpression(binary.Right);
+ return Combine(binary, left, right);
+ }
+
+ return ClassifyLeaf(expression);
+ }
+
+ private Operand ClassifyLeaf(ExpressionSyntax expression)
+ {
+ ITypeSymbol? type = _semanticModel.GetTypeInfo(expression, _cancellationToken).Type;
+ if (_typeReader.TryRead(type, out Dimension dimension, _cancellationToken))
+ {
+ return dimension == default ? default : new(OperandKind.Quantity, dimension);
+ }
+
+ ITypeSymbol doubleType = _semanticModel.Compilation.GetSpecialType(SpecialType.System_Double);
+ if (type is not null)
+ {
+ CommonConversion conversion = _semanticModel.Compilation.ClassifyCommonConversion(type, doubleType);
+ if (conversion.IsImplicit && (conversion.IsIdentity || conversion.IsNumeric) && !conversion.IsUserDefined)
+ {
+ return new(OperandKind.Scalar);
+ }
+ }
+
+ return expression is IdentifierNameSyntax identifier
+ && _semanticModel.GetSymbolInfo(identifier, _cancellationToken).Symbol is ILocalSymbol local
+ ? ResolveLocal(local) : default;
+ }
+
+ private Operand ResolveLocal(ILocalSymbol local)
+ {
+ if (_locals.TryGetValue(local, out Operand cached))
+ {
+ return cached;
+ }
+
+ if (!_inProgress.Add(local))
+ {
+ return default;
+ }
+
+ try
+ {
+ Operand result = default;
+ if (local.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(_cancellationToken) is VariableDeclaratorSyntax variable
+ && variable.Parent is VariableDeclarationSyntax declaration && declaration.Type.IsVar
+ && _semanticModel.GetSymbolInfo(declaration.Type, _cancellationToken).Symbol is null
+ && variable.Initializer is { Value: ExpressionSyntax initializer })
+ {
+ result = AnalyseExpression(initializer);
+ }
+
+ _locals.Add(local, result);
+ return result;
+ }
+ finally
+ {
+ _inProgress.Remove(local);
+ }
+ }
+
+ private Operand Combine(BinaryExpressionSyntax binary, Operand left, Operand right)
+ {
+ if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Scalar)
+ {
+ return new(OperandKind.Scalar);
+ }
+
+ if (left.Kind == OperandKind.Quantity && right.Kind == OperandKind.Scalar)
+ {
+ return left;
+ }
+
+ if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Quantity)
+ {
+ if (binary.IsKind(SyntaxKind.MultiplyExpression))
+ {
+ return right;
+ }
+
+ Dimension inverse = right.Dimension.Pow(-1);
+ _operations.Add(new(ArithmeticOperationKind.ScalarDivide, default, right.Dimension, inverse));
+ return new(OperandKind.Quantity, inverse);
+ }
+
+ if (left.Kind == OperandKind.Quantity && right.Kind == OperandKind.Quantity)
+ {
+ ArithmeticOperationKind kind = binary.IsKind(SyntaxKind.MultiplyExpression) ? ArithmeticOperationKind.Multiply : ArithmeticOperationKind.Divide;
+ Dimension result = kind == ArithmeticOperationKind.Multiply ? left.Dimension * right.Dimension : left.Dimension / right.Dimension;
+ if (result != default)
+ {
+ _operations.Add(new(kind, left.Dimension, right.Dimension, result));
+ return new(OperandKind.Quantity, result);
+ }
+ }
+
+ return default;
+ }
+
+ private static bool IsSupported(BinaryExpressionSyntax binary) => binary.IsKind(SyntaxKind.MultiplyExpression) || binary.IsKind(SyntaxKind.DivideExpression);
+
+ private enum OperandKind
+ {
+ Unsupported,
+ Scalar,
+ Quantity
+ }
+
+ private readonly record struct Operand(OperandKind Kind, Dimension Dimension = default);
+ }
+}