Interpreter: fix implicit multiplication LL(1) conflict and recursion handling

- Rewrite Term loop in Interpreter.atg to avoid deletable alternative causing '+'/'-' to be treated as '%'.
- Support implicit multiplication by explicit adjacency checks without affecting additive parsing.
- Add cycle-safe expression evaluation: track evaluation stack and collapse self/mutual recursion to zero via shared recursion scope.
- Update StandardLibrary project reference casing to 'CSMic.Core.csproj'.

All tests pass: 85/85.
This commit is contained in:
codex 2025-08-29 20:01:51 -05:00
commit 6ed3ded0ed
3 changed files with 95 additions and 38 deletions

View file

@ -22,8 +22,9 @@ namespace CSMic
// 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;
// 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<string, string>(StringComparer.Ordinal);
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
evaluationStack = new List<string>();
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
}
}

View file

@ -145,11 +145,31 @@ Term<out decimal r>
=
(. decimal r1 = 0; r = 0; .)
Factor<out r>
{ IF(IsImplicitMul()) Factor<out r1> (. r *= r1; .)
| '*' Factor<out r1> (. r *= r1; .)
| '/' Factor<out r1> (. r /= r1; .)
| '%' Term<out r1> (. 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<out decimal r>
@ -211,15 +231,37 @@ Value<out decimal r> (.
{
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

View file

@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\CsMic.Core.csproj" />
<ProjectReference Include="..\Core\CSMic.Core.csproj" />
</ItemGroup>
<Import Project="NuGetPublish.targets" Condition="Exists('NuGetPublish.targets')" />