Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,134 @@ public static object Test(Current current, Resistance resistance, Length length)
Assert.Equal(expected, result);
}

[Theory]
[InlineData("var squared = current * current; var combined = squared + squared;", "combined * resistance")]
[InlineData("var squared = current * current; var combined = squared - squared;", "combined * resistance")]
[InlineData("var squared = current * current; var combined = -squared;", "combined * resistance")]
[InlineData("var squared = current * current;", "(-(squared + squared)) * resistance")]
public void SameDimensionArithmeticPropagatesInferredLocalDimensions(string declarations, string expression)
{
string source = $$"""
public static class Consumer
{
public static object Test(Current current, Resistance resistance)
{
{{declarations}}
return {{expression}};
}
}
""";
ImmutableArray<ArithmeticOperation> 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 product = new(ArithmeticOperationKind.Multiply, current, current, squared);
ArithmeticOperation heating = new(ArithmeticOperationKind.Multiply, squared, resistance, power);
ArithmeticOperation[] expected = [product, heating];

Assert.Equal(expected, result);
}

[Theory]
[InlineData("current + length")]
[InlineData("current - length")]
[InlineData("current + 2")]
[InlineData("2 + current")]
[InlineData("current - 2")]
[InlineData("2 - current")]
[InlineData("current + missing")]
public void InvalidSumsDoNotSupplyDimensionsToEnclosingArithmetic(string sum)
{
string source = $$"""
public static class Consumer
{
public static object Test(Current current, Length length, Time time) => ({{sum}}) * time;
}
""";
ImmutableArray<ArithmeticOperation> result = Analyse(Compile(source));

Assert.Empty(result);
}

[Theory]
[InlineData("squared + area")]
[InlineData("squared - area")]
public void InvalidInferredSumsRetainChildOperationsWithoutInferringAResult(string expression)
{
string source = $$"""
public static class Consumer
{
public static object Test(Current current, Length length, Time time)
{
var squared = current * current;
var area = length * length;
var invalid = {{expression}};
return invalid * time;
}
}
""";
ImmutableArray<ArithmeticOperation> result = AnalyseReturn(source);
Dimension current = new(ElectricCurrent: 1);
Dimension length = new(Length: 1);
ArithmeticOperation[] expected =
[
new(ArithmeticOperationKind.Multiply, current, current, new(ElectricCurrent: 2)),
new(ArithmeticOperationKind.Multiply, length, length, new(Length: 2))
];

Assert.Equal(expected, result);
}

[Theory]
[InlineData("(2 - 3) * current * time")]
[InlineData("-(2 + 3) * current * time")]
[InlineData("(-current) * time")]
public void ScalarSumsAndNegationPreserveDimensions(string expression)
{
string source = $$"""
public static class Consumer
{
public static object Test(Current current, Time time) => {{expression}};
}
""";
ImmutableArray<ArithmeticOperation> result = Analyse(Compile(source));
Dimension current = new(ElectricCurrent: 1);
Dimension time = new(Time: 1);
ArithmeticOperation expected = new(ArithmeticOperationKind.Multiply, current, time, new(Time: 1, ElectricCurrent: 1));

Assert.Equal(expected, Assert.Single(result));
}

[Theory]
[InlineData("(wrapper + wrapper) * time")]
[InlineData("(wrapper - wrapper) * time")]
[InlineData("(-wrapper) * time")]
public void BoundOperatorReturnTypesRemainAuthoritative(string expression)
{
string source = $$"""
public sealed class Wrapper
{
public static Current operator +(Wrapper left, Wrapper right) => Current.FromAmps(2);
public static Current operator -(Wrapper left, Wrapper right) => Current.FromAmps(1);
public static Current operator -(Wrapper value) => Current.FromAmps(-1);
}
public static class Consumer
{
public static object Test(Wrapper wrapper, Time time)
{
return {{expression}};
}
}
""";
ImmutableArray<ArithmeticOperation> result = AnalyseReturn(source);
Dimension current = new(ElectricCurrent: 1);
Dimension time = new(Time: 1);
ArithmeticOperation expected = new(ArithmeticOperationKind.Multiply, current, time, new(Time: 1, ElectricCurrent: 1));

Assert.Equal(expected, Assert.Single(result));
}

private static ImmutableArray<ArithmeticOperation> Analyse(CSharpCompilation compilation)
{
SyntaxTree tree = compilation.SyntaxTrees.Single(candidate => candidate.FilePath == "Consumer.cs");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,88 @@ public static object Value(Current current)
() => Assert.Null(output.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")));
}

[Theory]
[InlineData("Current.FromAmps(2) + Current.FromMilliAmps(500)", "AsAmps", 2.5)]
[InlineData("Current.FromAmps(2) - Current.FromMilliAmps(500)", "AsAmps", 1.5)]
[InlineData("-Current.FromMilliAmps(500)", "AsAmps", -0.5)]
[InlineData("Energy.FromJoules(2) + Torque.FromNewtonMetres(3)", "AsJoules", 5)]
public void SameDimensionArithmeticUsesCoreOperatorsWithoutGeneratedOverloads(string expression, string accessor, double expected)
{
string source = $$"""
public static class Consumer
{
public static double Value() => ({{expression}}).{{accessor}}();
}
""";
double result = EvaluateSource<double>(source);
CSharpCompilation compilation = Compile(source);

Assert.Multiple(
() => Assert.Equal(expected, result),
() => Assert.Null(compilation.GetTypeByMetadataName("DimensionalQuantities.Generated.ArithmeticExtensions")));
}

[Theory]
[InlineData("firstSquared + secondSquared", 52)]
[InlineData("firstSquared - secondSquared", 20)]
[InlineData("-firstSquared", -36)]
public void SameDimensionArithmeticThroughInferredLocalsCompilesAndExecutes(string expression, double expected)
{
string source = $$"""
public static class Consumer
{
public static double Value()
{
Current first = Current.FromAmps(3);
Current second = Current.FromAmps(2);
Resistance resistance = Resistance.FromOhms(4);
var firstSquared = first * first;
var secondSquared = second * second;
var combined = {{expression}};
var heating = combined * resistance;
return ((Power)heating).AsWatts();
}
}
""";
double result = EvaluateSource<double>(source);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("((Current.FromAmps(3) * Current.FromAmps(3) + Current.FromAmps(2) * Current.FromAmps(2)) * Resistance.FromOhms(4)).AsWatts()", 52)]
[InlineData("((Current.FromAmps(3) * Current.FromAmps(3) - Current.FromAmps(2) * Current.FromAmps(2)) * Resistance.FromOhms(4)).AsWatts()", 20)]
[InlineData("(-(Current.FromAmps(3) * Current.FromAmps(3))).AsSiBaseUnit()", -9)]
public void SameDimensionArithmeticAroundNestedProductsCompilesAndExecutes(string expression, double expected)
{
double result = Evaluate<double>(expression);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("current + length")]
[InlineData("current - length")]
[InlineData("current + 2")]
[InlineData("2 - current")]
public void InvalidSameDimensionArithmeticRemainsACompilerError(string expression)
{
string source = $$"""
public static class Consumer
{
public static object Value(Current current, Length length) => {{expression}};
}
""";
CSharpCompilation input = CreateInputCompilation(source);
GeneratorDriver driver = CSharpGeneratorDriver.Create([new QuantityGenerator().AsSourceGenerator()], parseOptions: _parseOptions);
driver.RunGeneratorsAndUpdateCompilation(input, out Compilation output, out ImmutableArray<Diagnostic> diagnostics, TestContext.Current.CancellationToken);
Diagnostic[] errors = output.GetDiagnostics(TestContext.Current.CancellationToken).Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToArray();

Assert.Multiple(
() => Assert.Empty(diagnostics),
() => Assert.Equal("CS0019", Assert.Single(errors).Id));
}

private static T Evaluate<T>(string expression)
{
string source = $$"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,19 @@ private Operand AnalyseExpression(ExpressionSyntax expression)
{
Operand left = AnalyseExpression(binary.Left);
Operand right = AnalyseExpression(binary.Right);
return Combine(binary, left, right);
if (binary.IsKind(SyntaxKind.AddExpression) || binary.IsKind(SyntaxKind.SubtractExpression))
{
return AnalyseSum(binary, left, right);
}

return CombineProduct(binary, left, right);
}

if (expression is PrefixUnaryExpressionSyntax unary && unary.IsKind(SyntaxKind.UnaryMinusExpression))
{
Operand operand = AnalyseExpression(unary.Operand);
Operand bound = ClassifyLeaf(unary);
return bound.Kind != OperandKind.Unsupported ? bound : operand;
}

return ClassifyLeaf(expression);
Expand Down Expand Up @@ -117,7 +129,24 @@ private Operand ResolveLocal(ILocalSymbol local)
}
}

private Operand Combine(BinaryExpressionSyntax binary, Operand left, Operand right)
private Operand AnalyseSum(BinaryExpressionSyntax binary, Operand left, Operand right)
{
Operand bound = ClassifyLeaf(binary);
if (bound.Kind != OperandKind.Unsupported)
{
return bound;
}

if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Scalar)
{
return new(OperandKind.Scalar);
}

return left.Kind == OperandKind.Quantity && right.Kind == OperandKind.Quantity && left.Dimension == right.Dimension
? left : default;
}

private Operand CombineProduct(BinaryExpressionSyntax binary, Operand left, Operand right)
{
if (left.Kind == OperandKind.Scalar && right.Kind == OperandKind.Scalar)
{
Expand Down Expand Up @@ -155,7 +184,8 @@ private Operand Combine(BinaryExpressionSyntax binary, Operand left, Operand rig
return default;
}

private static bool IsSupported(BinaryExpressionSyntax binary) => binary.IsKind(SyntaxKind.MultiplyExpression) || binary.IsKind(SyntaxKind.DivideExpression);
private static bool IsSupported(BinaryExpressionSyntax binary) => binary.IsKind(SyntaxKind.MultiplyExpression) || binary.IsKind(SyntaxKind.DivideExpression)
|| binary.IsKind(SyntaxKind.AddExpression) || binary.IsKind(SyntaxKind.SubtractExpression);

private enum OperandKind
{
Expand Down
41 changes: 41 additions & 0 deletions src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,47 @@ namespace DimensionalQuantities.Tests.TypeSystem;

public sealed class QuantityTests
{
[Theory]
[InlineData(3, 2, 5)]
[InlineData(-3, 2, -1)]
[InlineData(3, -3, 0)]
public void AdditionPreservesDimensionAndAddsSiMagnitudes(double first, double second, double expectedValue)
{
Current original = Current.FromSiBaseUnit(first);
Current other = Current.FromSiBaseUnit(second);
Current result = original + other;
Current expected = Current.FromSiBaseUnit(expectedValue);

Assert.Equal(expected, result);
}

[Theory]
[InlineData(3, 2, 1)]
[InlineData(2, 3, -1)]
[InlineData(-3, -3, 0)]
public void SubtractionPreservesDimensionAndSubtractsSiMagnitudes(double first, double second, double expectedValue)
{
Current original = Current.FromSiBaseUnit(first);
Current other = Current.FromSiBaseUnit(second);
Current result = original - other;
Current expected = Current.FromSiBaseUnit(expectedValue);

Assert.Equal(expected, result);
}

[Theory]
[InlineData(3, -3)]
[InlineData(-3, 3)]
[InlineData(0, 0)]
public void NegationPreservesDimensionAndReversesSign(double value, double expectedValue)
{
SquareRootLength original = SquareRootLength.FromSiBaseUnit(value);
SquareRootLength result = -original;
SquareRootLength expected = SquareRootLength.FromSiBaseUnit(expectedValue);

Assert.Equal(expected, result);
}

[Fact]
public void MultiplyingAndDividingByScalarsPreservesQuantityTypeAndSiMagnitude()
{
Expand Down
3 changes: 3 additions & 0 deletions src/DimensionalQuantities/TypeSystem/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ namespace DimensionalQuantities.TypeSystem;
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public readonly record struct Quantity<TDimension>
{
public static Quantity<TDimension> operator +(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit + right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit - right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> quantity) => new(-quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator *(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit * scalar);
public static Quantity<TDimension> operator *(double scalar, Quantity<TDimension> quantity) => new(scalar * quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator /(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit / scalar);
Expand Down
Loading