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);
+ }
+ }
+}