using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using Unity.VisualScripting.Antlr3.Runtime; using UnityEngine; namespace Unity.VisualScripting.Dependencies.NCalc { public class Expression { private Expression() { // Fix: the original grammar doesn't include a null identifier. Parameters["null"] = Parameters["NULL"] = null; } public Expression(string expression, EvaluateOptions options = EvaluateOptions.None) : this() { if (string.IsNullOrEmpty(expression)) { throw new ArgumentException("Expression can't be empty", nameof(expression)); } // Fix: The original grammar doesn't allow double quotes for strings. expression = expression.Replace('\"', '\''); OriginalExpression = expression; Options = options; } public Expression(LogicalExpression expression, EvaluateOptions options = EvaluateOptions.None) : this() { if (expression == null) { throw new ArgumentException("Expression can't be null", nameof(expression)); } ParsedExpression = expression; Options = options; } public event EvaluateFunctionHandler EvaluateFunction; public event EvaluateParameterHandler EvaluateParameter; /// /// Textual representation of the expression to evaluate. /// protected readonly string OriginalExpression; protected Dictionary ParameterEnumerators; private Dictionary _parameters; public EvaluateOptions Options { get; set; } public string Error { get; private set; } public LogicalExpression ParsedExpression { get; private set; } public Dictionary Parameters { get { return _parameters ?? (_parameters = new Dictionary()); } set { _parameters = value; } } public void UpdateUnityTimeParameters() { Parameters["dt"] = Parameters["DT"] = Time.deltaTime; Parameters["second"] = Parameters["Second"] = 1 / Time.deltaTime; } /// /// Pre-compiles the expression in order to check syntax errors. /// If errors are detected, the Error property contains the message. /// /// True if the expression syntax is correct, otherwise false public bool HasErrors() { try { if (ParsedExpression == null) { ParsedExpression = Compile(OriginalExpression, (Options & EvaluateOptions.NoCache) == EvaluateOptions.NoCache); } // In case HasErrors() is called multiple times for the same expression return ParsedExpression != null && Error != null; } catch (Exception e) { Error = e.Message; return true; } } public object Evaluate(Flow flow) { if (HasErrors()) { throw new EvaluationException(Error); } if (ParsedExpression == null) { ParsedExpression = Compile(OriginalExpression, (Options & EvaluateOptions.NoCache) == EvaluateOptions.NoCache); } var visitor = new EvaluationVisitor(flow, Options); visitor.EvaluateFunction += EvaluateFunction; visitor.EvaluateParameter += EvaluateParameter; visitor.Parameters = Parameters; // If array evaluation, execute the same expression multiple times if ((Options & EvaluateOptions.IterateParameters) == EvaluateOptions.IterateParameters) { var size = -1; ParameterEnumerators = new Dictionary(); foreach (var parameter in Parameters.Values) { if (parameter is IEnumerable enumerable) { var localsize = 0; foreach (var o in enumerable) { localsize++; } if (size == -1) { size = localsize; } else if (localsize != size) { throw new EvaluationException("When IterateParameters option is used, IEnumerable parameters must have the same number of items."); } } } foreach (var key in Parameters.Keys) { var parameter = Parameters[key] as IEnumerable; if (parameter != null) { ParameterEnumerators.Add(key, parameter.GetEnumerator()); } } var results = new List(); for (var i = 0; i < size; i++) { foreach (var key in ParameterEnumerators.Keys) { var enumerator = ParameterEnumerators[key]; enumerator.MoveNext(); Parameters[key] = enumerator.Current; } ParsedExpression.Accept(visitor); results.Add(visitor.Result); } return results; } else { ParsedExpression.Accept(visitor); return visitor.Result; } } public static LogicalExpression Compile(string expression, bool noCache) { LogicalExpression logicalExpression = null; if (_cacheEnabled && !noCache) { try { Rwl.AcquireReaderLock(Timeout.Infinite); if (_compiledExpressions.ContainsKey(expression)) { Trace.TraceInformation("Expression retrieved from cache: " + expression); var wr = _compiledExpressions[expression]; logicalExpression = wr.Target as LogicalExpression; if (wr.IsAlive && logicalExpression != null) { return logicalExpression; } } } finally { Rwl.ReleaseReaderLock(); } } if (logicalExpression == null) { var lexer = new NCalcLexer(new ANTLRStringStream(expression)); var parser = new NCalcParser(new CommonTokenStream(lexer)); logicalExpression = parser.ncalcExpression().value; if (parser.Errors != null && parser.Errors.Count > 0) { throw new EvaluationException(String.Join(Environment.NewLine, parser.Errors.ToArray())); } if (_cacheEnabled && !noCache) { try { Rwl.AcquireWriterLock(Timeout.Infinite); _compiledExpressions[expression] = new WeakReference(logicalExpression); } finally { Rwl.ReleaseWriterLock(); } CleanCache(); Trace.TraceInformation("Expression added to cache: " + expression); } } return logicalExpression; } #region Cache management private static bool _cacheEnabled = true; private static Dictionary _compiledExpressions = new Dictionary(); private static readonly ReaderWriterLock Rwl = new ReaderWriterLock(); public static bool CacheEnabled { get { return _cacheEnabled; } set { _cacheEnabled = value; if (!CacheEnabled) { // Clears cache _compiledExpressions = new Dictionary(); } } } /// /// Removes unused entries from cached compiled expression. /// private static void CleanCache() { var keysToRemove = new List(); try { Rwl.AcquireWriterLock(Timeout.Infinite); foreach (var de in _compiledExpressions) { if (!de.Value.IsAlive) { keysToRemove.Add(de.Key); } } foreach (var key in keysToRemove) { _compiledExpressions.Remove(key); Trace.TraceInformation("Cache entry released: " + key); } } finally { Rwl.ReleaseReaderLock(); } } #endregion } }