From 3ccd3c2e21cbf1fabe9f9f85f2fabf00419a17c3 Mon Sep 17 00:00:00 2001 From: swmal <{ID}+username}@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:33:19 +0200 Subject: [PATCH 1/5] #2329 - improved handling of depdendency chain/formula stack in Lambda calculations --- .../DependencyChain/RpnFormula.cs | 12 +++++ .../DependencyChain/RpnFormulaExecution.cs | 4 +- .../Excel/Functions/RefAndLookup/Hstack.cs | 20 ++----- .../RefAndLookup/StackFunctionBase.cs | 54 +++++++++++++++++++ .../Excel/Functions/RefAndLookup/Vstack.cs | 27 ++-------- .../FormulaExpressions/LambdaCalculator.cs | 4 +- .../FormulaExpressions/LambdaEtaExpression.cs | 4 +- .../Issues/FormulaCalculationIssues.cs | 17 ++++++ 8 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/StackFunctionBase.cs diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormula.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormula.cs index 9539a3584a..f47f55fe9d 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormula.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormula.cs @@ -34,6 +34,7 @@ internal enum FormulaFlags : short { IsDynamic = 1, IsAlwaysDynamic = 2, + IsLambda = 4, } internal class RpnFormula { @@ -52,6 +53,9 @@ internal class RpnFormula internal int _arrayIndex = -1; internal FormulaFlags _flags = 0; internal FunctionExpression _currentFunction = null; + // saves the initial formula stack count when executing a lambda expression + // to avoid popping formulas pushed before the lambda expression was invoked. + internal int _lambdaFormulaStackCount = 0; private VariableStorageManager _variableStorage; public bool CanBeDynamicArray @@ -62,6 +66,14 @@ public bool CanBeDynamicArray } } + public bool IsLambda + { + get + { + return (_flags & FormulaFlags.IsLambda) == FormulaFlags.IsLambda; + } + } + public bool IgnoreCaching { get; set; diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs index 42e29e4c85..cb07a2a7c0 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs @@ -516,7 +516,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d cr = f._expressionStack.Pop().Compile(); } - if (cr != null && (writeToCell || depChain._formulaStack.Count > 0)) // If calculating single cell via the FormulaParser.Parse method we should not write to the cells + if (cr != null && f.IsLambda == false && (writeToCell || depChain._formulaStack.Count > 0)) // If calculating single cell via the FormulaParser.Parse method we should not write to the cells { SetValueToWorkbook(depChain, f, rd, cr, options, ref depChainPos); } @@ -526,7 +526,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d depChain._parsingContext.Parser.Logger.Log($"Set value in Cell\t{f.GetAddress()}\t{cr.ResultValue}\t{cr.DataType}"); } - if (depChain._formulaStack.Count > 0) + if (depChain._formulaStack.Count > f._lambdaFormulaStackCount) { f = depChain._formulaStack.Pop(); if (f._formulaEnumerator == null) diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Hstack.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Hstack.cs index a6d31fe4b7..e3bf2d81c1 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Hstack.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Hstack.cs @@ -25,26 +25,14 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup EPPlusVersion = "7", Description = "Combines arrays horizontally into a single array.", SupportsArrays = true)] - internal class Hstack : ExcelFunction + internal class Hstack : StackFunctionBase { - public override string NamespacePrefix => "_xlfn."; - - public override int ArgumentMinLength => 1; public override CompileResult Execute(IList arguments, ParsingContext context) { - var ranges = new List(); - foreach (var arg in arguments) + var ranges = GetRanges(arguments, out ExcelErrorValue err); + if (err != null) { - if (!arg.IsExcelRange) - { - var rng = new InMemoryRange(1, 1); - rng.SetValue(0, 0, arg.Value); - ranges.Add(rng); - } - else - { - ranges.Add(arg.ValueAsRangeInfo); - } + return CreateDynamicArrayResult(err, DataType.ExcelError); } var nRows = ranges.Max(x => x.Size.NumberOfRows); var nCols = Convert.ToInt16(ranges.Sum(x => x.Size.NumberOfCols)); diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/StackFunctionBase.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/StackFunctionBase.cs new file mode 100644 index 0000000000..7d7e5b4074 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/StackFunctionBase.cs @@ -0,0 +1,54 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 22/3/2025 EPPlus Software AB EPPlus v8 + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using OfficeOpenXml.FormulaParsing.Ranges; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup +{ + internal abstract class StackFunctionBase : ExcelFunction + { + public override string NamespacePrefix => "_xlfn."; + + public override int ArgumentMinLength => 1; + + protected List GetRanges(IEnumerable arguments, out ExcelErrorValue err) + { + err = default; + var ranges = new List(); + foreach (var arg in arguments) + { + if (arg.Value is not IRangeInfo) + { + var rng = new InMemoryRange(1, 1); + rng.SetValue(0, 0, arg.Value); + ranges.Add(rng); + } + else + { + var r = arg.ValueAsRangeInfo; + if (r == null) + { + err = ErrorValues.ValueError; + break; + } + ranges.Add(r); + } + } + return ranges; + } + } +} diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Vstack.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Vstack.cs index d776eb6c26..8d52e98de2 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Vstack.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Vstack.cs @@ -13,10 +13,8 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata; using OfficeOpenXml.FormulaParsing.FormulaExpressions; using OfficeOpenXml.FormulaParsing.Ranges; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup { @@ -25,31 +23,14 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup EPPlusVersion = "7", Description = "Combines arrays vertically into a single array.", SupportsArrays = true)] - internal class Vstack : ExcelFunction + internal class Vstack : StackFunctionBase { - public override string NamespacePrefix => "_xlfn."; - - public override int ArgumentMinLength => 1; public override CompileResult Execute(IList arguments, ParsingContext context) { - var ranges = new List(); - foreach(var arg in arguments) + var ranges = GetRanges(arguments, out ExcelErrorValue err); + if (err != null) { - if(!arg.IsExcelRange) - { - var rng = new InMemoryRange(1, 1); - rng.SetValue(0, 0, arg.Value); - ranges.Add(rng); - } - else - { - var r = arg.ValueAsRangeInfo; - if(r==null) - { - return CreateDynamicArrayResult(ErrorValues.ValueError, DataType.ExcelError); - } - ranges.Add(r); - } + return CreateDynamicArrayResult(err, DataType.ExcelError); } var nRows = ranges.Sum(x => x.Size.NumberOfRows); var nCols = ranges.Max(x => x.Size.NumberOfCols); diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs index a9eee92678..214f62a687 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs @@ -202,12 +202,14 @@ public CompileResult Execute(ParsingContext ctx) formula.IgnoreCaching = true; formula.ExpressionStack = _formula.ExpressionStack; formula.FunctionStack = _formula.FunctionStack; + formula._flags = _formula._flags | FormulaFlags.IsLambda; + formula._lambdaFormulaStackCount = ctx.DependencyChain._formulaStack.Count; var rpnTokens = new RpnTokens { Tokens = _currentTokens, Scope = _scope }; formula.SetTokens(rpnTokens, ctx, _scope); // SetTokens clears the variable storage... ctx.VariableStorage.Push(_scope); var chain = ctx.DependencyChain; - var compileResult = RpnFormulaExecution.ExecutePartialFormula(chain, formula, ctx.CalcOption, false); + var compileResult = RpnFormulaExecution.ExecutePartialFormula(chain, formula, ctx.CalcOption, true); return CompileResultFactory.CreateDynamicArrayResult(compileResult.Result, compileResult.Address, CompileResultType.DynamicArray_AlwaysSetCellAsDynamic); } diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaEtaExpression.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaEtaExpression.cs index 990ad0392f..2b9ed42db9 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaEtaExpression.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaEtaExpression.cs @@ -69,8 +69,10 @@ public override CompileResult Compile() ExpressionStack = _rpnFormula.ExpressionStack, FunctionStack = _rpnFormula.FunctionStack }; + rpnFormula._flags = _rpnFormula._flags | FormulaFlags.IsLambda; + rpnFormula._lambdaFormulaStackCount = Context.DependencyChain._formulaStack.Count; rpnFormula.SetTokens(rpnTokens, Context, _scope); - var result = RpnFormulaExecution.ExecutePartialFormula(Context.DependencyChain, rpnFormula, Context.CalcOption, false); + var result = RpnFormulaExecution.ExecutePartialFormula(Context.DependencyChain, rpnFormula, Context.CalcOption, true); if(result.DataType != DataType.LambdaCalculation) { return CompileResult.GetErrorResult(eErrorType.Value); diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 1a9f4a9220..271a97f72d 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1543,6 +1543,23 @@ public void s1029() Assert.AreEqual(6D, ws.Cells["A34"].Value); SaveAndCleanup(p); } + + [TestMethod] + public void s1031() + { + using var p = OpenTemplatePackage("Aico_CN02_FBL3N_TPU_2026-03.xlsx"); + var calcWs = p.Workbook.Worksheets["calculation"]; + var aicoWs = p.Workbook.Worksheets["Aico data"]; + //calcWs.Cells["A1"].Calculate(); + //var a1 = calcWs.Cells["A1"].Value; + //var a2 = calcWs.Cells["A2"].Value; + //var b2 = calcWs.Cells["B2"].Value; + //var c2 = calcWs.Cells["c2"].Value; + //var a_e22_f = aicoWs.Cells["E22"].Formula; + aicoWs.Cells["G33"].Calculate(); + //p.Workbook.Calculate(); + var v = aicoWs.Cells["G33"].Value; + } } } From d0d425b132b1d07a7d33594f1022b4cfb540a46e Mon Sep 17 00:00:00 2001 From: swmal <{ID}+username}@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:57:12 +0200 Subject: [PATCH 2/5] Modified unit test --- src/EPPlusTest/Issues/FormulaCalculationIssues.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 271a97f72d..1c6034e184 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1556,9 +1556,11 @@ public void s1031() //var b2 = calcWs.Cells["B2"].Value; //var c2 = calcWs.Cells["c2"].Value; //var a_e22_f = aicoWs.Cells["E22"].Formula; - aicoWs.Cells["G33"].Calculate(); + aicoWs.Cells["M4"].Formula = "G33"; + aicoWs.Cells["M4"].Calculate(); //p.Workbook.Calculate(); var v = aicoWs.Cells["G33"].Value; + var v2 = aicoWs.Cells["M4"].Value; } } } From fdd718dfb51b28a20cd1c52aca01dfae4fc11b47 Mon Sep 17 00:00:00 2001 From: swmal <{ID}+username}@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:57:32 +0200 Subject: [PATCH 3/5] modified unittest --- src/EPPlusTest/Issues/FormulaCalculationIssues.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 1c6034e184..aae20f4863 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1557,8 +1557,8 @@ public void s1031() //var c2 = calcWs.Cells["c2"].Value; //var a_e22_f = aicoWs.Cells["E22"].Formula; aicoWs.Cells["M4"].Formula = "G33"; - aicoWs.Cells["M4"].Calculate(); - //p.Workbook.Calculate(); + //aicoWs.Cells["M4"].Calculate(); + p.Workbook.Calculate(); var v = aicoWs.Cells["G33"].Value; var v2 = aicoWs.Cells["M4"].Value; } From e09c9ee011b24f4e5564049c4209e41767df0d90 Mon Sep 17 00:00:00 2001 From: swmal <897655+swmal@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:22:50 +0200 Subject: [PATCH 4/5] #2329 - fixed issue with expression caching and lambda/variable expressions --- .../FormulaExpressions/FunctionExpression.cs | 7 +++++++ src/EPPlusTest/Issues/FormulaCalculationIssues.cs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs index 03fb45959d..989b41a732 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs @@ -229,6 +229,13 @@ internal string GetExpressionKey(RpnFormula f) key.Append(f._tokens[i].Value); } } + else if(e.ExpressionType==ExpressionType.Variable || + e.ExpressionType==ExpressionType.LambdaVariableDeclaration || + e.ExpressionType == ExpressionType.LambdaCalculation || + e.ExpressionType == ExpressionType.LambdaInvoke) + { + return null; + } else { var fa = e.GetAddress(); diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index aae20f4863..08ab3b8b8e 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1556,8 +1556,8 @@ public void s1031() //var b2 = calcWs.Cells["B2"].Value; //var c2 = calcWs.Cells["c2"].Value; //var a_e22_f = aicoWs.Cells["E22"].Formula; - aicoWs.Cells["M4"].Formula = "G33"; - //aicoWs.Cells["M4"].Calculate(); + //aicoWs.Cells["M4"].Formula = "G33"; + //aicoWs.Cells["G33"].Calculate(); p.Workbook.Calculate(); var v = aicoWs.Cells["G33"].Value; var v2 = aicoWs.Cells["M4"].Value; From ec5a2dade0b9692d8428eb42f634d2fa9aa86644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 13 Apr 2026 12:51:29 +0200 Subject: [PATCH 5/5] Remove test s1031 Removed the s1031 test method that was commented out and not in use. --- .../Issues/FormulaCalculationIssues.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 08ab3b8b8e..1a9f4a9220 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1543,25 +1543,6 @@ public void s1029() Assert.AreEqual(6D, ws.Cells["A34"].Value); SaveAndCleanup(p); } - - [TestMethod] - public void s1031() - { - using var p = OpenTemplatePackage("Aico_CN02_FBL3N_TPU_2026-03.xlsx"); - var calcWs = p.Workbook.Worksheets["calculation"]; - var aicoWs = p.Workbook.Worksheets["Aico data"]; - //calcWs.Cells["A1"].Calculate(); - //var a1 = calcWs.Cells["A1"].Value; - //var a2 = calcWs.Cells["A2"].Value; - //var b2 = calcWs.Cells["B2"].Value; - //var c2 = calcWs.Cells["c2"].Value; - //var a_e22_f = aicoWs.Cells["E22"].Formula; - //aicoWs.Cells["M4"].Formula = "G33"; - //aicoWs.Cells["G33"].Calculate(); - p.Workbook.Calculate(); - var v = aicoWs.Cells["G33"].Value; - var v2 = aicoWs.Cells["M4"].Value; - } } }