Forthcoming changes to the X++ language
High level programming languages and their compilers solve many problems for developers who want to provide value to a particular business domain.
For one thing, compilers raise the abstraction level from the nitty-gritty details of the hardware and software systems running the code (like storage locations and memory allocation) to something more manageable (like tables representing customers etc). Another value proposition is to recognize as many errors or other problems in the code as possible; the more problems that are diagnosed this way (i.e. at compile time), the fewer problems end up as expensive and embarrassing bugs at runtime, i.e. when the product is deployed at the customer site. The X++ compiler has built-in diagnostics for hundreds of error situations, and the MorphX environment features a pluggable system (the best practice rules) that you can use to express rules that are important to you. We ship a large set of these best practice rules out of the box. Many of these compensate for problems that could just as easily be diagnosed by the X++ compiler itself; not using these seems ill advised.
We are continually looking at what we can do to make the value proposition of the X++ language stronger. We have identified that several changes are needed to avoid a number of runtime problems, and to make future alignment with managed languages possible. As always, we realize that any changes we make to the core language are a mixed blessing to people who maintain a large code base: On one hand the new semantics cause the compiler to catch more errors earlier, but on the other hand they will invariably cause changes to existing code.
In the next release of the Dynamics Ax product we will make several changes to the core X++ language, and even though the release of this is certainly not imminent it makes good sense for the development community to be aware of the changes that are in the pipeline. There is no reason to make things worse by writing new code that does not comply with the new, stricter rules.
The changes are described below. This list may not be conclusive: We will probably identify more such issues in the coming months.
Covariance vs. Contravariance
Currently, X++ allows overriding methods to supply signatures (including the return type) containing types that are derived from the types supplied in the defining method: The signatures do not need to list identical types. Consider the case below where the MyMethod method does provides a derived type (Derived) in the override in lieu of the type given in the defining method (Base):
class Base
{
Base MyMethod(Base b) {…}
}
class Derived extends Base
{
void DerivedMethod() {…}
Derived MyMethod(Derived d) { d.DerivedMethod(); }
}
This raises some critical considerations. Consider the very common case involving tables (that are all derived from Common):
class Base
{
public void foo(common b) {…}
}
class Derived extends Base
{
public void foo(SalesTable st)
{
st.CanBeDirectlyInvoiced(); // Calling a method on SalesTable
}
}
When the following is attempted:
{
CustTable ct; // Something other than SalesTable
BaseClass bc = new DerivedClass();
bc.foo(ct); // This method is never called???
}
The foo method is never actually called. We will remedy this by imposing the restriction that an overriding method must have identical parameter types has the method it overrides.
No compilation error is issued when parameters without default arguments follow default arguments.
This case is a simple laxness in the compiler, that currently accepts methods that have parameters with and without default values mixed, as shown below:
public client server RouteOpr routeOpr(
ItemId itemId,
ConfigId configId,
RouteOpr routeOpr = null,
InventSiteId siteId)
{
…
}
The intention is to have the parameters having default values as the last parameters, so the actual parameter values can be omitted in the call. The current behavior of the compiler is to simply ignore the default argument values. In the future the compiler will correctly diagnose the situation, and the application must be changed to comply with the rule that non-default parameters cannot follow default parameters:
public client server RouteOpr routeOpr(
ItemId itemId,
ConfigId configId,
RouteOpr routeOpr,
InventSiteId siteId)
{
…
}
The X++ compiler allows two methods with the same name where one is static and the other not.
X++ is quite happy to have a method by a given name as both an instance method and a static method if the two methods are not within the same class:
class Base
{
void int MyMethod(int i) {…}
}
class Derived extends Base
{
static void MyMethod(str s) {…}
}
This is reasonable in X++ because the two calls are syntactically different: The static call is done using the "::" operator and the instance call is done using the "." operator. However, this situation causes lots of confusion and will be deprecated. The workaround is to simply rename one of the methods, typically the static one.
No visibility rules are enforced for access of overriding methods
In the future, overriding methods must be at least as accessible as the defining method, not less. So, consider:
class Base
{
public void MyMethod() {}
}
class Derived
{
protected void MyMethod() {}
}
It does not make sense to have MyMethod (which is virtual, as all X++ methods) as a protected method in the derived class, because it can always be called from the outside using a reference to Base:
Base b = new Derived();
b.MyMethod(); // calls Derived method, even if marked protected or private.
So
- If a method is marked as private, no overrides are allowed. (as today).
- If a method is marked as protected, the compiler will only accept protected or public overrides.
- If a method is marked as public, the compiler will only accept public overrides.
Abstract methods can be reached through a SUPER() call.
When a derived class overrides an abstract one, the derived implementation may call super(), essentially calling a method that has no body and can return no value. If the method does not return void, the X++ interpreter will complain (at runtime) because the abstract method does not return a value:
class Base
{
abstract int MyMethod() {}
}
class Derived extends a
{
int MyMethod() ( return super(); } // Calling abstract method??
}
However, if the return type had been void, the situation would not have issued any problems at runtime. In the future, the compiler will diagnose this with a compile time error.
Static constrictors are not diagnosed
The X++ compiler currently accepts the static keyword on constructors:
static void new ()
{
info("");
}
This is wrong because a static constructor is never called. The fact that this is not disallowed by the compiler can lead programmers to think that static constructors (which after all is a very meaningful concept in many OO languages) are implemented, which is not the case. There are very few cases of this in the application that we ship, but the compiler will be fixed to diagnose the error with a suitable error message and the application must be fixed up accordingly.
Abstract class implementation.
The semantics for abstract classes in Ax and other modern languages (like C#) are very different. In the C# the first non abstract class is required to implement all the abstract methods defined by the abstract super classes. In X++, there is no such requirement: The only requirement in X++ is that all abstract methods are implemented for any types that are instantiated - This can be done at any derivation level. Any error messages relating to non-implemented abstract methods are issued when a type instantiation is attempted (using the new operator).
In the future the semantics in the X++ compiler will be changed to match the semantics of C#. The changes are as follows:
When a concrete (i.e. a non-abstract) class is encountered, the implementation must:
- Traverse the hierarchy from most basic to most derived maintaining a set of methods: When an abstract method is defined, it is entered into the set. When an abstract method is implemented (in either a concrete class or an abstract class), it is removed from the set. This will identify a set of methods that have not yet been implemented.
- All the methods in the set calculated as described above must be implemented in the current class. If this is not the case, an error message is issued.
All the instances in the application code base where a concrete class does not implement the abstract methods defined in its immediate abstract supertype must be modified.
Interfaces.
The same issue as described above holds for implementing interfaces. Currently it is not required in Ax that all methods defined in an interface be implemented in the class implementing the interface, as it is in C#.
A similar algorithm to what is listed above must be employed to maintain the C# semantics. The interfaces situation is a little more complex because interfaces can also be arranged in hierarchies of their own.
Open array assignments.
X++ features several types of arrays. One type allows the programmer to declare the array without providing an upper bound on the number of elements in the array.
{
int a[];
int b[];
b = a; // No effect
}
This works well for single arrays but assignments involving these values have no effect. The compiler will diagnose this case in the future.
We have prepared a set of best practice checks that diagnose most of these issues. Running these best practices on your code base should give you an idea of how much needs to be changed in your code base. Keep your eyes peeled at his site for these upcoming best practice checks.