From dcbdffb7794438a23d8c9b2a4e46269aa8ba0b17 Mon Sep 17 00:00:00 2001 From: Adithya Selvaprithiviraj Date: Mon, 23 Sep 2024 09:25:43 -0700 Subject: [PATCH] Enable UDF on REPL (#2559) Adds ability to define User-defined functions in REPL. Parsing UDFs requires a different parser from the Texl Expression parser and this is addressed by using the DefinitionsParser as a fallback to check when regular parsing fails. If definitions parsing is successful and we find UDF, we add it to the Engine. ![image](https://github.com/user-attachments/assets/ee86d499-2ee6-4232-a7cd-3b5ca9e09d1e) Fixes #2546 --- .../Public/Config/SymbolTable.cs | 46 +++-------- .../Public/DefinitionsCheckResult.cs | 79 ++++++++++++++++++- .../Microsoft.PowerFx.Core/Public/Engine.cs | 4 +- .../RecalcEngine.cs | 7 +- src/libraries/Microsoft.PowerFx.Repl/Repl.cs | 27 +++++++ .../UserDefinedFunctionTests.cs | 2 +- .../RecalcEngineTests.cs | 44 +++++------ .../ReplTests.cs | 25 ++++++ src/tools/Repl/Program.cs | 23 +++++- 9 files changed, 191 insertions(+), 66 deletions(-) diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs index 86b1d4e6ea..a8aa0d3ec9 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs @@ -208,63 +208,39 @@ public void AddConstant(string name, FormulaValue data) } /// - /// Adds an user defined function. + /// Adds user defined functions in the script. /// /// String representation of the user defined function. /// CultureInfo to parse the script againts. Default is invariant. /// Extra symbols to bind UDF. Commonly coming from Engine. /// Additional symbols to bind UDF. /// Allow for curly brace parsing. - internal void AddUserDefinedFunction(string script, CultureInfo parseCulture = null, ReadOnlySymbolTable symbolTable = null, ReadOnlySymbolTable extraSymbolTable = null, bool allowSideEffects = false) + internal DefinitionsCheckResult AddUserDefinedFunction(string script, CultureInfo parseCulture = null, ReadOnlySymbolTable symbolTable = null, ReadOnlySymbolTable extraSymbolTable = null, bool allowSideEffects = false) { // Phase 1: Side affects are not allowed. // Phase 2: Introduces side effects and parsing of function bodies. var options = new ParserOptions() { AllowsSideEffects = allowSideEffects, - Culture = parseCulture ?? CultureInfo.InvariantCulture + Culture = parseCulture ?? CultureInfo.InvariantCulture, }; - var sb = new StringBuilder(); - var parseResult = UserDefinitions.Parse(script, options); - - // Compose will handle null symbols var composedSymbols = Compose(this, symbolTable, extraSymbolTable); - var udfs = UserDefinedFunction.CreateFunctions(parseResult.UDFs.Where(udf => udf.IsParseValid), composedSymbols, out var errors); - - errors.AddRange(parseResult.Errors ?? Enumerable.Empty()); + var checkResult = new DefinitionsCheckResult(); - if (errors.Any(error => error.Severity > DocumentErrorSeverity.Warning)) - { - sb.AppendLine("Something went wrong when parsing user defined functions."); + var udfs = checkResult.SetText(script, options) + .SetBindingInfo(composedSymbols) + .ApplyCreateUserDefinedFunctions(); - foreach (var error in errors) - { - error.FormatCore(sb); - } - - throw new InvalidOperationException(sb.ToString()); - } + Contracts.AssertValue(udfs); - foreach (var udf in udfs) + if (checkResult.IsSuccess) { - AddFunction(udf); - var config = new BindingConfig(allowsSideEffects: allowSideEffects, useThisRecordForRuleScope: false, numberIsFloat: false); - var binding = udf.BindBody(composedSymbols, new Glue2DocumentBinderGlue(), config); - - List bindErrors = new List(); - - if (binding.ErrorContainer.GetErrors(ref bindErrors)) - { - sb.AppendLine(string.Join(", ", errors.Select(err => err.ToString()))); - } + AddFunctions(udfs); } - if (sb.Length > 0) - { - throw new InvalidOperationException(sb.ToString()); - } + return checkResult; } /// diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/DefinitionsCheckResult.cs b/src/libraries/Microsoft.PowerFx.Core/Public/DefinitionsCheckResult.cs index f4f6bfa9ac..4dad15d5c5 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/DefinitionsCheckResult.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/DefinitionsCheckResult.cs @@ -12,6 +12,9 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.PowerFx.Core.Binding; using Microsoft.PowerFx.Core.Errors; +using Microsoft.PowerFx.Core.Functions; +using Microsoft.PowerFx.Core.Glue; +using Microsoft.PowerFx.Core.Localization; using Microsoft.PowerFx.Core.Parser; using Microsoft.PowerFx.Core.Types; using Microsoft.PowerFx.Core.Utils; @@ -30,11 +33,16 @@ public class DefinitionsCheckResult : IOperationStatus private IReadOnlyDictionary _resolvedTypes; + private TexlFunctionSet _userDefinedFunctions; + private CultureInfo _defaultErrorCulture; private ParserOptions _parserOptions; private ParseUserDefinitionResult _parse; + // Local symboltable to store new symbols in a given script and use in binding. + private readonly SymbolTable _localSymbolTable; + // Power Fx expression containing definitions private string _definitions; @@ -43,6 +51,7 @@ public class DefinitionsCheckResult : IOperationStatus public DefinitionsCheckResult() { + _localSymbolTable = new SymbolTable { DebugName = "LocalUserDefinitions" }; } internal DefinitionsCheckResult SetBindingInfo(ReadOnlySymbolTable symbols) @@ -59,7 +68,7 @@ internal DefinitionsCheckResult SetBindingInfo(ReadOnlySymbolTable symbols) return this; } - internal DefinitionsCheckResult SetText(string definitions, ParserOptions parserOptions = null) + public DefinitionsCheckResult SetText(string definitions, ParserOptions parserOptions = null) { Contracts.AssertValue(definitions); @@ -97,6 +106,8 @@ internal ParseUserDefinitionResult ApplyParse() public IReadOnlyDictionary ResolvedTypes => _resolvedTypes; + public bool ContainsUDF => _parse.UDFs.Any(); + internal IReadOnlyDictionary ApplyResolveTypes() { if (_parse == null) @@ -114,6 +125,7 @@ internal IReadOnlyDictionary ApplyResolveTypes() if (_parse.DefinedTypes.Any()) { this._resolvedTypes = DefinedTypeResolver.ResolveTypes(_parse.DefinedTypes.Where(dt => dt.IsParseValid), _symbols, out var errors); + this._localSymbolTable.AddTypes(this._resolvedTypes); _errors.AddRange(ExpressionError.New(errors, _defaultErrorCulture)); } else @@ -125,16 +137,79 @@ internal IReadOnlyDictionary ApplyResolveTypes() return this._resolvedTypes; } + internal TexlFunctionSet ApplyCreateUserDefinedFunctions() + { + if (_parse == null) + { + this.ApplyParse(); + } + + if (_symbols == null) + { + throw new InvalidOperationException($"Must call {nameof(SetBindingInfo)} before calling ApplyCreateUserDefinedFunctions()."); + } + + if (_resolvedTypes == null) + { + this.ApplyResolveTypes(); + } + + if (_userDefinedFunctions == null) + { + _userDefinedFunctions = new TexlFunctionSet(); + + var partialUDFs = UserDefinedFunction.CreateFunctions(_parse.UDFs.Where(udf => udf.IsParseValid), _symbols, out var errors); + + if (errors.Any()) + { + _errors.AddRange(ExpressionError.New(errors, _defaultErrorCulture)); + } + + var composedSymbols = ReadOnlySymbolTable.Compose(_localSymbolTable, _symbols); + foreach (var udf in partialUDFs) + { + var config = new BindingConfig(allowsSideEffects: _parserOptions.AllowsSideEffects, useThisRecordForRuleScope: false, numberIsFloat: false); + var binding = udf.BindBody(composedSymbols, new Glue2DocumentBinderGlue(), config); + + List bindErrors = new List(); + + if (binding.ErrorContainer.HasErrors()) + { + _errors.AddRange(ExpressionError.New(binding.ErrorContainer.GetErrors(), _defaultErrorCulture)); + } + else + { + _localSymbolTable.AddFunction(udf); + _userDefinedFunctions.Add(udf); + } + } + + return this._userDefinedFunctions; + } + + return this._userDefinedFunctions; + } + internal IEnumerable ApplyErrors() { if (_resolvedTypes == null) { - ApplyResolveTypes(); + this.ApplyCreateUserDefinedFunctions(); } return this.Errors; } + public IEnumerable ApplyParseErrors() + { + if (_parse == null) + { + this.ApplyParse(); + } + + return ExpressionError.New(_parse.Errors, _defaultErrorCulture); + } + /// /// List of all errors and warnings. Check . /// This can include Parse, ResolveType errors />, diff --git a/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs b/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs index 869129ebc8..06152a8426 100644 --- a/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs +++ b/src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs @@ -546,10 +546,10 @@ public string GetDisplayExpression(string expressionText, ReadOnlySymbolTable sy return ExpressionLocalizationHelper.ConvertExpression(expressionText, ruleScope, GetDefaultBindingConfig(), CreateResolverInternal(symbolTable), CreateBinderGlue(), culture, Config.Features, toDisplay: true); } - internal void AddUserDefinedFunction(string script, CultureInfo parseCulture = null, ReadOnlySymbolTable symbolTable = null, bool allowSideEffects = false) + public DefinitionsCheckResult AddUserDefinedFunction(string script, CultureInfo parseCulture = null, ReadOnlySymbolTable symbolTable = null, bool allowSideEffects = false) { var engineTypesAndFunctions = ReadOnlySymbolTable.Compose(PrimitiveTypes, SupportedFunctions); - Config.SymbolTable.AddUserDefinedFunction(script, parseCulture, engineTypesAndFunctions, symbolTable, allowSideEffects); + return Config.SymbolTable.AddUserDefinedFunction(script, parseCulture, engineTypesAndFunctions, symbolTable, allowSideEffects); } } } diff --git a/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs b/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs index 8dd37baae8..c384cfe3d9 100644 --- a/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs +++ b/src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs @@ -460,15 +460,18 @@ private void AddUserDefinedFunctions(IEnumerable parsedUdfs, ReadOnlySymbol foreach (var udf in udfs) { - Config.SymbolTable.AddFunction(udf); var binding = udf.BindBody(nameResolver, new Glue2DocumentBinderGlue(), BindingConfig.Default, Config.Features); List bindErrors = new List(); - if (binding.ErrorContainer.GetErrors(ref errors)) + if (binding.ErrorContainer.GetErrors(ref bindErrors)) { sb.AppendLine(string.Join(", ", bindErrors.Select(err => err.ToString()))); } + else + { + Config.SymbolTable.AddFunction(udf); + } } if (sb.Length > 0) diff --git a/src/libraries/Microsoft.PowerFx.Repl/Repl.cs b/src/libraries/Microsoft.PowerFx.Repl/Repl.cs index 6b1bf6c1b0..1d0657ff1b 100644 --- a/src/libraries/Microsoft.PowerFx.Repl/Repl.cs +++ b/src/libraries/Microsoft.PowerFx.Repl/Repl.cs @@ -34,6 +34,9 @@ public class PowerFxREPL // Allow repl to create new definitions, such as Set(). public bool AllowSetDefinitions { get; set; } + // Allow repl to create new UserDefinedFunctions. + public bool AllowUserDefinedFunctions { get; set; } + // Do we print each command before evaluation? // Useful if we're running a file and are debugging, or if input UI is separated from output UI. public bool Echo { get; set; } = false; @@ -405,6 +408,30 @@ await this.Output.WriteLineAsync($"Error: Can't set '{name}' to a Void value.", var errors = check.ApplyErrors(); if (!check.IsSuccess) { + var definitionsCheckResult = new DefinitionsCheckResult(); + + definitionsCheckResult.SetText(expression, this.ParserOptions) + .ApplyParseErrors(); + + if (this.AllowUserDefinedFunctions && definitionsCheckResult.IsSuccess && definitionsCheckResult.ContainsUDF) + { + var defCheckResult = this.Engine.AddUserDefinedFunction(expression, this.ParserOptions.Culture, extraSymbolTable); + + if (!defCheckResult.IsSuccess) + { + foreach (var error in defCheckResult.Errors) + { + var kind = error.IsWarning ? OutputKind.Warning : OutputKind.Error; + var msg = error.ToString(); + + await this.Output.WriteLineAsync(lineError + msg, kind, cancel) + .ConfigureAwait(false); + } + } + + return new ReplResult(); + } + foreach (var error in check.Errors) { var kind = error.IsWarning ? OutputKind.Warning : OutputKind.Error; diff --git a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/UserDefinedFunctionTests.cs b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/UserDefinedFunctionTests.cs index 5716cd49c3..03dbc7e298 100644 --- a/src/tests/Microsoft.PowerFx.Core.Tests.Shared/UserDefinedFunctionTests.cs +++ b/src/tests/Microsoft.PowerFx.Core.Tests.Shared/UserDefinedFunctionTests.cs @@ -425,7 +425,7 @@ public void DefineEmpty() // Empty symbol table doesn't get builtins. var st = SymbolTable.WithPrimitiveTypes(); st.AddUserDefinedFunction("Foo1(x: Number): Number = x;"); // ok - Assert.Throws(() => st.AddUserDefinedFunction("Foo2(x: Number): Number = Abs(x);")); + Assert.False(st.AddUserDefinedFunction("Foo2(x: Number): Number = Abs(x);").IsSuccess); } // Show definitions on public symbol tables diff --git a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs index 8d476fa1a2..b681966a19 100644 --- a/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs +++ b/src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/RecalcEngineTests.cs @@ -447,11 +447,6 @@ public void FormulaCantRedefine() "func1(x:Number/*comment*/): Number = x * 10;\nfunc2(x:Number): Number = y1 * 10;", null, true)] - [InlineData( - "foo(x:Number):Number = If(x=0,foo(1),If(x=1,foo(2),If(x=2,Float(2))));", - "foo(Float(0))", - false, - 2.0)] [InlineData( "foo():Blank = foo();", "foo()", @@ -472,7 +467,11 @@ public void FormulaCantRedefine() false, 14.0)] - // Recursive calls are not allowed + // Recursive calls are not allowed + [InlineData( + "foo(x:Number):Number = If(x=0,foo(1),If(x=1,foo(2),If(x=2,Float(2))));", + "foo(Float(0))", + true)] [InlineData( "hailstone(x:Number):Number = If(Not(x = 1), If(Mod(x, 2)=0, hailstone(x/2), hailstone(3*x+1)), x);", "hailstone(Float(192))", @@ -574,7 +573,7 @@ public void DefinedFunctionsErrorsTest(string script) { var engine = new RecalcEngine(); - Assert.Throws(() => engine.AddUserDefinedFunction(script, CultureInfo.InvariantCulture)); + Assert.False(engine.AddUserDefinedFunction(script, CultureInfo.InvariantCulture).IsSuccess); } // Overloads and conflict @@ -651,30 +650,32 @@ public void ShadowingFunctionPrecedenceTest() "F1(x:Number) : Boolean = { Set(a, x); Today(); };", null, true, - "AddUserDefinedFunction", + "ErrUDF_ReturnTypeDoesNotMatch", 0)] - public void ImperativeUserDefinedFunctionTest(string udfExpression, string expression, bool expectedError, string expectedMethodFailure, double expected) + public void ImperativeUserDefinedFunctionTest(string udfExpression, string expression, bool expectedError, string errorKey, double expected) { var config = new PowerFxConfig(); config.EnableSetFunction(); var recalcEngine = new RecalcEngine(config); recalcEngine.UpdateVariable("a", 1m); + + var definitionsCheckResult = recalcEngine.AddUserDefinedFunction(udfExpression, CultureInfo.InvariantCulture, symbolTable: recalcEngine.EngineSymbols, allowSideEffects: true); - try - { - recalcEngine.AddUserDefinedFunction(udfExpression, CultureInfo.InvariantCulture, symbolTable: recalcEngine.EngineSymbols, allowSideEffects: true); + if (!expectedError) + { + Assert.True(definitionsCheckResult.IsSuccess); var result = recalcEngine.Eval(expression, options: _opts); var fvExpected = FormulaValue.New(expected); Assert.Equal(fvExpected.AsDecimal(), result.AsDecimal()); - Assert.False(expectedError); } - catch (Exception ex) + else { - Assert.True(expectedError, ex.Message); - Assert.Contains(expectedMethodFailure, ex.StackTrace); + Assert.False(definitionsCheckResult.IsSuccess); + Assert.Single(definitionsCheckResult.Errors); + Assert.Contains(definitionsCheckResult.Errors, err => err.MessageKey == errorKey); } } @@ -708,7 +709,7 @@ public void DelegableUDFTest() var recalcEngine = new RecalcEngine(config); - recalcEngine.AddUserDefinedFunction("A():MyDataSourceTableType = Filter(MyDataSource, Value > 10);C():MyDataSourceTableType = A(); B():MyDataSourceTableType = Filter(C(), Value > 11); D():MyDataSourceTableType = { Filter(B(), Value > 12); }; E():Void = { E(); };", CultureInfo.InvariantCulture, symbolTable: recalcEngine.EngineSymbols, allowSideEffects: true); + recalcEngine.AddUserDefinedFunction("A():MyDataSourceTableType = Filter(MyDataSource, Value > 10);C():MyDataSourceTableType = A(); B():MyDataSourceTableType = Filter(C(), Value > 11); D():MyDataSourceTableType = { Filter(B(), Value > 12); };", CultureInfo.InvariantCulture, symbolTable: recalcEngine.EngineSymbols, allowSideEffects: true); var func = recalcEngine.Functions.WithName("A").First() as UserDefinedFunction; Assert.True(func.IsAsync); @@ -730,11 +731,8 @@ public void DelegableUDFTest() Assert.True(func.IsAsync); Assert.True(!func.IsDelegatable); - func = recalcEngine.Functions.WithName("E").First() as UserDefinedFunction; - - // Imperative function is not delegable - // E():Void = { E() }; ---> binding will be null so no attempt to get datasource should happen - Assert.True(!func.IsDelegatable); + // Binding fails for recursive definitions and hence function is not added. + Assert.False(recalcEngine.AddUserDefinedFunction("E():Void = { E(); };", CultureInfo.InvariantCulture, symbolTable: recalcEngine.EngineSymbols, allowSideEffects: true).IsSuccess); } // Binding to inner functions does not impact outer functions. @@ -1836,7 +1834,7 @@ public void UDFImperativeVsRecordAmbiguityTest(string udf, string evalExpression } else { - Assert.Throws(() => recalcEngine.AddUserDefinedFunction(udf, CultureInfo.InvariantCulture, extraSymbols, true)); + Assert.False(recalcEngine.AddUserDefinedFunction(udf, CultureInfo.InvariantCulture, extraSymbols, true).IsSuccess); } } diff --git a/src/tests/Microsoft.PowerFx.Repl.Tests.Shared/ReplTests.cs b/src/tests/Microsoft.PowerFx.Repl.Tests.Shared/ReplTests.cs index 8ae0ecdfcf..bdfaa4d9e8 100644 --- a/src/tests/Microsoft.PowerFx.Repl.Tests.Shared/ReplTests.cs +++ b/src/tests/Microsoft.PowerFx.Repl.Tests.Shared/ReplTests.cs @@ -34,6 +34,8 @@ public ReplTests() Engine = engine, Output = _output, AllowSetDefinitions = true, + AllowUserDefinedFunctions = true, + ParserOptions = new ParserOptions() { AllowsSideEffects = true } }; } @@ -268,6 +270,29 @@ public void BadRedefinedNamedFormula() Assert.True(log.Length > 0); } + [Fact] + public void UserDefinedFunctions() + { + _repl.HandleLine("F(x: Number): Number = x;"); + _repl.HandleLine("F(42)"); + var log = _output.Get(OutputKind.Repl); + Assert.Equal("42", log); + + // we do not have a clear semantics defined yet for the below test + // should be addressed in future + /* + _repl.HandleLine("F(x: Text): Text = x;"); + var error1 = _output.Get(OutputKind.Error); + Assert.Equal("Error 0-1: Function F is already defined.", error1); + */ + + _repl.HandleLine("G(x: Currency): Currency = x;"); + var error2 = _output.Get(OutputKind.Error); + Assert.Equal( + @"Error 5-13: Unknown type Currency. +Error 16-24: Unknown type Currency.", error2); + } + // test that Exit() informs the host that an exit has been requested [Fact] public void Exit() diff --git a/src/tools/Repl/Program.cs b/src/tools/Repl/Program.cs index 37cabab2b6..f23481dc62 100644 --- a/src/tools/Repl/Program.cs +++ b/src/tools/Repl/Program.cs @@ -40,6 +40,9 @@ public static class ConsoleRepl private const string OptionTextFirst = "TextFirst"; private static bool _textFirst = false; + private const string OptionUDF = "UserDefinedFunctions"; + private static bool _enableUDFs = true; + private static readonly Features _features = Features.PowerFxV1; private static StandardFormatter _standardFormatter; @@ -64,7 +67,8 @@ private static RecalcEngine ReplRecalcEngine() { OptionPowerFxV1, OptionPowerFxV1 }, { OptionHashCodes, OptionHashCodes }, { OptionStackTrace, OptionStackTrace }, - { OptionTextFirst, OptionTextFirst } + { OptionTextFirst, OptionTextFirst }, + { OptionUDF, OptionUDF }, }; foreach (var featureProperty in typeof(Features).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) @@ -133,6 +137,7 @@ public MyRepl() this.HelpProvider = new MyHelpProvider(); this.AllowSetDefinitions = true; + this.AllowUserDefinedFunctions = _enableUDFs; this.EnableSampleUserObject(); this.AddPseudoFunction(new IRPseudoFunction()); this.AddPseudoFunction(new SuggestionsPseudoFunction()); @@ -255,6 +260,7 @@ public FormulaValue Execute() sb.Append(CultureInfo.InvariantCulture, $"{"LargeCallDepth:",-42}{_largeCallDepth}\n"); sb.Append(CultureInfo.InvariantCulture, $"{"StackTrace:",-42}{_stackTrace}\n"); sb.Append(CultureInfo.InvariantCulture, $"{"TextFirst:",-42}{_textFirst}\n"); + sb.Append(CultureInfo.InvariantCulture, $"{"UserDefinedFunctions:",-42}{_enableUDFs}\n"); foreach (var prop in typeof(Features).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { @@ -303,6 +309,11 @@ public FormulaValue Execute(StringValue option) return BooleanValue.New(_stackTrace); } + if (string.Equals(option.Value, OptionUDF, StringComparison.OrdinalIgnoreCase)) + { + return BooleanValue.New(_enableUDFs); + } + return FormulaValue.NewError(new ExpressionError() { Kind = ErrorKind.InvalidArgument, @@ -343,6 +354,13 @@ public FormulaValue Execute(StringValue option, BooleanValue value) return value; } + if (string.Equals(option.Value, OptionUDF, StringComparison.OrdinalIgnoreCase)) + { + _enableUDFs = value.Value; + _reset = true; + return value; + } + if (string.Equals(option.Value, OptionLargeCallDepth, StringComparison.OrdinalIgnoreCase)) { _largeCallDepth = value.Value; @@ -441,6 +459,9 @@ Displays the full stack trace when an exception is encountered. Options.None Removed all the feature flags, which is even less than Canvas uses. +Options.EnableUDFs + Enables UserDefinedFunctions to be added. + "; await WriteAsync(repl, msg, cancel)