|
|
|
In most languages, normal identifier rules apply to the naming of types. This is not the case in QDL; because of the limited contexts in which they are used, QDL type names can be more elaborate. Type names can consist of "tokens", where a token is:
The following cannot be used as part of a type name:
Here are some examples of valid and invalid type names:
Hello // Valid, one token !! // Valid, two tokens <G> // Valid, three tokens [Food/Drink] // Valid, four tokens *Read My Lips* // Valid, five tokens [5] // Invalid, contains a number :^) // Invalid, contains illegal symbols / * Um-hmmm * / // Invalid, contains /* A Cast // Invalid, contains reserved word
When the compiler is interpreting the meaning of a type-spec, it does it "greedily". That is, it doesn't stop at the least number of tokens required to identify a type-name, but prefers to find the longest matching type name. For example, suppose you have a type named My Type Name and another named My. If you declare a variable like this:
X: My Type Name;
The compiler will recognize My Type Name rather than just the first My.
This rarely becomes important, but here are two examples.
In normal expressions, a type name is only ever used as the left-hand side argument to the scope resolution operator. Therefore, the compiler will automatically recognize a type name if it is on the left of this operator:
X = MyTypeName::ClassMember;
If the type name does not follow the rules for normal identifiers (it has multiple tokens or punctuation characters), you must put the type name in brackets, like so:
X = (My $&#% type name)::ClassMember;
The scope resolution operator can optionally be used within these brackets, but one must be left outside the brackets, on the right:
X = (::First Level::Second Level::Third Level)::ClassMember;
Brackets used for this purpose cannot be nested:
X = ((::First Level::Second Level)::Third Level)::ClassMember; // Illegal!
You can put a function signature where a type-identifier is expected, as defined here. Here is an example:
FuncRef: @ Function (String): Integer;
This creates a variable that is a reference to a non-Static function that takes one argument, a string, and returns an integer. You cannot define a variable of a function type; this, for example, is invalid:
FuncRef: Function (String) Returns Integer; // ?!
To assign a function to the reference variable, you must use the @Function pseudo-function on the function you wish to store in the reference. For example:
Function Example1 (SomeString: String) Returns Integer; Function Example2 (SomeString: String) Returns Integer;
FuncRef: @ Function (S: String) Returns Integer (@ Function (ExampleFunction1)); // Right FuncRef = ExampleFunction2; // Wrong @FuncRef = @ExampleFunction2; // Wrong @FuncRef = @Function (ExampleFunction2); // Right
The reference variable can be used as if it were a function:
FuncRef "Hello";
N = FuncRef ("Hello");
A default return value cannot be provided in the function signature when defining a reference variable, because the opening bracket that would otherwise define the default return value is assumed to define a default value for the reference. There are still some contexts in which a function signature can include a default return value; however, except in function-statements, the default return value is irrelevant to the compiler.
Also, the names of the arguments and of the function in the reference variable's declaration are irrellevant. Therefore, the following declaration would have been equivalent:
FuncRef: @ Function Geronimo (Yahoo: String) Returns Integer;
A function reference can have default arguments and special delimiters. For example:
Function Line (X1, Y1, X2, Y2: Integer);
FuncRef: @ Function (X1, Y1: Integer(-1) "To" X2, Y2: Integer) (@Function (Line)); FuncRef To 100, 100; // Equivalent to: Line -1, -1, 100, 100;
As this example shows, the reference's default arguments and special delimiters do not have to match those of the function assigned to the reference. That's because compatibility between function references is determined by the function protocol, not the signature. Thus, although FuncRef above is compatible with Line, both of the following Var statements are illegal:
Function MyFunc (X, Y: Integer(0)): String;
FuncRef2: @ Function (X: Integer(0)) Returns String (@ Function (MyFunc)); FuncRef3: @ Function (X, Y: Integer(0)) Returns Char (@ Function (MyFunc));
Enumeration types, defined using the Enum statement, are integral types that use identifiers to represent the values they contain. An enumeration is defined using the following syntax:
enum-statement: [Explicit] Enum name-resolved-typeidentifier
[Inherits type-identifier] { enum-ident-list
}
enum-ident-list: <identifier [= const-int-expr]>
[,]
For example:
Enum Fruits Inherits Int8 {
Apple, Grape, Orange = 6, Tangerine
};
If no number is specified for the first identifier in the enumeration, it is assigned zero. All other identifiers that are not assigned a value are set to one more than the value of the previous identifier. In this example, then, the values given to the identifiers are 0, 1, 6, and 7, respectively.
The type name after the Inherits keyword specifies the integer type of identifiers in the enumeration, and of variables of the enum type. It can be any of the numeric types listed in this section. It can also be the name of another enumeration, in which case the enumeration inherits all the members of the specified enumeration and the integral type of that enumeration. An Enum type can inherit from multiple other Enum types; in this case, the promotion rules are applied to the base integral types to determine the type of the derived Enum type. When using this "multiple inheritance", a name collision between two Enums, where two identically-named identifiers have different values, will cause the compiler to generate an error.
Unlike in C++, the identifiers in an enumeration are not accessible from the enclosing scope. For example, the identifiers in a global Enum type are not accessible anywhere. Except in contexts where access to the enum identifiers is implied, qualification must be used to access the identifiers. For example:
Enum My Enum { Hi, Bye };
Var X: Integer;
X = Hi; // Wrong
X = (My Enum)::Bye; // Right
The Explicit keyword specifies that the enumerated type is incompatible with the built-in types. Normally, the compiler is allowed to implicitly convert values of an Enum type to and from integral types, floating-point types, String and Boolean. Explicit disables this behaviour, so that values of the Enum type cannot be converted to and from these types, or other Enum types, without a cast. There is one exception: if the Enum type Inherits another Enum type, implicit conversions can be made to that type and any indirect base types.
All the operations that can be performed on an integral value can be performed on an enumeration. For Enum types not declared as Explicit, in an operation involving an Enum type and an integral type, the Enum is treated as if it were an integer for the purpose of applying the promotion rules. If the Enum is from the same type of integer, the Enum is implicitly converted to the integral type.
Evaluation of an expression involving an Enum value is performed slightly differently from all other expressions. The compiler is designed to detect contexts in which access to the enumerated identifiers is appropriate, which requires some special logic. For example:
Enum My Enum { Hi, Bye };
Var X: My Enum;
X = Hi; // Legal
Since X is of the My Enum type, the compiler automatically recognises Hi as being a member of My Enum without qualification. You may be wondering how this is possible. Well, when the compiler recognizes a value as having an Enum type, the other component of any binary operator* applied to the value will implicitly have access to the identifiers in the Enum, as if a With statement enclosed the expression (even though this, per se, is not syntactically possible.) In the above example, the binary operator is =. Since X is recognized as being of the type My Enum, the other argument to operator = (Hi) is interpreted as a member of My Enum, if possible. This can work recursively and even backwards; for example, both of the following lines are also legal:
If Hi + Bye + X Then ; // Backwards X = (Hi * (Bye + Bye)); // Recursive
* This rule does not apply to the dot (.), scope resolution (::), or comma (,) operators, and (of course) it does not apply to the ternary operator (? :).
Lest you start thinking that the enum identifiers are available anywhere in the expression, though, realize that both of the following are illegal:
Var AnArray: [10] Integer;
Var AnInt: Integer;
X = AnArray[Hi]; // Illegal! Subscript is not given access to My Enum
X = AnArray[(My Enum)::Hi]; // Okay
AnInt = (X == Hi) + Bye; // Illegal! Since X == Hi results in a Boolean value,
// Bye is not accessible outside the brackets.
AnInt = (X == Hi) + (My Enum)::Bye; // Okay
The name of a variable of an Enum type, or a reference thereto, cannot match one of the identifiers in the Enum. If an enumerated identifier matches an identifier available in the context of the expression, the enumerated identifier overrides that identifier. The compiler should therefore issue a warning if the identifier in question is used in the expression. If two types of Enum values are used in the same expression, a compile-time error may be issued if (and only if) identifiers in the two enumerations collide with different values.
As described here, a similar automatic-enum-identifier-access thing goes on when an Enum type is required as a function argument. Access to the enumerated identifiers is available to the entire expression used to compose the function argument, except for arguments to other functions (not including the Cast pseudo-function), and array subscripts.
When arguments to a binary operator involve
Then the promotion rules are used to convert one operator argument to the type of the other, with Enum types treated (for the purposes of the promotion rules) as their equivalent integer type. In case (1) above, the integral type is favored in an expression involving the Enum type and the equivalent integer type. In case (2), if both Enum values are based on the same integral type:
The compiler may issue a warning about the confusion whenever two Enum types are used together.
Here are some examples of Enum usage:
Enum A { A=1, B=2, C=3, D=4 }
Enum B { D=4, E=5, F=6, G=7 }
Explicit Enum C Inhertis A, B { G=7, H=8 };
// Identifiers may be duplicated provided their values are the same
Enum D { Q };
Enum E { R };
Cast (EVar: E): C { // Cast to convert implicitly from E to C
// Return Cast (EVar: C);
// The above statement will compile, but it is wrong because it will
// result in this function being called recursively.
Return Cast (Cast (EVar: Integer): C);
// An intermediate type must be used
}
Function Main {
Var A: A; // Illegal, "A" is a member of Enum A; namespace conflict
Var AVar: A; // Okay
Var BVar: B;
Var CVar: C;
AVar = 10 + B;
BVar = G - C + AVar;
CVar = 10 + H; // Illegal, C is Explicit so 10 is incompatible
CVar = Cast (10: C) + H; // Okay
CVar = AVar + BVar; // Legal, A and B are base types
CVar = D::Q; // Illegal, C is Explicit so D::Q is incompatible
CVar = E::R; // Legal, there is a Cast function for E --> C
}
The Const keyword is short for "constant", and is used in several different contexts. In all cases, it is used by the compiler to ensure that a one or more variables are not changed. All the checking to make sure of this is done at compile-time, so no run-time overhead is incurred by using Const.
When Const is placed before a data type, it specifies that the data is constant. For example:
ConstVar: Const Integer (5);
This specifies that ConstVar may not be changed after its initialization. Thus, a statement such as the following would be illegal:
ConstVar = 6;
The compiler can assume that a Const variable initialized to a constant value always has that value, and thus save time by using a constant rather than looking up the variable's value. This applies whether the variable is static or not:
Class X: X: Const Integer (1); End Class; Y: Const Integer (2); Function Main: XInstance: X; Z: Const Integer (3); A: Integer (XInstance.X + Y + Z); End;
The compiler can determine at compile-time that A should be set to 6.
Const is often used to make sure a function doesn't modify its arguments, or to make sure a reference in a class is not changed. Moving right along now, Const may have different meanings depending on the particulars of the context. For example:
Var I, J: Integer; Var C1: Const @ Integer (I); Var C2: @ Const Integer (I); Var C3: Const @ Const Integer (I);
C1 is a constant reference to an Integer, C2 is a reference to an Integer constant, and C3 is a constant reference to an Integer constant. In other words, the Integer to which C1 refers can be changed, but C1 cannot be changed to refer to a different Integer. C2 is the opposite of this; it can be changed to point to another integer, but whichever integer is pointed to cannot be changed. So:
C1 = 5; // Legal @C1 = @J; // Illegal C2 = 5; // Illegal @C2 = @J; // Legal C3 = 5; // Illegal @C3 = @J; // Illegal
Const can be applied to function arguments and functions:
Const Function F (A: @ Integer; B: @ Const Integer);
Const can only be applied to functions in Classes, so assume that F is a member of a class called C. Because F is Const, it is not allowed to modify any of the members of C. However, this does not mean it cannot modify other objects, global variables, or its own arguments. Thus, it can modify A, but it cannot modify B because B is Const as well.
In order to ensure that a Const function does not modify the object on which it was called, the compiler does not allow the function to call other functions that are allowed to modify the object (except through aliasing.)
Const data types cannot be implicitly converted to non-const data types, but non-const types can be implicitly converted to Const types.
The Const keyword is not foolproof in ensuring that a variable or reference is not modified. The programmer can cancel Const status using a Cast, or call a function that indirectly modifies the Const variable. For example:
I: Integer (1);
Function A: I = 2; End; Function B (V: @ Const Integer): A; End; // Variable changed indirectly Function C (V: @ Const Integer): Cast (V: Integer @) = 3; End; // Change can be forced with a Cast Function Main: B(I); C(I); End;
Despite the fact that B and C take references to constants, the variable I in question still ends up being changed.
| Table of Contents | Qwertie's Site/Mirror |