diff --git a/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs b/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs
index 181c38b4f..e4d53f80d 100644
--- a/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs
+++ b/Sources/AngouriMath/Functions/Compilation/Compile.FE.Definition.cs
@@ -23,7 +23,8 @@ partial record Entity
///
/// 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
+ /// where no predicate holds, and computes only the case it takes, as evaluation does.
+ /// https://github.com/asc-community/AngouriMath/issues/2195
///
public FastExpression Compile(params Variable[] variables) => Compiler.Compile(this, variables);
diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs
index ec81631b9..a91b2dfa3 100644
--- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs
+++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
@@ -48,6 +48,7 @@ internal void InnerCompile(Compiler compiler)
cacheLine = ~cacheLine;
compiler.Cache[this] = cacheLine;
compiler.Instructions.Add(new(InstructionType.SAVE_CACHE, cacheLine));
+ compiler.Saved.Add(this);
}
}
}
@@ -536,29 +537,48 @@ public partial record Providedf
{
private protected override bool CompilesItself => true;
+ // The value where the predicate holds, and NaN elsewhere, the value not being computed there.
private protected override void CompileNode(Compiler compiler)
{
+ var start = compiler.Saved.Count;
Predicate.InnerCompile(compiler);
+ var unless = compiler.Jump(InstructionType.JUMP_UNLESS_HOLDS);
Expression.InnerCompile(compiler);
- compiler.Instructions.Add(new(InstructionType.CALL_PROVIDED));
+ var past = compiler.Jump(InstructionType.JUMP);
+ compiler.Land(unless);
+ compiler.Instructions.Add(new(InstructionType.PUSH_CONST, Value: double.NaN));
+ compiler.Land(past);
+ compiler.Forget(start);
}
}
- // 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.
+ // The first case whose predicate holds, and NaN where none does. Each predicate is tried in
+ // turn, and the first that holds has its case computed and jumps past the rest, so that a case
+ // not taken is not computed: one may be undefined, or costly, where it is not taken.
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())
+ var start = compiler.Saved.Count;
+ var past = new List();
+ foreach (var @case in Cases)
{
@case.Predicate.InnerCompile(compiler);
+ var unless = compiler.Jump(InstructionType.JUMP_UNLESS_HOLDS);
+ // What the predicates computed holds for the cases after them, as reaching one
+ // means every predicate before it was computed; what a case computed does not.
+ var predicates = compiler.Saved.Count;
@case.Expression.InnerCompile(compiler);
- compiler.Instructions.Add(new(InstructionType.CALL_SELECT));
+ compiler.Forget(predicates);
+ past.Add(compiler.Jump(InstructionType.JUMP));
+ compiler.Land(unless);
}
+ compiler.Instructions.Add(new(InstructionType.PUSH_CONST, Value: double.NaN));
+ foreach (var jump in past)
+ compiler.Land(jump);
+ compiler.Forget(start);
}
}
@@ -1010,6 +1030,33 @@ public partial class FastExpression
internal sealed record Compiler(List Instructions,
IReadOnlyDictionary VarNamespace, IDictionary Cache)
{
+ ///
+ /// The subexpressions saved to the cache, in the order their instructions were emitted. A
+ /// subexpression saved where a jump may pass over it has no value in its slot after the jump,
+ /// so once the code that may be passed over is compiled it is -ed back to
+ /// unsaved, and is computed again where it is next met: a slot is read only where every path
+ /// to the read has written it, as relies on.
+ ///
+ internal List Saved { get; } = new();
+
+ /// Marks every subexpression saved since were as unsaved.
+ internal void Forget(int count)
+ {
+ for (var i = count; i < Saved.Count; i++)
+ Cache[Saved[i]] = ~Cache[Saved[i]];
+ Saved.RemoveRange(count, Saved.Count - count);
+ }
+
+ /// Emits a jump whose target is not yet known, and returns where it is.
+ internal int Jump(InstructionType type)
+ {
+ Instructions.Add(new(type));
+ return Instructions.Count - 1;
+ }
+
+ /// Makes the jump at land on the next instruction emitted.
+ internal void Land(int jump) => Instructions[jump] = Instructions[jump] with { Reference = Instructions.Count };
+
/// Returns a compiled expression. Allows to boost substitution a lot
/// The function to be compiled
/// Must be equal to func's variables (ignoring constants)
diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs
index 37ce14fd9..dc88de476 100644
--- a/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs
+++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs
@@ -21,6 +21,12 @@ internal enum InstructionType
LOAD_CACHE,
SAVE_CACHE,
+ // Control flow, to the instruction its Reference names: a jump, and a jump taken
+ // unless the truth value popped holds, as a case of a piecewise is passed over unless
+ // its predicate holds.
+ JUMP = 20,
+ JUMP_UNLESS_HOLDS,
+
// 1-arg functions
CALL_SIN = 50,
CALL_COS,
@@ -86,8 +92,6 @@ internal enum InstructionType
CALL_OR,
CALL_XOR,
CALL_IMPLIES,
- // A value where its predicate holds, and NaN elsewhere.
- CALL_PROVIDED,
CALL_BINOMIAL,
CALL_FALLING_FACTORIAL,
CALL_RISING_FACTORIAL,
@@ -98,10 +102,8 @@ internal enum InstructionType
CALL_VALUATION,
CALL_DIVIDES,
- // 3-arg: a value where its predicate holds, and the one below it elsewhere.
- CALL_SELECT = 150,
- // Whether two numbers are congruent modulo a third.
- CALL_CONGRUENT,
+ // 3-arg: whether two numbers are congruent modulo a third.
+ CALL_CONGRUENT = 150,
}
internal sealed partial record Instruction(InstructionType Type, int Reference = -1, System.Numerics.Complex Value = default)
{
@@ -136,7 +138,7 @@ private sealed class Scratch
[System.ThreadStatic] private static Scratch? scratch;
- private readonly List instructions;
+ private readonly Instruction[] instructions;
private readonly int varCount;
private readonly int cacheCount;
@@ -147,7 +149,7 @@ private sealed class Scratch
internal FastExpression(int varCount, List instructions, int cacheCount)
{
this.varCount = varCount;
- this.instructions = instructions;
+ this.instructions = instructions.ToArray();
this.cacheCount = cacheCount;
}
@@ -171,9 +173,20 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value
var cache = mine.Cache;
if (cache.Length < cacheCount)
mine.Cache = cache = new System.Numerics.Complex[cacheCount];
- foreach (var instruction in instructions)
+ for (var next = 0; next < instructions.Length; next++)
+ {
+ var instruction = instructions[next];
switch (instruction.Type)
{
+ // Every jump is forward, so a call ends. A piecewise computes the case it takes
+ // and no other: https://github.com/asc-community/AngouriMath/issues/2195
+ case InstructionType.JUMP:
+ next = instruction.Reference - 1;
+ break;
+ case InstructionType.JUMP_UNLESS_HOLDS:
+ if (Truth(stack.Pop()) != true)
+ next = instruction.Reference - 1;
+ break;
case InstructionType.PUSH_VAR:
stack.Push(values[instruction.Reference]);
break;
@@ -364,12 +377,6 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value
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;
- }
// The binomial coefficient, the factorial powers and the double factorial.
// https://github.com/asc-community/AngouriMath/issues/2195
case InstructionType.CALL_BINOMIAL:
@@ -426,15 +433,8 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value
case InstructionType.CALL_CONGRUENT:
stack.Push(Truth(AngouriMath.Numerics.NumberStatements.Congruent(stack.Pop(), stack.Pop(), stack.Pop())));
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();
@@ -447,6 +447,6 @@ public System.Numerics.Complex Substitute(params System.Numerics.Complex[] value
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);
+ public override string ToString() => string.Join(" \n| ", instructions);
}
}
\ No newline at end of file
diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs
index e3cc1d133..6fa2929c7 100644
--- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs
+++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs
@@ -201,6 +201,22 @@ IUnaryNode oneArg
=> protocol.ConvertMembership(
BuildTree(element, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType), set),
+ // The value of a case is computed only where it is taken, inside the conditional, so that a case
+ // that is undefined, or costly, where it is not taken is not computed there. Every subexpression is
+ // otherwise a local assigned before the expression's value is, which would compute each case.
+ // https://github.com/asc-community/AngouriMath/issues/2195
+ Providedf(var value, var predicate)
+ => protocol.ConvertBinaryNode(
+ Conditional(value, cachedSubexpressions, protocol, constantType),
+ BuildTree(predicate, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),
+ expr),
+
+ // The first predicate is always computed; the others only where every one before them fails.
+ Piecewise piecewise
+ => protocol.ConvertOtherNode(piecewise.DirectChildren.Select((child, i) => i == 1
+ ? BuildTree(child, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType)
+ : Conditional(child, cachedSubexpressions, protocol, constantType)).ToArray(), expr),
+
IBinaryNode twoArg
=> protocol.ConvertBinaryNode(
BuildTree(twoArg.NodeFirstChild, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType),
@@ -216,5 +232,23 @@ IBinaryNode twoArg
newLocalVars.Add(newVar);
return newVar;
}
+
+ ///
+ /// as a block of its own, computed only where the block is: what it computes is
+ /// cached inside the block and not outside it, where the block may not have been computed. What was
+ /// cached before it is read as it is everywhere else.
+ ///
+ private static Expression Conditional(
+ Entity expr,
+ Dictionary cachedSubexpressions,
+ CompilationProtocol protocol,
+ Type? constantType)
+ {
+ var cached = new Dictionary(cachedSubexpressions);
+ var locals = new List();
+ var assignments = new List();
+ var tree = BuildTree(expr, cached, assignments, locals, protocol, constantType);
+ return assignments.Count == 0 ? tree : Expression.Block(locals, assignments.Append(tree));
+ }
}
}
diff --git a/Sources/Tests/AotSmokeTest/Program.cs b/Sources/Tests/AotSmokeTest/Program.cs
index b3516aed4..129658f38 100644
--- a/Sources/Tests/AotSmokeTest/Program.cs
+++ b/Sources/Tests/AotSmokeTest/Program.cs
@@ -294,6 +294,11 @@ private static void Compilation()
var piecewise = "piecewise(x provided x > 0, 0 provided x <= 0)".Compile(x);
Check("compiled piecewise at -3 and 3", piecewise(-3) == 0 && piecewise(3) == 3, $"{piecewise(-3)}, {piecewise(3)}");
+ // A case computed only where it is taken is a block with locals of its own.
+ // https://github.com/asc-community/AngouriMath/issues/2195
+ var computedCases = "piecewise(x^2 + 1 provided x > 0, x^2 - 1) + x^2".Compile(x);
+ Check("compiled piecewise of computed cases at -3 and 3", computedCases(-3) == 17 && computedCases(3) == 19,
+ $"{computedCases(-3)}, {computedCases(3)}");
// A comparison of numbers that are not ordered has no truth value, and the connectives
// follow Kleene's tables, in the interpreter a native build compiles to.
diff --git a/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs b/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs
new file mode 100644
index 000000000..f169785ef
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs
@@ -0,0 +1,161 @@
+//
+// 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;
+using System.Linq;
+using System.Numerics;
+using AngouriMath;
+using AngouriMath.Extensions;
+using Xunit;
+
+namespace AngouriMath.Tests.Common
+{
+ ///
+ /// A compiled provided or piecewise computes the case it takes and no other, as
+ /// evaluation does. Both compilers computed every case and kept one, so that
+ /// piecewise(primepi(x) provided x < 100, 0) at 9999991 sieved ten million numbers for a
+ /// value it threw away: 18 ms a call, where evaluation takes 13 µs.
+ /// #2195
+ ///
+ [Trait("Area", "Common")]
+ public sealed class BranchNotTakenCompilationTest
+ {
+ public static readonly TheoryData Expressions = new()
+ {
+ // A subexpression met first in a case that is not taken, and again where it is computed.
+ "piecewise(sin(x) provided x > 0, 0) + sin(x)",
+ "sin(x) + piecewise(sin(x) provided x > 0, 0)",
+ "piecewise(sin(x) provided x > 1, sin(x) + 1 provided x > 0, sin(x) + 2)",
+ "(cos(x) provided x > 0) + piecewise(cos(x) provided x < 0, 1)",
+ // One shared by a predicate and the cases after it.
+ "piecewise(x^2 provided x^2 > 1, x^2 + 1)",
+ "piecewise(1 provided x^3 > 8, x^3 provided x^3 > 0, x^3 - 1)",
+ // Nested.
+ "piecewise(piecewise(cos(x) provided x > 2, cos(x) + 1) provided x > 0, cos(x) - 1) * cos(x)",
+ "(x provided x > 0) provided x < 2",
+ // A case with no value where it is not taken.
+ "piecewise(1/x provided not x = 0, 0)",
+ "piecewise(ln(x) provided x > 0, ln(-x) provided x < 0, 0) + x",
+ "piecewise(sqrt(x - 1) provided x >= 1, sqrt(1 - x))",
+ "x provided x > 1", "iverson(x > 0) * piecewise(x provided x > 1, 2 x)",
+ };
+
+ // Real, negative, complex and NaN, and each visited twice in both orders, so that a value
+ // a call leaves behind cannot pass for the next call's.
+ private static readonly (Entity At, Complex Value)[] points =
+ {
+ (2, 2), ("1/2", 0.5), (0, 0), ("-13/10", -1.3), (-3, -3), ("i", Complex.ImaginaryOne),
+ ("1 + i", new(1, 1)), ("-1 + 2i", new(-1, 2)), (MathS.NaN, double.NaN),
+ };
+
+ private static Complex Interpreted(string expression, Entity at)
+ => expression.ToEntity().Substitute("x", at).EvalNumerical().ToNumerics();
+
+ private static bool Agrees(Complex expected, Complex actual)
+ => double.IsNaN(expected.Real) || double.IsNaN(expected.Imaginary)
+ ? double.IsNaN(actual.Real) || double.IsNaN(actual.Imaginary)
+ : Complex.Abs(expected - actual) <= 1e-12 * Math.Max(1, Complex.Abs(expected));
+
+ private static void AgreesAtEveryPoint(string expression, Func compiled, string compiler)
+ {
+ foreach (var order in new[] { points, points.AsEnumerable().Reverse().ToArray(), points })
+ foreach (var (at, value) in order)
+ {
+ var expected = Interpreted(expression, at);
+ var actual = compiled(value);
+ Assert.True(Agrees(expected, actual), $"{compiler}: {expression} at {at}: {expected} interpreted, {actual} compiled");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(Expressions))]
+ public void FastExpressionIsWhatEvaluationSays(string expression)
+ {
+ var compiled = expression.ToEntity().Compile("x");
+ AgreesAtEveryPoint(expression, value => compiled.Call(value), "FastExpression");
+ }
+
+ [Theory]
+ [MemberData(nameof(Expressions))]
+ public void LinqOverTheComplexNumbersIsWhatEvaluationSays(string expression)
+ {
+ var compiled = expression.ToEntity().Compile("x");
+ AgreesAtEveryPoint(expression, value => compiled(value), "LINQ over Complex");
+ }
+
+ [Theory]
+ [MemberData(nameof(Expressions))]
+ public void LinqOverTheRealsIsWhatEvaluationSaysWhereItIsReal(string expression)
+ {
+ var compiled = expression.ToEntity().Compile("x");
+ foreach (var (at, value) in points)
+ {
+ if (value.Imaginary != 0)
+ continue;
+ var expected = Interpreted(expression, at);
+ if (expected.Imaginary != 0)
+ expected = double.NaN;
+ var actual = compiled(value.Real);
+ Assert.True(Agrees(expected, actual), $"LINQ over double: {expression} at {at}: {expected} interpreted, {actual} compiled");
+ }
+ }
+
+ // primepi sieves up to its argument, and at 9999991 the sieve is 5 million bits, 625 KB, on every
+ // call. What a call allocates is the measure, not how long it takes, which is the machine's.
+ public static readonly TheoryData NotTakenAt9999991 = new()
+ {
+ "piecewise(primepi(x) provided x < 100, 0)",
+ "primepi(x) provided x < 100",
+ "piecewise(1 provided x > 100, primepi(x) provided primepi(x) > 0, 2)",
+ "piecewise(piecewise(primepi(x) provided x < 50, 1) provided x < 100, 2) + 1",
+ };
+
+ private const double Untaken = 9999991;
+ private const long WellBelowTheSieve = 64 * 1024;
+
+ private static long Allocated(Func call)
+ {
+ call();
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ call();
+ return GC.GetAllocatedBytesForCurrentThread() - before;
+ }
+
+ [Theory]
+ [MemberData(nameof(NotTakenAt9999991))]
+ public void FastExpressionDoesNotComputeACaseItDoesNotTake(string expression)
+ {
+ var compiled = expression.ToEntity().Compile("x");
+ Assert.True(Agrees(Interpreted(expression, Untaken), compiled.Call(Untaken)));
+ var bytes = Allocated(() => compiled.Call(Untaken));
+ Assert.True(bytes < WellBelowTheSieve, $"{expression} at {Untaken} allocated {bytes} bytes");
+ }
+
+ [Theory]
+ [MemberData(nameof(NotTakenAt9999991))]
+ public void LinqDoesNotComputeACaseItDoesNotTake(string expression)
+ {
+ var overComplex = expression.ToEntity().Compile("x");
+ var overReals = expression.ToEntity().Compile("x");
+ Assert.True(Agrees(Interpreted(expression, Untaken), overComplex(Untaken)));
+ Assert.True(Agrees(Interpreted(expression, Untaken), overReals(Untaken)));
+ var bytes = Allocated(() => overComplex(Untaken));
+ Assert.True(bytes < WellBelowTheSieve, $"{expression} over Complex at {Untaken} allocated {bytes} bytes");
+ bytes = Allocated(() => overReals(Untaken));
+ Assert.True(bytes < WellBelowTheSieve, $"{expression} over double at {Untaken} allocated {bytes} bytes");
+ }
+
+ /// Where the case is taken, it is computed: the sieve is there.
+ [Fact]
+ public void ACaseThatIsTakenIsComputed()
+ {
+ var compiled = "piecewise(primepi(x) provided x > 100, 0)".ToEntity().Compile("x");
+ Assert.Equal(new Complex(664579, 0), compiled.Call(Untaken));
+ Assert.True(Allocated(() => compiled.Call(Untaken)) > WellBelowTheSieve);
+ }
+ }
+}