From 7d3dee36fadd9788f3aedaa4f9a0862aa6670c43 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 16 Dec 2025 10:29:26 -0700 Subject: [PATCH] flag array_push and array_pop as modifying the array parameter --- .../Expressions/AssignmentExpression.cs | 4 + .../Expressions/FunctionCallExpression.cs | 25 ++++++ .../FunctionDefinitionExpression.cs | 76 +++++++++++++++++++ .../Parser/Expressions/VariableExpression.cs | 2 + Source/Parser/Functions/ArrayPopFunction.cs | 2 +- Source/Parser/Functions/ArrayPushFunction.cs | 2 +- .../IndexedVariableExpressionTests.cs | 49 ++++++++++++ .../Parser/Functions/ArrayPopFunctionTests.cs | 54 +++++++++++++ .../Functions/ArrayPushFunctionTests.cs | 51 +++++++++++++ 9 files changed, 263 insertions(+), 2 deletions(-) diff --git a/Source/Parser/Expressions/AssignmentExpression.cs b/Source/Parser/Expressions/AssignmentExpression.cs index 0417f164..5f531a86 100644 --- a/Source/Parser/Expressions/AssignmentExpression.cs +++ b/Source/Parser/Expressions/AssignmentExpression.cs @@ -149,6 +149,10 @@ void INestedExpressions.GetModifications(HashSet modifies) modifies.Add("." + classMember.Member.Name); else modifies.Add(variable.Name); + + var nested = Value as INestedExpressions; + if (nested != null) + nested.GetModifications(modifies); } } } diff --git a/Source/Parser/Expressions/FunctionCallExpression.cs b/Source/Parser/Expressions/FunctionCallExpression.cs index 7894ad4b..baebcc3e 100644 --- a/Source/Parser/Expressions/FunctionCallExpression.cs +++ b/Source/Parser/Expressions/FunctionCallExpression.cs @@ -35,6 +35,7 @@ internal FunctionCallExpression(IValueExpression source, ICollection _referenceParameters; /// /// Gets the name of the function to call. @@ -193,6 +194,10 @@ private bool Evaluate(InterpreterScope scope, bool isInvoking, out ExpressionBas } } + var userFunctionDefinition = functionDefinition as UserFunctionDefinitionExpression; + if (userFunctionDefinition != null) + userFunctionDefinition.UpdateReferenceParameters(scope); + var functionParametersScope = GetParameters(functionDefinition, scope, out result); if (functionParametersScope == null || result is ErrorExpression) return false; @@ -203,6 +208,21 @@ private bool Evaluate(InterpreterScope scope, bool isInvoking, out ExpressionBas return false; } + _referenceParameters = null; + if (functionDefinition.Parameters.Any(p => p.IsMutableReference)) + { + var referenceParameters = new HashSet(); + foreach (var mutableParameter in functionDefinition.Parameters.Where(p => p.IsMutableReference)) + { + var value = functionParametersScope.GetVariable(mutableParameter.Name) as VariableReferenceExpression; + if (value != null) + referenceParameters.Add(value.Variable.Name); + } + + if (referenceParameters.Count > 0) + _referenceParameters = referenceParameters.ToArray(); + } + functionParametersScope.Context = this; if (isInvoking) functionDefinition.Invoke(functionParametersScope, out result); @@ -661,6 +681,11 @@ void INestedExpressions.GetDependencies(HashSet dependencies) void INestedExpressions.GetModifications(HashSet modifies) { + if (_referenceParameters != null) + { + foreach (var parameter in _referenceParameters) + modifies.Add(parameter); + } } public override bool? IsTrue(InterpreterScope scope, out ErrorExpression error) diff --git a/Source/Parser/Expressions/FunctionDefinitionExpression.cs b/Source/Parser/Expressions/FunctionDefinitionExpression.cs index 438251c2..3045e3b6 100644 --- a/Source/Parser/Expressions/FunctionDefinitionExpression.cs +++ b/Source/Parser/Expressions/FunctionDefinitionExpression.cs @@ -850,6 +850,82 @@ public override bool Invoke(InterpreterScope scope, out ExpressionBase result) result = null; return true; } + + private bool _referenceParametersUpdated = false; + + internal void UpdateReferenceParameters(InterpreterScope scope) + { + if (_referenceParametersUpdated) + return; + + // set first to prevent infinite recursion if function calls itself + _referenceParametersUpdated = true; + + foreach (var expression in Expressions) + DetermineReferenceParameters(scope, expression); + } + + private void DetermineReferenceParameters(InterpreterScope scope, ExpressionBase expression) + { + var nestedExpressions = expression as INestedExpressions; + if (nestedExpressions == null) + return; + + var assignmentExpression = expression as AssignmentExpression; + if (assignmentExpression != null) + { + var indexingExpression = assignmentExpression.Variable as IndexedVariableExpression; + if (indexingExpression != null) + { + var parameter = Parameters.FirstOrDefault(p => p.Name == indexingExpression.Variable.Name); + if (parameter != null) + parameter.IsMutableReference = true; + } + + return; + } + + var functionCall = expression as FunctionCallExpression; + if (functionCall == null || functionCall.FunctionName == null) + { + foreach (var nestedExpression in nestedExpressions.NestedExpressions) + DetermineReferenceParameters(scope, nestedExpression); + + return; + } + + var functionDefinition = scope.GetFunction(functionCall.FunctionName.Name); + if (functionDefinition == null) + return; + + var userFunctionDefinition = functionDefinition as UserFunctionDefinitionExpression; + if (userFunctionDefinition != null) + userFunctionDefinition.UpdateReferenceParameters(scope); + + if (functionDefinition.Parameters.Any(p => p.IsMutableReference)) + { + var extraScope = new InterpreterScope(scope); + var dummyValue = new IntegerConstantExpression(0); + foreach (var parameter in Parameters) + extraScope.DefineVariable(parameter, new VariableReferenceExpression(parameter, dummyValue)); + + ExpressionBase result; + var functionParametersScope = functionCall.GetParameters(functionDefinition, extraScope, out result); + if (functionParametersScope != null) + { + foreach (var mutableParameter in functionDefinition.Parameters.Where(p => p.IsMutableReference)) + { + var value = functionParametersScope.GetVariable(mutableParameter.Name) as VariableReferenceExpression; + if (value != null) + { + var parameter = Parameters.FirstOrDefault(p => p.Name == value.Variable.Name); + if (parameter != null) + parameter.IsMutableReference = true; + } + } + } + } + } } internal class AnonymousUserFunctionDefinitionExpression : UserFunctionDefinitionExpression diff --git a/Source/Parser/Expressions/VariableExpression.cs b/Source/Parser/Expressions/VariableExpression.cs index 81468641..5e63cdfc 100644 --- a/Source/Parser/Expressions/VariableExpression.cs +++ b/Source/Parser/Expressions/VariableExpression.cs @@ -197,6 +197,8 @@ internal VariableDefinitionExpression(VariableExpressionBase variable) Location = variable.Location; } + public bool IsMutableReference { get; set;} + IEnumerable INestedExpressions.NestedExpressions { get diff --git a/Source/Parser/Functions/ArrayPopFunction.cs b/Source/Parser/Functions/ArrayPopFunction.cs index 71434a54..5d7c7b3b 100644 --- a/Source/Parser/Functions/ArrayPopFunction.cs +++ b/Source/Parser/Functions/ArrayPopFunction.cs @@ -7,7 +7,7 @@ internal class ArrayPopFunction : FunctionDefinitionExpression public ArrayPopFunction() : base("array_pop") { - Parameters.Add(new VariableDefinitionExpression("array")); + Parameters.Add(new VariableDefinitionExpression("array") { IsMutableReference = true }); } public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) diff --git a/Source/Parser/Functions/ArrayPushFunction.cs b/Source/Parser/Functions/ArrayPushFunction.cs index 29564786..664c4494 100644 --- a/Source/Parser/Functions/ArrayPushFunction.cs +++ b/Source/Parser/Functions/ArrayPushFunction.cs @@ -7,7 +7,7 @@ internal class ArrayPushFunction : FunctionDefinitionExpression public ArrayPushFunction() : base("array_push") { - Parameters.Add(new VariableDefinitionExpression("array")); + Parameters.Add(new VariableDefinitionExpression("array") { IsMutableReference = true }); Parameters.Add(new VariableDefinitionExpression("value")); } diff --git a/Tests/Parser/Expressions/IndexedVariableExpressionTests.cs b/Tests/Parser/Expressions/IndexedVariableExpressionTests.cs index 6a5cd728..c45547ad 100644 --- a/Tests/Parser/Expressions/IndexedVariableExpressionTests.cs +++ b/Tests/Parser/Expressions/IndexedVariableExpressionTests.cs @@ -392,5 +392,54 @@ public void TestArrayIndexOutOfRange() "3:5 Index 5 not in range 0-1"); } + + [Test] + public void TestGetModificationsNestedAssignment() + { + var input = + "function f(a) { a[0] = 3 }\r\n" + + "arr = [1,2,3,4]\r\n" + + "f(arr)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(2).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(1)); + Assert.That(modifications.Contains("arr")); + } + + [Test] + public void TestGetModificationsNestedRead() + { + var input = + "function f(a) { b = a[0] }\r\n" + + "arr = [1,2,3,4]\r\n" + + "f(arr)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(2).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do, but read shouldn't mark the parameter as modified + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(0)); + } } } diff --git a/Tests/Parser/Functions/ArrayPopFunctionTests.cs b/Tests/Parser/Functions/ArrayPopFunctionTests.cs index 5fd938ce..b16d224b 100644 --- a/Tests/Parser/Functions/ArrayPopFunctionTests.cs +++ b/Tests/Parser/Functions/ArrayPopFunctionTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using RATools.Parser.Expressions; using RATools.Parser.Functions; +using System.Collections.Generic; using System.Linq; namespace RATools.Parser.Tests.Functions @@ -16,6 +17,7 @@ public void TestDefinition() Assert.That(def.Name.Name, Is.EqualTo("array_pop")); Assert.That(def.Parameters.Count, Is.EqualTo(1)); Assert.That(def.Parameters.ElementAt(0).Name, Is.EqualTo("array")); + Assert.That(def.Parameters.ElementAt(0).IsMutableReference, Is.True); } private static ExpressionBase Evaluate(string input, InterpreterScope scope) @@ -154,5 +156,57 @@ public void TestPopComparison() Assert.That(comparison.Right, Is.InstanceOf()); Assert.That(((IntegerConstantExpression)comparison.Right).Value, Is.EqualTo(2)); } + + [Test] + public void TestGetModifications() + { + var input = + "arr = [1,2,3,4]\r\n" + + "b = array_pop(arr)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(1).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(1)); + Assert.That(modifications.Contains("b")); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(2)); + Assert.That(modifications.Contains("b")); + Assert.That(modifications.Contains("arr")); + } + + [Test] + public void TestGetModificationsNested() + { + var input = + "function f(a) { array_pop(a) }\r\n" + + "arr = [1,2,3,4]\r\n" + + "f(arr)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(2).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(1)); + Assert.That(modifications.Contains("arr")); + } } } diff --git a/Tests/Parser/Functions/ArrayPushFunctionTests.cs b/Tests/Parser/Functions/ArrayPushFunctionTests.cs index 021b4040..946a1ee5 100644 --- a/Tests/Parser/Functions/ArrayPushFunctionTests.cs +++ b/Tests/Parser/Functions/ArrayPushFunctionTests.cs @@ -4,6 +4,7 @@ using RATools.Parser.Expressions; using RATools.Parser.Expressions.Trigger; using RATools.Parser.Functions; +using System.Collections.Generic; using System.Linq; namespace RATools.Parser.Tests.Functions @@ -18,6 +19,7 @@ public void TestDefinition() Assert.That(def.Name.Name, Is.EqualTo("array_push")); Assert.That(def.Parameters.Count, Is.EqualTo(2)); Assert.That(def.Parameters.ElementAt(0).Name, Is.EqualTo("array")); + Assert.That(def.Parameters.ElementAt(0).IsMutableReference, Is.True); Assert.That(def.Parameters.ElementAt(1).Name, Is.EqualTo("value")); } @@ -148,5 +150,54 @@ public void TestPushMemoryComparison() Assert.That(comparison.Right, Is.InstanceOf()); Assert.That(((IntegerConstantExpression)comparison.Right).Value, Is.EqualTo(2)); } + + [Test] + public void TestGetModifications() + { + var input = + "arr = []\r\n" + + "array_push(arr, 3)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(1).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(1)); + Assert.That(modifications.Contains("arr")); + } + + [Test] + public void TestGetModificationsNested() + { + var input = + "function f(a) { array_push(a, 3) }\r\n" + + "arr = []\r\n" + + "f(arr)"; + var tokenizer = Tokenizer.CreateTokenizer(input); + var parser = new AchievementScriptInterpreter(); + var groups = parser.Parse(tokenizer); + + // before execution, we don't know if a parameter will be a reference + var expr = groups.Groups.ElementAt(2).Expressions.ElementAt(0); + var modifications = new HashSet(); + ((INestedExpressions)expr).GetModifications(modifications); + + AchievementScriptInterpreter.InitializeScope(groups, null); + parser.Run(groups); + + // after execution, we do + ((INestedExpressions)expr).GetModifications(modifications); + Assert.That(modifications.Count, Is.EqualTo(1)); + Assert.That(modifications.Contains("arr")); + } } }