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 @@ -90,8 +90,7 @@ public static class Consumer
[InlineData("a * 1m")]
[InlineData("a * \"text\"")]
[InlineData("a + b")]
[InlineData("a / a")]
public void DiscoverySkipsUnsupportedOrDimensionlessExpressions(string expression)
public void DiscoverySkipsUnsupportedExpressions(string expression)
{
string source = $$"""
public static class Consumer
Expand Down Expand Up @@ -265,7 +264,6 @@ public static object Test(Current current)
[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 = $$"""
Expand Down Expand Up @@ -460,6 +458,89 @@ public static object Test(Wrapper wrapper, Time time)
Assert.Equal(expected, Assert.Single(result));
}

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

Assert.Equal(expected, result);
}

[Theory]
[InlineData("var cancelled = current * inverse;", "cancelled * length * time")]
[InlineData("var cancelled = current * inverse; var copy = cancelled;", "copy * length * time")]
[InlineData("var cancelled = current * inverse;", "(cancelled - 2) * length * time")]
public void InverseProductsProduceScalarsForLaterArithmetic(string declaration, string expression)
{
string source = $$"""
public static class Consumer
{
public static object Test(Current current, Length length, Time time)
{
var inverse = 2 / current;
{{declaration}}
return {{expression}};
}
}
""";
ImmutableArray<ArithmeticOperation> result = AnalyseReturn(source);
Dimension current = new(ElectricCurrent: 1);
Dimension inverse = new(ElectricCurrent: -1);
Dimension length = new(Length: 1);
Dimension time = new(Time: 1);
ArithmeticOperation[] expected =
[
new(ArithmeticOperationKind.ScalarDivide, default, current, inverse),
new(ArithmeticOperationKind.Multiply, current, inverse, default),
new(ArithmeticOperationKind.Multiply, length, time, new(Length: 1, Time: 1))
];

Assert.Equal(expected, 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 @@ -52,6 +52,30 @@ public static class Consumer
() => Assert.Equal(4, divided));
}

[Theory]
[InlineData("Current.FromAmps(3) * InverseCurrent.FromSiBaseUnit(2)", false)]
[InlineData("InverseCurrent.FromSiBaseUnit(2) * Current.FromAmps(3)", true)]
public void CancellingProductEmitsAnOperatorReturningDouble(string expression, bool inverseFirst)
{
Dimension current = new(ElectricCurrent: 1);
Dimension inverse = new(ElectricCurrent: -1);
ArithmeticOperation operation = inverseFirst
? new(ArithmeticOperationKind.Multiply, inverse, current, default)
: new(ArithmeticOperationKind.Multiply, current, inverse, default);
QuantityGenerationPlan plan = QuantityDiscovery.Discover([operation]);
string source = $$"""
public static class Consumer
{
public static double Value() => {{expression}};
}
""";
Assembly assembly = CompileAndEmit(source, plan);
double result = (double)assembly.GetType("Consumer")!.GetMethod("Value")!.Invoke(null, null)!;
double expected = 6;

Assert.Equal(expected, result);
}

private static Assembly CompileAndEmit(string source, QuantityGenerationPlan plan)
{
string[] assemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ public void PlanKeepsCatalogueNameAndDeduplicatesOrderedOperations()
() => Assert.Single(plan.AdditionalQuantities));
}

[Fact]
public void CancellingProductPlansOperandsWithoutADimensionlessQuantity()
{
Dimension current = new(ElectricCurrent: 1);
Dimension inverse = new(ElectricCurrent: -1);
ArithmeticOperation operation = new(ArithmeticOperationKind.Multiply, current, inverse, default);
QuantityGenerationPlan result = QuantityDiscovery.Discover([operation, operation]);

Assert.Multiple(
() => Assert.Equal(operation, Assert.Single(result.Operations)),
() => Assert.Equal(inverse, Assert.Single(result.AdditionalQuantities).Dimension),
() => Assert.DoesNotContain(result.KnownQuantities, quantity => quantity.Dimension == default));
}

[Fact]
public void UnknownQuantityUsesSiComponentPowersAndBasePrefixes()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,13 @@ public static class Consumer
}

[Fact]
public void GeneratorIgnoresUnrelatedIncompleteAndDimensionlessArithmetic()
public void GeneratorIgnoresUnrelatedAndIncompleteArithmetic()
{
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);
Expand Down Expand Up @@ -502,6 +501,47 @@ public static class Consumer
() => Assert.Equal("CS0019", Assert.Single(errors).Id));
}

[Theory]
[InlineData("Length.FromMetres(3) / Length.FromMilliMetres(500)", 6)]
[InlineData("Energy.FromJoules(6) / Torque.FromNewtonMetres(2)", 3)]
[InlineData("Current.FromAmps(3) * (2 / Current.FromAmps(3))", 2)]
[InlineData("(2 / Current.FromAmps(3)) * Current.FromAmps(3)", 2)]
[InlineData("(Current.FromAmps(3) * Current.FromAmps(3)) / (Current.FromAmps(2) * Current.FromAmps(2))", 2.25)]
[InlineData("(Length.FromMetres(3) / Length.FromMetres(1)) + (Current.FromAmps(4) / Current.FromAmps(2))", 5)]
public void CancelledDimensionsCompileAndExecuteAsDouble(string expression, double expected)
{
double result = Evaluate<double>(expression);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("var ratio = first / second;", "(ratio * time).AsSeconds()", 6)]
[InlineData("var firstSquared = first * first; var secondSquared = second * second; var ratio = firstSquared / secondSquared;", "(ratio * time).AsSeconds()", 9)]
[InlineData("var inverse = 2 / first; var ratio = first * inverse; var copy = ratio;", "(copy * time).AsSeconds()", 8)]
[InlineData("var inverse = 2 / first; var ratio = first * inverse;", "(time / ratio).AsSeconds()", 2)]
[InlineData("var inverse = 2 / first; var ratio = first * inverse; var scaled = (ratio + 1) * first;", "(scaled * time).AsSiBaseUnit()", 36)]
[InlineData("var ratio = first / second; var inverseTime = ratio / time;", "inverseTime.AsSiBaseUnit()", 0.375)]
public void CancelledLocalsCanBeUsedAsScalarOperands(string declarations, string expression, double expected)
{
string source = $$"""
public static class Consumer
{
public static double Value()
{
Current first = Current.FromAmps(3);
Current second = Current.FromAmps(2);
Time time = Time.FromSeconds(4);
{{declarations}}
return {{expression}};
}
}
""";
double result = EvaluateSource<double>(source);

Assert.Equal(expected, result);
}

private static T Evaluate<T>(string expression)
{
string source = $$"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,18 @@ private Operand CombineProduct(BinaryExpressionSyntax binary, Operand left, Oper
{
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)
if (result == default && kind == ArithmeticOperationKind.Divide)
{
_operations.Add(new(kind, left.Dimension, right.Dimension, result));
return new(OperandKind.Quantity, result);
return new(OperandKind.Scalar);
}

_operations.Add(new(kind, left.Dimension, right.Dimension, result));
if (result == default)
{
return new(OperandKind.Scalar);
}

return new(OperandKind.Quantity, result);
}

return default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ internal static class ArithmeticExtensions
private static string RenderOperator(ArithmeticOperation operation, QuantityGenerationPlan plan)
{
string right = plan.GetQuantityName(operation.Right);
if (operation.Kind == ArithmeticOperationKind.Multiply && operation.Result == default)
{
string cancellingLeft = plan.GetQuantityName(operation.Left);
return $$"""
extension({{cancellingLeft}})
{
public static double operator *({{cancellingLeft}} left, {{right}} right) =>
left.AsSiBaseUnit() * right.AsSiBaseUnit();
}
""";
}

string result = plan.GetQuantityName(operation.Result);
if (operation.Kind == ArithmeticOperationKind.ScalarDivide)
{
Expand Down
13 changes: 13 additions & 0 deletions src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ namespace DimensionalQuantities.Tests.TypeSystem;

public sealed class QuantityTests
{
[Theory]
[InlineData(6, 2, 3)]
[InlineData(-6, 2, -3)]
[InlineData(0, 2, 0)]
public void DividingMatchingDimensionsReturnsDouble(double numerator, double denominator, double expected)
{
SquareRootLength original = SquareRootLength.FromSiBaseUnit(numerator);
SquareRootLength divisor = SquareRootLength.FromSiBaseUnit(denominator);
double result = original / divisor;

Assert.Equal(expected, result);
}

[Theory]
[InlineData(3, 2, 5)]
[InlineData(-3, 2, -1)]
Expand Down
1 change: 1 addition & 0 deletions src/DimensionalQuantities/TypeSystem/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public readonly record struct Quantity<TDimension>
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);
public static double operator /(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit / right.ValueInSiBaseUnit;

// Each closed quantity type intentionally caches its own decoded dimension.
// ReSharper disable once StaticMemberInGenericType
Expand Down
Loading