Function Calls

March 20, 2020 · View on GitHub

RCallNode AST schema

RCallNode represents and executes the syntax node for a function call.

default dispatch (RFunction)

// Dispatches a call to a function for actual arguments
FunctionDispatch
	// Reorders implicit actual arguments according to R's arg matching rules. See ArgumentMatcher
	PrepareArgumentsDefault
		// Used to increment temporarily the reference count of arguments until all arg nodes are evaluated
		ShareObjectNode
		// Used to decrement the reference count of arguments after all arg nodes have been evaluated
		UnShareObjectNode
	or
	// Reorders explicit actual arguments according to R's arg matching rules
	PrepareArgumentsExplicit

	// Evaluates arg promises and executes the builtin
	BuiltinCallNode(cachedTarget.builtinNode)
		// Used to force arg promises evaluation
		PromiseHelperNode[]
		// Used to force varargs promises evaluation
		PromiseCheckHelperNode
		// Executes the builtin
		RBuiltinNode
	or
	// Executes an R (non-builtin) function
	DispatchedCallNode
		// Executes a given function via the same call target (held by the DirectCallNode child node)
		CallRFunctionNode
			// Represents a direct call to a call target
			DirectCallNode
		// Used to execute the fast-path of the given function, if exists
		RFastPathNode(opt)

Note: For S3 dispatch of non-builtins see S3DispatchFunctions such as UseMethod and NextMethod
      For S4 dispatch look at StandardGeneric

internal generic dispatch of a builtin with implicit args (RFunction)

// The dispatch arg node to be evaluated for the actual dispatch arg
RNode
// Stores the dispatch arg in a temporary slot
TemporarySlotNode
// Retrieves the basic function for the builtin
GetBasicFunction(opt, used only if the dispatch arg is S4)
	// Retrieves the methods namespace 
	GetFromEnvironment
	// Retrieves .BasicFunsList from the methods namespace
	GetFromEnvironment
	// Retrieves the given basic function from .BasicFunsList
	AccessListField
// dtto, for S4, the basic function is used instead 
FunctionDispatch
// Returns a string vector with the class hierarchy of the dispatch arg (S3 only)
ClassHierarchyNode
// Searches for the correct S3 method for the given function name and the vector of class names  (S3 only)
S3FunctionLookupNode

internal generic dispatch of a builtin with explicit args (RFunction)

// Reads the dispatch arg from the frame
LocalReadVariableNode

Note: the basic function for an S4 arg is not retrieved using GetBasicFunction as above (should be?)

// dtto
FunctionDispatch
// dtto
ClassHierarchyNode
// dtto
S3FunctionLookupNode

group generic dispatch of a builtin with explicit or implicit args (RFunction)

Note: it comprises these builtin groups: MATH_GROUP_GENERIC, OPS_GROUP_GENERIC, SUMMARY_GROUP_GENERIC, COMPLEX_GROUP_GENERIC

// Reads the args from the frame
LocalReadVariableNode (opt, for explicit args only)
// Encapsulates arg nodes, signature, var arg indices and utility methods
CallArgumentsNode (opt, for implicit args only)
// dtto, for the first arg
ClassHierarchyNode
// dtto, for the second arg, if exists
ClassHierarchyNode
// dtto, for the first arg
S3FunctionLookupNode
// dtto, for the second arg, if exists
S3FunctionLookupNode
// Used at various places to evaluate arg and other promises
PromiseCheckHelperNode
// dtto
FunctionDispatch

foreign call (DeferredFunctionValue)

// Invokes a possible foreign member
ForeignInvoke
	// dtto
	CallArgumentsNode
	// Converts 'primitive' values from the outside world to internal FastR representations
	Foreign2R
	// Calls a foreign function using message INVOKE
	SendForeignInvokeMessage
		// Normalizes the internal FastR data representation for the outside world
		R2Foreign
		// The INVOKE message node
		Node

Function Fast-Paths Infrastructure

The RRootNode class, which is the base class for FunctionDefinitionNode and RBuiltinRootNode, provides an optional facility for an optimised execution of its AST. The optimisation is centered around the FastPathFactory interface, whose instance may be retrieved from an RRootNode using the getFastPath method returning an instance of FastPathFactory or null, if the root node does not support the fast path.

In terms of execution, the FastPathFactory#create() method is called by DispatchedCallNode to obtain an instance of RFastPathNode. This node is then used in DispatchedCallNode#execute to execute the fast path first. If the fast path execution fails, which is indicated by returning null, the execution defaults to the original path.

Currently, the optimisation using FastPathFactory takes place in two classes: ArgumentMatcher and CallMatcherCachedNode.

ArgumentMatcher

ArgumentMatcher, which is responsible for matching actual call argument nodes with the formal arguments of a function, is able to perform two optimisations based on the information provided by FastPathFactory: argument inlining and forced promise evaluation. The gist of both optimisations is that the argument node is evaluated on the caller site. Otherwise, the AST of the called function would aggregate AST trees of promises from various callers, which would lead to the swelling of the function's code (see PromiseHelperNode#generateValueDefault and InlineCacheNode). Furthermore, the argument promise could also be evaluated at more places in the function. Ideally, reading an argument in a function body should be as simple as reading a value from the frame, which would be the case if the argument is evaluated on the caller site. On the contrary, reading a value from a non-optimised promise leads to iterating an inline cache (InlineCacheNode) caching promise ASTs from various callers to find the AST of the current caller and evaluating it.

Argument inlining

As long as the FastPathFactory declares that a given argument is always evaluated by the function, then it safe to conclude that it is safe to evaluate the argument eagerly. In uch a case, the argument node will be evaluated on the caller site and no argument promise will be created, i.e. no promise node will be created either.

By default, builtins declare to evaluate all arguments. If some argument of a builtin is not evaluated, it is declared by adding its index to the nonEvalArgs attribute of the RBuiltin annotation. On the other hand, non-builtin functions do not evaluate the arguments (see EvaluatedArgumentsFastPath) by default. The default behavior can be overridden using RRootNode.setFastPath, as is done in BasePackage.loadOverrides, where fast paths of several functions are overridden.

Forced promise evaluation

Unless an argument is evaluated, its argument node is wrapped up into a promise wrapper node, which will be evaluated to obtain a promise for the argument. This promise is then passed to the function call. Also in this case it is possible, under certain circumstances, to create an optimised promise (OptForcedEagerPromiseNode), which eagerly evaluates the argument value on the caller site. This scenario is carried out when FastPathFactory#forcedEagerPromise(argIndex) returns true for a given argument index. See PromiseNode.create.

As to builtins, by default, the forcedEagerPromise method return false for all arguments. There are exceptions to this rule, such as builtins cbind and rbind, which override the default behavior by setting the FastPathFactory.FORCED_EAGER_ARGS fast path factory in BasePackage#loadOverrides.

As to non-functions, a special analysis of the body of a function is carried out in the constructor of FunctionDefinitionNode to determine which argument can be evaluated on the caller site. To be qualified for that, an argument must be read in any execution path of the function body and be "simple" (see EvaluatedArgumentsVisitor.isSimpleArgument). For more details go to EvaluatedArgumentsVisitor.

CallMatcherCachedNode

TODO

Incrementing/Decrementing Reference Count in Arguments

Before the value returned by an argument node is passed to a function call it may be subject to incrementing its reference count. It is done by WrapArgumentNode after the argument value is evaluated by the wrapped original argument node. The WrapArgumentNode uses the ArgumentStatePush node to increment the ref count. ArgumentStatePush also registers a request for the later decrementing of the ref count, which is done in the finally block of the function's execute method (i.e. FunctionDefinitionNode.execute). The decrementing procedure and the register of the arguments, whose refcounts are to be decremented, are encapsulated in PostProcessArgumentsNode, a child node of FunctionDefinitionNode. PostProcessArgumentsNode is created by RASTBuilder and passed to the FunctionDefinitionNode constructor.

// created for each "wrappable" argument node when creating CallArgumentsNode
WrapArgumentNode
	// the original arg node
	RNode
	// increments the ref count of a shareable argument and registers the request for decrementing
	ArgumentStatePush

CALLER_FRAME argument life-cycle

(see CallerFrameClosureProvider)

Legend:
(*)     - a caller frame closure holding a materialised frame; it never occurs in the interpreter
( )     - a caller frame closure with no frame; it never occurs in the compiler
*       - a materialised closure
^       - a request for the frame, e.g. stack introspection
<opt>   - optimisation; the boundary between interpreted and compiled code
<deopt> - deoptimisation; the boundary between compiled and interpreted code

The caller frame is available on the first call

a) no stack introspection

time
------------------------------------->

(*) (*) (*) <opt> ( ) ( ) ( )


b) early stack introspection (i.e. in the interpreter)

(*) (*)  *  <opt>  *   *   *
     ^					

c) late stack introspection (i.e. in the compiler)

(*) (*) (*) <opt> ( )  ( ) <deopt>  *  *
                        ^

The caller frame is not available on the first call

time
------------------------------------->

a) no stack introspection

( ) ( ) ( ) <opt> ( ) ( ) ( )


b) early stack introspection (i.e. in the interpreter)

( ) ( )  *  <opt>  *   *   *
     ^					

c) late stack introspection (i.e. in the compiler)

( ) ( ) ( ) <opt> ( )  ( ) <deopt>  *  *
                        ^