Skip to content

Commit

Permalink
Enable UDF on REPL (#2559)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
adithyaselv authored Sep 23, 2024
1 parent b87018f commit dcbdffb
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 66 deletions.
46 changes: 11 additions & 35 deletions src/libraries/Microsoft.PowerFx.Core/Public/Config/SymbolTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,63 +208,39 @@ public void AddConstant(string name, FormulaValue data)
}

/// <summary>
/// Adds an user defined function.
/// Adds user defined functions in the script.
/// </summary>
/// <param name="script">String representation of the user defined function.</param>
/// <param name="parseCulture">CultureInfo to parse the script againts. Default is invariant.</param>
/// <param name="symbolTable">Extra symbols to bind UDF. Commonly coming from Engine.</param>
/// <param name="extraSymbolTable">Additional symbols to bind UDF.</param>
/// <param name="allowSideEffects">Allow for curly brace parsing.</param>
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<TexlError>());
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<TexlError> bindErrors = new List<TexlError>();

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;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,11 +33,16 @@ public class DefinitionsCheckResult : IOperationStatus

private IReadOnlyDictionary<DName, FormulaType> _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;

Expand All @@ -43,6 +51,7 @@ public class DefinitionsCheckResult : IOperationStatus

public DefinitionsCheckResult()
{
_localSymbolTable = new SymbolTable { DebugName = "LocalUserDefinitions" };
}

internal DefinitionsCheckResult SetBindingInfo(ReadOnlySymbolTable symbols)
Expand All @@ -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);

Expand Down Expand Up @@ -97,6 +106,8 @@ internal ParseUserDefinitionResult ApplyParse()

public IReadOnlyDictionary<DName, FormulaType> ResolvedTypes => _resolvedTypes;

public bool ContainsUDF => _parse.UDFs.Any();

internal IReadOnlyDictionary<DName, FormulaType> ApplyResolveTypes()
{
if (_parse == null)
Expand All @@ -114,6 +125,7 @@ internal IReadOnlyDictionary<DName, FormulaType> 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
Expand All @@ -125,16 +137,79 @@ internal IReadOnlyDictionary<DName, FormulaType> 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<TexlError> bindErrors = new List<TexlError>();

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<ExpressionError> ApplyErrors()
{
if (_resolvedTypes == null)
{
ApplyResolveTypes();
this.ApplyCreateUserDefinedFunctions();
}

return this.Errors;
}

public IEnumerable<ExpressionError> ApplyParseErrors()
{
if (_parse == null)
{
this.ApplyParse();
}

return ExpressionError.New(_parse.Errors, _defaultErrorCulture);
}

/// <summary>
/// List of all errors and warnings. Check <see cref="ExpressionError.IsWarning"/>.
/// This can include Parse, ResolveType errors />,
Expand Down
4 changes: 2 additions & 2 deletions src/libraries/Microsoft.PowerFx.Core/Public/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
7 changes: 5 additions & 2 deletions src/libraries/Microsoft.PowerFx.Interpreter/RecalcEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -460,15 +460,18 @@ private void AddUserDefinedFunctions(IEnumerable<UDF> parsedUdfs, ReadOnlySymbol

foreach (var udf in udfs)
{
Config.SymbolTable.AddFunction(udf);
var binding = udf.BindBody(nameResolver, new Glue2DocumentBinderGlue(), BindingConfig.Default, Config.Features);

List<TexlError> bindErrors = new List<TexlError>();

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)
Expand Down
27 changes: 27 additions & 0 deletions src/libraries/Microsoft.PowerFx.Repl/Repl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(() => 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
Expand Down
Loading

0 comments on commit dcbdffb

Please sign in to comment.