This commit is contained in:
Jordan Wages 2025-08-26 23:24:12 -05:00
commit b6dee57aa1
2 changed files with 69 additions and 3 deletions

View file

@ -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
@ -121,7 +129,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)
{
@ -150,14 +178,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

View file

@ -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()
{