diff --git a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs index c73a7258f..e891d2d56 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoFE/Compiler.cs @@ -43,7 +43,18 @@ private protected override void CompileNode(Compiler compiler) => public partial record Variable { private protected override void CompileNode(Compiler compiler) => - compiler.Instructions.Add(new(InstructionType.PUSH_VAR, compiler.VarNamespace[this])); + compiler.Instructions.Add(new(InstructionType.PUSH_VAR, + compiler.VarNamespace.TryGetValue(this, out var slot) + ? slot + // A compiled expression is a function of the variables it was compiled + // over, so one it does not mention is not something it can be given a + // value for. Looking the name up and letting the lookup fail said only + // that some key was not present in some dictionary. + : throw new UncompilableNodeException( + $"{this} is not among the variables the expression is being compiled over" + + (compiler.VarNamespace.Count is 0 + ? ", which are none" + : $", which are {string.Join(", ", compiler.VarNamespace.Keys)}")))); /// public override int GetHashCode() diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs index eb5c3bc5c..ac3996c5f 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/IntoLinqCompiler.cs @@ -5,7 +5,9 @@ // Website: https://am.angouri.org. // +using AngouriMath.Core.Exceptions; using System; +using System.Linq; using System.Linq.Expressions; using static AngouriMath.Entity; @@ -57,7 +59,17 @@ internal static Expression BuildTree( Variable { IsConstant: true } c => BuildTree(c.Evaled, cachedSubexpressions, variableAssignments, newLocalVars, protocol), - Variable x => cachedSubexpressions[x], + // A compiled expression is a function of the variables it was compiled over, + // so one it does not mention is not something it can be given a value for. + // Reaching into the cache and letting the lookup fail said only that some + // key was not present in some dictionary. + Variable x => cachedSubexpressions.TryGetValue(x, out var argument) + ? argument + : throw new UncompilableNodeException( + $"{x} is not among the variables the expression is being compiled over" + + (cachedSubexpressions.Keys.OfType().ToList() is { Count: > 0 } over + ? $", which are {string.Join(", ", over)}" + : ", which are none")), Entity.Boolean or Number => protocol.ConvertConstant(expr), diff --git a/Sources/Tests/UnitTests/Common/CompileUnboundVariableTest.cs b/Sources/Tests/UnitTests/Common/CompileUnboundVariableTest.cs new file mode 100644 index 000000000..1984c2951 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/CompileUnboundVariableTest.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019-2022 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 AngouriMath; +using AngouriMath.Core.Exceptions; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// Compiling an expression over some of its variables and not the rest. No issue is + /// filed for this; it was found sweeping the compilers for expressions that fail in a + /// way that says nothing. + /// + public sealed class CompileUnboundVariableTest + { + /// + /// A compiled expression is a function of the variables it was compiled over, so one + /// it does not mention cannot be given a value. Both compilers reached into a + /// dictionary and let the lookup fail, which raised a KeyNotFoundException saying + /// only that some key was not present in some dictionary, naming neither the + /// variable nor the fact that a compilation was going on. + /// + [Theory] + [InlineData("a * x", "a")] + [InlineData("x + y", "y")] + [InlineData("sin(x) + b", "b")] + [InlineData("x ^ 2 + c * x + 1", "c")] + public void TheFastCompilerNamesTheVariableItWasNotGiven(string expression, string missing) + { + var thrown = Assert.Throws( + () => expression.ToEntity().Compile("x")); + Assert.Contains(missing, thrown.Message); + Assert.Contains("x", thrown.Message); + } + + [Theory] + [InlineData("a * x", "a")] + [InlineData("x + y", "y")] + [InlineData("sin(x) + b", "b")] + public void TheLinqCompilerNamesTheVariableItWasNotGiven(string expression, string missing) + { + var thrown = Assert.Throws( + () => expression.ToEntity().Compile("x")); + Assert.Contains(missing, thrown.Message); + } + + [Fact] + public void CompilingOverNothingSaysSo() + { + var thrown = Assert.Throws( + () => "y".ToEntity().Compile(Array.Empty())); + Assert.Contains("y", thrown.Message); + Assert.Contains("none", thrown.Message); + } + + // Everything that compiled before still does, constants included: pi and e are + // substituted for their values before any variable is looked up, so they are not + // variables the caller has to pass. + [Theory] + [InlineData("x + 2", 3.0)] + [InlineData("pi * x", Math.PI)] + [InlineData("e ^ x", Math.E)] + [InlineData("sin(x) ^ 2 + cos(x) ^ 2", 1.0)] + public void WhatCompiledBeforeStillCompiles(string expression, double atOne) + { + var compiled = expression.ToEntity().Compile("x"); + Assert.Equal(atOne, compiled.Call(1.0).Real, 8); + } + + [Fact] + public void EveryVariableGivenIsEnough() + { + var compiled = "x + y".ToEntity().Compile("x", "y"); + Assert.Equal(3.0, compiled.Call(1.0, 2.0).Real, 8); + var linq = "x + y".ToEntity().Compile("x", "y"); + Assert.Equal(3.0, linq(1.0, 2.0), 8); + } + } +}