From a39797670161b7cc2c7e9dc5f2d26f52f6b890b4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 13 Nov 2026 15:04:02 +0000 Subject: [PATCH 1/2] Compute only the case a compiled piecewise takes, in FastExpression A provided and a piecewise compiled every case and kept the one whose predicate held, so a case undefined or costly where it is not taken was computed anyway: piecewise(primepi(x) provided x < 100, 0) at 9999991 sieved ten million numbers, 625 KB and 18 ms a call, for a value it threw away, where evaluation takes 13 us. FastExpression now has two forward jumps, JUMP and JUMP_UNLESS_HOLDS, and runs its instructions by index. Each predicate is tried in turn; the first that holds has its case computed and jumps past the rest. Every jump is forward, so a call still ends. CALL_SELECT and CALL_PROVIDED go. A jump can pass over the instruction that saves a repeated subexpression to the cache, and the slot then holds what an earlier call left there. So the compiler logs what it saves, and once code a jump may pass over is compiled, what that code saved is marked unsaved again and computed afresh where next met. What a predicate saves stays valid for the cases after it, which are reached only once it was computed. No value changes: compilecheck's tables are identical, and its new branch section finds no disagreement with evaluation over ten samples sharing subexpressions between cases, at real, negative, complex and NaN points. Without the cache discipline three of them read a previous call's value. Part of #2195. Co-Authored-By: Claude Opus 5.5 --- .../Compilation/Compile.FE.Definition.cs | 3 +- .../Functions/Compilation/IntoFE/Compiler.cs | 61 ++++++++- .../Compilation/IntoFE/FastExpression.cs | 48 +++---- .../Common/BranchNotTakenCompilationTest.cs | 122 ++++++++++++++++++ 4 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs 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/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs b/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs new file mode 100644 index 000000000..6e7d868e3 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs @@ -0,0 +1,122 @@ +// +// 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 provided or piecewise compiled to a FastExpression computes the case it takes and no other, as + /// evaluation does. It 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"); + } + + // 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"); + } + + /// 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); + } + } +} From 8c15e5ddca21dbe865c04da8fd499ecbb597e10a Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 13 Nov 2026 15:04:02 +0000 Subject: [PATCH 2/2] Compute only the case a compiled piecewise takes, in the LINQ compiler Every subexpression became a local assigned before the expression's value, so the conditional a provided or a piecewise compiles to chose between cases already computed, as FastExpression did: piecewise(primepi(x) provided x < 100, 0) at 9999991 allocated 625 KB a call over Complex and over double. A case's value, and every predicate after the first, is now built as a block of its own inside the conditional, with a copy of the cache, so what it computes is cached inside the block and not read outside it, where it may not have been computed. The AOT smoke test compiles a piecewise whose cases are blocks. Part of #2195. Co-Authored-By: Claude Opus 5.5 --- .../Compilation/IntoLinq/IntoLinqCompiler.cs | 34 +++++++++++++++ Sources/Tests/AotSmokeTest/Program.cs | 5 +++ .../Common/BranchNotTakenCompilationTest.cs | 43 ++++++++++++++++++- 3 files changed, 80 insertions(+), 2 deletions(-) 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 index 6e7d868e3..f169785ef 100644 --- a/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs +++ b/Sources/Tests/UnitTests/Common/BranchNotTakenCompilationTest.cs @@ -15,8 +15,8 @@ namespace AngouriMath.Tests.Common { /// - /// A provided or piecewise compiled to a FastExpression computes the case it takes and no other, as - /// evaluation does. It computed every case and kept one, so that + /// 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 @@ -79,6 +79,31 @@ public void FastExpressionIsWhatEvaluationSays(string expression) 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() @@ -110,6 +135,20 @@ public void FastExpressionDoesNotComputeACaseItDoesNotTake(string expression) 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()