Skip to content
Open
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 @@ -23,7 +23,8 @@ partial record Entity
/// <remarks>
/// A statement compiles to its truth value: 1 where it holds, 0 where it does not, and NaN
/// where it has none, as <c>i &gt; 1</c> has none. A <c>provided</c> or a <c>piecewise</c> 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
/// </remarks>
public FastExpression Compile(params Variable[] variables) => Compiler.Compile(this, variables);

Expand Down
61 changes: 54 additions & 7 deletions Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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<int>();
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);
}
}

Expand Down Expand Up @@ -1010,6 +1030,33 @@ public partial class FastExpression
internal sealed record Compiler(List<Instruction> Instructions,
IReadOnlyDictionary<Variable, int> VarNamespace, IDictionary<Entity, int> Cache)
{
/// <summary>
/// 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 <see cref="Forget"/>-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 <see cref="FastExpression.Substitute"/> relies on.
/// </summary>
internal List<Entity> Saved { get; } = new();

/// <summary>Marks every subexpression saved since <paramref name="count"/> were as unsaved.</summary>
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);
}

/// <summary>Emits a jump whose target is not yet known, and returns where it is.</summary>
internal int Jump(InstructionType type)
{
Instructions.Add(new(type));
return Instructions.Count - 1;
}

/// <summary>Makes the jump at <paramref name="jump"/> land on the next instruction emitted.</summary>
internal void Land(int jump) => Instructions[jump] = Instructions[jump] with { Reference = Instructions.Count };

/// <summary>Returns a compiled expression. Allows to boost substitution a lot</summary>
/// <param name="func">The function to be compiled</param>
/// <param name="variables">Must be equal to func's variables (ignoring constants)</param>
Expand Down
48 changes: 24 additions & 24 deletions Sources/AngouriMath/Functions/Compilation/IntoFE/FastExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
{
Expand Down Expand Up @@ -136,7 +138,7 @@ private sealed class Scratch

[System.ThreadStatic] private static Scratch? scratch;

private readonly List<Instruction> instructions;
private readonly Instruction[] instructions;
private readonly int varCount;
private readonly int cacheCount;

Expand All @@ -147,7 +149,7 @@ private sealed class Scratch
internal FastExpression(int varCount, List<Instruction> instructions, int cacheCount)
{
this.varCount = varCount;
this.instructions = instructions;
this.instructions = instructions.ToArray();
this.cacheCount = cacheCount;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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();
Expand All @@ -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;

/// <summary>Might be useful for debug if a function works too slowly</summary>
public override string ToString() => string.Join(" \n| ", instructions);
public override string ToString() => string.Join<Instruction>(" \n| ", instructions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -216,5 +232,23 @@ IBinaryNode twoArg
newLocalVars.Add(newVar);
return newVar;
}

/// <summary>
/// <paramref name="expr"/> 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.
/// </summary>
private static Expression Conditional(
Entity expr,
Dictionary<Entity, ParameterExpression> cachedSubexpressions,
CompilationProtocol protocol,
Type? constantType)
{
var cached = new Dictionary<Entity, ParameterExpression>(cachedSubexpressions);
var locals = new List<ParameterExpression>();
var assignments = new List<Expression>();
var tree = BuildTree(expr, cached, assignments, locals, protocol, constantType);
return assignments.Count == 0 ? tree : Expression.Block(locals, assignments.Append(tree));
}
}
}
5 changes: 5 additions & 0 deletions Sources/Tests/AotSmokeTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ private static void Compilation()

var piecewise = "piecewise(x provided x > 0, 0 provided x <= 0)".Compile<double, double>(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<double, double>(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.
Expand Down
Loading
Loading