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
|
// Tracks expression variables currently being evaluated to prevent recursion
|
||||||
private readonly List<string> evaluationStack;
|
private readonly List<string> evaluationStack;
|
||||||
// Tracks whether recursion was encountered during evaluation
|
// Shared recursion tracker across nested evaluations
|
||||||
private int recursionHitCounter = 0;
|
private sealed class RecursionTracker { public int Hits; }
|
||||||
|
private readonly RecursionTracker recursion;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ namespace CSMic
|
||||||
expressionVariables = new Dictionary<string, string>(StringComparer.Ordinal);
|
expressionVariables = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
|
functions = new Dictionary<string, ICodedFunction>(StringComparer.Ordinal);
|
||||||
evaluationStack = new List<string>();
|
evaluationStack = new List<string>();
|
||||||
|
recursion = new RecursionTracker();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal constructor to create a child interpreter that shares stores
|
// Internal constructor to create a child interpreter that shares stores
|
||||||
|
@ -47,6 +49,8 @@ namespace CSMic
|
||||||
this.functions = parent.functions;
|
this.functions = parent.functions;
|
||||||
// Share the evaluation stack so recursion is tracked across nested parses
|
// Share the evaluation stack so recursion is tracked across nested parses
|
||||||
this.evaluationStack = parent.evaluationStack;
|
this.evaluationStack = parent.evaluationStack;
|
||||||
|
// Share recursion hit counter across nested evaluations
|
||||||
|
this.recursion = parent.recursion;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -130,27 +134,49 @@ namespace CSMic
|
||||||
|
|
||||||
internal bool TryGetExpression(string name, out string expr)
|
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!))
|
if (expressionVariables.TryGetValue(name, out expr!))
|
||||||
{
|
{
|
||||||
// Mark as currently evaluating; EvaluateExpression will unwind
|
|
||||||
evaluationStack.Add(name);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
expr = string.Empty;
|
expr = string.Empty;
|
||||||
return false;
|
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)
|
internal void AssignNumeric(string name, decimal value)
|
||||||
{
|
{
|
||||||
numericVariables[name] = value;
|
numericVariables[name] = value;
|
||||||
|
@ -178,8 +204,6 @@ namespace CSMic
|
||||||
{
|
{
|
||||||
// Create a child interpreter sharing stores, so ProduceOutput doesn't affect parent state
|
// Create a child interpreter sharing stores, so ProduceOutput doesn't affect parent state
|
||||||
var child = new InputInterpreter(this);
|
var child = new InputInterpreter(this);
|
||||||
int depth = evaluationStack.Count;
|
|
||||||
int hitStart = recursionHitCounter;
|
|
||||||
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(expressionText));
|
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(expressionText));
|
||||||
var scanner = new CSMic.Interpreter.Scanner(ms);
|
var scanner = new CSMic.Interpreter.Scanner(ms);
|
||||||
var parser = new CSMic.Interpreter.Parser(scanner)
|
var parser = new CSMic.Interpreter.Parser(scanner)
|
||||||
|
@ -189,20 +213,11 @@ namespace CSMic
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
parser.Parse();
|
parser.Parse();
|
||||||
// If recursion was detected during this evaluation, collapse to zero
|
|
||||||
if (recursionHitCounter > hitStart)
|
|
||||||
{
|
|
||||||
return new FunctionValue(FunctionValueType.Numeric, 0m);
|
|
||||||
}
|
|
||||||
return parser.Result;
|
return parser.Result;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Unwind any names pushed during this evaluation to avoid leaking state
|
// no-op: evaluation stack is managed by the parser around calls
|
||||||
while (evaluationStack.Count > depth)
|
|
||||||
{
|
|
||||||
evaluationStack.RemoveAt(evaluationStack.Count - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,11 +145,31 @@ Term<out decimal r>
|
||||||
=
|
=
|
||||||
(. decimal r1 = 0; r = 0; .)
|
(. decimal r1 = 0; r = 0; .)
|
||||||
Factor<out r>
|
Factor<out r>
|
||||||
{ IF(IsImplicitMul()) Factor<out r1> (. r *= r1; .)
|
(.
|
||||||
| '*' Factor<out r1> (. r *= r1; .)
|
// Manual loop to avoid LL(1) conflict introduced by implicit multiplication
|
||||||
| '/' Factor<out r1> (. r /= r1; .)
|
// which caused '+'/'-' to be treated as successors of a deletable alternative
|
||||||
| '%' Term<out r1> (. r %= r1; .)
|
// 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>
|
Factor<out decimal r>
|
||||||
|
@ -211,15 +231,37 @@ Value<out decimal r> (.
|
||||||
{
|
{
|
||||||
if (this.interpreter.TryGetExpression(ident, out expr))
|
if (this.interpreter.TryGetExpression(ident, out expr))
|
||||||
{
|
{
|
||||||
FunctionValue eval = this.interpreter.EvaluateExpression(expr);
|
// Prevent self or cyclic recursion
|
||||||
if (eval.Type == FunctionValueType.Numeric && eval.Value != null)
|
if (this.interpreter.IsEvaluating(ident))
|
||||||
{
|
{
|
||||||
r = signum * Convert.ToDecimal(eval.Value);
|
this.interpreter.MarkRecursionHit();
|
||||||
|
r = 0;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SemErr("expression variable did not evaluate to a number");
|
int depth = this.interpreter.BeginEvaluating(ident);
|
||||||
r = 0;
|
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
|
else
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Core\CsMic.Core.csproj" />
|
<ProjectReference Include="..\Core\CSMic.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="NuGetPublish.targets" Condition="Exists('NuGetPublish.targets')" />
|
<Import Project="NuGetPublish.targets" Condition="Exists('NuGetPublish.targets')" />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue