Skip to content

function definition and dynamic invocations

Stéphane Lozier edited this page Jan 18, 2021 · 1 revision

5 Function Definition and Dynamic Invocations

This section focuses on defining Sympl functions and invoking them. Section on function call and dotted expressions discusses Sympl semantics and AnalyzeFunCallExpr quite a bit. It may be useful to read the Sympl language definition about function calls, section .

5.1 Defining Functions

Sympl uses the defun keyword form to define functions. Defun takes a name as the first argument and a list of parameter names as the second. These are non-evaluated contexts in the Sympl code. The rest of a defun form is a series of expressions. The last expression executed in a Sympl function provides the result value of the function. Sympl does not currently support a return keyword form, but you'll see the implementation is plumbed for its support. You could look at how Sympl implements break for loops and probably add this in 20 minutes or so as an exercise. You can see example function definitions in the .sympl files.

Code generation for defun is pretty easy. At a high-level, you add the parameter names to a new, nested AnalysisScope, analyze the sub expressions, and then emit an assignment to a file scope global whose value is a LambdaExpression. When the code in a file is gathered into an outer Lambda and compiled, all the contained lambdas get compiled (see section for more information on executing Sympl files). Here's the code from etgen.cs, which is discussed more below:

public static DynamicExpression AnalyzeDefunExpr

(SymplDefunExpr expr, AnalysisScope scope) {

if (!scope.IsModule) {

throw new InvalidOperationException(

"Use Defmethod or Lambda when not defining " +

"top-level function.");

}

return Expression.Dynamic(

scope.GetRuntime().GetSetMemberBinder(expr.Name),

typeof(object),

scope.ModuleExpr,

AnalyzeLambdaDef(expr.Params, expr.Body, scope,

"defun " + expr.Name));

}

private static Expression AnalyzeLambdaDef

(IdOrKeywordToken[] parms, SymplExpr[] body,

AnalysisScope scope, string description) {

var funscope = new AnalysisScope(scope, description);

funscope.IsLambda = true; // needed for return support.

var paramsInOrder = new List<ParameterExpression>();

foreach (var p in parms) {

var pe = Expression.Parameter(typeof(object), p.Name);

paramsInOrder.Add(pe);

funscope.Names[p.Name.ToLower()] = pe;

}

var bodyexprs = new List<Expression>();

foreach (var e in body) {

bodyexprs.Add(AnalyzeExpr(e, funscope));

}

var funcTypeArgs = new List<Type>();

for (int i = 0; i < parms.Length + 1; i++) {

funcTypeArgs.Add(typeof(object));

}

return Expression.Lambda(

Expression.GetFuncType(funcTypeArgs.ToArray()),

Expression.Block(bodyexprs),

paramsInOrder);

Looking at the helper AnalyzeLambdaDef first, functions definitions need to push an AnalysisScope on the chain. The scope can serve a few purposes. Sympl uses it to hold the locals for the function's parameters. Any references to these names, barring any intervening let* bindings, will resolve to the ParameterExpressions stored in this new AnalysisScope. References from nested lambda expressions will automatically become closure environment references thanks to Expression Trees.

The new AnalysisScope also has IsLambda set to true as plumbing for adding return keyword forms. Then analyzing a return form would just search the AnalysisScope chain for a first IsLambda scope and use a return LabelTarget stored in the scope. The body of the lambda would also need to be wrapped in a LabelExpression that used the return LabelTarget.

After storing ParameterExpressions for the locals, AnalyzeDefunExpr analyzes all the body expressions in the context of the new AnalysisScope.

To create a LambdaExpression, Sympl needs to create an array of Types determined from the parameter definitions. Of course, in Sympl, these are all object.

Now AnalyzeDefunExpr can emit the code. There is a SetMember DynamicExpression to store into the ExpandoObject for the file module. It stores a binding for the function's name to the LambdaExpression. The LambdaExpression is just a BlockExpression of the body expressions. As explained a couple of times now, just assume GetSetMemberBinder is a call to the constructor for SymplSetMemberBinder and see section for an explanation. Note, still at this point in the evolution of Sympl, its SetMemberBinder doesn't really do any work, other than convey the name and ignoreCase metadata.

This is the code from AnalyzeFunCallExpr in etgen.cs (discussed further in section ) for invoking Sympl functions (or any callable object from another language, a library, or a delegate from .NET):

var fun = AnalyzeExpr(expr.Function, scope);

List<Expression> args = new List<Expression>();

args.Add(fun);

args.AddRange(expr.Arguments.Select(a => AnalyzeExpr(a, scope)));

return Expression.Dynamic(

scope.GetRuntime()

.GetInvokeBinder(new CallInfo(expr.Arguments.Length)),

typeof(object),

args);

5.2 SymplInvokeBinder and Binding Function Calls

At runtime, when trying to call a Sympl function, a delegate will flow into the CallSite. The default .NET meta-object will call FallbackInvoke on SymplInvokeBinder. This code is much simpler than the code for binding InvokeMember we looked at before. As a reminder, if the object that flows into the CallSite is some dynamic object that's callable (for example, an IronPython runtime function or first class type object), then its DynamicMetaObject's BindInvoke will produce a rule for invoking the object with the given arguments.

Here's the code for SymplInvokeBinder from runtime.cs, which is discussed in detail below:

public class SymplInvokeBinder : InvokeBinder {

public SymplInvokeBinder(CallInfo callinfo) : base(callinfo) {

}

public override DynamicMetaObject FallbackInvoke(

DynamicMetaObject targetMO, DynamicMetaObject[] argMOs,

DynamicMetaObject errorSuggestion) {

// ... Deleted COM support and checking for Defer for now ...

if (targetMO.LimitType.IsSubclassOf(typeof(Delegate))) {

var parms = targetMO.LimitType.GetMethod("Invoke")

.GetParameters();

if (parms.Length == argMOs.Length) {

var callArgs = RuntimeHelpers.ConvertArguments(

argMOs, parms);

var expression = Expression.Invoke(

Expression.Convert(targetMO.Expression,

targetMO.LimitType),

callArgs);

return new DynamicMetaObject(

RuntimeHelpers.EnsureObjectResult(expression),

BindingRestrictions.GetTypeRestriction(

targetMO.Expression,

targetMO.LimitType));

}

}

return errorSuggestion ??

RuntimeHelpers.CreateThrow(

targetMO, argMOs,

BindingRestrictions.GetTypeRestriction(

targetMO.Expression,

targetMO.LimitType),

typeof(InvalidOperationException),

"Wrong number of arguments for function -- " +

targetMO.LimitType.ToString() + " got " +

argMOs.ToString());

Let's first talk about what we aren't talking about now. This code snippet omits the code to check if the target is a COM object and to use built-in COM support. See section for information adding this to your binders. The snippet also omits some very important code that protects binders and DynamicMetaObjects from infinitely looping due to producing bad rules. It is best to discuss this in one place, so see section for how the infinite loop happens and how to prevent it for all binders.

Because language binders are expected to provide the rules for binding to static .NET objects, and the convention is that DynamicMetaObjects fall back to binders for this purpose, first this code checks for a Delegate type. This code doesn't do much for Sympl invocations. Dynamic objects fall back to languages for Delegates because the language needs to control implicit argument conversions, may provide special mappings like nil to false, and so on.

FallbackInvoke then checks the parameter and argument counts. If the delegate looks callable, Sympl optimistically converts each argument to the corresponding parameter type. See section on TypeModelMetaObject's BindInvokeMember for details on ConvertArguments. Sympl doesn't need to do more argument and parameter type matching here. This method doesn't need the information for resolving methods, and users will get an error about not being able to convert the argument to the function's parameter type as appropriate.

Sympl then creates code to convert the target object to the specific Delegate sub type, which is the LimitType of the DynamicMetaObject. See section for a discussion of using LimitType over RuntimeType. It may seem odd to convert the object to the type that LimitType reports it to be, but the type of the meta-object's expression might be more general and require an explicit Convert node to satisfy the strict typing of the Expression Tree factory or the actual emitted code that executes. The Expression Tree compiler removes unnecessary Convert nodes.

Then FallbackInvoke creates an InvokeExpression. This expression passes through EnsureObjectResult in case it needs to be wrapped to ensure it is strictly typed as assignable to object. For more information, see section 3.2.4. Then FallbackInvoke wraps this expression in the DynamicMetaObject result with the restrictions for when this rule is valid. The restrictions for Sympl's Invoke are much simpler than the InvokeMember restrictions. This rule should apply to any Delegate with the same type since the rule converts the target and arguments to the types captures in the Delegate type.

If the parameters do not match the arguments, then Sympl either uses the suggested result or creates a DynamicMetaObject result that throws an Exception. See section for a discussion of CreateThrow and restrictions. ErrorSuggestion is more interesting to discuss as it relates to member fetching and setting since it is most likely null in InvokeBinder.FallbackInvoke. If the target callable object were dynamic, then its meta-object would have directly returned a rule, rather than fall back with a result to use if the language didn't know how to invoke the dynamic object.

SymPL Implementation on the Dynamic Language Runtime

Frontmatter
1 Introduction
  1.1 Sources
  1.2 Walkthrough Organization
2 Quick Language Overview
3 Walkthrough of Hello World
  3.1 Quick Code Overview
  3.2 Hosting, Globals, and .NET Namespaces Access
    3.2.1 DLR Dynamic Binding and Interoperability -- a Very Quick Description
    3.2.2 DynamicObjectHelpers
    3.2.3 TypeModels and TypeModelMetaObjects
    3.2.4 TypeModelMetaObject's BindInvokeMember -- Finding a Binding
    3.2.5 TypeModelMetaObject.BindInvokeMember -- Restrictions and Conversions
  3.3 Import Code Generation and File Module Scopes
  3.4 Function Call and Dotted Expression Code Generation
    3.4.1 Analyzing Function and Member Invocations
    3.4.2 Analyzing Dotted Expressions
    3.4.3 What Hello World Needs
  3.5 Identifier and File Globals Code Generation
  3.6 Sympl.ExecuteFile and Finally Running Code
4 Assignment to Globals and Locals
5 Function Definition and Dynamic Invocations
  5.1 Defining Functions
  5.2 SymplInvokeBinder and Binding Function Calls
6 CreateThrow Runtime Binding Helper
7 A Few Easy, Direct Translations to Expression Trees
  7.1 Let* Binding
  7.2 Lambda Expressions and Closures
  7.3 Conditional (IF) Expressions
  7.4 Eq Expressions
  7.5 Loop Expressions
8 Literal Expressions
  8.1 Integers and Strings
  8.2 Keyword Constants
  8.3 Quoted Lists and Symbols
    8.3.1 AnalyzeQuoteExpr -- Code Generation
    8.3.2 Cons and List Keyword Forms and Runtime Support
9 Importing Sympl Libraries and Accessing and Invoking Their Globals
10 Type instantiation
  10.1 New Keyword Form Code Generation
  10.2 Binding CreateInstance Operations in TypeModelMetaObject
  10.3 Binding CreateInstance Operations in FallbackCreateInstance
  10.4 Instantiating Arrays and GetRuntimeTypeMoFromModel
11 SymplGetMemberBinder and Binding .NET Instance Members
12 ErrorSuggestion Arguments to Binder FallbackX Methods
13 SymplSetMemberBinder and Binding .NET Instance Members
14 SymplInvokeMemberBinder and Binding .NET Member Invocations
  14.1 FallbackInvokeMember
  14.2 FallbackInvoke
15 Indexing Expressions: GetIndex and SetIndex
  15.1 SymplGetIndexBinder's FallbackGetIndex
  15.2 GetIndexingExpression
  15.3 SymplSetIndexBinder's FallbackSetIndex
16 Generic Type Instantiation
17 Arithmetic, Comparison, and Boolean Operators
  17.1 Analysis and Code Generation for Binary Operations
  17.2 Analysis and Code Generation for Unary Operations
  17.3 SymplBinaryOperationBinder
  17.4 SymplUnaryOperationBinder
18 Canonical Binders or L2 Cache Sharing
19 Binding COM Objects
20 Using Defer When MetaObjects Have No Value
21 SymPL Language Description
  21.1 High-level
  21.2 Lexical Aspects
  21.3 Built-in Types
  21.4 Control Flow
    21.4.1 Function Call
    21.4.2 Conditionals
    21.4.3 Loops
    21.4.4 Try/Catch/Finally and Throw
  21.5 Built-in Operations
  21.6 Globals, Scopes, and Import
    21.6.1 File Scopes and Import
    21.6.2 Lexical Scoping
    21.6.3 Closures
  21.7 Why No Classes
  21.8 Keywords
  21.9 Example Code (mostly from test.sympl)
22 Runtime and Hosting
  22.1 Class Summary
23 Appendixes
  23.1 Supporting the DLR Hosting APIs
    23.1.1 Main and Example Host Consumer
    23.1.2 Runtime.cs Changes
    23.1.3 Sympl.cs Changes
    23.1.4 Why Not Show Using ScriptRuntime.Globals Namespace Reflection
    23.1.5 The New DlrHosting.cs File
  23.2 Using the Codeplex.com DefaultBinder for rich .NET interop
  23.3 Using Codeplex.com Namespace/Type Trackers instead of ExpandoObjects
  23.4 Using Codeplex.com GeneratorFunctionExpression


Other documents:

Dynamic Language Runtime
DLR Hostirng Spec
Expression Trees v2 Spec
Getting Started with the DLR as a Library Author
Sites, Binders, and Dynamic Object Interop Spec

Clone this wiki locally