Contents
Index
Summary
Citation
Doornik
Ox Syntax Reference
Contents:
- Lexical conventions
- Tokens
- Comment
- Identifiers
- Keywords
- Constants
- Integer constants
- Character constants
- Double constants
- Matrix constants
- String constants
- Array constants
- Objects
- Types
- Type conversion
- Lvalue
- Scope
- External declarations
- Enumerations
- Specifiers
- External variable declarations
- Function declarations
- Function definitions
- Variable length argument list
- Classes
- Member function definitions
- Constructor and destructor functions
- The this pointer and member scope
- Static members
- Derived classes
- Virtual functions
- Statements
- Selection statements
- Iteration statements
- Jump statements
- Declaration statements
- Expressions
- Primary expressions
- Postfix expressions
- Member reference
- Function calls
- Explicit type conversion
- Indexing vector and array types
- Postfix incrementation
- Transpose
- Unary expressions
- Prefix incrementation
- Unary minus and plus
- Logical negation
- Address operator
- New and delete
- Power expressions
- Multiplicative expressions
- Additive expressions
- Concatenation expressions
- Relational expressions
- Equality expressions
- Logical dot-AND expressions
- Logical-AND expressions
- Logical dot-OR expressions
- Logical-OR expressions
- Conditional expression
- Assignment expressions
- Comma expression
- Constant expressions
- Preprocessing
- File inclusion
- Conditional compilation
- Pragmas
- Difference with ANSI C and C++
Tables:
- Escape sequences
- Operator precedence
- Result from dot operators
- Result from relational operators
Lexical conventions
Tokens
Comment
Anything between /* and */ is considered comment.
This comment can be nested (unlike C and C++).
Everything following // up to the end of the line is also
comment, but is ignored inside /* ... */ type comment.
So nested comment is possible:
one = cons + 1; // comment
/* two = cons + 1; // comment
*/
Identifiers
Identifiers are made up of letters and digits. The first character
must be a letter. Underscores (_) count as a letter.
The maximum length of an identifier is 60 characters, additional characters
are ignored.
Keywords
The following keywords are reserved:
array decl extern new this
break default for operator virtual
case delete goto return while
char do if short
class double inline static
const else int string
continue enum matrix switch
Constants
Integer constants
A sequence of digits is an integer constant. A hexadecimal constant is
a sequence of digits and the letters A to F or a to f,
prefixed by 0x or 0X.
Character constants
Character constants are interpreted as an integer constant.
A character constant is an integer constant consisting of
a single character enclosed in single quotes
(e.g. 'a' and '0') or an escape sequence enclosed in single quotes.
Table syn.1: Escape-sequence
\" double quote
\' single quote
\0 null character
\\ backslash
\a alert (bel)
\b backspace
\f formfeed
\n newline
\r carriage return
\t horizontal tab
\v vertical tab
\xhh hexadecimal number hh
So '\n' is the integer constant corresponding to the newline
character.
Double constants
A double constant consists of an integer part, a decimal point, a fraction
part, an e, E, d or D and an
optionally signed integer exponent. Either the
integer or the fraction part may be missing (not both); either the decimal
point or the full exponent may be missing (not both). A hexadecimal
double constant is written as 0x.hhhhhhhhhhhhhhhh. The
format used is an 8 byte IEEE real. The hexadecimal string is written
with the most significant byte first (the sign and exponent are on the left).
If any hexadecimal digits are missing,
the string is left padded with 0's.
Double constants in an external declaration may use
a dot to represent a missing values. This sets the variable to
NaN (Not a Number).
Matrix constants
A matrix constant lists within < and > the elements of the matrix,
row by row. Each row is delimited by a semicolon, successive elements
in a row are separated by a comma.
For example:
< 00, 01, 02; 10, 11, 12 >
< 0.0, 0.1, 0.2 >
< 1100 >
which are respectively a 2 by 3 matrix, a 1 by 3 matrix and
a 1 by 1 matrix, the first constant is:
00 01 02
10 11 12
The index of each row is one higher than the previous row.
Within each row, the column index of an element is one higher than
that created with the previous element in the same row.
An integer range may be specified, e.g. 2:5 corresponds to 2,3,4,5.
The range may decrease, so that 5.3:2.8 corresponds to
5.3,4.3,3.3.
A stepsize may be specified, as in 2:[2]8, which gives 2,4,6,8.
A specific element in the matrix can be set. This overrides the location
implicit in the position of the element in the matrix constant.
Note that the top left element is [0][0], the second element in
the first row [0][1], etc.
A number of identical elements can be specified, e.g. [3]*0
corresponds to 0,0,0. Unspecified elements are set to zero.
As an example involving all types, consider:
< [4]*1,2; 10,11,14-2; 1:4; [3][4]=99,2; 8:[-3]2 >
which corresponds to:
1 1 1 1 2 0
10 11 12 0 0 0
1 2 3 4 0 0
0 0 0 0 99 2
8 5 2 0 0 0
Missing values in a matrix constant could be represented with a dot,
which represents NaN (Not a Number), e.g.:
< .,2,3; 4,.,6 >
Further examples are given in
external declarations.
String constants
A string constant is a text enclosed in double quotes,
for example: "tailor". Adjacent string
constants are concatenated. A null character is always appended to
indicate the end of a string. The maximum length of a string constant
is 1024 characters.
Escape sequences can be used to represent
special characters.
Array constants
An array constant is a list of constants in braces, separated by
a comma. This is a recursive definition, because the constant can
itself be an array
constant. The terminating level consists
of non-array constants. Each level of array constants creates an array
of pointers.
For example:
{ "tinker", "tailor", "soldier" }
{{ "tinker", "tailor"}, {"soldier"} }
Objects
Types
Variables in Ox are implicitly typed, and can change type during their
lifetime. The life of a variable corresponds to the level of its
declaration. Its scope is the section of the program in which it can be seen.
Scope and life do not have to coincide.
There are three basic types and four derived types.
- int: a signed integer.
- double: double precision floating point type.
- matrix: a two-dimensional array of doubles which can be manipulated as a whole.
- string
- array: an array of pointers.
- function
- class
- pointer to class object
Type conversion
When a double is converted to an int, the fractional part is discarded;
if the resulting value cannot be represented, the behaviour is undefined.
When an int is converted to a double, the nearest representation will
be used. For example, conversion to int of 1.3 and 1.7 will be 1 on both
occasions.
A single element of a string (a character) is of type int.
An int or double can be assigned to a string element, which first results
in conversion to int, and then to a single byte character.
Also see explicit type conversion.
Scope
Variables declared at the start of a statement block have scope and life
restricted to the block. These variables are called
automatic:
they are created and initialized whenever the block is entered, and removed
as soon as the block is exited. Variables declared outside any statement
block have global scope and life; these are called static.
Note that Ox assignment of arithmetic types and string type
implies copying over
the contents from the right-hand side to the left-hand side.
Automatic variables of any type can
be assigned to variables with broader scope.
External declarations
An Ox program consists of a sequence of external declarations. These either
reserve storage for an object, or serve to inform of the existence of
objects created elsewhere. Each program must define one function called
main, where execution of the program will start.
Enumerations
An enumeration defines a list of integer constants.
By default,
the first member will have value 0, and each successive member will
have a value of one plus that of the previous member. The value of
a member can be set by assigning it a constant integer value.
Enumerator names only exist in the file in which they occur.
Enumerations should be placed in header files if they need to be shared
between several source files.
Here are some examples with corresponding values:
enum { C_FIRST, C_SECOND, C_THIRD }; // 0,1,2
enum { T_INT, T_DBL=2, T_STR, T_MAT=C_THIRD }; // 0,2,3,2
enum { FLAG0,FLAG1, FLAG2=FLAG1*2, FLAG3=FLAG2*2};//0,1,2,4
enum { T_ERR = 1.0 } ; // error
Specifiers
The static specifier restricts the scope of the declared object to
the remainder of the file. Although it will exist throughout the
program's life, it cannot be seen from other files.
In classes, the static keyword
is used with a different meaning.
The extern specifier informs the remainder of the file
that the object can be accessed, although defined (created) in
another file.
The extern and static specifiers are mutually exclusive.
External declarations are most conveniently placed in header files.
External variable declarations
The static or extern specifier and the const qualifier
preceding an external variable declaration list applies to all variables
in the list. Each identifier creates space for an object with
global lifetime, unless declared extern or const.
A const object must be initialized (unless declared extern)
but its value may not be changed thereafter. Unless declared extern,
a const object cannot be accessed from other files.
If of scalar type, a const can
appear in a constant-expression.
At the external level of declarations, as treated here, it is possible
to specify a matrix size, and initialize that matrix to zero.
If an external variable is created without explicit value and without
dimensions, it will
default to an int with value 0. Here are some examples:
decl a, b; // default to type int, value 0
enum { AAP, NOOT, MIES, WIM };
const decl ia = NOOT, ib = NOOT + WIM; // type: int
const decl ma = < NOOT, AAP; 0, 1 >; // type: matrix
const decl aa = {"tinker", "tailor"}; // type: array
decl id = ia * (WIM - 1) * MIES + ib; // type: int
decl da = ia + 0.; // type: double
decl mb = <0:3; 4:7; 8:11>; // type: matrix
decl ab = { ma, ma}; // type: array
extern decl elsewhere; // defined in other file
decl mc[3][3] = 1.5; // 3 x 3 matrix with values 1.5
decl md[2][1]; // 3 x 1 matrix of zeros
enum { ZUS = id }; // error: id is not const
decl ih = id; // error: id is not const
decl ia; // error: already defined
Function declarations
A function declaration communicates the number of arguments and their types
to a file, so that the function can be called correctly from that file.
The actual creation of the function is done through a function
definition (which at the same time declares the function).
A function can be declared many times, but type and number of arguments must
always be identical:
test0(); // function takes no arguments
test1(const a1); // one const argument
test2(const a2, a3); // two arguments, first is const
static test3(a1); // cannot be used outside this file
extern test4(a1); // function defined outside this file
print(a1, ...); // variable number of arguments
test1(a1); // error: previous declaration was different
A second form, which uses extern string-constant,
provides dynamic linking of extension functions
(which could be written in C, Pascal, etc.). This feature is not
available on all platforms, and implementation
is platform dependent.
In the following example, test5
corresponds to the external function MyCFunc(), located in the
dynamic library mydll. If this feature is supported, mydll will
be automatically loaded, and the function imported.
extern "mydll,MyCFunc" test5(a1);
Function definitions
A function definition specifies the function header and body, and declares
the function so that it can be used in the remainder of the file.
A function can be declared many times, but
defined only once.
Arguments declared const cannot be changed inside
the function. If the argument is a const pointer, the pointer cannot
be changed, but what it points to can.
An empty argument list indicates that the function takes no arguments at all.
The ... indicates a variable
number of arguments; it must have the last position in the header, but
cannot be the first.
test1(const a1); // declaration of test1
print(a1, ...); // variable number of arguments
test2(const a1, a2) // definition of test2
{
test1(a2); // call function test1
print(a1, 1, 2, "\n"); // at least one argument
test1(a2, 1); // error: wrong number of arguments
a2 = 1; // a2 may be changed
a1 = 1; // error: a1 is const
/* ... */
}
All function arguments are passed by value. This means that a copy
of the actual object is made.
For int, double, matrix and string types
the whole object is copied.
Any changes to the copy are lost as
soon as the function returns.
Derived types are accessed
through a pointer, and that pointer is passed by value. However, what
is pointed to may be changed, and that change will remain in effect after
function return. So passing pointers allows a function to
make a permanent change to a variable, for examples see
function calls.
It is good practice to label an argument const
if a function doesn't change the variable. This increases program clarity
and enables the compiler to generate more efficient code.
All functions may have a return value, but this return value need not
be used by the caller. If a function does not return a value, its
actual return value is undefined.
Variable length argument list
A special library function va_arglist() is used to access
arguments in the variable argument list. It returns the
arguments supplied for the ellipse as an array.
An example illustrates:
test(const a, ...)
{
decl i, args = va_arglist();
for (i = 0; i < sizeof(args); i++)
print (" vararg ", i, ": ", args[i]);
}
main()
{
test("tinker", "tailor", "soldier");
}
which prints vararg 0: tailor vararg 1: soldier.
Classes
A class is a collection of data objects combined with functions operating on
those objects. Access to data members from outside the class
is through member functions:
only member functions can access data directly. All data members are private,
and all function members public, using C++ parlance.
Consider a simple line class,
which supports drawing lines from the current cursor position to the next,
and moving the cursor:
class Line // Line is the class name
{
decl x, y; // two data members
const decl origin; // const data member
static decl cLines; // static data member
Line(const orig); // constructor
moveto(const x, const y); // move cursor
lineto(const x, const y); // draw line and move cursor
static getcLines(); // static function
static setcLines(c); // static function
}; // don't forget the ;
All member names within a class must be unique. A class declaration
introduces a type, and can be shared between source files through
inclusion in header files. Ox accesses an object through a pointer
to the object which is created using the new operator.
Data members cannot be initialized in the
class, but can in the constructor function.
Member function definitions
A member function provides access to data members of an object.
It is defined as its class name, followed by :: and
the function name. The function name must have been declared in the
class. Member functions cannot be declared outside a class; the
class declaration contains the member function declaration.
Only a member function can use data members of its own class
directly.
Here are the definitions of the member functions of class Line:
Line::Line(const orig)
{
x = y = orig; // set cursor at the origin
origin = orig; // only allowed in constructor
cLines++; // count number of Line objects
}
Line::moveto(const ax, const ay)
{
x = ax; y = ay;
print("moved to ", ax, " ", ay, "\n");
return this;
}
Line::lineto(const ax, const ay)
{
// draw the line from (x,y) to (ax,ay) ...
x = ax; y = ay;
print("line to ", ax, " ", ay, "\n");
return this;
}
The new operator creates an object of the specified class,
calls the constructor function, and returns a pointer to it.
A member function is called through a member reference.
For example:
lineobj = new Line(0); // create object and
// set cursor to (0,0)
lineobj->lineto(10, 10); // draw line to (10, 10)
lineobj->Line::lineto(10, 10); // same call
lineobj::lineto(10, 10); // error, needs ->
Since lineobj is of class Line, both calls to lineto
are to the same function. The only difference is one of efficiency.
Ox has implicit typing, so can only know the class of lineobj
at run time. In the second case the class is specified, and the
function address can be resolved at compile time.
Constructor and destructor functions
The member function with the same name as the class is called the
constructor, and is automatically invoked when creating an object
of the class. If the constructor function is absent, a default constructor
function will be assumed which takes no arguments. A constructor may
not be static.
A constructor always returns a pointer to the object for which it was
called and may not specify a return value.
Only the constructor function may set const data members.
In the Line class, the origin is only set during construction,
and not thereafter. However, each Line object has its own
origin (unless origin is made static).
A destructor is called after a request to delete an object, and before
the object is actually removed. It may be used to clear up any
allocated objects inside the object to be deleted. A destructor function
has the same name as the class, is prefixed by ~, and may neither
take arguments, nor return a value. It
does however receive the this pointer.
class Line
{ /* ... */
Line(const orig); // constructor
~Line(); // destructor
/* ... */
};
test()
{
decl lineobj;
lineobj = new Line(0);//create object, call constructor
delete lineobj; // call destructor, delete object
}
The this pointer and member scope
All non-static member functions receive a hidden argument called this,
which points to the object for which the function is called.
So the constructor function Line obtains in this
a pointer to the newly created object. The assignment to x and
y refer to the members of the this object.
When accessing a variable in a member function, it is determined
first whether the function is a local variable or an argument.
Next it is considered as a member of this.
If all these fail, it is considered as a global variable.
So local variables and arguments hide members, together these hide
global variables. The following example shows how the scope
resolution operator :: may be used to resolve
conflicts:
decl x, y; // global variables
extern moveto(x, y); // external function
Line::moveto(const x, const y)
{
::x = x; // assign arguments to global variables
::y = y;
this->x = x; // assign arguments to data members
this->y = y;
::moveto(x, y); // call non-member function
moveto(x, y); // error: call to itself will
} // cause infinite loop
Static members
There is only one copy
of a static member, shared by all objects of a class.
A static member may not have the same name as the class it is in.
Derived classes
A class may derive from a previously declared class. A derived
class will inherit all members from its base class, and
can access these inherited members as its own members.
However, if the derived class has members with the same
name as members of the base class, the former take precedence.
In this way, a
class can redefine functionality of its base class.
If a function is redefined, the base class name followed by
:: may be used to refer to the base class function.
Deriving from the Line class:
class Angle : Line // Line is the base class
{
Angle(); // constructor
lineto(const x, const y); // draw dash, move cursor
};
Angle::Angle()
{
Line(0); // starts at zero
}
Angle::lineto(const ax, const ay)
{
Line::lineto(ax, y); // horizontal line
Line::lineto(ax, ay); // vertical line
print("is angle to ", ax, " ", ay, "\n");
moveto(ax, ay);
}
Angle's constructor just calls the base class constructor,
as the body may be read as this->Line(0);.
Note that the base class constructor and destructor functions
are not called automatically (unlike in C++).
In the new
lineto object, Line::lineto is used to make sure that we
call the correct function (otherwise it would make a recursive call).
For the moveto that is no problem, moveto calls the base
function, as it was not redefined in the Angle class.
Non-static member functions may be declared as virtual (that is, they can
be redefined by a derived class), this is discussed in the next section.
New classes may be derived from a class which is itself derived, but Ox
only supports single inheritance: a class can only derive from one other
class at a time.
Virtual functions
Virtual functions allow a derived class to
supply a new version of the virtual function in the
derived class, replacing the version of the base class.
When the base class calls the virtual function, it will actually
use the function of the derived class.
For a virtual function, the call can only be resolved at run time.
Then, the object type is known, and the called function is the one
first found in the object, when moving from the highest class towards
the base class.
Statements
The executable part of a program consists of a sequence of statements.
Expression statements are expressions or function calls. It can be
a do-nothing expression, as in:
for (i = 0; i < 10; i++)
;
A compound statement groups statements together in a block, e.g.:
for (i = 0; i < 10; i++)
{
a = test(b);
b = b + 10;
}
A statement can be prefixed by a label as in:
:L001
for (i = 0; i < 10; i++)
;
Labels are the targets of goto
statements; labels are local
to a function and have a separate name space (which
means that variables and labels may have the same name).
Note that labels are defined in a non-standard way: the colon
is prefixed, rather than suffixed as in C or C++.
Selection statements
The conditional expression in an if statement is evaluated, and if it is
nonzero (for a matrix: no element is zero), the
statement is executed.
The conditional expression may not be a declaration statement.
Some examples for the if statement:
if (i == 0)
i++; // do only if i equals 0
if (i >= 0)
i = 1; // do only if i >= 0
else
i = 0; // set negative i to 0
if (i == 0)
if (k > 0)
j = 1; // do only if i != 0 and k > 0
else // this else matches the inner if
j = -1; // do only if i != 0 and k <= 0
if (i == 0)
{ if (k > 0)
j = 1; // do only if i != 0 and k > 0
}
else // this else matches the outer if
j = -1; // do only if i != 0
Each else part matches the closest previous if, but this can
be changed by using braces. When coding nested ifs, it is advisable
to use braces to make the program more readable and avoid potential mistakes.
Further examples involving matrices are given in equality expressions.
Iteration statements
The while statement excutes the substatement as long as the test
expression is nonzero (for a matrix: at least one element is nonzero).
The test is performed before the substatement is executed.
The do statement excutes the substatement, then repeats this
as long as the test expression is nonzero
(for a matrix: at least one element is nonzero).
The test is performed after the substatement is executed.
So for the do
statement the substatement is executed one or more times, whereas
for the while statement this is zero or more times.
The for expression:
for (init_expr; test_expr ; increment_expr) statement
corresponds to:
- init_expr;
- while (test_expr )
- {
- statement
- increment_expr;
- }
Note that, unlike C++, the init_expr can not be preceded
by a declaration.
Jump statements
A continue statement may only appear within an iteration statement
and causes control to pass to the loop-continuation portion of the smallest
enclosing iteration statement.
The use of goto should be kept to a minimum, but could be useful
to jump out of a nested loop, jump to the end of a routine or
when converting Fortran code. It is always possible to rewrite
the code such that no gotos are required.
A break statement may only appear within an iteration statement
and terminates the smallest enclosing iteration statement.
Two examples:
for (i = 0; i < 10; i++)
{
if (test1(i))
continue;
test2(); // only done if test1(i) returns 0
}
for (i = 0; i < 10; i++)
{
if (test1(i) == 0)
break; // jump out of loop if test1(i) returns 0
test2();
}
Declaration statements
Declarations at the external level were discussed before.
Here we treat declaration within a block.
Declaration statements create a variable for further manipulation
as long as it stays within scope. The created object is removed as
soon as the block in which it was created is exited.
Variables can be intitialized in a declaration statement.
Variables in Ox are implicitly typed, and their type can change
during program execution. Non-externally declared variables
must be initialized before they can be used in an expression.
It is not possible to specify matrix dimension as can be done
at the external level, so instead of decl ma[3][3] = 1.5
write decl ma = constant(1.5,3,3).
Unlike C, declaration statements do not have to occur at the start of
a block. Consider for example:
test1(arg0)
{
decl k, a = arg0;
decl ident = <1, 0; 0, 1>;
decl identsq = ident * ident;
print("test\n");
decl i, j;
for (i = 0; i < 10; i++)
{
test2(i);
test3(j); // error: j has no value
}
Variables declared in an inner block hide variables in the
outer block.
Expressions
Table syn.2: Operator precedence
Category operators associativity
primary () :: left to right
postfix -> () [] ++ -- ' left to right
unary ++ -- + - ! & new delete right to left
power ^ .^ left to right
multiplicative ** * .* / ./ left to right
additive + - left to right
horizontal concat. ~ left to right
vertical concat. | left to right
relational < > <= >= < > <= >= left to right
equality == != .== .!= left to right
logical dot-and .&& left to right
logical-and && left to right
logical dot-or .|| left to right
logical-or || left to right
conditional ? : .? .: right to left
assignment = *= /= += -= ~= |= right to left
comma , left to right
Table syn.2 gives a summary if the operators available in Ox,
together with their precedence (in order of decreasing precedence)
and associativity. The precedence is in decreasing order. Operators
on the same line have the same precedence, in which case the associativity
gives the order of the operators. Note that the order of
evaluation of expressions is not fully specified. In:
i = a() + b();
it is unknown whether a or b is called first.
Subsections below give a
more comprehensive discussion. Several operators require an
lvalue, which is a region of memory to which an assignment can
be made. Note that an object which was declared const is
not an lvalue. Many operators require operands of arithmetic
type, that is int, double or matrix.
The most common operators are dot-operators
(operating element-by-element) and relational operators
(operating element by element, but returning a single boolean value).
The resulting value is given Tables syn.3 and syn.4
respectively. In addition, there are special matrix operations,
such as matrix multiplication and division; the result from
these operators is explained below.
Table syn.3: Result from dot operators
left a op right b result computes
int op int int a op b
int/double op double double a op b
double op int/double double a op b
int/double op matrix m x n matrix m x n a op b_{ij}
matrix m x n op int/double matrix m x n a_{ij} op b
matrix m x n op matrix m x n matrix m x n a_{ij} op b_{ij}
matrix m x n op matrix m x 1 matrix m x n a_{ij} op b_{i0}
matrix m x n op matrix 1 x n matrix m x n a_{ij} op b_{0j}
matrix m x 1 op matrix m x n matrix m x n a_{i0} op b_{ij}
matrix 1 x n op matrix m x n matrix m x n a_{0j} op b_{ij}
matrix m x 1 op matrix 1 x n matrix m x n a_{i0} op b_{0j}
matrix 1 x n op matrix m x 1 matrix m x n a_{0j} op b_{i0}
matrix m x n op matrix 1 x 1 matrix m x n a_{ij} op b_{00}
matrix 1 x 1 op matrix m x n matrix m x n a_{00} op b_{ij}
Table syn.4: Result from relational operators
left a op right b result computes
int op int int a op b
int/double op double int a op b
double op int/double int a op b
int/double op matrix m x n int a op b_{ij}
matrix m x n op int/double int a_{ij} op b
matrix m x n op matrix m x n int a_{ij} op b_{ij}
matrix m x n op matrix m x 1 int a_{ij} op b_{i0}
matrix m x n op matrix 1 x n int a_{ij} op b_{0j}
matrix m x 1 op matrix m x n int a_{i0} op b_{ij}
matrix 1 x n op matrix m x n int a_{0j} op b_{ij}
string op string int a op b
Primary expressions
An expression in parenthesis is a primary expression. Its main
use is to change the order of evaluation, or clarify the
expression.
All types of constants
form a primary expression.
The operator :: followed by an identifier references
a variable declared externally.
Examples are given.
A class name followed by :: and a
function member of that class references
a static function
member, or any function member if preceded by an object pointer.
The this pointer is only available inside non-static
class member functions, and points to the object for which the
function was called.
Postfix expressions
Member reference
The -> operator selects a member from an object pointer.
The left-hand expression must evaluate to a pointer to an
object, the right-hand expression must result in a member of
that object. See classes.
Function calls
A function call is a postfix expression consisting of the function
name, followed in parenthesis by a possibly empty, comma-separated
list of assignment expressions.
All argument passing is by value, but when an array is passed, its
contents may be changed by the function (unless they are const).
The order of evaluation of the arguments is unspecified;
all arguments are evaluated
before the function is entered. Recursive function calls are allowed.
A function must be declared before it can be called, and the number of
arguments in the call must coincide with the number in the
declaration, unless the declaration
has ... as the last argument.
Some examples:
func1(a0, a1, a2, a3)
{ print("func1(", a0, ",", a1, ",", a2, ",", a3, ")\n");
}
func2()
{ return 0;
}
func3(a0)
{ a0[0] = 1;
}
test1()
{ decl a, b;
a = 1;
func1(a, b = 10, func2(), a != 0); // func1(1,10,0,1)
a = func2(); // a = 0
func3(&a); // a = 1
func3(a); // error
}
In the latter example a will have been changed by func3.
Function arguments are passed by giving the name of the function:
func4(a0, a1)
{ a1(a0); // make function call
}
func5(a0)
{ print("func5(", a0, ")\n");
}
test2()
{ decl a = func5;
func4(1, func5); // prints "func5(1)"
func4(1, a); // prints "func5(1)"
func4(1, func5(a)); // error: requires function
func4(1, func2); // error: func2 takes incorrect
} // number of arguments
Note that the parentheses in func5() indicate that it is a function call,
whereas lack of brackets just passes the function itself.
Explicit type conversion
Explicit type conversion has the same syntax as a function call,
using types int, double, matrix and string:
int double matrix string
v=0; v=0.6; v=<0.6,1>; v="tinker";
matrix(v) <0> <0.6> v <97>
double(v) 0.0 v 0.6 see below
int(v) v 0 0 97
Use double("tinker") to store the string in a double value. Since a
double is 8 bytes, the string is truncated at 8 characters (or padded
by null characters).
Conversely, string(dbl) extracts the string from a double value
dbl (a null character is automatically appended).
Storing strings in doubles or matrix elements is better avoided:
more flexibility is offered by the string type.
Indexing vector and array types
Vector types (that is, string or matrix) and array types are
indexed by postfixing square brackets.
A matrix must always have
two indexes, a string only one. For an array type it depends on the
level of indirection.
Note that indexing always
starts at zero.
So a 2 by 3 matrix has elements:
[0][0] [0][1] [0][2]
[1][0] [1][1] [1][2]
Three ways of indexing are distinguished:
indexing type matrix, string array example
scalar yes yes m[0][0]
matrix yes no m[0][<0,1,2>]
range yes no m[][1:]
In the first indexing case (allowed for all non-scalar types),
the expression inside square brackets must have
scalar type, whereby double is converted to integer.
Vector types may also be indexed by a matrix
or have a range expression inside the brackets.
In a matrix index to a string
the first column of the matrix specifies the selected elements of
the string.
If a matrix is used as an index to a matrix, then each element (row by
row, i.e. the vecr of the argument) is used as an index.
As a consequence, indexing by a column vector or its transpose (a row
vector) have the same effect.
A matrix in the first index selects rows, a matrix in the second index
selects columns. The resulting matrix is the intersection of those rows
and columns.
A range index has the form start-index : end-index.
Either the start-index or the end-index may be missing, which results
in the lower-bound or upper-bound being used respectively.
An empty index selects all elements. The resulting type from a
range or empty index is always a vector type.
Some examples:
decl mat = < 0:3; 10:13 >, d, m;
decl str = "tinkertailor", s;
decl arr = { "tinker", "tailor", "soldier" };
// mat = <0,1,2,3; 10,11,12,13>
d = mat[0][0]; // d = 0
d = mat[1][2]; // d = 12
m = mat[1][]; // m = <10,11,12,13>
m = mat[][2]; // m = <2; 12>
m = mat[][]; // same as: m = mat;
m = mat[0][<1:3>]; // matrix indexes columns: m = <1,2,3>
m = mat[<1,0,1>][<1,3>]; // m = < 11,13; 1,3; 11,13 >
mat[0][1:3] = 9; // range indexes columns:
// mat = <0,9,9,9; 10,11,12,13>
s = str[6:11]; // s = "tailor"
str[6:11] = 'a'; // str = "tinkeraaaaaa"
s = arr[1]; // s = "tailor"
arr[1][0] = 'a'; // arr[1] = "aailor"
Postfix incrementation
A postfix expression followed by ++ or -- leads to the value
of the expression being evaluated and then incremented or
decremented by 1.
The operand must be an lvalue and
must have arithmetic type. For a matrix the operator is applied to
each element separately.
The result of the expression is the value prior to the
increment/decrement operation.
Transpose
The postfix operator ' takes the transpose of a matrix. It has
no effect on other arithmetic types of operands.
Some care is required when using variable
names consisting of one character, in case the expression can be interpreted
as a character constant:
mat = m' * a';
mat = m' a'; // interpreted as m' * a'
mat = m''; // two '' cancel out
mat = m'a'; // error: 'a' is character constant, use m' a'
Unary expressions
Prefix incrementation
A prefix expression preceded by ++ or -- leads to the lvalue
being incremented or decremented by 1. This new value is the
result of the operation. The operand must be an lvalue and
must have arithmetic type.
For a matrix the operator is applied to
each element separately.
Unary minus and plus
The operand of the unary minus operator must have arithmetic type,
and the result is the negative of the operand. For a matrix each element
is set to its negative. Unary plus is ignored.
Logical negation
The operand of the logical negation operator must have arithmetic type,
and the result is 1 if the operand is equal to 0 and 0 otherwise.
For a matrix, logical negation is applied to each element.
j = 0; k = 10;
i = !j; // i = 1
i = !k; // i = 0
Address operator
The operand of the address operator \& must be an lvalue.
In addition, it must be an object: it is possible to take the address
of a class object or a function, but not of an array element, or matrix
element. The result
is an array of one element, pointing to the region of space occupied
by the lvalue.
Ox's limited pointer support works through arrays,
the C indirection operator * is not supported.
Some examples were in the section on
function calls.
New and delete
The new operator can be used to create an object of a class,
or to create a matrix, string or array. The delete operator
removes an object created by new. Note that matrices, strings
and arrays are automatically removed when they go out of scope. Only one
array level at a time can be created by new;
however, delete removes all sublevels.
A string created by new consists of null characters,
a matrix will have all elements zero.
Examples involving objects of classes are given.
Power expressions
The operands of the power operator must have arithmetic type,
and the result is given in the table. If the first operand is
not a matrix .^ and ^ are the same.
left a operator right b result computes
int ^ .^ int or double int a^b
int/double ^ .^ double double a^b
double ^ .^ int/double double a^b
int/double ^ .^ matrix m x n matrix m x n a^{b_{ij}}
matrix m x n .^ int/double matrix m x n a_{ij}^b
matrix m x n .^ matrix m x n matrix m x n a_{ij}^{b_{ij}}
matrix m x m ^ int/double matrix m x m a^b
When a and b are integers, then a ^ b is an integer if
b >= 0 and if the result can be represented as a 32 bit signed integer.
If b < 0 and a != 0 or the integer result would lead to overflow,
the return type is double, giving the outcome of the floating point
power operation.
The second line in the example shows that unary
minus has higher precedence
than the power operator:
i = 1 - 2 ^ 3; // i = -7
i = 1 - - 2 ^ 3; // i = 9
decl r, m1 = <1,2; 2,1>, m2 = <2,3; 3,2>;
r = m1 .^ 3; // <1,8; 8,1>
r = m1 .^ 3.7; // <1,12.996; 12.996,1>
r = 3 .^ m1; // <3,9; 9,3>
r = 3 ^ m1; // <3,9; 9,3>
r = m1 .^ m2; // <1,8; 8,1>
r = m1 ^ 3; // <13,14; 14,13>
r = m1 ^ 3.7; // <13,14; 14,13>
r = m1 ^ -3; // equivalent to: r = (1 / m1) ^ 3;
r = m1 ^ m2; // error
Multiplicative expressions
The operators **, *, .*, /, and ./
group left-to-right and require operands of arithmetic type.
These operators conform to Table syn.3, except for:
left a operator right b result computes
matrix m x n * matrix n x p matrix m x p a_{i.}b_{.k}
matrix m x n ** matrix p x q matrix mn x nq a_{ij}b
matrix m x n / matrix p x n matrix p x m a_{i.}b_{.k}^{+}
int/double / matrix m x n matrix n x m ab_{ij}^{+}
int/double / ./ int/double double a/b
This implies that * ** are the same as .* when
one or both arguments are scalar, and similarly for / and ./
when the right-hand operand is not a matrix.
Kronecker product is denoted by **. If neither operand is a matrix, this
is identical to normal multiplication.
The binary * operator denotes multiplication.
If both operands are a matrix, this is matrix multiplication and
the number of columns of the first operand has to be identical to the number
of rows of the second operand.
The .* operator defines element by element multiplication. It is only
different from * if both operands are a matrix (these must have
identical dimensions).
The product of two integers remains an integer. This means that
overflow could occur (when it would not occur in operations
where one of the argument is a double).
The binary / operator denotes division. If the second operand is a
matrix, this is identical to post-multiplication by the inverse
(if the matrix is square the matrix is inverted using the
invert() library function;
if that fails, or the matrix is non-square, the generalized inverse is
used.
The ./ operator defines element by element division. If either
argument is not a matrix, this is identical to normal division.
It is only
different from / if both operands are a matrix (these must have
identical dimensions).
Note that / does not support integer division (such as e.g. 3 / 2
resulting in 1). In Ox, the result of dividing
two integers is a double (3 / 2 gives 1.5). Integer division can be
performed using the idiv library function.
The remainder operator (% in C and C++)
is supported through the library function imod.
Multiplication of two integers returns an integer.
Some examples of multiplication and division involving matrices:
decl m1 = <1,2; 2,1>, m2 = <2,3; 3,2>;
r = m1 * 2.; // <2,4; 4,2>
r = 2. * m2; // <4,6; 6,4>
r = m1 * m2; // <8,7; 7,8>
r = m1 .* m2; // <2,6; 6,2>
r = m1 .* <2,3>; // <2,6;6,2>
r = m1 ** m2; // <2,3,4,6; 3,2,6,4; 4,6,2,3; 6,4,3,2>
r = 2 / 3; // 0.666667
r = 2 / 3.; // 0.666667
r = m1 / 2.; // <0.5,1; 1,0.5>
r = m1 ./ <2;3>; // <0.5,1; 0.666667,0.333333>
r = 2./ m2; // <0.8,1.2; 1.2,-0.8>
r = 2 ./ m2; // <1,0.666667; 0.666667,1>
r = m2 / m2; // <1,-2.22045e-016; 0,1>
r = 1/<1;2>; // <0.2,0.4>
r = 1/<1,2>; // <0.2; 0.4>
r = 1/<0,0;0,0>; // <0,0; 0,0>
Notice the difference between 2./ m2 and 2 ./ m2. In the first
case, the dot is interpreted as part of the real number 2.,
whereas in the second case it is part of the ./ dot-division operator.
The white space is used here to change the syntax (as in the example
in transpose); it would be more clear to write the second
case as 2.0 ./ m2.
The same difference applies for dot-multiplication,
but note that 2.0*m2 and 2.0.*m2 give the same result.
Additive expressions
The additive operators + and - are dot-operators,
conforming to Table syn.3.
Both operators group left-to-right.
They respectively return the sum and the difference of the operands, which
must both have arithmetic type.
Matrices must be conformant in both dimensions, and the operator
is applied element by element.
For example:
decl m1 = <1,2; 2,1>, m2 = <2,3; 3,2>;
r = 2 - m2; // <0,-1; -1,0>
r = m1 - m2; // <-1,-1; -1,-1>
Concatenation expressions
left operator right result
int/double ~ int/double matrix 1 x 2
int/double ~ matrix m x n matrix m x (1+n)
matrix m x n ~ int/double matrix m x (n+1)
matrix m x n ~ matrix p x q matrix max(m,p) x (n+q)
int/double | int/double matrix 2 x 1
int/double | matrix m x n matrix (1+m) x n
matrix m x n | int/double matrix (m+1) x n
matrix m x n | matrix p x q matrix (m+p) x max(n,q)
int ~ | string string
string ~ | int string
string ~ | string string
array ~ | array array
If both operands have arithmetic type, the concatenation operators
are used to create a larger matrix out of the operands. If both
operands are scalar the result is a row vector (for ~) or
a column vector (for |). If one operand is scalar, and the
other a matrix, an extra column (~) or row (|) is
pre/appended. If both operands are a matrix, the matrices are joined.
Note that the dimensions need not match: missing elements are set to
zero. Horizontal concatenation has higher precedence than vertical
concatenation.
Two strings or an integer and a string can be concatenated, resulting
in a longer string. Both horizontal and vertical concatenation yield
the same result.
The result is most easily demonstrated by examples:
print(1 ~ 2 ~ 3 | 4 ~ 5 ~ 6); // <1,2,3; 4,5,6>
print("tinker" ~ '&' ~ "tailor" ); // "tinker&tailor"
print(<1,0; 0,1> ~ 2); // <1,0,2; 0,1,2>
print(2 | <1,0; 0,1>); // <2,2; 1,0; 0,1>
print(<2> ~ <1,0; 0,1>); // <2,1,0; 0,0,1>
The first two lines could have been written as:
print(<1,2,3; 4,5,6>);
print("tinker" "&" "tailor" );
In the latter case, the matrix and string are created at
compile time, whereas in the former case this is done at run time.
Clearly, the compile time evaluation is more efficient. However,
only the concatenation expressions can involve non-constant variables:
decl i1 = 1, i2 = 2, s1 = "tinke";
print(i1 ~ i2); // <1,2>
print(s1 ~ 'r'); // "tinker"
Array concatenation results in an array with combined size,
with assignment of each member of both arrays to the new array.
decl i, a1 = {"tinker", "tailor"}, a2 = {"soldier"};
a1 ~= a2;
for (i = 0; i < sizeof(a1); i++)
print(a1[i]);
prints tinkertailersoldier.
Relational expressions
The relational operators are <, <=, >, >=,
standing for `less',
`less or equal', `greater', `greater or equal'. They all yield 0 if
the specified relation is false, and 1 if it is true. The type of the
result is always an integer, see Table syn.4.
If both operands are a matrix the return value is true if the relation
holds for each element.
where it is false. If one of the
operands is of scalar-type, and the other of matrix-type, each element
in the matrix is compared to the scalar, and the result is true
if each comparison is true.
The dot relational operators are .<, .<=, .>, .>=,
standing for `dot less',
`dot less or equal', `dot greater', `dot greater or equal'.
They conform to Table syn.3, except when both arguments are a string,
in which case the result is as for the non-dotted versions.
If both arguments are scalar, the result type inherits the higher type,
so 1 >= 1.5 yields a double with value 0.0.
If both operands are a matrix the return value is
a matrix with a 1 in each position where the relation is true and zero
where it is false. If one of the
operands is of scalar-type, and the other of matrix-type, each element
in the matrix is compared to the scalar returning a matrix with 1 at each
position where the relation holds.
String-type operands can be compared in a similar way. If both operands
are a string, the results is int with value 1 or 0,
depending on the case sensitive string comparison.
Examples are given in the next section.
Equality expressions
The == (is equal to), != (is not equal to),
.== (is dot equal to) and .!= (is not dot equal to)
are analogous to the relational operators, but have lower precedence.
The non-dotted versions conform to Table syn.4.
The dotted versions conform to Table syn.3, except when both arguments are a string,
in which case the result is as for the non-dotted versions.
For example:
decl m1 = <1,2; 2,1>, m2 = <2,3; 3,2>, s1 = "tinke";
print(m1 == 1); // 0
print(m1 != 1); // 0
print(!(m1 == 1)); // 1
print(m1 > m2); // 0
print(m1 < m2); // 1
print(s1 <= "tinker"); // 1
print(s1 <= "tink" ); // 0
print(s1 == "tinker"); // 0
print(s1 >= "tinker"); // 0
print(s1 == "Tinke"); // 0
print(m1 .== 1); // <1,0; 0,1>
print(m1 .!= 1); // <0,1; 1,0>
print(m1 .> m2); // <0,0; 0,0>
print(m1 .< m2); // <1,1; 1,1>
The non-dotted versions only return true if the relation holds for each
element. In the first two examples neither m1 == 1 nor m1 != 1
is true for each element, hence the return value 0. The third example
shows how to test if a matrix is not equal to a value. The parenthesis
are necessary, because ! has higher precedence than ==, and
!m1 == 1 results in <0,0; 0,0> == 1 which is false.
The last four examples use dot-relational expressions, resulting
in a matrix of zeros and ones. In if statements, it
is possible to use such matrices.
Remember that a matrix is true if all elements are true (i.e. no
element is zero).
The any library function evaluates to
TRUE if any element is TRUE, e.g.
evaluates to leads to
if (any(m1 .== 1)) if (any(<1,0;0,1>)) if part
if (any(m1 .!= 1)) if (any(<0,1;1,0>)) if part
if (m1 == 1) if (0) else part
if (m1 != 1) if (0) else part
Logical dot-AND expressions
The .&& operator returns 1 if both of its
operands compare unequal to 0, 0 otherwise. Both operands must have
arithmetic type. Handling of matrix-type is as for dot-relational operators:
if one or both operands is a matrix, the result is a matrix of zeros and ones.
Unlike the non-dotted version, both operands will always be executed.
For example, in the expression func1() .&& func2()
the second function is called, regardless of the return value of func1().
Logical-AND expressions
The && operator returns the integer 1 if both of its
operands compare unequal to 0, and the integer 0 otherwise.
Both operands must have arithmetic type. First the left operand is evaluated,
if it is false (for a matrix: there is at least one zero element),
the result is false, and the right operand will not be evaluated. So in
the expression func1() && func2() the second function will not
be called if the first function returned false.
Logical dot-OR expressions
The .|| operator returns 1 if either of its
operands compares unequal to 0, 0 otherwise. Both operands must have
arithmetic type. Handling of matrix-type is as for dot-relational operators:
if one or both operands is a matrix, the result is a matrix of zeros and ones.
Unlike the non-dotted version, both operands will always be executed.
For example, in the expression func1() .|| func2()
the second function is called, regardless of the return value of func1().
Logical-OR expressions
The || operator returns the integer 1 if
either of its operands compares unequal to 0, integer value 0 otherwise.
Both operands must have arithmetic type. First the left operand is evaluated,
it it is true (for a matrix: no element is zero),
the result is true, and the right operand will not be evaluated. So in
the expression func1() .|| func2() the second function will not
be called if the first function returned true.
Conditional expression
Both the conditional and the dot-conditional expression
are ternary expressions. For the conditional expression, the first expression
(before the ?) is evaluated. If it is unequal to 0, the result is the
second expression, otherwise the third expression.
The dot-conditional expression only differs from the conditional
expression if the first expression evaluates to a matrix,
here called the test matrix.
In that case the result is a matrix of the same size as the test matrix,
and the test matrix can be seen as a filter: non zero elements get
a value corresponding to the second expression, zero elements
corresponding to the third expression. If the second or third expression
is scalar, each matrix element will get the appropriate scalar value.
If it is a matrix, the corresponding matrix element will be used, unless
the matrix is too small, in which case the value 0. will be used.
Note that in the dot-conditional expression both parts are executed,
whereas in the conditional expression only one of the two parts is executed.
decl r, m2;
r = <1,0; 0,1> ? 4 : 5; // 4, <1,0; 0,1> has not all 0s
r = <1,0; 0,1> .? 4 .: 5; // <4,5; 5,4>
m2 = <1>;
r = r .== 4 .? m2 .: 0; // <1,0; 0,0>
Assignment expressions
The assignment operators are the = *= /= += -= ~= |= symbols.
An lvalue is required as the left operand. The type of an assignment
is that of its left operand.
The combined assignment l op= r is equivalent
to l = l op (r).
The following code:
decl i, k;
for (i = 0, k = 1; i < 5; i += 2)
k *= 2, print("i = ", i, " k = ", k, "\n");
writes:
i = 0 k = 2
i = 2 k = 4
i = 4 k = 8
Comma expression
A pair of expressions separated by a comma is evaluated left to right,
and the value of the left expression is discarded. The result
will have type and value corresponding to the right operand.
The example in the previous section has two instances of the comma operator
in the for loop: i = 0, k = 1.
Constant expressions
An expression that evaluates to a constant is required in initializers
and certain preprocessor expressions.
A constant expression can have the operators * / + -, but only
if the operands have scalar type.
Some examples were given in sections on
enumerations and
external declarations.
Preprocessing
Preprocessing in Ox is primarily used for inclusion of files
and conditional compilation of code. As such it is more restricted
than the options available in C or C++.
Escape sequences in strings literals are interpreted
when used in preprocessor statements.
File inclusion
A line of the form
#include "filename"
will insert the contents of the specified file at that position. The
file is searched for as follows:
-
in the directory containing the source file (if just a filename,
or a filename with a relative path is specified),
or in the specified directory (if the filename has an absolute path);
- the directories specified on the compiler command line (if any);
- the directories specified in the INCLUDE environment string (if any).
- in the current directory.
A line of the form
#include
will skip the first step, and search as follows:
- the directories specified on the compiler command line (if any);
- the directories specified in the INCLUDE environment string (if any);
- in the current directory.
The quoted form is primarily for inclusion of user created header
or code files, whereas the second form will be mainly for system
and library files. Note that escape sequences are
interpreted in the include string, but not in the version
which uses <...>
(so in
#include "dir\nheader.h", the \n is replaced by a newline
character). Both forward and backslashes are allowed (use
#include "dir/nheader.h", to avoid the newline
character).
Conditional compilation
The first step in conditional compilation is to define (or undefine)
identifiers:
- #define identifier
- #undef identifier
Identifiers so defined only exist during the scanning process of the
input file, and can subsequently be used by #ifdef and
#ifndef preprocessor statements:
- #ifdef identifier
- #ifndef identifier
- #else
- #endif
As an example, consider the following header file:
#ifndef OXSTD_INCLUDED
#define OXSTD_INCLUDED
// header statements
#endif
Now multiple inclusion of the header file into a source code file will
only once include the actual header statements; on second inclusion,
OXSTD_INCLUDED will be defined, and the code skipped.
Pragmas
Pragmas influence the parsing process of the Ox compiler.
Pragmas may only occur at the level of external declarations.
Defined are:
- #pragma array_base(integer)
- #pragma link("filename")
As discussed at various points, indices in matrices, arrays and strings
always start at 0. This is the C and C++ convention. Ox, however,
allows circumventing this convention by using the array_basepragma.
It is strongly recommended to adopt the zero-based convention,
and not use the array_base pragma.
More useful is the link pragma, which leads to inclusion of the
named file (which should be compiled source code, with default extension
.oxo) at the point of the pragma. This provides an alternative to
specifying the link files on the command line.
The search machanism is the same as for
#include "filename".
Link pragmas will normally occur in the same file as the
main function;
multiple linking of the same file will lead to errors:
#include
#pragma link("test.oxo")
main()
{ // main code
}
Difference with ANSI C and C++
This section lists some of the differences between Ox
and C/C++ which might cause confusion:
- /* */ type comments can be nested in Ox.
- sizeof is a function in Ox, not an operator
(and not a reserved word).
- Labels (targets of goto statements) have the colon prefixed, rather than suffixed.
- Unlike C++, the init_expr of a for loop
cannot be preceded by a declaration.
- All data members of a class are private, all function members public.
- The base class constructor and destructor functions
are not called automatically.
- Integer division is not used, so 1 / 2 yields 0.5,
instead of 0. Use idiv(1, 2) for integer division of 1 by 2.
- The preprocessor does not allow: #define XXX value,
for integer constants, enums could be used, but more
convenient is:
const decl XXX = value;.
Ox version 1.11. This file last changed 31-Jul-1996.