Recently there were a question on Yammer that prompted me to do this little writeup about how the inner functions work in X++. The original problem was brought to us by Ievgen Miroshnikov. He provided this sample:
public static void main(Args _args)
{
void test(int _a)
{
info(int2Str(_a));
int i = 42;
info(int2Str(_a));
info(int2Str(i));
}
int i = 1;
test(i);
}
and asked why the sample printed (1, 1, 1) instead of (1, 1, 42) as he expected. In other words: Why does the declaration of i in the test function (i.e., the inner function) not introduce a new i, given that the scope is different? Surely the i declared in the main level and the inner function are different things, mapped to different storage locations?
We will answer that question at the end of this post, just to keep you on your toes for a while longer. To understand what is going on you need to understand how we generate IL code for code like this. There is no concept of nested functions in MSIL, so we have to implement the functions as methods that are defined in a special scope in a special IL class created at compile-time for that purpose. The semantics of inner functions is such that the inner function has in scope itself (to allow for recursive functions) and everything it declares, but also everything that is declared before it in the surrounding scope. This is what is known as a closure in Nerdish. We had to keep this in Ax when we transitioned to IL code, to keep things running from older versions. Inner functions have now become available in C# as well, by the way.
For the following discussion, please consider
class MyClass
{
int MyMethod(int p1)
{
int v1, v2, ... vn;
void f1()
{
// f1, p1, and v1, ... vn are in scope here.
}
str s1, ... sm;
void f2()
{
// f1, f2, p1, v1, ... vn and s1, .. sm are in scope here.
}
// f1, f2, p1, v1, ... vn and s1, .. sm are in scope here.
}
}
Let us know take a look at what the X++ compiler actually generated for this:
As you can see, we generate a special class (with a name, <>c_DisplayClass so that it cannot clash with anything a developer could have written). This is the scope for the locals in the method as you can see in lines 35 to 40 above. In lines 6 to 11 we use an instance of that class to reference the variables - They are not in the scope of the method anymore. The method hosting the inner functions is also implemented on that class and called in line 12.
If we now go back to the sample that Ievgen gave us we need to dig into the IL to see what the problem is. The 42 is assigned to a variable in the scope of the method, not to the variables that are hoisted to the class state. The debugger will show two different i values. So yes, there is a code generation problem here, and I am surprised that it has not been reported by anyone yet.
Incidentally, if you declare the int variable before the inner scope in the test function, like this:
public static void main(Args _args)
{
int i = 1;
void test(int _a)
{
info(int2Str(_a));
i = 42;
info(int2Str(_a));
info(int2Str(i));
}
test(i);
}
everything works fine. Also, if you try to reuse the name defined in the outer scope inside the method you will get the expected error:
public static void main(Args _args)
{
int i = 1;
void test(int _a)
{
int i = 42;
}
...
}
will generate the error:
Severity |
Description |
Line |
Error |
A local variable named 'i' cannot be declared in this scope because it would give a different meaning to 'i', which is already used in a parent or current scope to denote something else. |
14 |
We will fix this when we have determined whether this is a breaking change or not.
Thanks for bringing this to our attention. We can only fix things if we know about them, and this was a case that had escaped us.
*This post is locked for comments