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:
parent
135af2cc49
commit
6ed3ded0ed
3 changed files with 95 additions and 38 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue