diff --git a/src/Core/InputInterpreter.cs b/src/Core/InputInterpreter.cs index 46addbf..b009bda 100644 --- a/src/Core/InputInterpreter.cs +++ b/src/Core/InputInterpreter.cs @@ -22,8 +22,9 @@ namespace CSMic // Tracks expression variables currently being evaluated to prevent recursion private readonly List evaluationStack; - // Tracks whether recursion was encountered during evaluation - private int recursionHitCounter = 0; + // Shared recursion tracker across nested evaluations + private sealed class RecursionTracker { public int Hits; } + private readonly RecursionTracker recursion; #endregion @@ -36,6 +37,7 @@ namespace CSMic expressionVariables = new Dictionary(StringComparer.Ordinal); functions = new Dictionary(StringComparer.Ordinal); evaluationStack = new List(); + recursion = new RecursionTracker(); } // Internal constructor to create a child interpreter that shares stores @@ -47,6 +49,8 @@ namespace CSMic this.functions = parent.functions; // Share the evaluation stack so recursion is tracked across nested parses this.evaluationStack = parent.evaluationStack; + // Share recursion hit counter across nested evaluations + this.recursion = parent.recursion; } #endregion @@ -130,27 +134,49 @@ namespace CSMic internal bool TryGetExpression(string name, out string 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; } + // Recursion tracking helpers managed by the parser when evaluating expression variables + internal bool IsEvaluating(string name) => evaluationStack.Contains(name); + + internal int BeginEvaluating(string name) + { + int depth = evaluationStack.Count; + evaluationStack.Add(name); + return depth; + } + + internal void EndEvaluating(int depth) + { + while (evaluationStack.Count > depth) + { + evaluationStack.RemoveAt(evaluationStack.Count - 1); + } + } + + // Recursion hit scoping across a single top-level expression evaluation + internal int BeginRecursionScope() + { + return recursion.Hits; + } + + // Returns true if recursion occurred within this scope + internal bool EndRecursionScope(int startHits) + { + return recursion.Hits > startHits; + } + + internal void MarkRecursionHit() + { + recursion.Hits++; + } + internal void AssignNumeric(string name, decimal value) { numericVariables[name] = value; @@ -178,8 +204,6 @@ 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) @@ -189,20 +213,11 @@ namespace CSMic 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); - } + // no-op: evaluation stack is managed by the parser around calls } } diff --git a/src/Core/cocor/Interpreter.atg b/src/Core/cocor/Interpreter.atg index b693d6e..27b1192 100644 --- a/src/Core/cocor/Interpreter.atg +++ b/src/Core/cocor/Interpreter.atg @@ -145,11 +145,31 @@ Term = (. decimal r1 = 0; r = 0; .) Factor - { IF(IsImplicitMul()) Factor (. r *= r1; .) - | '*' Factor (. r *= r1; .) - | '/' Factor (. r /= r1; .) - | '%' Term (. r %= r1; .) - } + (. + // Manual loop to avoid LL(1) conflict introduced by implicit multiplication + // which caused '+'/'-' to be treated as successors of a deletable alternative + // and misparsed as '%'. + while (IsImplicitMul() || la.val == "*" || la.val == "/" || la.val == "%") + { + if (IsImplicitMul()) + { + decimal tmp = 0; + Factor(out tmp); r *= tmp; + } + else if (la.val == "*") + { + Get(); Factor(out r1); r *= r1; + } + else if (la.val == "/") + { + Get(); Factor(out r1); r /= r1; + } + else // "%" + { + Get(); Term(out r1); r %= r1; + } + } + .) . Factor @@ -211,15 +231,37 @@ Value (. { if (this.interpreter.TryGetExpression(ident, out expr)) { - FunctionValue eval = this.interpreter.EvaluateExpression(expr); - if (eval.Type == FunctionValueType.Numeric && eval.Value != null) + // Prevent self or cyclic recursion + if (this.interpreter.IsEvaluating(ident)) { - r = signum * Convert.ToDecimal(eval.Value); + this.interpreter.MarkRecursionHit(); + r = 0; } else { - SemErr("expression variable did not evaluate to a number"); - r = 0; + int depth = this.interpreter.BeginEvaluating(ident); + int recScope = this.interpreter.BeginRecursionScope(); + decimal computed = 0; + bool hasValue = false; + try + { + FunctionValue eval = this.interpreter.EvaluateExpression(expr); + if (eval.Type == FunctionValueType.Numeric && eval.Value != null) + { + computed = Convert.ToDecimal(eval.Value); + hasValue = true; + } + else + { + SemErr("expression variable did not evaluate to a number"); + } + } + finally + { + bool recurse = this.interpreter.EndRecursionScope(recScope); + this.interpreter.EndEvaluating(depth); + r = recurse ? 0 : (hasValue ? signum * computed : 0); + } } } else diff --git a/src/StandardLibrary/CSMic.StandardLibrary.csproj b/src/StandardLibrary/CSMic.StandardLibrary.csproj index 3b21b3d..c5cb9dc 100644 --- a/src/StandardLibrary/CSMic.StandardLibrary.csproj +++ b/src/StandardLibrary/CSMic.StandardLibrary.csproj @@ -12,7 +12,7 @@ - +