• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
cellulose

Function call operators in the future?

17 posts in this topic

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.

[CODE]
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);


// ...
};
[/CODE]

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
0

Share this post


Link to post
Share on other sites
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:

[code]
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);
[/code]

Regards,
Andreas
1

Share this post


Link to post
Share on other sites
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 [i]giant[/i] 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. :)
0

Share this post


Link to post
Share on other sites
[quote name='cellulose' timestamp='1340324348' post='4951549']
To be honest I'd take property accessors over function-call operators any day of the week.
[/quote]

[url="http://www.angelcode.com/angelscript/sdk/docs/manual/doc_script_class_prop.html"]Property accessors are already supported[/url].

Or perhaps you had something different in mind?
0

Share this post


Link to post
Share on other sites
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. :)
0

Share this post


Link to post
Share on other sites
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:

[quote]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.[/quote]

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?)
0

Share this post


Link to post
Share on other sites
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.

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

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.

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

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
0

Share this post


Link to post
Share on other sites
[quote name='Andreas Jonsson' timestamp='1346353646' post='4974882']
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.
[/quote]

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?
0

Share this post


Link to post
Share on other sites
Already at work on it.

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

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

EDIT: Ah, it looks like it's superfluous... Edited by cellulose
0

Share this post


Link to post
Share on other sites
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
0

Share this post


Link to post
Share on other sites
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
0

Share this post


Link to post
Share on other sites
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()

[code]
// 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);
[/code]

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
1

Share this post


Link to post
Share on other sites
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.

[CODE]
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;
}
[/CODE]

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?
[color=#0000ff]CompileVariableAccess(name, scope, &callableVar, node, true, true);[/color] Edited by cellulose
0

Share this post


Link to post
Share on other sites
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.

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


---

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:


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

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:

[code]
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
}
}
[/code]

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.
1

Share this post


Link to post
Share on other sites
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.
0

Share this post


Link to post
Share on other sites
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.
0

Share this post


Link to post
Share on other sites
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.
0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0