Jump to content

  • Log In with Google      Sign In   
  • Create Account

- - - - -

Function call operators in the future?


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
17 replies to this topic

#1 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 21 June 2012 - 11:35 AM

I'm curious about future prospects on a function call operator in Angelscript. Messy though they can be, operator() overloads are super-useful and something I use frequently in C++ to avoid wordy APIs. It also allows for functors and the like.

As an example, I have a module called "Random" in my game engine, exposed to Angelscript as a global, which can generally be treated like an overloaded function to generate random numbers with various parameters. Additional Random objects can also be created for deterministic simulations and the like, so I'd be losing some functionality if I just exposed this stuff as global functions.

class Random
{
public:
	Uint32 operator()(Uint32 max);
	Uint32 operator()(Uint32 min, Uint32 max);

	Sint32 operator()(Sint32 max);
	Sint32 operator()(Sint32 min, Sint32 max);

	float operator()(float max);
	float operator()(float min, float max);
		  
	double operator()(double max);
	double operator()(double min, double max);

	// Normal distribution
	float normal(float mean, float deviation);

	// Exponential normal distribution
	float normalExp(float mean, float multDeviation);


	// ...
};

Is this something that is likely to be added to Angelscript in the future, or would it throw a wrench in the design?

Edited by cellulose, 21 June 2012 - 11:38 AM.


Sponsor:

#2 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
1Likes
Like

Posted 21 June 2012 - 12:49 PM

I currently do not have any plans on adding support for function call operators in the script language. Though I won't go so far as saying that it will never be done, I just don't feel they add enough value to make it worth it at the moment.

That doesn't mean you can't register your Random class with AngelScript though. You just have to give a name for your nameless functions when registering them, example:

engine->RegisterObjectMethod("Random", "uint32 max(uint32)", asMETHODPR(Random, operator(), (UInt32), UInt32), asCALL_THISCALL);
engine->RegisterObjectMethod("Random", "uint32 between(uint32, uint32)", asMETHODPR(Random, operator(), (UInt32, UInt32), UInt32), asCALL_THISCALL);

engine->RegisterObjectMethod("Random", "int32 max(int32)", asMETHODPR(Random, operator(), (SInt32), SInt32), asCALL_THISCALL);
engine->RegisterObjectMethod("Random", "int32 between(int32, int32)", asMETHODPR(Random, operator(), (SInt32, SInt32), SInt32), asCALL_THISCALL);
engine->RegisterObjectMethod("Random", "float max(float)", asMETHODPR(Random, operator(), (float), float), asCALL_THISCALL);
engine->RegisterObjectMethod("Random", "float between(float, float)", asMETHODPR(Random, operator(), (float, float), float), asCALL_THISCALL);

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

#3 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 21 June 2012 - 06:19 PM

For my part, it's a feature I'd really like to see, but I've also written a parser or two and know it would be a giant pain in the butt to implement. So I understand if it's late or never. To be honest I'd take property accessors over function-call operators any day of the week.

I'll keep my fingers crossed nonetheless. :)

#4 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 21 June 2012 - 06:35 PM

To be honest I'd take property accessors over function-call operators any day of the week.


Property accessors are already supported.

Or perhaps you had something different in mind?
AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

#5 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 24 June 2012 - 01:15 PM

Oh, no, I'm quite aware. I'm saying I'm happy to have property accessors. But anyway. Keeping my fingers crossed. And who knows? Maybe if I'm antsy enough it's the sort of thing I could write a preprocessor for. :)

#6 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 30 August 2012 - 12:12 PM

Pardon the necro-post; the release of version 2.24 makes this relevant again.

A statement from Andreas in my topic on hardcoding geometry and initializer lists:

TheAtom reported a bug today about making calls with function defs. I still need to investigate what changes needs to be made to support those scenarios, but as they look similar in syntax to what the use of a function call operator might look like I recommend you wait a little until I've fixed that bug before you dig too deep into the code.


Since the improved semantics for function pointers are now implemented, how straightforward would the implementation of an opCall be? (Either as a new official addition to the language or, barring that, as a hack to my own copy?)

#7 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 30 August 2012 - 01:07 PM

I believe it ought to be pretty straight forward now. It should only require a few changes to the asCCompiler class. There are basically two places that needs to be updated, CompileFunctionCall() and CompileExpressionPostOp().

The first is for when the expression looks like an ordinary function call, i.e.

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

The second is for when the expression preceeding the argument list isn't a simple symbol, but in itself another expression that returns an object that implements an opCall method, e.g.

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

The easiest way to determine what to change is by debugging the compiler while compiling a script you want to work.


While I'm not a big fan of call operators myself, I can still incorporate them into the language as they don't cause any negative impacts.

Edited by Andreas Jonsson, 30 August 2012 - 01:09 PM.

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

#8 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 30 August 2012 - 05:11 PM

In that case I eagerly await the next release. :D

#9 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 30 August 2012 - 06:28 PM

While I'm not a big fan of call operators myself, I can still incorporate them into the language as they don't cause any negative impacts.


I do not want to disappoint you but I didn't say I would implement this. ;)

I meant that if someone were to implement it and send me a patch I would incorporate it into the library. Why don't you give it a try yourself?
AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

#10 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 31 August 2012 - 12:06 PM

Already at work on it.

Are there any particular caveats responsible for this remark in CompileFunctionCall? (Version 2.24.1)
// TODO: funcdef: It is still possible that there is a global variable of a function type

Or would it be safe to add the variable-access resolution code?

EDIT: Ah, it looks like it's superfluous...

Edited by cellulose, 31 August 2012 - 12:09 PM.


#11 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 31 August 2012 - 06:37 PM

There is nothing particular about it. It's just a comment I added for myself to check upon at a later stage.

Let me know if you encounter any difficulties and I'll try to help.

Another tip to make it easier to understand, is to compile a script where the opCall method is called explicitly and then checking the debug output of the bytecode (written to the AS_DEBUG folder). You'll want the bytecode to be identical both when the method is called implicitly and explicitly. Turn off the bytecode optimization when doing this (asEP_OPTIMIZE_BYTECODE) so you'll know what instructions the compiler needs to output before the code goes through the optimization pass.

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

#12 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 06 September 2012 - 02:48 PM

I got sidetracked from this for about a week. Back into the fray of things.

Question for Andreas: Where in CompileFunctionCall is the "this" value indicated to the bytecode? I'm getting my head around expression contexts et cetera.

EDIT: Partially answered my own question. (at least sort of -- CompileFunctionCall is evidently only used for "get" accessors and function calls that are not part of a larger expression?) I can see now how callable variables (funcPtrs and opCall-able objects) throw a wrench into the compiler's current handling of function calling...

EDIT: I'm going to invest the time to get a better top-down understanding of the compiler's architecture, I think... Still no idea how the 'this' reference is handled.

Edited by cellulose, 06 September 2012 - 03:41 PM.


#13 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
1Likes
Like

Posted 06 September 2012 - 04:41 PM

In case of an implicit this pointer, i.e. when a class method is called from within another method, the CompileFunctionCall() pushes the this pointer onto the stack after it has determined that it is a class method that is being called. You'll find the following code, early in the CompileFunctionCall()

   // 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);

When it is not an implicit this pointer, then the object pointer is pushed on the stack in CompileVariableAccess() (look for THIS_TOKEN). In this case the compiler will go through the CompileExpressionPostOp where the . operator (if( op == ttDot )) case will invoke the CompileFunctionCall().

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

#14 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 06 September 2012 - 05:29 PM

Below is my current progress with CompileFunctionCall. It looks like the CompileVariableAccess that looks for a callable variable is already doing the work of pushing the called object onto the stack. It's hilarious to think this might work in some naive way. However, I doubt it does. I'm guessing I need to deal with the format of the variable (reference/dereference it?) and possibly store a temporary copy, as is done for funcdefs toward the bottom.

The supposed necessity of a temporary variable here actually baffles me a little, given the only eligible identifiers are local, class and global variables of funcdef type.

int asCCompiler::CompileFunctionCall(asCScriptNode *node, asSExprContext *ctx, asCObjectType *objectType, bool objIsConst, const asCString &scope)
{
asCString name;
asCTypeInfo tempObj;
asCArray<int> funcs;
int r = -1;
asCScriptNode *nm = node->lastChild->prev;
name.Assign(&script->code[nm->tokenPos], nm->tokenLength);
// If we're compiling a class method, then the call may be to a class method
// even though it looks like an ordinary call to a global function. If it is
// to a class method it is necessary to implicitly add the this pointer.
if( objectType == 0 && outFunc && outFunc->objectType && scope != "::" )
{
  // The special keyword 'super' may be used in constructors to invoke the base
  // class' constructor. This can only be used without any scoping operator
  if( m_isConstructor && name == SUPER_TOKEN && scope == "" )
  {
   // We are calling the base class' constructor, so set the objectType
   objectType = outFunc->objectType;
  }
  else
  {
   // Are there any class methods that may match?
   // TODO: namespace: Should really make sure the scope also match. Because the scope
   //				  may match a base class, or it may match a global namespace. If it is
   //				  matching a global scope then we're not calling a class method even
   //				  if there is a method with the same name.
   asCArray<int> funcs;
   builder->GetObjectMethodDescriptions(name.AddressOf(), outFunc->objectType, funcs, false);
   if( funcs.GetLength() )
   {
	// We're calling a class method, so set the objectType
	objectType = outFunc->objectType;
   }
  }

  // If a class method is being called then implicitly add the this pointer for the call
  if( 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);
  }  
}
// First check for a local variable of a callable type
// Must not allow function names, nor global variables to be returned in this instance
asSExprContext callableVar(engine);
if( objectType == 0 )
{
  //In the case of a callable object, this adds the this pointer for the call.
  r = CompileVariableAccess(name, scope, &callableVar, node, true, true);
}
if( r < 0 )
{
  if( objectType )
  {
   // 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( objectType->derivedFrom )
	 funcs = 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;
   }
   else
   {
	// The scope is can be used to specify the base class
	builder->GetObjectMethodDescriptions(name.AddressOf(), objectType, funcs, objIsConst, scope);
   }
   // It is still possible that there is a class member of a callable type
   if( funcs.GetLength() == 0 )
	CompileVariableAccess(name, scope, &callableVar, node, true, true, objectType);
  }
  else
  {
   // The scope is used to define the namespace
   asSNameSpace *ns = DetermineNameSpace(scope);
   if( ns )
	builder->GetFunctionDescriptions(name.AddressOf(), funcs, ns);
   else
   {
	asCString msg;
	msg.Format(TXT_NAMESPACE_s_DOESNT_EXIST, scope.AddressOf());
	Error(msg.AddressOf(), node);
	return -1;
   }
   // TODO: funcdef: It is still possible that there is a global variable of a callable type
   //	...is it?  (Code above should account for this)  -- EDB 09/2012
  }
}
// Handle the callable variable
if ( callableVar.type.dataType.IsValid() )
{
  //Determine callability of object
  r = -1;
  if( callableVar.type.dataType.GetFuncDef() )
  {
   //Function pointers are always callable
   funcs.PushLast(callableVar.type.dataType.GetFuncDef()->id);
   r = 0;
  }
  else if( callableVar.type.dataType.IsObject() )
  {
   //Check whether object implements opCall
   const char *opCall = "opCall";
   asCObjectType *callType = callableVar.type.dataType.GetObjectType();
   builder->GetObjectMethodDescriptions(opCall, callType, funcs, false);
   if ( funcs.GetLength() )
   {
	r = 0;
	objectType = callType;
   }
  }
  if (r < 0)
  {
   // The variable is not a function
   asCString msg;
   msg.Format(TXT_NOT_A_FUNC_s_IS_VAR, name.AddressOf());
   Error(msg.AddressOf(), node);
   return -1;
  }
}

// Compile the arguments
asCArray<asSExprContext *> args;
asCArray<asCTypeInfo> temporaryVariables;
if( CompileArgumentList(node->lastChild, args) >= 0 )
{
  // Special case: Allow calling func(void) with a void expression.
  if( args.GetLength() == 1 && args[0]->type.dataType == asCDataType::CreatePrimitive(ttVoid, false) )
  {
   // Evaluate the expression before the function call
   MergeExprBytecode(ctx, args[0]);
   asDELETE(args[0],asSExprContext);
   args.SetLength(0);
  }
  MatchFunctions(funcs, args, node, name.AddressOf(), objectType, objIsConst, false, true, scope);
  if( funcs.GetLength() != 1 )
  {
   // The error was reported by MatchFunctions()
   // Dummy value
   ctx->type.SetDummy();
  }
  else
  {
   int r = asSUCCESS;
   // Add the default values for arguments not explicitly supplied
   asCScriptFunction *func = (funcs[0] & 0xFFFF0000) == 0 ? engine->scriptFunctions[funcs[0]] : 0;
   if( func && args.GetLength() < (asUINT)func->GetParamCount() )
	r = CompileDefaultArgs(node, args, func);
   // TODO: funcdef: Do we have to make sure the handle is stored in a temporary variable, or
   //				is it enough to make sure it is in a local variable?
   // For function pointer we must guarantee that the function is safe, i.e.
   // by first storing the function pointer in a local variable (if it isn't already in one)
   if( r == asSUCCESS )
   {
	if( func && func->funcType == asFUNC_FUNCDEF )
	{
	 if( objectType )
	 {
	  Dereference(ctx, true); // Dereference the object pointer to access the member
	  // The actual function should be called as if a global function
	  objectType = 0;
	 }
	 Dereference(&callableVar, true);
	 ConvertToVariable(&funcPtr);
	 ctx->bc.AddCode(&funcPtr.bc);
	 if( !funcPtr.type.isTemporary )
	  ctx->bc.Instr(asBC_PopPtr);
	}
	MakeFunctionCall(ctx, funcs[0], objectType, args, node, false, 0, callableVar.type.stackOffset);
	// If the function pointer was copied to a local variable for the call, then
	// release it again (temporary local variable)
	if( func && func->funcType == asFUNC_FUNCDEF )
	{
	 ReleaseTemporaryVariable(funcPtr.type, &ctx->bc);
	}
   }
  }
}
else
{
  // Failed to compile the argument list, set the dummy type and continue compilation
  ctx->type.SetDummy();
}
// Cleanup
for( asUINT n = 0; n < args.GetLength(); n++ )
  if( args[n] )
  {
   asDELETE(args[n],asSExprContext);
  }
return 0;
}

EDIT: Just realized why that global-variable lookup would be necessary. I'll probably be adding code there...

EDIT: I can't seem to determine what disqualifies global variables from the first variable lookup attempt. Any insight into that?
CompileVariableAccess(name, scope, &callableVar, node, true, true);

Edited by cellulose, 06 September 2012 - 05:54 PM.


#15 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
1Likes
Like

Posted 06 September 2012 - 09:22 PM

CompileVariableAccess() is pushing the object pointer on the stack. The code just need to make sure to dereference it if necessary.

Test compiling a script that calls the opCall method explicitly and you'll see what needs to be done.

class Class
{
  void opCall() {}
}
void main()
{
  Class c;
  c();              // This should produce the exact same bytecode as the next line
  c.opCall();
}


---

The temp variable for the funcdef is to make sure function object stays valid throughout the function call. Otherwise a script like this might crash:


funcdef void FUNC();
FUNC @func = CompileFunction("void myfunc() { @func = null; }");
void main()
{
   func();
}

It's obviously a very stupid example, but since AngelScript is supposed to be sandboxed, even stupid examples must not crash.

---

The first call to CompileVariableAccess() must not return global variables, otherwise those will be found before the class methods of the same name. Let's say you have the following script:

funcdef void FUNC();
FUNC @func;
class Class
{
  void func() {}

  void method()
  {
     func();           // <-- func is a class method and a global function pointer, both match, but the class method is seen first 
  }
}

If the first call to CompileVariableAccess() would find the global variable then Class::method() would call the wrong function.

It looks like there might be a bug here. I need to verify, but it looks like the first call to CompileVariableAccess() currently will find matching global variables. If so I'll have to fix that.
AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

#16 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 07 September 2012 - 08:42 AM

There was indeed a bug, but it turns out that it was different from what I expected. Rather than global variables taking precedence over local class methods incorrectly, the problem is that local class methods take precedence over local variables of the same name.

I'll have to make some changes in CompileFunctionCall() to properly handle this. But don't worry too much about this in your work. It's after all only a problem when the variable happens to have the same name as the class method.
AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

#17 Andreas Jonsson   Moderators   -  Reputation: 3376

Like
0Likes
Like

Posted 07 September 2012 - 09:33 AM

I've fixed this problem in revision 1410. The CompileFunctionCall() has been rearranged to properly handle the visibility of local versus member versus global symbols, and CompileVariableAccess() has a new argument to exclude global variables in searches.
AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

#18 cellulose   Members   -  Reputation: 169

Like
0Likes
Like

Posted 07 September 2012 - 12:21 PM

Awesome; I'll perform a merge with trunk then.

I may take the liberty of creating a new compiler method to unify the logic for callable objects.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS