LANGUAGE OVERVIEW

This  chapter introduces the language syntax and program constructs in terms of the "C" programming language. Since this is the language that PI  most closely resembles, it will be easy for those who have  worked in  "C"  to  understand  the  concepts and  constructs  used  by  this language.

Below  is  a  sample program that shows the basic structure  of  a  PI program. Many of the program structures and directives are features of the  glossary  file  concept  which is described  in  the  an  earlier chapter.  These same features are reintroduced here in terms of  their usage within PI.
 
. Simple program to calculate the area of a circle.

#include $Utools/Utools/include/pi.h
#define PI 3.14159

#main(argc, argv)
    local radius, a, p;

    radius = (float)argv[argc - 1];
 /* Calculate and then write */
 /* the result to the output */
    a = area(radius);
    p = format("Radius = %f, Area = %f\n",radius,a);
    fputs(p, stdout);
))

. The following function returns the area
. of a circle with the given radius.
#area(radius)
    return ( #PI * radius * radius );
))

Example 1: A simple program to calculate area

To run this program using Print Control, execute the following command (shown in bold) from the shell prompt ($);

$ pcf /tmp/sample 3
Radius = 3.000000, Area = 28.274310
$
To run this program using UxFax2, execute the following command (shown in bold) from the shell prompt ($);
$ faxcmd run /tmp/sample 3
Radius = 3.000000, Area = 28.274310
$
In both instances above, the program has been saved as "/tmp/sample". This file is named as the first argument on the command line. The number "3" is being passed as the radius argument to the program, and it will be used to calculate the area which is displayed by the program.

Comments

All  lines  beginning  with  a  period in the  left  most  column  are comments.  All  of  the  comments which  appear  outside  of  glossary entries,  such  a  the  comment at the top of the file,  and  the  two comment  lines  preceding the area() glossary entry will not  even  be encountered by the interpreter during execution.

Any line with a comment period in the first column is ignored. Besides allowing  the  inclusion of comments, this provides a quick method  of removing  lines  during debugging of a program, simply by inserting  a period.

The language also supports "C" style comments, such as the two comment lines within the main() routine. These will be ignored by the interpreter.

Include Directives

The  #include directive is a feature of glossary files. The file to be included  is  named after the directive. Unlike "C", the name  appears without surrounding quotation marks or "<" or ">" symbols.

Also,  environment  variable  substitution will be  performed  on  the pathname.  The  example  program  shows  the  $Utools  variable  being referenced.  Although  the inclusion of pi.h is not  necessary  within this program, it will be commonly included other PI programs.

Any  defined  glossary  variables  in an included file  will  also  be accessible  within the file which includes it. This facility may  also be  used  to  include  other source files  with  common  (interpreted) libarary  functions.  For example, in the above sample program, the  #define directive  and  the  area() routine could be removed and  place  in  a separate  file  called  "math.h". This file could then be named  with  a #include directive. e.g;
 
#define PI 3.14159

#area(radius)
    return ( #PI * radius * radius );
))

Library file: $Utools/Utools/include/math.h
 
. Simple program to calculate the area of a circle.

#include $Utools/Utools/include/math.h

#main(argc, argv)
    local radius, a, p;

    radius = (float)argv[argc - 1];
 /* Calculate and then write */
 /* the result to the output */
    a = area(radius);
    p = format("Radius = %f, Area = %f\n",radius,a);
    fputs(p, stdout);
))

Example 2: A program using library functions

This  above  program  is similar to the original example,  except  the area()  function  and definition of PI have been removed to  a  header file  called  math.h  which  is included.  Also,  since  the  glossary variable  PI  is  visible  within both files, it is  also  used  as  a constant within the main routine.

Define Directives

This  directive is a feature of the glossary file concept, and is also the definition of a glossary variable. So the #define directive varies from it's normal usage within "C" in several aspects.

It  does  not allow macro definitions. Macro definitions are  just  as well  provided with function declarations. This is demonstrated in the previous example where area()  is  defined  in  an included header file.

The  directive  may  only  be  used to  declare  constants  which  are substituted  in-place  wherever  the defined  variable  name  appears, preceded by a hash (#) symbol.

If  the  defined  value  includes spaces, the entire  string  must  be surrounded  in double quotes. Unlike "C", only the value which appears within  the  quotes is defined, so if it is used as a string  constant within  the  program source, the quotation marks must be given  again, e.g.;

#define Welcome "Welcome to Utopia\n"
. . . .
fputs("#Welcome", stdout);
The value of the defined name is substitued in it's entirety, although there  are a couple of extensions to the syntax which may be used when the  defined  variable  is used within PI  reports.  These  extensions affect  the placement of the substsitued string by allowing the  value to only over-write white space within the glossary entry. This is used to maintain the format of reports.

Declarations

All  functions  and  variables are declared as  they  are  encountered during  execution. Unlike "C", variables are not declared with a type. Only the name is declared; the type is determined by the value that it stores.  But  the  declaraion  of a variable  involves  two  important things; In  addition,  the  visibility  and lifetime of  a  variable  is  also affected by whether it is declared as global or local.

Local  Variables  are visible only within the compound statement  (the local  block)  in  which  they are declared, or  any  nested  compound statements  (inner  blocks). The variables are not visible outside  of the  local  block.  As soon as execution leaves the local  block,  the variable  declaration  is lost, and so it only has a lifetime for  the execution of the local block.

Variable declaration is implicit. Any unrecognised symbol will be automatically declared as a local variable as it is encountered. But care needs to be taken when relying upon automatic declaration, because these variables are only visible fromwithin the block in which they are first encountered. i.e. Variables which are automatically declared within an inner block will not be visible in an outer block. e.g.
 
. The following will cause an error because y
. is only ever declared in the inner block.
#main()
       for (x=0;x<10;x++) {
             if (x == 0) y = x;
             else y += x;
       }
       write(stderr, x, y);
))

. The following is correct because both
. x and y are first declared in the outer block.
#main()
       y = 0;
       for (x=0;x<10;x++) {
             if (x == 0) y = x;
             else y += x;
       }
       write(stderr, x, y);
))

Global  Variables  are  only declared as they are  encountered  during normal  execution  of  the  program. Once declared  though,  they  are visible  within  any  program  block.  If a  local  block  includes  a declaration  of  a variable with the same name as an already  declared global, the local variable will take precedence during the lifetime of that  block.  The variable has infinite lifetime once declared  during execution of the program.

The  following  example  shows the ideal method  of  declaring  global variables  within a program. By containing the global declaration  and initiation  of  these variables within the one routine,  that  routine only needs to be called once at program startup.
 
. Declare all globals used by the program
#globals()
      global status, message;
      status = 1;
      message = "Waiting"
))

#main(argc,argv)
      globals();
      main_loop();
))

Example 3: Ideal method of declaring global variables

Functions are declared as they are encountered during execution of the program.  Once declared they have global visibility and lifetime. Only the  function  name  is significant during declaration.  The  function arguments  are  matched  when  the  function is  called,  and  like  a variable, a function may return any data type.

When  functions are to be called indirectly, the function name must be declared  before  it  is used. This is demonstrated in  the  following program  which will give a different result depending upon the  number of arguments given when the program is called.
 
#funct_a()
      fputs("funct A\n",stdout);
))

#funct_b()
      fputs("funct B\n",stdout);
))

#main(argc,argv)
      global funct_a(), funct_b();

      list = {funct_a, funct_b};
      argc = (argc > 2) ? 1 : 0;
      list[argc]();
))

In  this example, the functions are declared as global. They may  also be  declared  as local which will only give then local visibility.  So any  future  indirect  reference to these functions would have  to  be declared  again.  For  efficiency,  functions should  be  declared  as global.

Data Types

Basic Data Types
Variables  and functions may assume any data type. i.e. any data  type may  be  assigned  to a variable, and a function may return  any  data type. The basic data types supported within PI are; A  declared  variable  has  a type of IS_NULL until  another  type  is assigned  to  it.  The data type will equate to the constant  NULL  in conditional expressions. NULL has no type and no value.

The  language allows conversion by casting between each of these basic data  types, except that precision may be lost when converting from  a float type to an integer type.

Arrays
Arrays variables are comprised of a variable number of elements of any type.   An  array  element  may  also  be  an  array.  In   this   way multi-dimensioned  arrays  may  be  built  up.  Array  dimensions  are dynamic,  and will be determined by the largest array index  reference made during program execution.

local a;
a[0] = 12;
a[1] = 2.2;
a[4] = "Text";
In  the  above  extract, when the variable "a" is declared it  has  no type.  After the first statement it becomes a one dimension array, and the  first  element  is an integer. After the second statement  it  is resized  to a two element array, and the second element is a  floating point  number.  And after the last statement it is resized as  a  five element  array,  with  the fifth deing a string, while the  third  and fourth elements are null.
local a;
a[0] = 12;
a[1] = 2.2;
a[2] = "Text";
a[3] = a;
In  this  extract,  the fourth element of the array will  be  a  three element  array, so a[3][0] will be the same as a[0], and a[3][1]  will be the same as a[1], and a[3][2] will be the same as a[2].

Complex Types
The inbuilt libraray functions introduce several different data types. These include;
 

The IS_FILE type is most popular within the standard library, since it is  similar  to the "file pointer" type in standard "C". This type  is used  in functions concerned with reading from and writing to standard I/O and file streams.

The  IS_FUNCTION data type results from an expression which yields  an indirect function reference. An example of this is shown in the sample program in the previous section on "Declarations".

PI  provides  "address  of" and "contents of"  operations,  just  like standard C.  These  have limited application where it is necessary  to work  with indirect variable references. The "&" (address of) operator will yield an IS_POINTER data type, while a "*" (contents of) operator will expect an IS_POINTER data type.

The  IS_ISAM data types is used to access ISAM data tables used within UxFax2.  Most  of the ISAM library functions use this data  type  when refering to opened data tables.

Constants

Constants  are  virtually identical to "C", and use standard  notation for  the  three basic data types, integer, float and string. The  NULL constant will yield a null data type; not zero as standard "C" does.

Array  constants use a similar notation to "C" when assigning  initial values  to structures. The array constant is a comma separated list of constants  surroundex by "curly" braces. Each constant may also be  an array  constant,  thus allowing the specification  of  multi-dimension array constants.

a = { 12, 2.2, NULL, NULL, "Text" };
For example, the above source extract is identical to the first sample in  the  previous section on "Data Types" under the heading  "Arrays". And  the extract below is identical to the second sample in that  same section;
a = { 12, 2.2, "Text", { 12, 2.2, "Text" } };

Operators

PI  provides an identical set of operators as standard "C". These  are summarised in the following tables.

Assignment

 
= Assignment a = b; Assign b to a.
Arithmetic
 
+ Addition a + b; Add a and b.
- Subtraction a - b;  Subtract b from a.
* Multiplication a * b; Multiply a by b.
/ Division a / b;  Divide a by b.
% Modulo a % b;  Remainder of a/b.
Conditional
 
> Greater than a > b True if a exceeds b
< Less than a < b True if b exceeds a
>= GT or equal a >= b False if b exceeds a
<= LT or equal a <= b False if a exceeds b
== Equal a == b True if a equals b
!= Not equal a != b False if a equals b
|| Logical OR || True if either a or b are true
&& Logical AND && True if both a and b are true
String
 
+ Concatenate a + b Append a with b
- Compare a - b -1 (a<b), 0 (a==b) or 1 (a>b)
> Greater than a > b True if a is lexically GT b
< Less than a < b True if b is lexically GT a
>= GT or equal a <= b False if b is lexically GT a
<= LT or equal a >= b False if a is lexically GT b
== Equal a == b True if a is the same as b
!= Not equal a != b False if a is the same as b
Bitwise
 
>> Right shift a >> b Shift a right b bit positions
<< Left shift a << b Shift a left b bit positions
| Bitwise OR a | b Boolean a OR b
& Bitwise AND a & b Boolean a AND b
^ Exclusive OR a ^b Boolean a XOR b
Unary
 
~ Complement ~a olean ones complement of a
- Negate -a Arithmetic negation of a
! Not !a Logical complement of a
& Address of &a Indirect address of a
* Contents of *a Indirect contents of a
++ Increment ++a Pre-increment a
-- Decrement a-- Post-decrement a
(type) Cast (int)a Convert a to an integer
Compound
 
+= Addition a += b a equals a plus b
-= Subtraction a -= b a equals b subtracted from a
*= Multiplication a *= b a equals a multiplied by b
/= Division a /= b  equals a divided by b
%= Modulo a %= b a equals the remainder of a/b
>>= Right shift a >>= b a equals a shifted right b bits
<<= Left shift a <<= b a equals a shifted right b bits
|= Bitwise OR  a |= b a equals boolean a OR b
&= Bitwise AND a &= b a equals boolean a AND b
^= Exclusive OR a ^= b a equals boolean a XOR b
Ternary
 
?: Conditional a ? b : c b if a is true, otherwise c

Operator Precedence

Unlike  most  languages, there is no order of precedence  between  the operators.  Expressions are simply evaluated from left to right,  with all  expressions  on  the right side of an  operator  being  evaluated before being applied to the left side.
x = 1 + 2 * 3 - 4.0 / 5;
For  example,  the  above expression is evaluated in the  order  shown below.  The  interpreter will scan along the line recursively  looking for  the  first complete expression. Because each expression  contains another  expression  on  the  right  hand  side,  the  first  complete expression is found at the end of the line;
x = 1 + 2 * 3 - 4.0 / 5;
x = 1 + 2 * 3 - 0.8;
x = 1 + 2 * 2.2;
x = 1 + 4.4;
x = 5.4;
The  only way to enforce an order of precedence is through the use  of parenthesis. An expression surrounded by parenthesis will be evaluated separately  before  any expressions to the right of it. The  following example  shows  the effect of parethesis on the previous example,  and the order of evaluation;
x = (1 + (2 * 3)) - (4.0 / 5);
x = (1 + 6) - (4.0 / 5);
x = 7 - (4.0 / 5);
x = 7 - 0.8;
x = 6.2;

Statements

A  statement  may be any expression, including an operator,  variable, constant  or function call. The inbuilt statements may start with  any one  of the keywords described below. PI implements all the statements built into "C", except goto. i.e.;
 
if statement

If the compound expression following the if keyword is true,  then  execute  the first  statement.  Otherwise execute the statement following the else keyword if it is provided.

Example:

if ( y != 0 )
x /= y;
else
x = 0;
while statement

While  the  compound  expression following  the  while keyword is true, execute the statement.

Example:

while ( i >= 0 ) {
     arr2[i] = arr1[i];
     i--;
}
do statement

Execute the statement following the do keyword until the compound expression following the while keyword is false.

Example:

do arr2[i] = arr1[i];
while ( ++i < 10 );
switch/case/default statement

Evaluate  the compound expression following the switch keyword  and start executing the statements within the compound  statement  following a case keyword  with  a constant  matching the result. If the result does  not match  any  constant  following a case  keyword,  then start  executing the statements following the  default keyword if it is provided.

Example:

switch (option) {
   case #O_QUIT:
      break;
   case #O_RUN:
      mode = option;
      break;
   default:
      option = 0;
}
for statement

Evaluate  the first expression once, and then  execute the  statement for as long as the second expression is true.  After  each  iteration,   evaluate  the   third expression.  If any expression is ommitted, it will be evaluated as true.

Example:

for ( i = 0; i < 10; i++ ) sum += arg[i];
return statement

Stop  executing  the current sub-routine,  and  return control to the calling routine. The expression will be evaluated   and   returned  as   the  result  of   the sub-routine.

Example:

return(answer);
break statement

End  the innermost do, for, switch or while statement, and return control to the statement that follows.

Example:

while ( i > 0 ) {
     if ( i < 10) break;
     i--;
}
continue statement

Return to the start of the innermost do, for, or while statement,  bypassing any remaining statements in that iteration.

Example:

for ( i = 0; i < 100; i++ ) {
     if ( i == 50 ) continue;
     arr2[i] = arr1[i];
}

Function Arguments

The  definition of a function is made in a glossary entry of the  same name  as  the  function,  followed by  opened/closed  braces  with arguments. The declaration of the function and it's  arguments does not include any type information. The type of the arguments  are determined when the function is called, and the type of the function return value is determined by the function,
#max(x, y)
     if ( x > y ) return(x);
     return(y);
))
The  number of arguments passed when the function is called must match the  number  of arguments declared with the function, otherwise a  run time error message will be generated. So the language does not support variable  argument  list  directly.  To use a variable  number of arguments, pass an array;
#sort(l)
     local x, y, size, again;

     size = sizeof(l);
     do {
          again = 0;
          for ( x = 0; x < (size - 1); x++) {
               if (l[x] > l[x + 1]) {
                    y = l[x];
                    l[x] = l[x + 1];
                    l[x + 1] = y;
                    again = 1;
               }
          }
     } while (again);
))

Similary,  a  function  may return a variable number of  arguments  by returning  an  array.  The following example will  return  a  variable sized array depending upon the argument, which is also an array;
#status(stat)
     ret = NULL;
     switch (stat[0]) {
          case #O_WAIT:
               ret[0] = #O_WAIT;
               ret[1] = "Waiting";
               break;
          case #O_RUN:
               ret[0] = 0;
               break;
          default:
               ret = stat;
     }
     return(ret);
))