fix: prevent infinite recursion in expression evaluation
Add evaluation stack and recursion detection in InputInterpreter to short-circuit self/cyclic expression references. Return zero when recursion is detected during evaluation and unwind stack state after parse. Add unit tests covering self-recursion and mutual recursion cases.
This commit is contained in:
parent
ead706e38b
commit
ad78611f9c
2 changed files with 69 additions and 3 deletions
|
@ -20,6 +20,11 @@ namespace CSMic
|
|||
// Function registry
|
||||
private readonly Dictionary<string, ICodedFunction> functions;
|
||||
|
||||
// Tracks expression variables currently being evaluated to prevent recursion
|
||||
private readonly List<string> evaluationStack;
|
||||
// Tracks whether recursion was encountered during evaluation
|
||||
private int recursionHitCounter = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
@ -30,6 +35,7 @@ namespace CSMic
|
|||
numericArrayVariables = new Dictionary<string, decimal[]>(StringComparer.Ordinal);
|
||||
expressionVariables = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
|
||||
evaluationStack = new List<string>();
|
||||
}
|
||||
|
||||
// Internal constructor to create a child interpreter that shares stores
|
||||
|
@ -39,6 +45,8 @@ namespace CSMic
|
|||
this.numericArrayVariables = parent.numericArrayVariables;
|
||||
this.expressionVariables = parent.expressionVariables;
|
||||
this.functions = parent.functions;
|
||||
// Share the evaluation stack so recursion is tracked across nested parses
|
||||
this.evaluationStack = parent.evaluationStack;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -118,7 +126,27 @@ namespace CSMic
|
|||
=> numericArrayVariables.TryGetValue(name, out values!);
|
||||
|
||||
internal bool TryGetExpression(string name, out string expr)
|
||||
=> expressionVariables.TryGetValue(name, out expr!);
|
||||
{
|
||||
// Short-circuit self or cyclic references by evaluating to zero
|
||||
// If name is already in the stack, report a zero expression to caller
|
||||
if (evaluationStack.Contains(name))
|
||||
{
|
||||
expr = "0";
|
||||
// Mark that a recursion was detected so caller can zero the whole evaluation
|
||||
recursionHitCounter++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expressionVariables.TryGetValue(name, out expr!))
|
||||
{
|
||||
// Mark as currently evaluating; EvaluateExpression will unwind
|
||||
evaluationStack.Add(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
expr = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void AssignNumeric(string name, decimal value)
|
||||
{
|
||||
|
@ -147,14 +175,32 @@ namespace CSMic
|
|||
{
|
||||
// Create a child interpreter sharing stores, so ProduceOutput doesn't affect parent state
|
||||
var child = new InputInterpreter(this);
|
||||
int depth = evaluationStack.Count;
|
||||
int hitStart = recursionHitCounter;
|
||||
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(expressionText));
|
||||
var scanner = new CSMic.Interpreter.Scanner(ms);
|
||||
var parser = new CSMic.Interpreter.Parser(scanner)
|
||||
{
|
||||
Interpreter = child
|
||||
};
|
||||
parser.Parse();
|
||||
return parser.Result;
|
||||
try
|
||||
{
|
||||
parser.Parse();
|
||||
// If recursion was detected during this evaluation, collapse to zero
|
||||
if (recursionHitCounter > hitStart)
|
||||
{
|
||||
return new FunctionValue(FunctionValueType.Numeric, 0m);
|
||||
}
|
||||
return parser.Result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Unwind any names pushed during this evaluation to avoid leaking state
|
||||
while (evaluationStack.Count > depth)
|
||||
{
|
||||
evaluationStack.RemoveAt(evaluationStack.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary developer-facing API: interpret input and return numeric result
|
||||
|
|
|
@ -116,6 +116,26 @@ public class InputInterpreterTests
|
|||
AssertSuccess(_interp.Interpret("arr[i+1]"), 3, _interp);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Expression_SelfRecursion_EvaluatesToZero()
|
||||
{
|
||||
// Define an expression that references itself
|
||||
AssertSuccess(_interp.Interpret("a := a + 1"), 0, _interp);
|
||||
// Using it should not crash and should yield zero
|
||||
AssertSuccess(_interp.Interpret("a"), 0, _interp);
|
||||
// And in larger expressions it should still be zero
|
||||
AssertSuccess(_interp.Interpret("a + 5"), 5, _interp);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Expression_MutualRecursion_EvaluatesToZero()
|
||||
{
|
||||
AssertSuccess(_interp.Interpret("a := b + 1"), 0, _interp);
|
||||
AssertSuccess(_interp.Interpret("b := a + 1"), 0, _interp);
|
||||
AssertSuccess(_interp.Interpret("a"), 0, _interp);
|
||||
AssertSuccess(_interp.Interpret("b"), 0, _interp);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringLiteral_Alone_IsError()
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue