Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Join function #2728

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6da4a77
Base classes
anderson-joyle Oct 16, 2024
c1ed08f
Commit
anderson-joyle Oct 22, 2024
3ce5033
WIP join function
anderson-joyle Nov 4, 2024
c62088c
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 4, 2024
8ab3eb0
Join logic fix.
anderson-joyle Nov 4, 2024
225e2ca
Error fixes and adding test cases.
anderson-joyle Nov 5, 2024
02f236a
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 5, 2024
fea25f5
Handling scope errors. Adding test cases.
anderson-joyle Nov 6, 2024
6097896
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 6, 2024
d865592
Adding error cases.
anderson-joyle Nov 6, 2024
9d09a1b
Fixes and test cases.
anderson-joyle Nov 6, 2024
8904e96
PR feedback.
anderson-joyle Nov 7, 2024
498342d
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 7, 2024
a2a7b8f
Adding renaming logic to IR.
anderson-joyle Nov 18, 2024
e5d5311
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 18, 2024
9193a6b
Adding signatures after arg 5
anderson-joyle Nov 18, 2024
46d747d
PR feedback.
anderson-joyle Nov 19, 2024
6c4abed
Moving Join out from BuiltInCore
anderson-joyle Nov 19, 2024
d43732a
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 19, 2024
e5e2fe3
PR feedback.
anderson-joyle Nov 20, 2024
875857b
Fix.
anderson-joyle Nov 21, 2024
a438704
Restoring BuiltinFunctionsCore.cs file
anderson-joyle Nov 21, 2024
4f8a99c
PR feedback.
anderson-joyle Nov 22, 2024
627550d
Merge branch 'main' into andersonf/join-function
anderson-joyle Nov 22, 2024
aa719fc
XML comment fix.
anderson-joyle Nov 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal sealed class CallInfo

// ScopeIdentifier will be "" if the function does not support a scope identifier
// RequiresScopeIdentifier is true if scopeIdentifier is set and there was an As node used
public readonly DName ScopeIdentifier;
public readonly DName[] ScopeIdentifier;
public readonly bool RequiresScopeIdentifier;

public readonly object Data;
Expand Down Expand Up @@ -58,7 +58,7 @@ public CallInfo(TexlFunction function, CallNode node, object data)
Data = data;
}

public CallInfo(TexlFunction function, CallNode node, DType cursorType, DName scopeIdentifier, bool requiresScopeIdentifier, int scopeNest)
public CallInfo(TexlFunction function, CallNode node, DType cursorType, DName[] scopeIdentifier, bool requiresScopeIdentifier, int scopeNest)
{
Contracts.AssertValue(function);
Contracts.AssertValue(node);
Expand Down
104 changes: 70 additions & 34 deletions src/libraries/Microsoft.PowerFx.Core/Binding/Binder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,6 @@ internal sealed partial class TexlBinding
/// </summary>
public const int MaxSelectsToInclude = 100;

/// <summary>
/// Default name used to access a Lambda scope.
/// </summary>
internal static DName ThisRecordDefaultName => new DName("ThisRecord");

public Features Features { get; }

// Property Name or NamedFormula Name to which current rule is being bound to. It could be null in the absence of NameResolver.
Expand Down Expand Up @@ -1348,7 +1343,7 @@ public bool TryGetFullRecordRowScopeAccessInfo(TexlNode node, out FirstNameInfo
/// <returns></returns>
private bool GetScopeIdent(TexlNode node, DType rowType, out DName scopeIdent)
{
scopeIdent = ThisRecordDefaultName;
scopeIdent = FunctionScopeInfo.ThisRecord;
if (node is AsNode asNode)
{
scopeIdent = GetInfo(asNode).AsIdentifier;
Expand Down Expand Up @@ -2426,7 +2421,7 @@ private sealed class Scope
public readonly DType Type;
public readonly bool CreatesRowScope;
public readonly bool SkipForInlineRecords;
public readonly DName ScopeIdentifier;
public readonly DName[] ScopeIdentifiers;
public readonly bool RequireScopeIdentifier;

// Optional data associated with scope. May be null.
Expand All @@ -2438,7 +2433,7 @@ public Scope(DType type)
Type = type;
}

public Scope(CallNode call, Scope parent, DType type, DName scopeIdentifier = default, bool requireScopeIdentifier = false, object data = null, bool createsRowScope = true, bool skipForInlineRecords = false)
public Scope(CallNode call, Scope parent, DType type, DName[] scopeIdentifiers = default, bool requireScopeIdentifier = false, object data = null, bool createsRowScope = true, bool skipForInlineRecords = false)
{
Contracts.Assert(type.IsValid);
Contracts.AssertValueOrNull(data);
Expand All @@ -2449,7 +2444,7 @@ public Scope(CallNode call, Scope parent, DType type, DName scopeIdentifier = de
Data = data;
CreatesRowScope = createsRowScope;
SkipForInlineRecords = skipForInlineRecords;
ScopeIdentifier = scopeIdentifier;
ScopeIdentifiers = scopeIdentifiers;
RequireScopeIdentifier = requireScopeIdentifier;

Nest = parent?.Nest ?? 0;
Expand Down Expand Up @@ -2492,7 +2487,7 @@ public Visitor(TexlBinding txb, INameResolver resolver, DType topScope, bool use
_nameResolver = resolver;
_features = features;

_topScope = new Scope(null, null, topScope ?? DType.Error, useThisRecordForRuleScope ? TexlBinding.ThisRecordDefaultName : default);
_topScope = new Scope(null, null, topScope ?? DType.Error, useThisRecordForRuleScope ? new[] { FunctionScopeInfo.ThisRecord } : default);
_currentScope = _topScope;
_currentScopeDsNodeId = -1;
}
Expand Down Expand Up @@ -2809,7 +2804,21 @@ public override void Visit(FirstNameNode node)
return;
}

var nodeType = scope.Type;
DType nodeType = null;

if (scope.ScopeIdentifiers == null || scope.ScopeIdentifiers.Length == 1)
{
nodeType = scope.Type;
}
else
{
// If scope.ScopeIdentifier.Length > 1, it meant the function creates more than 1 scope and the scope types are contained within a record.
// Example: Join(t1, t2, LeftRecord.a = RigthRecord.a, ...)
// The expression above will create LeftRecord and RigthRecord scopes. The scope type will be ![LeftRecord:![...],RigthRecord:![...]]
// Example: Join(t1 As X1, t2 As X2, X1.a = X2.a, ...)
// The expression above will create LeftRecord and RigthRecord scopes. The scope type will be ![X1:![...],X2:![...]]
Contracts.Assert(scope.Type.TryGetType(node.Ident.Name, out nodeType));
}

if (!isWholeScope)
{
Expand Down Expand Up @@ -3162,14 +3171,14 @@ private bool IsRowScopeField(FirstNameNode node, out Scope scope, out bool fErro

var expandedEntityType = GetExpandedEntityType(typeTmp, parentEntityPath);
var type = scope.Type.SetType(ref fError, DPath.Root.Append(nodeName), expandedEntityType);
scope = new Scope(scope.Call, scope.Parent, type, scope.ScopeIdentifier, scope.RequireScopeIdentifier, expandedEntityType.ExpandInfo);
scope = new Scope(scope.Call, scope.Parent, type, scope.ScopeIdentifiers, scope.RequireScopeIdentifier, expandedEntityType.ExpandInfo);
}

return true;
}
}

if (scope.ScopeIdentifier == nodeName)
if (scope.ScopeIdentifiers?.Any(dname => dname.Value == nodeName) ?? false)
{
isWholeScope = true;
return true;
Expand Down Expand Up @@ -4448,7 +4457,7 @@ public override bool PreVisit(CallNode node)
{
var scope = DType.Invalid;
var required = false;
DName scopeIdentifier = default;
DName[] scopeIdentifiers = default;
if (scopeInfo.ScopeType != null)
{
scopeNew = new Scope(node, _currentScope, scopeInfo.ScopeType, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords);
Expand All @@ -4466,19 +4475,19 @@ public override bool PreVisit(CallNode node)
scopeInfo = maybeFunc.ScopeInfo;
}

// Determine the Scope Identifier using the 1st arg
required = _txb.GetScopeIdent(nodeInp, _txb.GetType(nodeInp), out scopeIdentifier);
// Determine the Scope Identifier using the 1st arg
required = scopeInfo.GetScopeIdent(node.Args.Children.ToArray(), out scopeIdentifiers);

if (scopeInfo.CheckInput(_txb.Features, node, nodeInp, _txb.GetType(nodeInp), out scope))
{
if (_txb.TryGetEntityInfo(nodeInp, out expandInfo))
{
scopeNew = new Scope(node, _currentScope, scope, scopeIdentifier, required, expandInfo, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords);
scopeNew = new Scope(node, _currentScope, scope, scopeIdentifiers, required, expandInfo, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords);
}
else
{
maybeFunc.TryGetDelegationMetadata(node, _txb, out metadata);
scopeNew = new Scope(node, _currentScope, scope, scopeIdentifier, required, metadata, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords);
scopeNew = new Scope(node, _currentScope, scope, scopeIdentifiers, required, metadata, skipForInlineRecords: maybeFunc.SkipScopeForInlineRecords);
}
}

Expand All @@ -4488,7 +4497,7 @@ public override bool PreVisit(CallNode node)
// If there is only one function with this name and its arity doesn't match,
// that means the invocation is erroneous.
ArityError(maybeFunc.MinArity, maybeFunc.MaxArity, node, carg, _txb.ErrorContainer);
_txb.SetInfo(node, new CallInfo(maybeFunc, node, scope, scopeIdentifier, required, _currentScope.Nest));
_txb.SetInfo(node, new CallInfo(maybeFunc, node, scope, scopeIdentifiers, required, _currentScope.Nest));
_txb.SetType(node, maybeFunc.ReturnType);
}

Expand Down Expand Up @@ -4544,26 +4553,40 @@ public override bool PreVisit(CallNode node)
{
_currentScopeDsNodeId = dsNode.Id;
}

var typeInput = argTypes[0] = _txb.GetType(nodeInput);
argTypes[0] = _txb.GetType(nodeInput);

// Get the cursor type for this arg. Note we're not adding document errors at this point.
DType typeScope;
DName scopeIdent = default;
DName[] scopeIdent = default;
var identRequired = false;
var fArgsValid = true;
var fArgsValid = true;

var typeInputs = new Dictionary<TexlNode, DType>
{
{ nodeInput, _txb.GetType(nodeInput) },
};

if (scopeInfo.ScopeType != null)
{
typeScope = scopeInfo.ScopeType;

// For functions with a Scope Type, there is no ScopeIdent needed
}
else
{
fArgsValid = scopeInfo.CheckInput(_txb.Features, node, nodeInput, typeInput, out typeScope);
{
// Starting from 1 since 0 was visited above.
for (int i = 1; i < maybeFunc.ScopeArgs; i++)
{
_txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i]));
args[i].Accept(this);
typeInputs[args[i]] = _txb.GetType(args[i]);
}

fArgsValid = scopeInfo.CheckInput(_txb.Features, node, args, out typeScope, typeInputs.Values.ToArray());

// Determine the scope identifier using the first node for lambda params
identRequired = _txb.GetScopeIdent(nodeInput, typeScope, out scopeIdent);
// Determine the scope identifier using the first node for lambda params
identRequired = scopeInfo.GetScopeIdent(args, out scopeIdent);
}

if (!fArgsValid)
Expand Down Expand Up @@ -4635,10 +4658,17 @@ public override bool PreVisit(CallNode node)
_currentScope = (isIdentifier || isLambdaArg) ? scopeNew : scopeNew.Parent;

if (!isIdentifier || maybeFunc.GetIdentifierParamStatus(args[i], _features, i) == TexlFunction.ParamIdentifierStatus.PossiblyIdentifier)
{
args[i].Accept(this);
_txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i]));
argTypes[i] = _txb.GetType(args[i]);
{
if (typeInputs.TryGetValue(args[i], out _))
{
argTypes[i] = _txb.GetType(args[i]);
}
else
{
args[i].Accept(this);
_txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i]));
argTypes[i] = _txb.GetType(args[i]);
}

Contracts.Assert(argTypes[i].IsValid);
}
Expand Down Expand Up @@ -5014,7 +5044,10 @@ private void PreVisitMetadataArg(CallNode node, TexlFunction func)
}
else
{
args[i].Accept(this);
if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid)
{
args[i].Accept(this);
}
}

if (args[i].Kind == NodeKind.As)
Expand Down Expand Up @@ -5226,8 +5259,11 @@ private void PreVisitBottomUp(CallNode node, int argCountVisited, Scope scopeNew
{
_txb.AddVolatileVariables(args[i], volatileVariables);
}

args[i].Accept(this);

if (_txb.GetTypeAllowInvalid(args[i]) != null && !_txb.GetTypeAllowInvalid(args[i]).IsValid)
{
args[i].Accept(this);
}

// In case weight was added during visitation
_txb.AddVolatileVariables(node, _txb.GetVolatileVariables(args[i]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ internal class FunctionScopeInfo
// True indicates that this function cannot guarantee that it will iterate over the datasource in order.
// This means it should not allow lambdas that operate on the same data multiple times, as this will
// cause nondeterministic behavior.
public bool HasNondeterministicOperationOrder => IteratesOverScope && SupportsAsyncLambdas;
public bool HasNondeterministicOperationOrder => IteratesOverScope && SupportsAsyncLambdas;

/// <summary>
/// Default name used to access a Lambda scope.
/// </summary>
public static DName ThisRecord => new DName("ThisRecord");

public FunctionScopeInfo(
TexlFunction function,
Expand All @@ -85,6 +90,20 @@ public FunctionScopeInfo(
_function = function;
AppliesToArgument = appliesToArgument ?? (i => i > 0);
CanBeCreatedByRecord = canBeCreatedByRecord;
}

/// <summary>
/// Allows to type check multiple scopes.
/// </summary>
/// <param name="features">Features flags.</param>
/// <param name="callNode">Caller call node.</param>
/// <param name="inputNodes">ArgN node.</param>
/// <param name="typeScope">Calculated DType type.</param>
/// <param name="inputSchema">List of data sources to compose the calculated type.</param>
/// <returns></returns>
public virtual bool CheckInput(Features features, CallNode callNode, TexlNode[] inputNodes, out DType typeScope, params DType[] inputSchema)
{
return CheckInput(features, callNode, inputNodes[0], inputSchema[0], out typeScope);
}

// Typecheck an input for this function, and get the cursor type for an invocation with that input.
Expand Down Expand Up @@ -200,6 +219,18 @@ public void CheckLiteralPredicates(TexlNode[] args, IErrorContainer errors)
}
}
}
}

public virtual bool GetScopeIdent(TexlNode[] nodes, out DName[] scopeIdents)
{
scopeIdents = new[] { ThisRecord };
if (nodes[0] is AsNode asNode)
{
scopeIdents = new[] { asNode.Right.Name };
return true;
}

return false;
}
}

Expand Down Expand Up @@ -247,4 +278,62 @@ public override bool CheckInput(Features features, CallNode callNode, TexlNode i
return ret;
}
}

internal class FunctionJoinScopeInfo : FunctionScopeInfo
{
public static DName LeftRecord => new DName("LeftRecord");

public static DName RightRecord => new DName("RightRecord");

public FunctionJoinScopeInfo(TexlFunction function)
: base(function, appliesToArgument: (argIndex) => argIndex > 1)
{
}

public override bool CheckInput(Features features, CallNode callNode, TexlNode[] inputNodes, out DType typeScope, params DType[] inputSchema)
{
var ret = true;
var input0 = inputSchema[0];
var input1 = inputSchema[1];

typeScope = DType.EmptyRecord;

ret = base.CheckInput(features, callNode, callNode.Args.ChildNodes[0], input0, out var type0);
ret &= base.CheckInput(features, callNode, callNode.Args.ChildNodes[1], input1, out var type1);

GetScopeIdent(inputNodes, out DName[] idents);

typeScope = typeScope.Add(idents[0], type0).Add(idents[1], type1);

return ret;
}

public override bool GetScopeIdent(TexlNode[] nodes, out DName[] scopeIdents)
{
var ret = false;
scopeIdents = new DName[2];

if (nodes[0] is AsNode leftAsNode)
{
scopeIdents[0] = leftAsNode.Right.Name;
ret = true;
}
else
{
scopeIdents[0] = LeftRecord;
}

if (nodes[1] is AsNode rightAsNode)
{
scopeIdents[1] = rightAsNode.Right.Name;
ret = true;
}
else
{
scopeIdents[1] = RightRecord;
}

return ret;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ internal abstract class TexlFunction : IFunction
/// </summary>
public virtual bool CanSuggestInputColumns => false;

/// <summary>
/// Identifies which args to use to compose the scope type for subsequent lambdas.
/// Example:
/// Filter(t1, ...) => ScopeArgs is 1.
/// Join(t1, t2, ...) => ScopeArgs is 2.
/// </summary>
public virtual int ScopeArgs => 1;

/// <summary>
/// If this returns false, the Intellisense will use Arg[0] type to suggest the type of the argument.
/// e.g. Collect(), Remove(), etc.
Expand Down
Loading