diff --git a/src/Core/InputInterpreter.cs b/src/Core/InputInterpreter.cs index f743ac4..5c2c985 100644 --- a/src/Core/InputInterpreter.cs +++ b/src/Core/InputInterpreter.cs @@ -20,6 +20,11 @@ namespace CSMic // Function registry private readonly Dictionary functions; + // Tracks expression variables currently being evaluated to prevent recursion + private readonly List evaluationStack; + // Tracks whether recursion was encountered during evaluation + private int recursionHitCounter = 0; + #endregion #region Constructors @@ -30,6 +35,7 @@ namespace CSMic numericArrayVariables = new Dictionary(StringComparer.Ordinal); expressionVariables = new Dictionary(StringComparer.Ordinal); functions = new Dictionary(StringComparer.Ordinal); + evaluationStack = new List(); } // 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 diff --git a/src/Tests/InputInterpreterTests.cs b/src/Tests/InputInterpreterTests.cs index bda2886..db0356c 100644 --- a/src/Tests/InputInterpreterTests.cs +++ b/src/Tests/InputInterpreterTests.cs @@ -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() {