diff --git a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs index 10d9d8ea92..077f733a93 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs @@ -3389,6 +3389,14 @@ public override bool PreVisit(DottedNameNode node) return true; } + public override bool PreVisit(AsNode node) + { + Contracts.AssertValue(node); + + _txb.AddVolatileVariables(node.Left, _txb.GetVolatileVariables(node)); + return true; + } + public override void PostVisit(DottedNameNode node) { AssertValid(); diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/BinderTests.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/BinderTests.cs index b5d4d2a785..09334e3a60 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/BinderTests.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/BinderTests.cs @@ -6,7 +6,9 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Numerics; using System.Reflection.Metadata; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.PowerFx.Core.App.Controls; using Microsoft.PowerFx.Core.Binding; @@ -163,5 +165,90 @@ public void TestHasErrorsInTreeCallNode() Assert.True(binding.ErrorContainer.HasErrorsInTree(binding.Top)); } + + /// + /// Tests that variable volatility propagates to the children of As nodes. + /// + [Fact] + public void TestVolatileVariablesWithForAllAsKeyword() + { + var config = new PowerFxConfig(); + config.SymbolTable.AddVariable( + "volatileVariable", + new KnownRecordType(TestUtils.DT("![DummyField:*[Value:n]]")), + mutable: true); + config.SymbolTable.AddVariable( + "gblTable", + new TableType(TestUtils.DT("*[Value:n]")), + mutable: true); + + config.AddFunction(new FakeSetFunction()); + + var engine = new Engine(config); + var parserOptions = new ParserOptions { AllowsSideEffects = true }; + + const string expression = @"With( + {dummyWith: gblTable}, + Set( + volatileVariable, + {DummyField: dummyWith} + ); + ForAll( + volatileVariable.DummyField| As currentRecord, + currentRecord.Value + 1 + ) + )"; + + var indices = Regex.Matches(expression, Regex.Escape("|")) + .Select(m => m.Index) + .ToArray(); + + string result = Regex.Replace(expression, Regex.Escape("|"), string.Empty); + var checkResult = engine.Check(result, parserOptions); + Assert.True(checkResult.IsSuccess); + + var binding = checkResult.Binding; + + // Navigate to the ForAll node + foreach (var index in indices) + { + var node = FindNodeVisitor.Run(checkResult.Binding.Top, index); + Assert.True(binding.IsUnliftable(node)); + } + } + + private class FakeSetFunction : BuiltinFunction + { + public FakeSetFunction() + : base( + "Set", + _ => "Mocks the set function", + FunctionCategories.Behavior, + DType.Boolean, + 0, + 2, + 2) + { + } + + public override bool IsSelfContained => false; + + public override IEnumerable GetSignatures() => + Enumerable.Empty(); + + public override IEnumerable GetIdentifierOfModifiedValue( + TexlNode[] args, + out TexlNode identifierNode) + { + if (args.FirstOrDefault() is FirstNameNode { Ident: var ident } firstNameNode) + { + identifierNode = firstNameNode; + return new List { ident }; + } + + identifierNode = null; + return null; + } + } } }