function call operators?

Started by
12 comments, last by gjl 10 years, 3 months ago

Hi again,

sorry for posting so many questions in a short time, but I am getting really interested in angelscript :-).

I have noticed this thread and was wondering if anything has come out of it to support function call operators:

http://www.gamedev.net/topic/626746-function-call-operators-in-the-future/

Otherwise, is there a way to define cast operators in scripts just like we can do it when registering the application interface with the behaviors? Maybe being able to cast to a function handle could already help...

Thanks!

Advertisement
Function call operators are not yet implemented. I'm not sure what happened with celulose' effort, but if he finished the implementation he didn't share it with me.

Scriptable cast operators are also something that isn't implemented at the moment, though it is something I have on my to-do list.

What is it that you want to accomplish? Perhaps it can already be done in some other manner.

Regards,
Andreas

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Too bad the implementation was not shared sad.png .

The idea is that my application is publishing some math objects that can be scripted. Some of them represent curves or surfaces, and it would be great if the scripter could use them just like functions (in addition to other properties that can be configured for them). For example, I would prefer to be able to write:


double x=0;
double y=myCurve(x);
double z=mySurface(x,y);

rather than something like:


double y=myCurve.f(x);
double z=mySurface.f(x,y);

Especially because it is already possible in the application to reference these objects with this simple syntax to write formulas. It would not be consistent otherwise. It was also possible to do it using javacript (in a much less clean way than what could be done with angelscript if we had such operators, for sure!).

I have tried to define a casting operator to a function handle and generate the appropriate functions to be returned, but it does not let you write it with a user friendly syntax. I have only been able to do the following:


// works
MathFunction@ f=myCurve;
double y=f(x);

// does not work
y=myCurve(x);

I don't see any other solution than operator () right now, but maybe I am missing something.

How about using delegates?


funcdef double funcX(double);

Curve curve1(...);
funcX @myCurve = funcX(curve1.f);

double y = myCurve(x);
[\code]

Like I said to celulose before, I'm not a fan of function operators myself, but if you or someone else would implement the support I'll gladly include it in the library.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Thanks for the fast reply. I tend to agree with you about function operators, but this is a particular case with existing syntax. Delegates are indeed nice, but the problem is that they cannot have the same name as the original object.

I have started looking at the code of the compiler. It may take some time for me to get into it, but if I happen to implement any improvement, I will share it for sure!

I have good news! Starting with your post in the previous thread about opCall operators:


// Case 1
?Object obj;
obj();			 // <-- CompileFunctionCall() needs to look for an opCall method in the Object class

// Case 2
array<Object> arr;
arr[0]();	  // <-- CompileExpressionPostOp() needs to look for an opCall method in the Object class

I think I have case #1 covered! I just added a few lines (less than 10!) to CompileFunctionCall() so that the opCall case is recognized and treated as if it were a usual function call. And it works!

Now looking at case #2... I'll post the patch when I have both covered (it may probably require some adjustments since I am a complete beginner with angelscript and its implementation, so it may not be as clean as it could be).

All right, so it appears that it was not such a big deal (thanks to your explanations)! It seems to be working pretty well so far! I have not looked at the tests suites yet, though, as I was working on the release package.

Please find attached my current diff (compared to the current release, 2.28.0). It may seem large, but that's mainly because of re-indentation. I have just added a few lines of code. If you open it up with a smart diff tool (TortoiseSVN's for example), it should look quite simple. I hope you will find it useful. I apologize in advance if I have not understood all the ins and outs of it... I have been looking at your code for just a couple of days! But I must say I am impressed: your code is pretty easy to read and debug in the end!

BTW, in order to share as much as possible with the existing implementation, I have had to add a parameter to the CompileFunctionCall method so that the function name is available in the case of an expression post op.

Also, I have tried to make sure that error messages are consistent when calling the opCall operator on a class that does not define it (what you get is the usual "ERR : No matching signatures to 'MyClass::opCall()', which sounds good to me).

It seems that there is actually a problem with this patch: it works only on local variables (it will fail on globals or members). I guess it is due to the fact that I am using the CompileVariableAccess method that handles LOCAL variables only.

[EDIT]: simply adding false to the first CompileVariableAccess for noGlobal fixes it. However I am wondering if this may not introduce regressions, since it is stated in the comments above that for precedence globals should not be searched for. What is the reason for that?

Update: changing the noGlobal to false in the current source tree only breaks on test, because it just produces a different error message. Is it safe to assume that this won't bring major regressions?

Failed on line 684 in angelscript-code/sdk/tests/test_feature/projects/xcode/../../source/test_inheritance.cpp

script (1, 11) : Info : Compiling void A::method()

script (1, 27) : Error : Unknown scope 'B'

script (1, 27) : Error : Namespace 'B' doesn't exist.

script (1, 38) : Error : No matching signatures to 'A::method(const int)'

script (1, 38) : Info : Candidates are:

script (1, 38) : Info : void A::method()

script (1, 65) : Error : No matching signatures to 'A::method(const double)'

script (1, 65) : Info : Candidates are:

script (1, 65) : Info : void A::method()

script (1, 79) : Error : Unknown scope 'B::A'

script (1, 79) : Error : Namespace 'B::A' doesn't exist.

After some more testing, there is still a missing piece, to support opCall calls on class members, typically:


myObject.functor();

I have managed to find out the path that I had missed, and I thought that adding similar code would solve it, but the problem is that the script compiles, and when executed, it crashes in asCContext::CallInterfaceMethod crashes. So obviously the generated method call has not the right stack...

The only modification that I have made compared to the previous iteration (diff attached), consists in generating a function call on member when it is detected that this is a member (see code in bold below):


int asCCompiler::CompileFunctionCall(asCScriptNode *node, asSExprContext *ctx, asCObjectType *objectType, bool objIsConst, const asCString &scope,const asCString& functionName)
{
	asCString name;
	asCTypeInfo tempObj;
	asCArray<int> funcs;
	int localVar = -1;
	bool initializeMembers = false;
    bool opCallOnObject=false;
    asCTypeInfo opCallObjectType;
    
    // if no function name, use the current node's name (default)
    if(functionName.GetLength()==0)
    {
        asCScriptNode *nm = node->lastChild->prev;
        name.Assign(&script->code[nm->tokenPos], nm->tokenLength);
    }
    else
        name=functionName;
    
	// First check for a local variable of a function type as it would take precedence
	// Must not allow function names, nor global variables to be returned in this instance
	// If objectType is set then this is a post op expression and we shouldn't look for local variables
	asSExprContext funcPtr(engine);
	if( objectType == 0 )
	{
        // check if a global or local variable with object type can be found
		localVar = CompileVariableAccess(name, scope, &funcPtr, node, true, true, false);
		if( localVar >= 0 && !funcPtr.type.dataType.GetFuncDef() && funcPtr.methodName == "" )
		{
            // if the variable is an object type, could be an opCall
            if(funcPtr.type.dataType.IsObject()||funcPtr.type.dataType.IsObjectHandle())
            {
                // actually compile the variable access
                CompileVariableAccess(name, scope, ctx, node);
                
                // set operation to opCall on our object
                name="opCall";
                objectType = ctx->type.dataType.GetObjectType();
                opCallOnObject=true;
                opCallObjectType=ctx->type;
                
                // continue
                localVar = -1;
            }
            else
            {
                // The variable is not a function
                asCString msg;
                msg.Format(TXT_NOT_A_FUNC_s_IS_VAR, name.AddressOf());
                Error(msg, node);
                return -1;
            }
		}

		// If the name matches a method name, then reset the indicator that nothing was found
		if( funcPtr.methodName != "" )
			localVar = -1;
	}

	if( localVar < 0 )
	{
		// If this is an expression post op, or if a class method is
		// being compiled, then we should look for matching class methods
		if( objectType || (outFunc && outFunc->objectType && scope != "::") )
		{
			// If we're compiling a constructor and the name of the function is super then
			// the constructor of the base class is being called.
			// super cannot be prefixed with a scope operator
			if( scope == "" && m_isConstructor && name == SUPER_TOKEN )
			{
				// If the class is not derived from anyone else, calling super should give an error
				if( outFunc && outFunc->objectType->derivedFrom )
					funcs = outFunc->objectType->derivedFrom->beh.constructors;

				// Must not allow calling base class' constructor multiple times
				if( continueLabels.GetLength() > 0 )
				{
					// If a continue label is set we are in a loop
					Error(TXT_CANNOT_CALL_CONSTRUCTOR_IN_LOOPS, node);
				}
				else if( breakLabels.GetLength() > 0 )
				{
					// TODO: inheritance: Should eventually allow constructors in switch statements
					// If a break label is set we are either in a loop or a switch statements
					Error(TXT_CANNOT_CALL_CONSTRUCTOR_IN_SWITCH, node);
				}
				else if( m_isConstructorCalled )
				{
					Error(TXT_CANNOT_CALL_CONSTRUCTOR_TWICE, node);
				}
				m_isConstructorCalled = true;

				// We need to initialize the class members, but only after all the deferred arguments have been completed
				initializeMembers = true;
			}
			else
			{
				// The scope is can be used to specify the base class
				builder->GetObjectMethodDescriptions(name.AddressOf(), objectType ? objectType : outFunc->objectType, funcs, objIsConst, scope);
			}

			// It is still possible that there is a class member of a function type of of object type that
			if( funcs.GetLength() == 0 )
			{
				int r = CompileVariableAccess(name, scope, &funcPtr, node, true, true, true, objectType);
				if( r >= 0 && !funcPtr.type.dataType.GetFuncDef() && funcPtr.methodName == "" )
				{
                    // if this is an opCall, compile the variable access and opCall
                    if((funcPtr.type.dataType.IsObject()||funcPtr.type.dataType.IsObjectHandle()))
                    {
                        int r = CompileVariableAccess(name, scope, ctx, node, true, true, true, objectType);
                        if(r<0)
                            return r;
                        bool isConst = false;
                        if( ctx->type.dataType.IsObjectHandle() )
                            isConst = ctx->type.dataType.IsHandleToConst();
                        else
                            isConst = ctx->type.dataType.IsReadOnly();
                        
                        asCObjectType *trueObj = ctx->type.dataType.GetObjectType();
                        
                        asCTypeInfo objType = ctx->type;
                        
                        // Compile function call
                        r = CompileFunctionCall(node, ctx, trueObj, isConst,scope,"opCall");
                        if( r < 0 )
                            return r;
                        
                        return r;
                    }
                    else
                    {
                        // The variable is not a function
                        asCString msg;
                        msg.Format(TXT_NOT_A_FUNC_s_IS_VAR, name.AddressOf());
                        Error(msg, node);
                        return -1;
                    }
				}
			}

			// If a class method is being called implicitly, then add the this pointer for the call
			if( funcs.GetLength() && !objectType )
			{
				objectType = outFunc->objectType;

				asCDataType dt = asCDataType::CreateObject(objectType, false);

				// The object pointer is located at stack position 0
				ctx->bc.InstrSHORT(asBC_PSF, 0);
				ctx->type.SetVariable(dt, 0, false);
				ctx->type.dataType.MakeReference(true);

				Dereference(ctx, true);
			}
		}

		// If it is not a class method or member function pointer,
		// then look for global functions or global function pointers,
		// unless this is an expression post op, incase only member
		// functions are expected
		if( objectType == 0 && funcs.GetLength() == 0 && funcPtr.type.dataType.GetFuncDef() == 0 )
		{
			// The scope is used to define the namespace
			asSNameSpace *ns = DetermineNameSpace(scope);
			if( ns )
			{
				// Search recursively in parent namespaces
				while( ns && funcs.GetLength() == 0 && funcPtr.type.dataType.GetFuncDef() == 0 )
				{
					builder->GetFunctionDescriptions(name.AddressOf(), funcs, ns);
					if( funcs.GetLength() == 0 )
					{
						int r = CompileVariableAccess(name, scope, &funcPtr, node, true, true);
						if( r >= 0 && !funcPtr.type.dataType.GetFuncDef() )
						{
							// The variable is not a function
							asCString msg;
							msg.Format(TXT_NOT_A_FUNC_s_IS_VAR, name.AddressOf());
							Error(msg, node);
							return -1;
						}
					}

					ns = builder->GetParentNameSpace(ns);
				}
			}
			else
			{
				asCString msg;
				msg.Format(TXT_NAMESPACE_s_DOESNT_EXIST, scope.AddressOf());
				Error(msg, node);
				return -1;
			}
		}
	}

	if( funcs.GetLength() == 0 && funcPtr.type.dataType.GetFuncDef() )
	{
		funcs.PushLast(funcPtr.type.dataType.GetFuncDef()->id);
	}

	// Compile the arguments
	asCArray<asSExprContext *> args;
	asCArray<asCTypeInfo> temporaryVariables;
[...]

After debugging for a while it seems that it is very similar to what is generated when myObject.functor.opCall() is called, but there is obviously something different that I cannot figure out. Does anyone have an idea?

[attachment=19508:diff.txt]

I am pretty sure that it is related to stack cleanup (I haev tried adding the following code after CompileFunction Call, but it just crashes differently and I must admit I do not understand everything of it yet...):


                        // If the method returned a reference, then we can't release the original
			// object yet, because the reference may be to a member of it
			if( !objType.isTemporary ||
				!(ctx->type.dataType.IsReference() || (ctx->type.dataType.IsObject() && !ctx->type.dataType.IsObjectHandle())) ||
				ctx->type.isVariable ) // If the resulting type is a variable, then the reference is not a member
			{
				// As the method didn't return a reference to a member
				// we can safely release the original object now
				ReleaseTemporaryVariable(objType, &ctx->bc);
			}

This topic is closed to new replies.

Advertisement