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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Product>Simpra</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>2.0.3</Version>
<Version>2.1.4</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
6 changes: 3 additions & 3 deletions src/AltaSoft.Simpra/InternalLanguageFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ internal static class InternalLanguageFunctions
public static SimpraString @string(SimpraBool input) => new(input.ToString());
public static SimpraString @string(SimpraDate input) => new(input.ToString());

public static SimpraNumber number(SimpraString input) => new(input.Value);
public static SimpraNumber number(SimpraBool input) => new(input.Value);
public static SimpraNumber number(SimpraString input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue;
public static SimpraNumber number(SimpraBool input) => input.HasValue ? new SimpraNumber(input.Value) : (SimpraNumber)SimpraNumber.NoValue;

public static SimpraDate date(SimpraString input) => new(input.Value);
public static SimpraDate date(SimpraString input) => input.HasValue ? new SimpraDate(input.Value) : (SimpraDate)SimpraDate.NoValue;

#pragma warning restore IDE1006 // Naming Styles
// ReSharper enable InconsistentNaming
Expand Down
24 changes: 12 additions & 12 deletions src/AltaSoft.Simpra/Types/SimpraNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ private void SetValue(decimal? value)
public static implicit operator decimal(SimpraNumber value) => value.Value;
public static implicit operator bool(SimpraNumber value) => value.Value != 0m;

public static implicit operator sbyte?(SimpraNumber value) => (sbyte?)value.Value;
public static implicit operator byte?(SimpraNumber value) => (byte?)value.Value;
public static implicit operator short?(SimpraNumber value) => (short?)value.Value;
public static implicit operator ushort?(SimpraNumber value) => (ushort?)value.Value;
public static implicit operator int?(SimpraNumber value) => (int?)value.Value;
public static implicit operator uint?(SimpraNumber value) => (uint?)value.Value;
public static implicit operator long?(SimpraNumber value) => (long?)value.Value;
public static implicit operator ulong?(SimpraNumber value) => (ulong?)value.Value;
public static implicit operator float?(SimpraNumber value) => (float?)value.Value;
public static implicit operator double?(SimpraNumber value) => (double?)value.Value;
public static implicit operator decimal?(SimpraNumber value) => value.Value;
public static implicit operator sbyte?(SimpraNumber value) => value.HasValue ? (sbyte?)value.Value : null;
public static implicit operator byte?(SimpraNumber value) => value.HasValue ? (byte?)value.Value : null;
public static implicit operator short?(SimpraNumber value) => value.HasValue ? (short?)value.Value : null;
public static implicit operator ushort?(SimpraNumber value) => value.HasValue ? (ushort?)value.Value : null;
public static implicit operator int?(SimpraNumber value) => value.HasValue ? (int?)value.Value : null;
public static implicit operator uint?(SimpraNumber value) => value.HasValue ? (uint?)value.Value : null;
public static implicit operator long?(SimpraNumber value) => value.HasValue ? (long?)value.Value : null;
public static implicit operator ulong?(SimpraNumber value) => value.HasValue ? (ulong?)value.Value : null;
public static implicit operator float?(SimpraNumber value) => value.HasValue ? (float?)value.Value : null;
public static implicit operator double?(SimpraNumber value) => value.HasValue ? (double?)value.Value : null;
public static implicit operator decimal?(SimpraNumber value) => value.HasValue ? value.Value : null;
public static implicit operator bool?(SimpraNumber value) => value.HasValue ? value.Value != 0m : null;

public static implicit operator SimpraNumber(sbyte value) => new(value);
Expand Down Expand Up @@ -113,7 +113,7 @@ public override readonly bool Equals(object? obj)
{
if (obj is null)
return !HasValue;
if (obj is SimpraDate simpraVar)
if (obj is SimpraNumber simpraVar)
return Equals(simpraVar);

return obj.GetType().IsNumber() && Value == Convert.ToDecimal(obj, CultureInfo.InvariantCulture);
Expand Down
19 changes: 17 additions & 2 deletions src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using AltaSoft.DomainPrimitives;
using AltaSoft.Simpra.Types;
using Antlr4.Runtime.Tree;

Expand Down Expand Up @@ -229,9 +230,23 @@ public Expression VisitMemberAccess(SimpraParser.MemberAccessContext context)
var exprProperty2 = Expression.Property(variable, propertyName);
var notNull = Expression.NotEqual(variable, Expression.Constant(null, exprBaseObject.Type));

var defaultValue = Expression.Default(exprProperty2.Type);
// Domain primitives throw when their uninitialized default value is read, so if the property
// is a domain primitive, unwrap it to its underlying type before building the null-fallback
// default. Otherwise the fallback would be an uninitialized domain primitive that throws as
// soon as ConvertToSimpraType tries to convert it.
Expression truePart = exprProperty2;
Expression defaultValue;
if (exprProperty2.Type.TryGetUnderlyingDomainPrimitiveType(out var domainType))
{
truePart = Expression.Convert(exprProperty2, domainType);
defaultValue = Expression.Default(domainType);
}
else
{
defaultValue = Expression.Default(exprProperty2.Type);
}

var expr = Expression.Condition(notNull, exprProperty2, defaultValue);
var expr = Expression.Condition(notNull, truePart, defaultValue);

var block = Expression.Block([variable], assignExpr, expr);

Expand Down
3 changes: 2 additions & 1 deletion src/AltaSoft.Simpra/Visitor/SimpraParserVisitor.tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,13 @@ private Expression CallFunction(string methodName, Expression[] arguments, Parse
if (method is null)
throw new SimpraException(context, $"Function '{methodName}({string.Join(',', paramTypes.Select(x => x.Name))})' not found");

var methodParameters = method.GetParameters();
var convertedArguments = new Expression[paramTypes.Length + (matchWithToken ? 1 : 0)];
for (var i = 0; i < paramTypes.Length; i++)
{
var expr = arguments[i];

convertedArguments[i] = ConvertToType(expr, paramTypes[i], context);
convertedArguments[i] = ConvertToType(expr, methodParameters[i].ParameterType, context);
}

if (matchWithToken)
Expand Down
94 changes: 94 additions & 0 deletions tests/AltaSoft.Simpra.tests/AsyncExecutionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using AltaSoft.Simpra.Tests.Models;
using static AltaSoft.Simpra.Tests.Models.TestModelFactory;

namespace AltaSoft.Simpra.Tests;

public class AsyncExecutionTests
{
[Fact]
public async Task ExecuteExpression_Should_ReturnTrue_When_CcyIsInListOfCurrencyCodes()
{
var simpra = new Simpra();
var model = GetTestModel();

const string expressionCode = "return Ccy in (ListOfCurrencyCodes('VisaB2B') + 'USD')";
var result = await simpra.ExecuteAsync<bool, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
Assert.True(result);
}

[Fact]
public async Task Expression_Should_EvaluateSanctionedCountriesExpression_Correctly()
{
const string expressionCode =
"""
let russianCountries = BigList('sanctioned_countries')
let amount = Transfer.Amount
let ccy = Transfer.Currency

let isValidCcy = (ccy is '' or ccy not in ['USD', 'EUR']) and (ccy like 'I%' or ccy matches '[a-zA-Z_][a-zA-Z_0-9]')
let isValidAmount = amount > 1000 and amount < 2000
let isValidAmount2 = amount > 1000 and < 2000
let isValidRemittance = length(Remittance) > 4

return isValidCcy and isValidAmount and isValidRemittance
""";

var simpra = new Simpra();
var model = GetTestModel(); // Model is irrelevant here
var result = await simpra.ExecuteAsync<bool, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode,
new SimpraCompilerOptions { MutabilityOption = MutabilityOption.Immutable, StringComparisonOption = StringComparisonOption.IgnoreCase },
CancellationToken.None);
Assert.False(result);
}

[Fact]
public async Task ExecuteExpression_Should_EvaluateAsyncCallDirectlyWithinArithmeticBinary()
{
const string expressionCode = "return Compute(3, 4) + 1";

var simpra = new Simpra();
var model = GetTestModel();

var result = await simpra.ExecuteAsync<decimal, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
Assert.Equal(8m, result);
}

[Theory]
[InlineData("Compute(1, 1) > 1 and Compute(3, 3) > 5", true)] // left true -> right (async) must be evaluated
[InlineData("Compute(1, 1) > 5 and Compute(3, 3) > 5", false)] // left false -> AND short-circuits, right (async) never evaluated
[InlineData("Compute(1, 1) > 5 or Compute(3, 3) > 5", true)] // left false -> OR must evaluate right (async)
[InlineData("Compute(1, 1) > 1 or Compute(3, 3) > 100", true)] // left true -> OR short-circuits, right (async) never evaluated
public async Task ExecuteExpression_Should_ShortCircuitCorrectly_When_AsyncCallIsOnRightOfAndOr(string condition, bool expected)
{
var simpra = new Simpra();
var model = GetTestModel();

var result = await simpra.ExecuteAsync<bool, TestModel, TestFunctions>(model, new TestFunctions(), $"return {condition}", null, CancellationToken.None);
Assert.Equal(expected, result);
}

[Fact]
public async Task ExecuteExpression_Should_EvaluateAsyncCallWithinConditional()
{
const string expressionCode =
"return when Compute(1, 1) > 1 then 'yes' else 'no' end";

var simpra = new Simpra();
var model = GetTestModel();

var result = await simpra.ExecuteAsync<string, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
Assert.Equal("yes", result);
}

[Fact]
public async Task ExecuteExpression_Should_EvaluateMultipleAsyncCallsCombinedInSingleBinaryExpression()
{
const string expressionCode = "return Compute(1, 2) + Compute(3, 4)";

var simpra = new Simpra();
var model = GetTestModel();

var result = await simpra.ExecuteAsync<decimal, TestModel, TestFunctions>(model, new TestFunctions(), expressionCode, null, CancellationToken.None);
Assert.Equal(10m, result);
}
}
Loading
Loading