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
|
// Function registry
|
||||||
private readonly Dictionary<string, ICodedFunction> functions;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
@ -30,6 +35,7 @@ namespace CSMic
|
||||||
numericArrayVariables = new Dictionary<string, decimal[]>(StringComparer.Ordinal);
|
numericArrayVariables = new Dictionary<string, decimal[]>(StringComparer.Ordinal);
|
||||||
expressionVariables = new Dictionary<string, string>(StringComparer.Ordinal);
|
expressionVariables = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
|
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
|
||||||
|
evaluationStack = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal constructor to create a child interpreter that shares stores
|
// Internal constructor to create a child interpreter that shares stores
|
||||||
|
@ -39,6 +45,8 @@ namespace CSMic
|
||||||
this.numericArrayVariables = parent.numericArrayVariables;
|
this.numericArrayVariables = parent.numericArrayVariables;
|
||||||
this.expressionVariables = parent.expressionVariables;
|
this.expressionVariables = parent.expressionVariables;
|
||||||
this.functions = parent.functions;
|
this.functions = parent.functions;
|
||||||
|
// Share the evaluation stack so recursion is tracked across nested parses
|
||||||
|
this.evaluationStack = parent.evaluationStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -118,7 +126,27 @@ namespace CSMic
|
||||||
=> numericArrayVariables.TryGetValue(name, out values!);
|
=> numericArrayVariables.TryGetValue(name, out values!);
|
||||||
|
|
||||||
internal bool TryGetExpression(string name, out string expr)
|
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)
|
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
|
// Create a child interpreter sharing stores, so ProduceOutput doesn't affect parent state
|
||||||
var child = new InputInterpreter(this);
|
var child = new InputInterpreter(this);
|
||||||
|
int depth = evaluationStack.Count;
|
||||||
|
int hitStart = recursionHitCounter;
|
||||||
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(expressionText));
|
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(expressionText));
|
||||||
var scanner = new CSMic.Interpreter.Scanner(ms);
|
var scanner = new CSMic.Interpreter.Scanner(ms);
|
||||||
var parser = new CSMic.Interpreter.Parser(scanner)
|
var parser = new CSMic.Interpreter.Parser(scanner)
|
||||||
{
|
{
|
||||||
Interpreter = child
|
Interpreter = child
|
||||||
};
|
};
|
||||||
parser.Parse();
|
try
|
||||||
return parser.Result;
|
{
|
||||||
|
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
|
// 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);
|
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]
|
[Test]
|
||||||
public void StringLiteral_Alone_IsError()
|
public void StringLiteral_Alone_IsError()
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue