Function call operators in the future?

Started by
16 comments, last by cellulose 11 years, 7 months ago
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

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

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

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

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

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.

This topic is closed to new replies.

Advertisement