Free Web Hosting by Netfirms
Web Hosting by Netfirms | Free Domain Names by Netfirms

Operators Overview
Divider

This section describes the operators available in QDL.

Operator Types

QDL uses standard infix notation, with operator precedence and all that jazz. QDL languages has two "normal" operator types:

In addition, there are four constructs that might be called operators, but which are formatted differently:

Operator Precedence and Associativity

In an expression containing more than one operator, operator precedence determines how arguments are associated with operators. For example, in the expression X + Y * Z, * is higher precedence than +, so it gets first dibs at getting arguments. So it takes Y and Z. Then, + gets the leftover arguments, of which there are two: X, and the result of Y * Z.

A more complicated example: MyClass.AnArray[X + Y]++ (where MyClass is a Class instance that contains an array called AnArray.)

This can be stated concicely using brackets:

((MyClass.AnArray)[(X + Y)])++

Associativity determines the order of evaluation when two operators with the same precedence level are found in the same expression. If associativity is left-to-right, the operators on the left are given arguments first; if associativity is right-to-left, the operators on the right are given arguments first.

In the expression X / Y * Z, associativity is left-to-right. Therefore, it is equivalent to (X / Y) * Z. On the other hand, in the expression X = Y = Z, associativity is right-to-left. Therefore, it is equivalent to X = (Y = Z). If it were not so, the assignment would occur in a counterintuitive fasion: X would be assigned the value of Y before Y is assigned the value of Z.

In the following table, different levels of precedence are separated by a change in color of the table. In other words, two adjacent rows with the same background color have the same precedence level. Earlier entries in the table have higher precedence.

Symbol(s) & Syntax Operator Name Associativity
( x ) Parenthesis N/A
:: x Unary scope resolution (aka global scope) Right-to-left
x :: y Scope Resolution Left-to-right
x . y Dot (Access class member) Left-to-right
x [ y ] Array element access Left-to-right
x ( y ) Pseudo-function use Left-to-right
x ( y ) Function Call Left-to-right
@ x Address-of Right-to-left
* x Dereference (opposite of @) Right-to-left
x ++ Post-increment Left-to-right
x -- Post-decrement Left-to-right
++ x Pre-increment Right-to-left
-- x Pre-decrement Right-to-left
- x Unary minus (reverse numeric sign) Right-to-left
+ x Unary plus (absolute value) Right-to-left
~ x Binary not (invert bits) Right-to-left
! x Logical not Right-to-left
x * y Multiply Left-to-right
x / y Divide Left-to-right
x % y Modulo (Remainder) Left-to-right
x << y Shift Left Left-to-right
x >> y Shift Right Left-to-right
x + y Add (binary plus) Left-to-right
x - y Subtract (binary minus) Left-to-right
x & y Bitwise and Left-to-right
x ^ y Bitwise exclusive-or Left-to-right
x | y Bitwise or Left-to-right
x == y Equal to Left-to-right
x != y Not equal to Left-to-right
x < y Less than Left-to-right
x > y Greater than Left-to-right
x <= y Less than or equal to Left-to-right
x >= y Greater than or equal to Left-to-right
x && y Logical And Left-to-right
x || y Logical Or Left-to-right
x ? y : z Ternary (Conditional) Right-to-left
x = y Assignment Right-to-left
x *= y, x /= y, x %= y,
x <<= y, x >>= y,
x += y, x -= y,
x &= y, x ^= y, x |= y
Compound assignment Right-to-left
x @= y Address assignment Right-to-left
x , y Comma Left-to-right

Note: Despite the precedence of the post-increment and post-decrement operators on the table, the actual increment and decrement operations are always performed after the value of that being incremented/decremented is used in the expression.

Note: The precedence of the shift operators (<<, >>) and the bitwise operators (&, ^, |) is different from C/C++. In both cases, I have done it because these operators perform operations that programmers most often want to be performed first. The shift operators have been placed above addition and subtraction, while the bitwise operators have been placed above relational operators.

Operator/Cast Overloading

QDL allows operators to be overloaded, which means functions can be created that are called with a syntax that is the same as the usage of operators. The types of the arguments to an operator determines what overloaded operator function to call. C++ programmers will feel right at home, since overloading works very similarly in QDL. Usually, when a programmer overloads an operator, one or both of the arguments to the operator is of a programmer-defined Class type.

Not all operators can be overloaded. Those operators whose name field is displayed in italics cannot be overloaded.

In general, you cannot overload an operator with a set of argument types that the compiler has a built-in ability to deal with, without needing to perform a type-cast. For example, you can't overload the addition of two Integers.

The use of operator overloading cannot change the precedence of an operator, or create new operators.

To define an overloaded operator function, use a function signature that has Operator or Cast in place of Function, and, in the case of the form that uses the Operator keyword, the operator characters in place of the function name.

operator-function-signature: [Const | Static | Final | Abstract | Inline | Explicit | tag]* (Operator [member-scope]operator-symbol | Cast [member-scope]) (argument-list) [: return-type-spec]

First, about the form of the function signature that uses the Operator keyword:

The argument-list gives names and types for the arguments to the operator. The argument list may not contain special delimiters or default values. When creating a global operator function, there must be either one or two arguments to the function: one argument for unary operators and two for binary operators. But, when a Operator function is defined as being part of a Class,

A global function that overloads operator x and takes, as its first argument, a value of a Class type y, is legal. However, despite the apparent difference in scope, such a function would conflict with another function, also overloading operator x, which takes one less argument and is a member of Class y.

When overloading a binary operator, the first argument represents the left-hand argument and the second argument represents the right-hand argument.

The operator-symbol is one of the following: [], (), ~, !, ++, --, *, /, %, +, -, <<, >>, &, |, ^, &&, ||, =, *=, /=, %=, +=, -=, <<=, >>=, &=, |=, ^=, New, Delete, @=, , (comma), or . (dot).

By default, an Operator function is reflective, meaning, for most operators, that the order of the arguments to the operator can be reversed, but the Operator function can still be called.  How this works is, where the compiler would otherwise issue an error due to a lack of a function being available, the compiler reverses the arguments and again attempt to find an appropriate Operator function.  The Explicit keyword overrides this behavior, by specifying that the operator is not reflective.  Assignment operators are never reflective, so specifying Explicit for assignment operators is redundant.  When overloading unary operators, reflectivity is meaningless, so the presence of Explicit is an error.  Here are a few illustrative examples:

         Operator + (A: @ ClassA; B: @ ClassB): ClassC;
Explicit Operator - (A: @ ClassA; B: @ ClassB): ClassC;
         Operator = (C: @ ClassC; C: @ ClassC): @ ClassC;
         Operator = (A: @ ClassA; B: Integer): @ ClassA;

. . .

Var X: ClassA;
Var Y: ClassB;
Var Z: ClassC;
Z = X + Y;
Z = Y + X; // Legall; Operator + is reflective
Z = X - Y;
Z = Y - X; // Illegal; Operator - is Explicit
X = 5;
5 = X;     // Illegal; assignment is never reflective.
           // In other words, Operator = is implicitly Explicit.

Reflectivity works a bit differently for the relational operators (>, <, ==, >=, <=, !=).  For the purposes of this discussion, the following pairs are "reflections":

And the following pairs are "opposites":

If an overloaded operator function is not available for a relational operator, the compiler tries to evaluate the binary expression:

  1. First, by reversing the arguments and using the reflected operator.
  2. Second, by using the opposite operator (without reversing the arguments) and applying the ! (unary not) operator to the result.

If neither of these work, an error is issued.  Under these rules, your class can support all the relational operators by overloading one of these sets of three operators:

The form of the statement that uses the Cast keyword overloads the type-casting operator. The return type is what to cast to. As for what to cast,

When a type cast is used, extra arguments can be provided after the type name that specifies the destination type, as described here. When writing a type-cast function, the extra arguments are specified in the Cast function signature. For example, suppose I'm writing a global function to convert from Class MyClass to String. If my declaration is:

Enum Ways { Short, Long };
Cast (M: @ MyClass "[Format]" W: Ways(Short)): String;

If I have a MyClass object called M, Then I can use the type cast in code using something like

Var S: String;
S = Cast (M: String Format Long);

The Explicit keyword has a different meaning when applied to Cast functions. It means that the type cast can only be performed explicitly—the compiler will never use it automatically. Of course, if any non-default arguments are provided, or any non-optional delimiters, the requirement that the cast be used explicitly is implied.

The above example can be used implicitly because its argument and delimiter can be omitted. So, if I have a function MyFunc that takes a String argument, I can use it like this:

MyFunc M;

A Cast function may not have delimiter(s) before the first argument, or a default value for the first argument.

When the compiler finds that the function requires a String, it will look for a cast function to convert from MyClass to String. It will find that the above Cast function can do it, and will call it implicitly with the default argument Long for W. If you do not want the compiler to do this, you can use the Explicit keyword when declaring the function.

With the exception of the overloaded dot operator, all overloaded Operator and Cast functions are in the global namespace, even if they are within a Class. Operator and Cast functions cannot be defined within another Function.

Here's an example using Operator functions:

Class MyClass {
	Var X: Integer;
	Inline Operator *(): Integer { Return X * X; }
};
Operator + (X: Integer; MyClass: MyClass @): Integer
{
	Return X + MyClass.X;
}
Function Main()
{
	Var M: MyClass;
	M.X = 10;
	StdOut.Print *M, '\t';
	StdOut.Print 5 + M;
}
Output: 100	15

The overloading of some operators requires some extra explanation:

Dot Operator

Overloading of the dot (.) operator is a very special case. It is intended to replace the void left by the fact that QDL doesn't have the -> operator that C++ does. An Operator . function must be located in a Class (which I'll call A), and must not take any arguments. It must return a value or a reference, which I'll call B. Basic usage is as follows: when access is made (using the dot operator) to the class in which the Operator . function is located, the compiler first attempts to access the member as if it were in A. If this doesn't work, B is searched for the same member.

The compiler will access B even if the dot operator is not explicitly used, making Operator . a much more powerful feature than it would be otherwise. Essentially, Operator . in Class A returning B causes the scopes of B to be merged with the scopes of A, with A taking precedent when there is any conflict. The implications of this include:

Operator . may not return a reference to or copy of an instance of the class in which that Operator . is located. It is legal for an Operator . in A to return a B@, when B contains an Operator . returning A@; however, said Operator . function in B will not be accessible through an object of type A, unless that object is first type-casted to B or B@.

Unlike all other functions, there may be multiple Operator . functions in a single class with the same signature, provided that the return values are different. Assume that a class C has two Operator . functions; the first returns B@ and the second A@.When there are multiple Operator . functions, those that are declared first in the class declaration take precedent. So, if the right-hand-side of Operator . on an instance of C is D, instances of classes are searched for members called D in this order: C, B, classes returned from Operator . functions in B, A, classes returned from Operator . functions in A. Note that no error is issued if more than one class contains a D. The search for D is made without regard to access modes, the arguments of D (if any), or even whether D is a variable or a function.

Unlike all other Operator functions, An Operator . may not be defined outside a class declaration.

An Operator . function must return a class type (or a reference to a class.) It cannot return an array type.

An Operator . function (in a class C) can be thought of as a "last resort" by the compiler, in that it is used only if there is no way for an operation involving C to be completed without calling the function. Furthermore, if any non-typecast and non-operator function or variable X is accessed, the compiler will not search the class returned by Operator . if X can be found in C. This is true even if an error is caused by treating X as if it were in C. For example:

Class C:
	Operator .: B @;
	X: Integer;
	Operator + (N: Boolean): String;
	Operator - (N: Function @): String;
End Class;
Class B:
	Function X ("The Function");
	Operator + (N: String): String;
	Operator - (N: Integer): String;
End Class;
Function Main:
	C: C;
	StdOut.Print C - 10;
		// Cannot use C::Operator -, so B::Operator - is used
	StdOut.Print C + "Hello";
		// "Hello" can be converted to Boolean, so compiler uses
		// C::Operator + even though B::Operator - might be better
	C.X The Function;  // Compiler stops searching at C::X
		// And subsequently generates a syntax error.
End;

Operator . is only part of the way QDL supports "smart references".  To make the support complete, a special keyword Reference was added to the language and is used while defining a Class, as described later.

@= (Change address)

This operator represents assignment to the address of the class. I stuffed it into the language, in conjunction with Operator ., for just one reason: smart references. You see, I read a book called C++ for Real Programmers by Jeff Alger, and there is a chapter dedicated to smart pointers - classes that are used like pointers (by overloading -> and =), but aren't. I realized that QDL had no complete equivalent, and while I've only actually overloaded -> once in an actual project, the idea has many potential uses. Indeed, some of the most useful features of QDL (in the standard library) are made possible because of smart references.

First I figured the overloading of @ might be sufficient. For example:

Class ReferenceTo:
	Template type: Spec;
	Class Spec:
	End Class;
Private:
	Template (type) Class Address of {
		Var x: @ type;
		Inline Operator = (NewRef: type @) { @x = @NewRef; }
	}
	Var Address: Address of type;
Public:
	Inline Operator @ (): @ Address of type  { Return Address; }
	Inline Operator . (): @ type { Return Address.x; }
	Inline Constructor() { @Address.x = Null; }
	Inline Constructor(Addr: @ type) { @Address.x = @Addr; }
	Inline TypeCast (): @ type; { return Address.x; }
}
Function MyFunc
{
	Var X: Integer;
	Var XRef: ReferenceTo Integer;

	@XRef = X;
	X = 5;
	StdOut.Print Cast (XRef: Integer);
}

Unfortunately, there are at least two serious problems with this approach:

Operator @=, in combination with the addition of the Reference keyword to the language, solves these problems, reduces the size of the class, and eliminates the need for two classes. For example:

Class ReferenceTo:
	Reference Type;
Private:
	Address: @ Type (Null);
Public:
	Inline Operator @ () Returns @ Type: Return Address; End;
	Inline Operator . () Returns @ Type: Return Address; End;
	Inline Operator @= (NewAddr: @ Type) Returns @ ReferenceTo Type:
		@Address = @NewAddr; Return This; End;
	Inline Constructor (Addr: @ Type): @Address = @Addr; End;
End Class;
Function MyFunc:
	X: Integer;
	XRef: ReferenceTo Integer;

	@XRef = @X;
	X = 5;
	StdOut.Print XRef;
End;

The fact that QDL variables cannot be of a pointer type would seem to pose a problem for both Operator @ and Operator @=. The former must return a pointer, and the latter must take a pointer as an argument. The solutions provided by the language are as follows:

New and Delete

New must take an Int32 as its first argument, and return @. The first argument specifies the amount of memory that needs to be allocated, and the return value is the address of the newly created block.

Delete must take @ as its first argument; no restriction is placed on the return value. The first argument specifies the address of the block to be deleted (in a properly running program, this will be the address that was returned earlier by New.)

Both New and Delete may have additional arguments and delimiters, but the type of the first argument is fixed and there may be no delimiters before the first argument.

For the following discussion, assume New and Delete are used with a class C.

When allocating single objects, the compiler passes the value of SizeOf (C) to New. It then calls the constructor for C using the returned reference for This, and converts the reference to C * and returns that pointer to the program.

When allocating an array of objects, it's a different matter. Let N be the number of objects.  The compiler passes the value of SizeOf (C) * N + SizeOf (Int32) to New. The first four bytes of the returned memory block are assigned N by the compiler, and the rest of the block is used to store the elements of the array. It calls the constructor for C for each of the array elements, converts the reference to C [] *, adds four bytes to the pointer, and returns that pointer to the program.

When allocating a multi-dimensional array, the minor dimensions are multiplied in to calculate N.  For example,

Var X: @ [][2][3] Integer;
@X = New ([4][2][3] Integer);

In this case, N is 24, so the integer located before @X in memory is set to 24.

When deallocating objects, one of two actions is taken depending on whether the type of the argument (which I'll call A) is a reference/pointer to an array or to a single object.  If A refers to a single object, the destructor for the object is called and the compiler passes A (as a reference) to Delete.  Otherwise, the compiler retrieves the bytes before the array to determine the size of the array, then calls the destructor for each of the objects in the array, before calling Delete with a reference to four bytes before A.

Promotion Rules

Many binary operators require that their arguments be of the same type. The promotion rules determine to what type each argument should be converted ("promoted"). It also determines the type of the result of the operation. If both of the arguments are of the same type, no promotion is done (the types remain the same.)  The two values and the operator in question may be referred to as a "binary expression", not to be confused with boolean and bitwise operations and expressions.  In the case of Enums, the promotion rules may be applied to two or more integral types during derivation, even though no operator is involved.

The promotion rules apply only to the built-in types listed below.  In a binary expression in which one of the types matches none of those listed below, the promotion rules do not apply.  The promotion rules also do not apply to binary expressions where both values are constants, as defined in the sections on character and integral constants.

The promotion rules assign a "precision" to each data type.  In the following list, types are listed in order from lowest to highest precision:

Boolean, Int8, UInt8 and Char8, Int16, UInt16 and Char16, Int32, UInt32, Int64, UInt64, String

Types joined with "and" have the same precision.  The Char, Integer, and UInteger types may vary in size and so are not at a fixed location in the list.  They do fit logically in the list somewhere, though; for example, if Char is 16-bit, Integer is 32-bit and UInteger is 32-bit, the list would look like this:

Boolean, Int8, UInt8 and Char8, Int16, UInt16 and Char16 and Char, Int32 and Integer, UInt32 and UInteger, Int64, UInt64, String

When two different integer types are used together:

  1. If one type is higher precision than the other, the lower-precision value should be converted to the higher-precision types.
    Note: if the any conversion is made from signed to unsigned, the compiler should issue a warning about the mismatch. Exception: a warning need not be issued if the conversion is being made on a positive integer constant.
    If the precisions are the same:
  2. If there is a character/integer mismatch, the character should be promoted to the integer type.
  3. If one of the types is Integer or UInteger, the other value should be promoted. 

When one argument is Single and the other Double, the Single is converted to a Double.

When one argument is floating-point and the other integral, the integer is converted to the type of the floating-point value.

When one argument is Boolean and the other is floating-point or integral, the Boolean is converted to the type of the other argument.

When one argument is String and the other is Boolean, floating-point or integral, the other argument is converted to a String.

The compiler can implicitly "downcast" (i.e. reduce precision or ability to hold information) from one of these types to another, e.g. for function arguments or assignments. The compiler should issue a waning if any of these conversions have to be made implicitly:

Operator Ambiguity

The following operators are repeated to make another operator:

+ - & | > < :

When strings of two or more of one of these characters (an "opstring") are found in code, the meaning is not always obvious. How the compiler interprets opstrings is implementation-dependant. In determining meaning, a compiler may use the context in which the string is found in whatever way the compiler-maker sees fit. To make sure there is no ambiguity, the programmer should separate adjacent operators with spaces.

Table of Contents Qwertie's Site/Mirror
Next
Previous