Emmiting assembly for calling member functions

Started by
5 comments, last by HellRaiZer 17 years, 9 months ago
Hi... First of all, sorry for the lengthy post. I hope someone will read it all and have some suggestions to make. I just found a bug in my scripting engine which seems really hard to remove. The code that emits assembly for a member function call is doing things wrong. Here is a little example. (Sorry for writing in scripting syntax; the whole thing could be easily translated in C but i wanted to show that this happens in a scripting language). I hope the code isn't that confusing.

struct Vector3
{
	var float x;
	var float y;
	var float z;

	function void Set(float xx, float yy, float zz);
	function Vector3 ScaleCopy(float scale);
};

function void Vector3::Set(float xx, float yy, float zz)
{
	x = xx;
	y = yy;
	z = zz;
}

function Vector3 Vector3::ScaleCopy(float scale)
{
	local Vector3 v;
	v.Set(x * scale, y * scale, z * scale);
	return v;
}

The function that has the problem is ScaleCopy(). I also put Set() above, in case for the example to be complete. Here is the emmited assembly for ScaleCopy().

1 : function ?ScaleCopy$Vector3&float!1#Global
2 : {
3 : 	param float scale[1] : -3;
4 :	local Vector3 v[1] : -1;
5 :
6 : 	new _stack[v + 0], "Vector3";				// because Vector3 is a struct, we have to create a local instance in case to return it.
7 :	push _this;						// store _this register for using it.
8 :	mov _this, _stack[v + 0];				// set _this register to point to the object for which we are calling a function.
9 :	mov _reg_0, _this[?x&float!1@Vector3 + 0];		// _reg_0 = _this.x;
10:	fmul _reg_0, _stack[scale + 0];				// _reg_0 = _this.x * scale;
11:	push _reg_0;						// push _this.x * scale on stack for the function call that follows.
12:	mov _reg_2, _this[?y&float!1@Vector3 + 0];		// _reg_2 = _this.y;
13:	fmul _reg_2, _stack[scale + 0];				// _reg_2 = _this.y * scale;
14:	push _reg_2;						// push _this.y * scale on stack for the function call that follows.
15:	mov _reg_1, _this[?z&float!1@Vector3 + 0];		// _reg_1 = _this.z;
16:	fmul _reg_1, _stack[scale + 0];				// _reg_1 = _this.z * scale;
17:	push _reg_1;						// push _this.z * scale on stack for the function call that follows.
18:	call ?Set$void&float!1&float!1&float!1, 1, "Vector3";	// call Vector3::Set(float, float, float) on the object pointed by _this register;
19:	pop _this;						// restore _this pointer to point to the correct object.
20:	mov _ret_val, _stack[v + 0];				// set _ret_val register to point to the local copy of the scale vector.
21:	ret ;							// return from the function.
22: }

I hope the assembly isn't that confusing. It is very similar to x86 asm, so i hope there is no problem understanding what is going on if you know asm. In the above code, there is a mistake. This is the first mov instruction (line 8) which moves the local Vector3 to _this register in case to call the corresponding function later. When this happens, all the following references to _this members (lines 9, 12, 15) are incorrect, because _this points to the v, instead of the true "this" object. The problem starts from the AST representation of the member function call. When emitting assembly code for a call, the first thing that is visible is the object on which we want to call a function. This means that i have to somehow set the _this register to point to the correct object, before assembling the function argument expressions. This is correct from one point of view, because if i was doing it the other way (first assembling function arguments, and then setting _this register to point to the correct object) i would have pushed too many unnessecary things on stack. In case to see what i'm saying just move lines 7 and 8 just before the call (line 18). One can suggest leaving line 7 where it is (in case not to pollute the stack) and just move line 8 just before the call. But then there is a problem if i have statements like this:

Matrix3x3 mat.
mat.Row0.Set(x * scale, y * scale, z * scale);
In this case, i have to push _this register twice on stack in case to reach the correct member for which i want to call a function. But again, function arguments are referencing members of the current object. I hope the above is clear enough. I know this is a very specific problem, but i want to believe someone will have something to suggest. If you have anything to say please say it. I'm desperate for a solution. If you need more info or anything please ask. Thanks in advance. HellRaiZer
HellRaiZer
Advertisement
Why do you have a dedicated _this register? Where does push _this store the value of _this (on the same stack as everything else)? Can you do mov _reg_1, _this; and mov _this,_reg_1;? What is your procedure calling convention?

Quote:This is correct from one point of view, because if i was doing it the other way (first assembling function arguments, and then setting _this register to point to the correct object) i would have pushed too many unnessecary things on stack. In case to see what i'm saying just move lines 7 and 8 just before the call (line 18).

I don't understand that part. Moving this-change directly in front of the call doesn't change the number of stack-moves, does it?
Quote:Original post by HellRaiZer
The problem starts from the AST representation of the member function call. When emitting assembly code for a call, the first thing that is visible is the object on which we want to call a function. This means that i have to somehow
set the _this register to point to the correct object, before assembling the function argument expressions. This is correct from one point of view, because if i was doing it the other way (first assembling function arguments, and then setting _this register to point to the correct object) i would have pushed too many unnessecary things on stack.


So the only reason you're going with AST is optimization, is that correct?

I've written two compilers currently, and I was always caling functions the "other way" - first all arguments, in reversed order, then "this", then call (possibly virtual). There are numerous reasons things can go wrong if you're not calling this way. For example: in my language, object pointed to by "this" might have been replaced by one of the subfunctions, so by the point of the call, value from saved _this might have been invalid (crash!), or simply wrong virtual function might be called.

A* g_a = new A;
function int changeA(A* a)
{
g_a = a;
return 6;
};
g_a->func1( 7, changeA(new B), 18 ); // g_a has to be accessed last.

Having said that, I guess that it is unlikely for this to matter in your scripting language. But you've got to think about logical consequences of optimizations, before performing them.


Ok, more on the topic:
If you've decided to make _this register the only one (besides _stack) that can do memory accesses, then you're stuck with constantly swapping _this. If you really want do do optimizations, make other registers able to do such magic.

mov _this, _stack[myVariable0 + 0]; // myVariable0mov _reg_0, _this[someMember + 0];mov _this, _stack[myVariable1 + 0];mov _reg_1, _this[someMember + 0];add _reg_0, _reg_1;mov _this, _stack[myVariable0 + 0]; // myVariable0 again!mov _reg_1, _this[someOtherMember + 0];add _reg_0, _reg_1;// now: let _reg_2 also access memorymov _this, _stack[myVariable0 + 0]; // myVariable0mov _reg_0, _this[someMember + 0];mov _reg_2, _stack[myVariable1 + 0];mov _reg_1, _reg_2[someMember + 0];add _reg_0, _reg_1;//mov _this, _stack[myVariable0 + 0]; // myVariable0 again? not needed!mov _reg_1, _this[someOtherMember + 0];add _reg_0, _reg_1;


Of course, with this you do not need to have _this register at all, just as Trap suggested.


Offtopic: 666th post on teh forums!

[Edited by - deffer on July 5, 2006 8:15:24 AM]
Thanks for replies.

Trap:
Quote:
Why do you have a dedicated _this register?

Just think of it as ecx. This is a generic register, but when calling functions from objects, it keeps the pointed object. It can keep any value, just like _reg_x registers.
The problem starts when generating assembly from high level code. Despite the fact that _this can hold anything, i only use it to access member variables from objects.

Quote:
Where does push _this store the value of _this (on the same stack as everything else)?

Yes. Push operates on stack whatever thing it has to push. It doesn't distinguish between registers, variables and constants.

Quote:
Can you do mov _reg_1, _this; and mov _this,_reg_1;?

Yes. As i said above, _this is just another general purpose register.

Quote:
What is your procedure calling convention?

Parameters are passed from left to right, and the called function is responsible for cleaning the stack (or more correctly, the vm when a ret instruction is encountered). So it looks like __stdcall with reversed parameter passing.
I haven't really thought of passing parameters from right to left (as __stdcall does). I think, it doesn't really matter if the called function keeps parameter indexes in correct order.

Quote:
Moving this-change directly in front of the call doesn't change the number of stack-moves, does it?

Altering _this register by moving something to it (line 8) doesn't change the stack. But pushing _this on the stack, does change it. I'll try to explain the problem better.

deffer:
Quote:
So the only reason you're going with AST is optimization, is that correct?

With AST (Abstract Syntax Tree) i mean the parser output. I'm not doing any AST level optimizations (which i should, btw).

Quote:
If you've decided to make _this register the only one (besides _stack) that can do memory accesses, then you're stuck with constantly swapping _this. If you really want do do optimizations, make other registers able to do such magic.

No. Every register (as long as it holds a reference to an object) can do memory accesses. So _reg_1[someMember] is possible as long as _reg_1 holds an object.

The problem isn't how things work at assembly level. If i was writing scripts in assembly, then everything would be fine. The problem is how to generate assembly code from the high level representation of the statement.
A member function call looks like this:

object.member1.member2.function(argument1, argument2, ...);


where arguments can be function calls, constants, objects and members.

The way i'm handling the above piece of code now, is this:

1) I have an identifier followed by period ('.'). Check to see if it's an object. It is.
1.1) In case to be able to do anything on it (access its members) i have to move it somewhere. Move it to _this, so i know where to search for its members.
1.2) After this step, previous _this is on the stack, and _this now holds 'object'.
2) Check to see if the identifier following the period is a function or a variable. If it is a variable go to 1. Else go to 3.
3) We reached the actual function call. For every argument, calculate the expression using the stack and leave the result on the stack.
4) At this point the stack has arguments pushed, _this pointer points to member2 (the last variable), so call the function.
5) pop all object from the stack so _this returns to its initial value.

If i understood what you suggested correctly, i have to first find the actual function call, assemble all expressions (function arguments) and then change _this to point to member2?
Is this correct?

Thanks again for the replies.

HellRaiZer
HellRaiZer
Quote:Original post by HellRaiZer
The way i'm handling the above piece of code now, is this:

1) I have an identifier followed by period ('.'). Check to see if it's an object. It is.
1.1) In case to be able to do anything on it (access its members) i have to move it somewhere. Move it to _this, so i know where to search for its members.


'.' is only an expression saying: add an offset to given address. It can be treated same as '+'.
obj.memb1.memb2.memb3
is nothing more than
obj + offset1 + offset2 + offset3

Quote:Original post by HellRaiZer
1.2) After this step, previous _this is on the stack, and _this now holds 'object'.


Remove that completely.

Quote:Original post by HellRaiZer
2) Check to see if the identifier following the period is a function or a variable. If it is a variable go to 1. Else go to 3.


And here (at function call) you can finally begin thinking about the computed address as an 'object'.

Quote:Original post by HellRaiZer
3) We reached the actual function call. For every argument, calculate the expression using the stack and leave the result on the stack.


Remember, that in following sample:
obj1.memb1[expr1].func1( expr2, expr3 )
expressions are:
expr1, expr2, expr3
Don't forget about the expr1!

Quote:Original post by HellRaiZer
4) At this point the stack has arguments pushed, _this pointer points to member2 (the last variable), so call the function.


And here the point you should start calcuating the actual 'this' pointer, at the last possible moment.


Quote:Original post by HellRaiZer
If i understood what you suggested correctly, i have to first find the actual function call, assemble all expressions (function arguments) and then change _this to point to member2?
Is this correct?


Not quite. It is best shown on a "sample":
obj1.memb1.memb2[expr1].func1( expr2, expr3 )

1. calculate and push on the stack:
expr3, expr2, expr1 (in that order).
Watch, that the whole "sample" is a perfectly good expression, so expr2, for example should be calculated with exactly the same strategy.
2. calculate 'member operator' ('.')
(obj1).memb1
3. calculate 'member operator' ('.')
(obj1.memb1).memb2
4. calculate 'element operator' ('[]') using expr1 (yes, we know where it is, since we calculated it last)
(obj1.memb1.memb2)[expr1]
5. call the function (you can move the result of previous calculation to _this pointer if you like, or just push it on the stack, just like c++ does)


So, first all the sub-expressions (recursively, in (reversed) order that you'd like to use them later), then the main expression (from left to right, just like you'd calculate "a+b+c+d"). Note that with such strategy, you'll never use "this" pointer before is is necessary to call a function.

Uff, gosh, I know this is messy, I'll check on my old compiler later, to see how I exactly did things.
deffer thanks a lot for the example. It is more clear now.

Quote:
'.' is only an expression saying: add an offset to given address. It can be treated same as '+'.
obj.memb1.memb2.memb3
is nothing more than
obj + offset1 + offset2 + offset3

I think this is true only for structs that hold other object by value. If memb1 was a pointer (despite of the '.' operator; we are talking about scripting here ;) ) then i think i couldn't just add offset2 to obj + offset1 and get the correct address for memb2. Anyway i understood what you were trying to say, so no need to argue or get into the details of the engine. One note on that though. Every object, despite being handled as a reference, it's a pointer. It's just the symbol that is different from C/C++. I always use '.' instead of '->'

About the example. I have to test it now to see what i can get (if i understood it correctly). The only problem right now is that i have to push _this on the stack immediatelly before the call, but this requires only a small change in how i calculate local variable indexes for functions. Reaching the actual object on which i call the function, from a complex chain of objects, can be done with registers (i think) without needing the stack.

I could completely remove _this register from the language, but this requires a lot of changes. Maybe in version 2 :)

Quote:
Uff, gosh, I know this is messy, I'll check on my old compiler later, to see how I exactly did things.

No it's not. It's exactly the explanation i was looking for. Thanks again. :)

HellRaiZer
HellRaiZer
Problem seems to be solved thanks to deffer. Here is a script which with the old code generator would have problems.

struct Matrix3x3{	var Vector3 Row[3];	var Vector3 test;		function void Test(void);};function void Matrix3x3::Test(void){	local Matrix3x3 m;	local int i;	i = 2;	m.Row[0].Set(test.x, Row[1].y, 4.0 * test.z + Row.z);	m.Row[2].w[2] = m.Row[0].Dot(Row[2]);}


After doing what deffer suggested, the assembly output is correct!

function ?Test$void#Global{	local Matrix3x3 m[1] : -2;	local int i[1] : -1;	new _stack[m + 0], "Matrix3x3";			mov _stack, <span class="cpp-number">2</span>; <span class="cpp-comment">// i = 2;</span><br>	<br>	mov _reg_0, _this[?test&amp;Vector3!<span class="cpp-number">1</span>@Matrix3x3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_0 = this.test;</span><br>	mov _reg_2, <span class="cpp-number">4</span>.<span class="cpp-number">000000</span>; <span class="cpp-comment">// _reg_2 = 4.0;</span><br>	fmul _reg_2, _reg_0[?z&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>@Vector3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_2 = 4.0 * this.test.z;</span><br>	push _reg_2; <span class="cpp-comment">// push _reg_2;</span><br>		<br>	mov _reg_0, _stack; <span class="cpp-comment">// _reg_0 = i;</span><br>	mov _reg_1, _this[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_0]; <span class="cpp-comment">// _reg_1 = this.Row<span style="font-weight:bold;">;</span><br>	pop _reg_0; <span class="cpp-comment">// _reg_0 = 4.0 * this.test.z;</span><br>	fadd _reg_0, _reg_1[?z&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>@Vector3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_0 = 4.0 * this.test.z + this.Row<span style="font-weight:bold;">.z;</span><br>	push _reg_0; <span class="cpp-comment">// push 4.0 * this.test.z + this.Row<span style="font-weight:bold;">.z;</span><br>		<br>	mov _reg_1, <span class="cpp-number">1</span>; <span class="cpp-comment">// _reg_1 = 1;</span><br>	mov _reg_2, _this[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_1];	<span class="cpp-comment">// _reg_2 = this.Row[1];</span><br>	push _reg_2[?y&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>@Vector3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// push this.Row[1].y;</span><br>	mov _reg_0, _this[?test&amp;Vector3!<span class="cpp-number">1</span>@Matrix3x3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_0 = this.test;</span><br>	push _reg_0[?x&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>@Vector3 + <span class="cpp-number">0</span>]; <span class="cpp-comment">// push this.test.x;</span><br>	mov _reg_1, _stack[m + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_1 = m;</span><br>	mov _reg_2, <span class="cpp-number">0</span>; <span class="cpp-comment">// _reg_2 = 0;</span><br>	mov _reg_1, _reg_1[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_2]; <span class="cpp-comment">// _reg_1 = m.Row[0];</span><br>		<br>	push _this;<br>	mov _this, _reg_1[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_2];<br>	call ?Set$<span class="cpp-keyword">void</span>&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">1</span>, <span class="cpp-number">1</span>, <span class="cpp-literal">"Vector3"</span>;<br>	pop _this;<br>		<br>	mov _reg_0, <span class="cpp-number">2</span>; <span class="cpp-comment">// _reg_0 = 2;</span><br>	push _this[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_0]; <span class="cpp-comment">// push _this.Row[2];</span><br>		<br>	mov _reg_1, _stack[m + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_1 = m;</span><br>	mov _reg_2, <span class="cpp-number">0</span>; <span class="cpp-comment">// _reg_2 = 0;</span><br>	mov _reg_1, _reg_1[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_2]; <span class="cpp-comment">// _reg_1 = m.Row[0];</span><br>		<br>	push _this;<br>	mov _this, _reg_1[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_2];<br>	call ?Dot$<span class="cpp-keyword">float</span>&amp;Vector3!<span class="cpp-number">1</span>, <span class="cpp-number">1</span>, <span class="cpp-literal">"Vector3"</span>;<br>	pop _this;<br>		<br>	mov _reg_0, _stack[m + <span class="cpp-number">0</span>]; <span class="cpp-comment">// _reg_0 = m;</span><br>	mov _reg_1, <span class="cpp-number">2</span>; <span class="cpp-comment">// _reg_1 = 2;</span><br>	mov _reg_2, _reg_0[?Row&amp;Vector3!<span class="cpp-number">3</span>@Matrix3x3 + _reg_1]; <span class="cpp-comment">// _Reg_2 = m.Row[2];</span><br>	mov _reg_0, <span class="cpp-number">2</span>; <span class="cpp-comment">// _reg_0 = 2;</span><br>	mov _reg_2[?w&amp;<span class="cpp-keyword">float</span>!<span class="cpp-number">4</span>@Vector3 + _reg_0], _ret_val; <span class="cpp-comment">// m.Row[2].w[2] = m.Row[0].Dot(this.Row[2]);</span><br>	ret ;<br>}<br><br></pre></div><!–ENDSCRIPT–><br><br>Thanks a lot deffer :) <br>Any ideas what else i should try in case to see if it is really working?<br><br>HellRaiZer
HellRaiZer

This topic is closed to new replies.

Advertisement