diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index b8e73e5c5..e3f02e093 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -343,6 +343,23 @@ compiled to `bool`, it is `false`, as one that does not hold | `"x provided x > 1".ToEntity().Compile("x")(2)` | `UncompilableNodeException` | `(2, 0)` | | `"x provided x > 1".ToEntity().Compile("x")(Complex.ImaginaryOne)` | `UncompilableNodeException` | `(NaN, 0)` | +### `Compile` compiles statements, `provided` and `piecewise` + +**Not silent: what was refused is compiled.** `Entity.Compile(...)`, which returns a `FastExpression`, refused a +comparison, a connective, a Boolean, a `provided`, a `piecewise` and an Iverson bracket with `UncompilableNodeException`. +A statement now compiles to its truth value, `1` where it holds, `0` where it does not, and `NaN` where it has none, as +`i > 1` has none. Comparisons and connectives are those of the LINQ compiler. A `provided` is `NaN` where its predicate +does not hold, and a `piecewise` is the first case whose predicate holds, or `NaN`. Every case is computed, since the +instructions have no jump ([#2195](https://github.com/asc-community/AngouriMath/issues/2195)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x > 1".ToEntity().Compile("x").Call(2)` | `UncompilableNodeException` | `(1, 0)` | +| `"x > 1".ToEntity().Compile("x").Call(Complex.ImaginaryOne)` | `UncompilableNodeException` | `(NaN, 0)` | +| `"x provided x > 0".ToEntity().Compile("x").Call(-1)` | `UncompilableNodeException` | `(NaN, 0)` | +| `"piecewise(x provided x > 0, -x)".ToEntity().Compile("x").Call(-3)` | `UncompilableNodeException` | `(3, 0)` | +| `"iverson(x > 0)".ToEntity().Compile("x").Call(2)` | `UncompilableNodeException` | `(1, 0)` | + ### The conjugate, the argument, the parts and the Iverson bracket compile **Not silent: these compile where they threw `UncompilableNodeException`.** `conj`, `arg`, `Re` and `Im` compile diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 4eff3ecb3..828911739 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -354,6 +354,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/ThreeValuedCompilationTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Common/FastExpressionStatementTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Common/CompileMatrixTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs b/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs index 00eb96491..181c38b4f 100644 --- a/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs +++ b/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs @@ -20,6 +20,11 @@ partial record Entity /// Constants, i.e. and will be ignored. /// /// + /// + /// A statement compiles to its truth value: 1 where it holds, 0 where it does not, and NaN + /// where it has none, as i > 1 has none. A provided or a piecewise is NaN + /// where no predicate holds. https://github.com/asc-community/AngouriMath/issues/2195 + /// public FastExpression Compile(params Variable[] variables) => Compiler.Compile(this, variables); /// @@ -31,6 +36,7 @@ partial record Entity /// Constants, i.e. and will be ignored. /// /// + /// A statement compiles to its truth value, as says. public FastExpression Compile(params string[] variables) => Compiler.Compile(this, variables.Select(x => (Variable)x)); } diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index 3efcddd1d..ec259b5de 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -386,6 +386,175 @@ private protected override void CompileNode(Compiler compiler) } } + // A statement compiles to its truth value: 1 where it holds, 0 where it does not, and NaN + // where it has none, as evaluation leaves one. A provided and a piecewise compile every + // case and keep the one whose predicate holds, which needs no jump in the instructions. + // https://github.com/asc-community/AngouriMath/issues/2195 + public partial record Boolean + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + => compiler.Instructions.Add(new(InstructionType.PUSH_CONST, Value: Value ? 1 : 0)); + } + + public partial record Notf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Argument.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_NOT)); + } + } + + // The Iverson bracket of a statement is its truth value. + public partial record Iversonf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) => Argument.InnerCompile(compiler); + } + + public partial record Andf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_AND)); + } + } + + public partial record Orf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_OR)); + } + } + + public partial record Xorf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_XOR)); + } + } + + public partial record Impliesf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Conclusion.InnerCompile(compiler); + Assumption.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_IMPLIES)); + } + } + + public partial record Equalsf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_EQUAL)); + } + } + + public partial record Lessf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_LESS)); + } + } + + public partial record LessOrEqualf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_LESS_OR_EQUAL)); + } + } + + public partial record Greaterf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_GREATER)); + } + } + + public partial record GreaterOrEqualf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Right.InnerCompile(compiler); + Left.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_GREATER_OR_EQUAL)); + } + } + + public partial record Providedf + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + Predicate.InnerCompile(compiler); + Expression.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_PROVIDED)); + } + } + + // The first case whose predicate holds, and NaN where none does: from the last case up, + // each is kept where its predicate holds and the cases below it are kept elsewhere. + public partial record Piecewise + { + private protected override bool CompilesItself => true; + + private protected override void CompileNode(Compiler compiler) + { + compiler.Instructions.Add(new(InstructionType.PUSH_CONST, Value: double.NaN)); + foreach (var @case in Cases.Reverse()) + { + @case.Predicate.InnerCompile(compiler); + @case.Expression.InnerCompile(compiler); + compiler.Instructions.Add(new(InstructionType.CALL_SELECT)); + } + } + } + public partial record Maxf { private protected override bool CompilesItself => true; diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs index 36a301313..1894e6729 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using AngouriMath.Core.Exceptions; +using Logic = AngouriMath.Core.Compilation.IntoLinq.ThreeValuedLogic; namespace AngouriMath.Core @@ -53,6 +54,7 @@ internal enum InstructionType CALL_ARG, CALL_RE, CALL_IM, + CALL_NOT, // 2-arg functions CALL_SUM = 100, @@ -64,6 +66,20 @@ internal enum InstructionType CALL_LOG, CALL_MAX, CALL_MIN, + CALL_LESS, + CALL_LESS_OR_EQUAL, + CALL_GREATER, + CALL_GREATER_OR_EQUAL, + CALL_EQUAL, + CALL_AND, + CALL_OR, + CALL_XOR, + CALL_IMPLIES, + // A value where its predicate holds, and NaN elsewhere. + CALL_PROVIDED, + + // 3-arg: a value where its predicate holds, and the one below it elsewhere. + CALL_SELECT = 150, } internal sealed partial record Instruction(InstructionType Type, int Reference = -1, System.Numerics.Complex Value = default) { @@ -285,12 +301,65 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value case InstructionType.CALL_IM: stack.Push(AngouriMath.Numerics.ComplexParts.Im(stack.Pop())); break; + // A truth value is 1, 0, or NaN where there is none, as evaluation leaves one; + // the comparisons and the connectives are the LINQ compiler's. + // https://github.com/asc-community/AngouriMath/issues/2195 + case InstructionType.CALL_LESS: + stack.Push(Truth(Logic.Less(stack.Pop(), stack.Pop()))); + break; + case InstructionType.CALL_LESS_OR_EQUAL: + stack.Push(Truth(Logic.LessOrEqual(stack.Pop(), stack.Pop()))); + break; + case InstructionType.CALL_GREATER: + stack.Push(Truth(Logic.Greater(stack.Pop(), stack.Pop()))); + break; + case InstructionType.CALL_GREATER_OR_EQUAL: + stack.Push(Truth(Logic.GreaterOrEqual(stack.Pop(), stack.Pop()))); + break; + case InstructionType.CALL_EQUAL: + stack.Push(Truth(Logic.Equal(stack.Pop(), stack.Pop()))); + break; + case InstructionType.CALL_NOT: + stack.Push(Truth(Logic.Not(Truth(stack.Pop())))); + break; + case InstructionType.CALL_AND: + stack.Push(Truth(Logic.And(Truth(stack.Pop()), Truth(stack.Pop())))); + break; + case InstructionType.CALL_OR: + stack.Push(Truth(Logic.Or(Truth(stack.Pop()), Truth(stack.Pop())))); + break; + case InstructionType.CALL_XOR: + stack.Push(Truth(Logic.Xor(Truth(stack.Pop()), Truth(stack.Pop())))); + break; + case InstructionType.CALL_IMPLIES: + stack.Push(Truth(Logic.Implies(Truth(stack.Pop()), Truth(stack.Pop())))); + break; + case InstructionType.CALL_PROVIDED: + { + var value = stack.Pop(); + stack.Push(Truth(stack.Pop()) == true ? value : double.NaN); + break; + } + case InstructionType.CALL_SELECT: + { + var value = stack.Pop(); + var holds = Truth(stack.Pop()) == true; + var otherwise = stack.Pop(); + stack.Push(holds ? value : otherwise); + break; + } } if (stack.Count != 1) throw new AngouriBugException($"Unused values remain in the stack: {stack.Count} instead of 1"); return stack.Pop(); } + /// A truth value on the stack: 1 where it holds, 0 where it does not, NaN where it has none. + private static System.Numerics.Complex Truth(bool? value) => value is { } holds ? (holds ? 1 : 0) : double.NaN; + + /// The truth value a number on the stack stands for: none but for exactly 1 or 0. + private static bool? Truth(System.Numerics.Complex value) => value == 1 ? true : value == 0 ? false : null; + /// Might be useful for debug if a function works too slowly public override string ToString() => string.Join(" \n| ", instructions); } diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs index ebb6ae5fd..9ddf33779 100644 --- a/Sources/Tests/AotSmokeTest/Program.cs +++ b/Sources/Tests/AotSmokeTest/Program.cs @@ -303,6 +303,12 @@ private static void Compilation() $"{undecided(Complex.ImaginaryOne, true)}, {undecided(Complex.ImaginaryOne, false)?.ToString() ?? "null"}"); var underIt = "x provided x > 1".Compile(x); Check("compiled x provided x > 1 at 2 and at i", underIt(2) == 2 && double.IsNaN(underIt(Complex.ImaginaryOne).Real), $"{underIt(2)}, {underIt(Complex.ImaginaryOne)}"); + // FastExpression: a statement is its truth value, and a piecewise the case that holds. + var fastPiecewise = "piecewise(x provided x > 0, -x)".Compile(x); + var fastStatement = "x > 1 or x < 0".Compile(x); + Check("FastExpression piecewise at -3 and 2, x > 1 or x < 0 at 2 and at i", + fastPiecewise.Call(-3) == 3 && fastPiecewise.Call(2) == 2 && fastStatement.Call(2) == 1 && double.IsNaN(fastStatement.Call(Complex.ImaginaryOne).Real), + $"{fastPiecewise.Call(-3)}, {fastPiecewise.Call(2)}, {fastStatement.Call(2)}, {fastStatement.Call(Complex.ImaginaryOne)}"); // A matrix, which compiles to a tensor: the arithmetic on it must stay ahead-of-time // compilable, which GenericTensor's own expression-compiled loops are not. diff --git a/Sources/Tests/UnitTests/Common/FastExpressionStatementTest.cs b/Sources/Tests/UnitTests/Common/FastExpressionStatementTest.cs new file mode 100644 index 000000000..da306ae3b --- /dev/null +++ b/Sources/Tests/UnitTests/Common/FastExpressionStatementTest.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System.Numerics; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// compiles a statement to its truth value, 1, 0, + /// or NaN where it has none, and a provided and a piecewise to the case whose predicate holds. + /// Each was refused. #2195 + /// + [Trait("Area", "Common")] + public sealed class FastExpressionStatementTest + { + public static readonly TheoryData Expressions = new() + { + "x > 1", "x < 1", "x >= 1", "x <= 1", "x = 1", + "not (x > 1)", "x > 1 or true", "x > 1 and false", "x > 1 or x < 2", "x > 0 and x < 2", + "x > 1 xor true", "x > 1 implies false", "false implies x > 1", "(x > 1) = (x < 2)", + "x provided x > 1", "x^2 provided x > 0 and x < 3", + "piecewise(1 provided x > 0, -1 provided x < 0)", "piecewise(x provided x > 1, 2 x provided x > 0, 0)", + "iverson(x > 1) * x", "sin(x) + iverson(x >= 0)", + }; + + private static readonly (Entity At, Complex Value)[] points = + { (2, 2), (1, 1), ("1/2", 0.5), (-3, -3), ("i", Complex.ImaginaryOne), ("1 + i", new(1, 1)), (MathS.NaN, double.NaN) }; + + /// + /// What evaluation gives: True is 1, False is 0, and a statement with no truth value, NaN. + /// + private static Complex Interpreted(string expression, Entity at) + => expression.ToEntity().Substitute("x", at).Evaled switch + { + Entity.Boolean value => (bool)value ? 1 : 0, + Entity.Number.Complex number => number.ToNumerics(), + var other => throw new Xunit.Sdk.XunitException($"{expression} at {at} is {other}") + }; + + [Theory] + [MemberData(nameof(Expressions))] + public void ItIsWhatEvaluationSays(string expression) + { + var compiled = expression.ToEntity().Compile("x"); + foreach (var (at, value) in points) + { + var expected = Interpreted(expression, at); + var actual = compiled.Call(value); + Assert.True(double.IsNaN(expected.Real) ? double.IsNaN(actual.Real) : Complex.Abs(expected - actual) < 1e-12, + $"{expression} at {at}: {expected} interpreted, {actual} compiled"); + } + } + + /// The cases of a piecewise are tried in order: the first whose predicate holds is the value. + [Fact] + public void TheFirstCaseThatHoldsIsTheValue() + { + var compiled = "piecewise(1 provided x > 0, 2 provided x > -1, 3)".ToEntity().Compile("x"); + Assert.Equal(new Complex(1, 0), compiled.Call(5)); + Assert.Equal(new Complex(2, 0), compiled.Call(-0.5)); + Assert.Equal(new Complex(3, 0), compiled.Call(-5)); + Assert.Equal(new Complex(3, 0), compiled.Call(Complex.ImaginaryOne)); + } + } +}