Oberon 2 is worth learning because of its clarity, simplicity and ease of use compared with the dominant language of today, C. C started out as a full fledged production language, but a very tricky one to use. Pascal was developed about the same time as C but as a simple teaching language. Pascal was developed by Niklaus Wirth through successive languages called Modula, Modula-2, Oberon and Oberon-2 to become a production language. Thus Oberon-2 in a very weak sense is almost Pascal-5, though different enough from the original Pascal that Pascal-5 would not quite be appropriate as a name for it. Oberon-2 is clear and safe, C is obscure and tricky. No doubt C's trickiness has convinced many a beginner that he did not have what it takes to be a programmer, and has cost many a project cost overruns because of the difficulty of writing bug free C programs. Like the C, Oberon has separate compilation, and a separate library. Like the newest version of C, C++, it also has objects. This introductory course does not cover objects. Objects are covered in the book Object-Oriented Programming in Oberon-2 by Hanspeter Mossenbock. Oberon-2 is much simpler than other contending Pascal based languages such as Ada and Modula3.
The fact that Oberon-2 is a production language is easily supported. Oberon-2 is an extension of the earlier language Oberon. An operating system and compiler were written in Oberon. The source code is given in "Project Oberon, The Design of an Operating System and Compiler" by Niklaus Wirth and Jurg Gutknecht, 1992. The use of the operating system is documented in "The Oberon System, User Guide and Programming Manual" by Martin Reiser 1991. Some facilities needed for a production language that are part of the Oberon-2 language definition are omitted from the oo2c implementation. Namely, some machine dependent parts of the module SYSTEM. This omission is no doubt because oo2c is implemented for automatic portable installation on a wide variety of systems. These facilities could be added by a sophisiticated user who needed them for his particular machine. An example would be type conversions which violate type compatiblity rules.
Programmers who would like to use Oberon, but are required to produce source code in C, should be aware of a an Oberon compiler that produces readable high level C code, namely Ofront, available at ftp.inf.ethz.ch/pub/Oberon/Ofront.
This article is based on the oo2c implementation of Oberon-2. See http://sourceforge.net/projects/ooc. It runs on Pentiums, Alphas and other chips that support the operating systems that it runs under. It runs under unix or linux. If you have a windows computer, a bit later a description will be given of how to try linux for free on your windows computer. It works under MacIntosh OS X, but from what I read it seem to be harder to install than under linux. With special techniques, it can be made to work under windows. I have only used it on linux. There is also a different oberon compiler that runs on windows that is available at the ethz website below. The oo2c compiler is supplied as source code, and is compiled on your system using your C compiler during installation. The installation is mostly automatic, requiring little on your part. The oo2c implementation produces unreadable low level C code, then calls the C compiler to produce native code. For other implementations of oberon see www.zel.org/oberon/osci.htm. For comparisons with C see www.zel.org/oberon/fromctoo.htm. Another important site is www.oberon.ethz.ch. See also www.oberon.ws.
This article that you are now reading must be supplemented by a definition of the Oberon-2 language. Sufficient instruction is available free online. A good place to start is the description for beginners of the original Oberon language at http://www-old.oberon.ethz.ch/WirthPubl/ProgInOberon.pdf . Once the original Oberon is understood, the few features added in Oberon-2 can be understood from the more advanced description of Oberon-2 available online at ftp://ftp.inf.ethz.ch/pub/Oberon/OberonV4/Docu/Oberon2.Report.ps. A hardcover textbook covering Oberon-2 for beginners is "Into the Realm of Oberon", by Eric Nikitin, Springer-Verlag 1997. These texts only describe the language, not the library. The language must be supplemented by an Oberon library to be useable. The oo2c library manual "OOCRef" is freely downloadable from the sourceforge link above. These detailed references may be too lengthy for the impatient. This article is brief, but complete enough to convey the basic nature of the language and the library. It shows working examples to solve the most common problems the beginner will face writing programs for text, graphics and mathematical applications. Care has been taken to make each example as brief and clear as possible. All of the source code presented in this article is in the public domain, and may be used without attribution.
This course has been developed on Linux. Linux can be obtained for free, so it does not cost anything to try it. People who would like to try linux, but do not have Linux, should know that it is possible to have both Windows and Linux on the same computer with a single hard drive, but it requires a fresh install of both Windows and Linux. It used to be hard to do this kind of installation, but now it is easy. Here is how I did it. Go to www.whatismyscreenresolution.com and write down your screen resolution. Go to debian.org, download the free "netinst" iso file. If your PC runs windows, select "i386" in the "stable" category, then the netinst file will be near the bottom of a long list of iso files right here. Burn it onto a blank CD. Backup your personal files onto a floppy or a blank CD, because they will be erased by the installation procedure. Find your windows CD and the associated product key number, which you must have to install windows. Put in your windows CD and shut down the computer with the windows CD in the drive. Turn on the computer with the windows CD in the drive and you will be prompted with questions to install windows. You can accept defaults except when it asks if you want to install windows on the whole disk. You do not. It will tell you the size of the hard disk. Enter a number about half the size of the disk. You will have to shutdown and reboot a time or two to complete the windows installation. When you have finished the windows installation put in the linux net install CD and shut down the computer. Turn on the computer with the linux CD in the drive. During the installation process you will be asked questions. You will need to enter your screen resolution, your name, and make up your user name root name and two passwords. You can accept defaults to all the other questions except where to put linux. You do not want to put linux on the whole drive, put it on the empty partition. Now both windows and linux are on the computer. When you start up the computer you have a few seconds to choose between linux or windows to boot up. If you do not choose, linux will boot up. If your screen is slow to light up, the choice may be made before you can see the screen. If this happens, shutdown and reboot the linux and the sreen will already be lit up when the choice appears. The "grub" program can be reconfigured to boot windows before linux, but I have not done this. In linux click on "desktop", "administration", "printers" to configure your printer. In addition, in a terminal as user, not root, I had to issue the command "lpoptions -d " and the name of my printer to finish printer configuration so the "lpr" command would work. I understand that Ubuntu linux is a bit more user friendly than debian linux, but I have not tried it.
The author wishes to acknowledge help from Michael Van Acken of the oo2c project when the author was learning some of the tricks disclosed herein.
If you are new to linux, open a terminal window on your linux system and click on it to make sure it is active. You will do all your work in the terminal window. To familiarize yourself with linux, type "apropos move" and you will see a list of commands that relate to "move". If the list is too long to see on the screen, type "apropos move | more", then hit the spacebar to see each successive screen of the list. The most important is "mv". To see how to use it type "man mv" to see the manual page for the command "mv". Type "q" when you want to get out of the man page. Using this technique you can find how to do whatever you want to do on linux. At a minimum you should know the commands "mv", "cd", "cp", "rm", "mkdir" and "ls". More information can be found at /usr/share/doc. Most of the files there end in ".gz". These files can be read by the command "zless file.gz". Installing additional software on linux is done with different commands on different versions of linux. On debian linux to get additional software from debian, "su" and enter password to become superuser, then "apt-get install pkg" if you want to install a software package named "pkg", then "exit" when done so that you are no longer superuser.
To see if your linux system has the software already installed that is needed to support oo2c, use the "whereis" command to see that you have the programs "g++", "gc", "make" and "libtool". Thus "whereis gc" might typically show "/usr/bin/gc", which shows that you do indeed have gc somewhere on your computer. These can be installed by their names except for "gc". When you go to install gc, you will have to ask for "libgc-dev", but after installation, "whereis" will report it simply as "gc".
If you have the inexpensive Asus eee PC linux laptop, detailed installation instructions are given here. The instructions given in the next paragraph are less detailed and are for generic linux installation.
"mkdir progs" to create a directory "progs" or some other name where you will be doing your programming. In the directory "progs", "mkdir sym" to make a new directory "sym", similarly make directories "obj" "bin" and "src", all in the directory "progs". Go to the sourceforge website listed above and download the oo2c software, oo2c_32-2.1.11.tar.bz2, into the directory "progs". "bunzip2 oo*" to unzip the software. You will now have the file oo2c_32-2.1.11.tar. Now "tar xvf oo*". You will now, in addition to the tar file, have a new directory oo2c_32-2.1.11 . Now "cd oo*" to get into the directory oo2c_32-2.1.11 . In that directory do "./configure", and some text will quickly scroll by. Then do "make" and lots of text will scroll by over a period of minutes, and your computer fan may speed up. When this is done, you will need to use the "su" command and enter the password to become superuser. As superuser do "make install". When this is finished, oo2c should be installed on your linux computer.
Do not delete the directory oo2c_32-2.1.11, because it documents the changes from version 1 to version 2. The version 1 manual , OOCref-000229.tar.gz, should also be downloaded and unpacked with "tar xvzf OO*".
The directories "src" and "bin" are new additions with version 2 of oo2c; version 1 did not need them. With version 2, if you are in your directory "progs", or whatever you choose to call it, you will put your program source text files in "src". To use the vi editor to edit the program "tiny.Mod" you would enter "vi src/tiny.Mod". After compilation, to execute the program you would enter "bin/tiny".
You will need to learn the vi editor to copy programs from this article and to write your own programs. To learn the vi editor click here.
The best way to get the programs and files is to save the html source file to disk complete with embedded html commands. If you save the html source of this web page to your disk, you can use the vi editor on your Linux system to copy the programs out of this document so that you do not have to copy them by hand. In vi, in the edit mode, not the input mode, put the cursor at the beginning of a program and type ":nu". This will show the line number in this document. Do the same for the end of the program. Suppose the name of the program is "tiny", the beginning was 201 and the end was 208. Then type ":201,208 w! tiny.Mod". You will then have a copy in a file "tiny.Mod" that you can compile and run. Unfortunately, some symbols, namely "<", ">" and "&", are not the same in html as in plain text. They are all represented in the plain text as code words beginning with "&". These symbols appear correct when viewed with your browser, but not in the original file. Therefore, it will be necessary to compare a few programs with the way they appear in your browser to see what changes are necessary so that they will compile correctly. The lines of program text where this is a problem are marked with comments in the programs. If you use the vi editor to search for the ampersand character, "&" you will find all the lines that need to be changed.
An Oberon program consists of the word MODULE, then the name you choose to call the program followed by declarations, if any, of special things you wish to use in the program, then BEGIN followed by the program instructions, then END followed by the name of the program and a period. All words that are a permanent part of Oberon-2 must be capitalized; traditionally user defined words are in lower case to distinguish them from Oberon-2 words. This capitalization would be a nuisance if you had to do it as you write the program. The programming hints section of this document shows how to automate capitalization after you have written the program in lower case.
The simplest program which does nothing is:
MODULE nothing; BEGIN END nothing.
This or any other program is broken into lines as needed for readability; it would work the same if it were all on one line. Assume this program is in a file named nothing.Mod. The file name must end in ".Mod". When you are in the directory "progs" and the program is in the directory "progs/src" it is compiled to check for errors by:
oo2c --error-style char-pos nothing | ooef | more
This is a lot to type. To reduce the typing, create a script named "ooc" as follows:
oo2c --error-style char-pos $1 | ooef | more
Then "chmod +x ooc" to make the script exececutable. Then type "./ooc nothing" and achieve the same result with less typing. To test this, put the letter "a" between BEGIN and END, and you will get an error message.
If no errors, it could also be compiled to produce an executable program by:
oo2c -M nothing
It is then executed by:
bin/nothing
and nothing happens because the program does nothing.
It is useful to have the script "ooc" shown above and an additional script "oocm" which shows errors and produces an executable:
oo2c -M --error-style char-pos $1 | ooef | more
The declaration headings in a program are optional, and not needed unless they are actually going to be used to declare something. A very simple program which uses some declarations is:
MODULE tiny; IMPORT Out; CONST a=3; VAR b,c:LONGINT; BEGIN b:=2; c:=a+b; Out.LongInt(c,4);Out.Ln; END tiny. 5
The "5" after the program is the output of the program when it is executed. Since there is no I/O (input/output) in the Oberon language, the external module Out must be imported to give simple output capability. For fancier I/O capability, there are other modules described in the oo2c manual. These modules are part of the oo2c implementation of Oberon, and not part of Oberon itself. You are free to write your own I/O modules if you are not happy with the ones supplied. "a" was declared as a constant just to illustrate that constants can be declared. The variables "b" and "c" were declared as LONGINT rather than INTEGER because most computers have 32 bit words which match the size of LONGINT. While any type supported on a machine can be used on a machine, types that match the machine word can be expected to compute faster than other types. The number "4" in the Out.LongInt statement allowed four spaces for the number to be printed out in. Out.Ln moved output to the next line.
Oberon has integer types SHORTINT, 8 bits, INTEGER, 16 bits, LONGINT, 32 bits, and, on 64 bit machines only, and only with the oo2c implementation, HUGEINT, 64 bits. On both 32 bit and 64 bit machines it has real types REAL, 32 bits, and LONGREAL, 64 bits. It has type conversion functions SHORT and LONG to convert to the next larger or smaller version of integer or real. Real division is expressed by the symbol "/", integer division by "DIV". REAL can have the fractional part removed to make the next smaller integer by the function ENTIER. Integer "i" can be converted to real "r" by "r:=i". SET types have a size equal to the number of bits in a machine word, 32 elements on a 32 bit machine. A set variable is most usefully thought of as a set of bits in a machine word, each bit either 0 or 1.
An important feature of modern computer languages is an emphasis on hierarchical structure of the program as achieved by internal procedures and functions. The motivation behind intensive use of procedures is readability, understandability and modifiability of the program. The perfectly valid Oberon statement "REPEAT stirring UNTIL thick();" sounds like plain English. It is valid if "stirring" is the name we gave to a procedure and "thick" is the name we gave to a boolean function. A major part of writing a good program is figuring out the best way to subdivide the program and the best names to call procedures and variables.
The variables that a procedure uses to communicate with the rest of the program are called its parameters. There are different kinds of parameters, namely global, value and variable parameters. In Wirth's terminology, a "variable" is a memory location or machine address where a "value" or data can be stored. If you pass a value parameter to a procedure you are giving it some data to work with in one of its own internal memory locations. If you pass a variable parameter to a procedure you are giving it the address or memory location of a variable outside the procedure.
The simplest case is the global parameter:
MODULE proc1; IMPORT Out; VAR a:LONGINT; PROCEDURE add1; BEGIN a:=a+1;END add1; BEGIN a:=1; add1; Out.LongInt(a,4);Out.Ln;END proc1. 2
In the program proc1 there is no declaration of the variable "a" in the procedure so "a" is a global parameter. The "a" in the procedure is the same "a" as the "a" outside the procedure. This is the same as the GOSUB..RETURN of BASIC except that now names, rather than line numbers, can be used to refer to the procedures for enhanced readability.
Next we consider variable parameters:
MODULE proc2; IMPORT Out; VAR x,y:LONGINT; PROCEDURE add1(VAR a:LONGINT); BEGIN a:=a+1; END add1; BEGIN x:=10;y:=20;add1(x);add1(y); Out.LongInt(x,3);Out.LongInt(y,3);Out.Ln;END proc2. 11 21
The appearance of the variable "a" in the parameter list of the procedure constitutes a declaration, as opposed to a use, of the variable "a". Since "a" is declared in the procedure it is not a global parameter. Notice that the procedure add1 was called twice in the main program and given different variables to work on each time. The statement a:=a+1 actually went to the address "x" and did x:=x+1, then went to "y" and did y:=y+1. There is no address "a". This is the same as the parameters of a FORTRAN subroutine in early versions of FORTRAN.
MODULE proc3; IMPORT Out; VAR x,y:LONGINT; PROCEDURE add1(a:LONGINT; VAR b:LONGINT); BEGIN b:=a+1; END add1; BEGIN x:=1;add1(x,y); Out.LongInt(x,4);Out.LongInt(y,4);Out.Ln;END proc3. 1 2
In the above example "a" is a value parameter because it is not preceded by VAR in the parameter list. The call add1(x,y) accomplishes the implied assignment a:=x; then b:=a+1 in the procedure accomplishes y:=a+1. There actually is an address "a" in the procedure because "a" is a value parameter. CONST items can be passed into a procedure as value parameters, but not as VAR parameters.
Variables used in a procedure may be declared outside the procedure, in the procedure parameter list, or in the declaration section of the procedure. Parameters in the calling statement may use the same name as the procedure does for the parameters or different names. What follows is the simplest example that covers all these possibilities. You should painstakingly thread your way through the program to see how each number in the output was calculated. The notes that follow the program should help. Put your mind in low gear, this is the heart of the course. Note that anything enclosed within the pair (* *) is a comment for human readers, and is ignored by the compiler.
MODULE proc4;
IMPORT Out; VAR a,b,c,d,e,f:LONGINT;
PROCEDURE change(a,x:LONGINT;VAR c,y:LONGINT);
(*The stuff in parentheses on the line above is the
parameter list. *)
VAR f:LONGINT;
BEGIN
f:=60; a:=1+a;x:=1+x;c:=1+c;y:=1+y;e:=1+e;f:=f+1;
Out.String('in procedure:');
Out.LongInt(a,3);Out.LongInt(x,3);Out.LongInt(c,3);
Out.LongInt(y,3);Out.LongInt(e,3);Out.LongInt(f,3);
Out.Ln;
END change;
BEGIN(*this is where the execution of the program
starts*)
a:=10;b:=20;c:=30;d:=40;e:=50;f:=70;
Out.String('in program-1:');Out.LongInt(a,3);
Out.LongInt(b,3);Out.LongInt(c,3);Out.LongInt(d,3);
Out.LongInt(e,3);Out.LongInt(f,3);Out.Ln;
change(a,b,c,d); (*this is the procedure call*)
Out.String('in program-2:');Out.LongInt(a,3);
Out.LongInt(b,3);Out.LongInt(c,3);Out.LongInt(d,3);
Out.LongInt(e,3);Out.LongInt(f,3);Out.Ln;
END proc4.
in program-1: 10 20 30 40 50 70
in procedure: 11 21 31 41 51 61
in program-2: 10 20 31 41 51 70
Notes:
1. The procedure named change is only called once in this example, but a procedure can be called as often as you like in different parts of a program. Different variables could be used each different place in the program where the procedure is called. Thus, a,b,c,d could be replaced by r,s,t,u somewhere else, just so long as the data type of the variables used in the call agrees with those in the procedure declaration.
2. The order of the variables in the procedure call and in the procedure declaration determines which variables in the call will be associated with which variables in the declaration. The names of the variables in the call have nothing whatever to do with the names of the variables in the procedure declaration.
3. The parameters (a,x) in the declaration are called "value parameters". They are not preceded by VAR. Value parameters are names of variables in the procedure. The calling statement transfers data to these variables just as if the two assignment statements a(*in procedure*):=a(*in program*); x:=b; had been executed. The calling statement can have an expression such as x+y in the position corresponding to a value parameter. An expression, once evaluated, has a value, and that is what is needed in a value parameter. In the example, after the data has been transferred, the statements a:=a+1 and x:=x+1 in the procedure are performed on the variables in the procedure, not on the ones in the calling statement.
4. The parameters (c,y) in the declaration are called "variable parameters". They follow the word VAR. The variable parameters (c,y), are dummy names for variables in the calling statement that will be operated on by the procedure. Thus the statement y:=y+1 in the procedure actually performs the operation d:=d+1 on the variable d in the calling statement. There is no actual variable y in the procedure, y is just what the procedure calls d. An expression such as x+y cannot be used in the calling statement in the position corresponding to a variable parameter in the parameter list.
5. The variable f is declared in the procedure and therefore has nothing whatever to do with the variable f in the main program. The variable e is used, but not declared in the procedure. Since it is not declared in the procedure, it is called a "global variable". It is the e in the main program.
6. The procedure has two value parameters and two variable parameters to illustrate that it makes no difference whether the variable name used in the calling statement is the same as or different from the corresponding variable in the declaration. Variables declared in the parameter list or in the body of the procedure are called "local variables" because it doesn't matter if their names happen to be the same as other variables outside the procedure.
7. If variable parameters of more than one data type are used in a parameter list, a separate VAR word is needed for each data type. Thus (VAR x,y:REAL;VAR i,j:INTEGER;VAR k,l:BOOLEAN).
Procedures can have procedures inside them. This is illustrated by the following example:
MODULE proc5; IMPORT Out; VAR a:LONGINT; PROCEDURE add1; PROCEDURE add6; BEGIN(*add6*) a:=a+6;END add6; BEGIN(*add1*) a:=a+1;add6;END add1; BEGIN(*proc5*) a:=1;add1;Out.LongInt(a,4);Out.Ln; END proc5. 8
In the example proc5 the procedure add6 is part of the declaration section in procedure add1; it is nested inside add1. The procedure add6 can be called from add1, but it could not be called from proc5.
A function procedure is similar to an ordinary procedure except that it may take on a value. It can only take on a single value, not a set of values. Since a function can take on a value it can be used in an expression:
MODULE fcn1; IMPORT Out; VAR x,y:LONGINT; PROCEDURE add1(a:LONGINT):LONGINT; BEGIN RETURN a+1;END add1; BEGIN x:=0;y:=1+add1(x);Out.LongInt(y,4); Out.Ln;END fcn1. 2
In the main program add1 is a name to which a value will be assigned when add1 is called; add1 is given its value by the RETURN statement inside the function procedure add1.
If the memory location representing a variable resides in a procedure, as with value parameters or variables declared in the procedure, the variable cannot be depended on to retain its value between the time the procedure finishes executing and the next time the procedure is called. I could not construct an example which proved this with the oo2c compiler. Even if the oo2c compiler prevents this problem, other Oberon-2 compilers cannot be counted on to do likewise. Therefore do not rely on a procedure to "remember" the values of such variables between calls.
A somewhat esoteric example illustrates procedure types and procedure variables:
MODULE pv1;
IMPORT Out;
TYPE
pt=PROCEDURE(z:LONGINT):LONGINT;
VAR
p1,p2:pt;
PROCEDURE square(x:LONGINT):LONGINT;
BEGIN RETURN x*x;END square;
PROCEDURE cube(x:LONGINT):LONGINT;
BEGIN RETURN x*x*x;END cube;
PROCEDURE print(i:LONGINT;p:pt);
BEGIN
Out.LongInt(p(i),5);Out.Ln;
END print;
BEGIN
p1:=square;p2:=cube;
print(3,p1);print(3,cube);
END pv1.
9
27
Procedure variables are needed when different procedures must be passed as a parameter to another procedure and called within the other procedure. It is most often used with function procedures which take on a value. If each function only needed to be called once, it would not be necessary to pass the function as a parameter, just the value resulting from calling each function once could be passed. If, however, each function must be called many times within another procedure, it is necessary to pass the function as a parameter. Applications include procedures which plot graphs of different functions, and procedures which numerically integrate different functions. The above example did not need to call each function more than once, but it serves to illustrate the technique. In one case a variable whose value was the name of a procedure was used. In the other case the name of a procedure was used directly.
Occasionally you will want to use a procedure before it is declared. The word "before" needs to be clarified. The program execution starts at the last "begin" in the program. The compiler compiles the program starting at the top and going to the bottom. Thus a the compiler could find a procedure being used before it is declared, even though it is declared before the last begin where execution starts. To let the compiler know that the procedure will be declared after it is used, use a "forward" declaration, which is only the heading of the procedure, with the word PROCEDURE followed by "^" with no space. This is illustrated by the program fwd.
MODULE fwd; IMPORT Out; VAR x:LONGINT; PROCEDURE^ add1(a:LONGINT;VAR b:LONGINT); PROCEDURE useit(c:LONGINT); VAR d:LONGINT; BEGIN add1(c,d); Out.LongInt(d,4);Out.Ln; END useit; PROCEDURE add1(a:LONGINT;VAR b:LONGINT); BEGIN b:=a+1; END add1; BEGIN x:=1;useit(x); END fwd. 2
The next example shows illustrates the FOR statement which is used where a fixed number of iterations are needed.
MODULE for1;
IMPORT Out;
VAR i:LONGINT;
BEGIN
FOR i:=1 TO 9 DO Out.LongInt(i,2);END(*FOR*);
Out.String(' end');Out.Ln;
END for1.
1 2 3 4 5 6 7 8 9 end
Other repetitive statements are "REPEAT...UNTIL...;", "WHILE...DO...END", and "LOOP...EXIT...END". For details consult the text book.
The next example declares meaningful names as numerical constants so they can be used to clarify the meaning of a CASE statement:
MODULE case1;
IMPORT Out;
CONST mon=1;tue=2;wed=3;thurs=4;fri=5;sat=6;sun=7;
VAR day:LONGINT;
BEGIN
FOR day:=mon TO sun DO
CASE day OF
fri:Out.String('tgif');Out.Ln;
|tue:Out.String('what a grind');Out.Ln;
|wed:Out.String('meetings all day');Out.Ln;
|mon:Out.String('not again!');Out.Ln;
|sat,sun:Out.String('zzzz...');Out.Ln;
|thurs:Out.String('big weekend ahead');Out.Ln;
END(*CASE*);END(*FOR*);END case1.
not again!
what a grind
meetings all day
big weekend ahead
tgif
zzzz...
zzzz...
In the above example note that the order in which the items are printed out corresponds to the order the days are executed in the FOR statement, which in turn was defined by the numerical values given to the days of the week. The order in which the items are printed out does not correspond to the order of their listing in the CASE statement. Note also that the extra work needed to provide names, not just numbers, for the case selectors has made the program more readable. CONST items can be passed into procedures as value parameters, but not as VAR parameters.
The IF statement is a simple basic control statement:
MODULE if1;
IMPORT Out;
VAR x,y:BOOLEAN;
BEGIN
x:=TRUE;y:=FALSE;
IF x=TRUE THEN
Out.String('x=TRUE');Out.Ln;END;
IF y=TRUE THEN
Out.String('y=TRUE');Out.Ln;END;
END if1.
x=TRUE
The IF THEN ELSE statement may be used in the form of nested ELSIFs to provide something similar to the CASE statement where the selectors are logical conditions rather than values of a variable. The following example illustrates this:
MODULE elsif1;
IMPORT Out;
VAR i:LONGINT;
BEGIN
FOR i:=1 TO 9 DO
IF i=1 THEN Out.String('C1');Out.LongInt(i,2);
Out.Ln;
(*the following two lines must be modified to look the way
they look when seen in your browser*)
ELSIF i>8 THEN Out.String('C2');Out.LongInt(i,2);
Out.Ln;
ELSIF i>7 THEN Out.String('C3');Out.LongInt(i,2);
Out.Ln;
ELSE Out.String('C4');Out.LongInt(i,2);Out.Ln;
END(*IF*);END(*FOR*);END elsif1.
C1 1
C4 2
C4 3
C4 4
C4 5
C4 6
C4 7
C3 8
C2 9
The following is a short example using an array:
MODULE array1; IMPORT Out; VAR i:LONGINT;a:ARRAY 5 OF LONGINT; BEGIN a[0]:=6;a[1]:=7;a[2]:=8;a[3]:=9; (*watch for uninitialized variables*) FOR i:=0 TO 4 DO Out.LongInt(a[i],3);Out.Ln;END; END array1. 6 7 8 9 0
Notice that we failed to give a[4] a value, but we used it anyway. In this case the compiler was apparently set up to initialize all variables to zero. Most compilers cannot be counted on to do this. If not, a random number could have been printed out for a[4]. Using array elements that have not been initialized is perhaps the most common cause of errors in computer programs.
Note especially that an array of size 5 has elements numbered from 0 to 4, not from 1 to 5. Thus, if you plan to access array elements from 1 to "n", you should declare the array to be of size "n+1".
Arrays of more than one dimension are also possible.
Next we give some examples of records. Records are a user defined type. They are defined in the TYPE section and used elsewhere in the program. A record is a way of combining items of different data types into a single variable.
A simple example of a record definition:
CONST male=1;female=2;engineer=1;technician=2;
laborer=3; admin=4;management=5;
TYPE
employeerec=RECORD
name:ARRAY 40 OF CHAR;
salary:REAL;
occupation,sex:LONGINT;
END;
VAR employee:ARRAY 5000 OF employeerec;
To test the occupation of employee 323 we would say:
IF employee[323].occupation=engineer THEN (whatever)...
For an example of use of records see MODULE cxarith in the "mathematical programming" section of this document.
Records can be very complicated. They can be used to make some programs much simpler and more compact than they would otherwise be. They can also contain pointers, allowing the construction of linked lists, binary trees etc. There is no provision for file I/O of record types. Any such files would not be portable between different Oberon implementations. Instead, each field of the record type should be converted to ASCII text for file I/O, then portability can be assured.
Records can be extended to form objects, for object oriented programming, a topic beyond the scope of this introductory course.
When in doubt, it is best to write short test programs to get input/output statements right.
Test a specific character read from the keyboard:
MODULE testch;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR str1:str;
BEGIN
Out.String('enter a');Out.Ln;
In.Line(str1);
IF str1='a' THEN Out.String('right character');Out.Ln;
ELSE Out.String('wrong character'); Out.Ln; END;
END testch.
Read real numbers from the keyboard:
MODULE readreal;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR str1:str;x,y:REAL;
BEGIN
Out.String('enter x,y, real numbers');Out.Ln;
In.Real(x);In.Real(y);In.Line(str1);
Out.Real(x,5,0);Out.Ln;
Out.Real(y,5,0);Out.Ln;
END readreal.
Read a word, such as a filename, from the keyboard:
MODULE getname;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR filename:str;
BEGIN
Out.String('enter filename');Out.Ln;
In.Line(filename);
Out.String('fileid=');
Out.String(filename);Out.Ln;
END getname.
The output of the preceding programs would go to the screen.
The following pair of programs illustrate reading and writing to a disk with text files. In the following examples the filenames are compiled into the program. However, the variable filename with no quotes in the example above could be substituted for 'demo1.txt' with quotes in the following example, if it were desired for the user to enter filenames.
MODULE ritfil1;
IMPORT Msg, Files, TextRider;
VAR resv:Msg.Msg;outvar:Files.File;
outfile:TextRider.Writer;
a,b,c,d:LONGINT;
BEGIN
a:=5;b:=6;c:=23;d:=-2;
outvar:=Files.New('demo1.txt',{Files.write},resv);
outfile:=TextRider.ConnectWriter(outvar);
outfile.WriteLInt(a,3);outfile.WriteLInt(b,3);
outfile.WriteString(' some text');outfile.WriteLn;
outfile.WriteLInt(c,3);outfile.WriteLInt(d,3);
outfile.WriteString(' more text');outfile.WriteLn;
outvar.Close;END ritfil1.
After running ritfil1 we can look at the file demo1 with the editor and we will find our text in it. We can also use the lpr command to transfer the file to the printer. Finally we can read the file with the following program which displays it on the screen.
MODULE redfil1;
IMPORT Out, Msg, Files, TextRider;
VAR resv:Msg.Msg;invar:Files.File;
infile:TextRider.Reader;
str:ARRAY 40 OF CHAR;x,y:LONGINT;
BEGIN
invar:=Files.Old('demo1.txt',{Files.read},resv);
infile:=TextRider.ConnectReader(invar);
LOOP infile.ReadLInt(x);
IF infile.res#TextRider.done THEN EXIT END;
infile.ReadLInt(y); infile.ReadLine(str);
Out.LongInt(x,3);Out.LongInt(y,3);Out.String(' ');
Out.String(str);Out.Ln;END;
invar.Close;
END redfil1.
5 6 some text
23 -2 more text
Text files represent numbers as decimal digits, each digit being represented in turn by an 8 bit ASCII character. Such a file of numbers can be read by a person with a text editor. It is more efficient in storage and speed to represent numbers in binary form. Binary files cannot be read by a person with a text editor. The following pair of programs write and read a file of real (floating point) numbers in binary form.
MODULE ritfil2;
IMPORT Msg, Files, BinaryRider;
VAR resv:Msg.Msg;outvar:Files.File;
outfile:BinaryRider.Writer;
i:LONGINT;r:REAL;
BEGIN
outvar:=Files.New('demo2',{Files.write},resv);
outfile:=BinaryRider.ConnectWriter(outvar);
FOR i:=1 TO 5 DO
r:=i; outfile.WriteReal(r); END;
outvar.Close; END ritfil2.
MODULE redfil2;
IMPORT Out, Msg, Files, BinaryRider;
VAR resv:Msg.Msg;invar:Files.File;
infile:BinaryRider.Reader;r:REAL;
BEGIN
invar:=Files.Old('demo2',{Files.read},resv);
infile:=BinaryRider.ConnectReader(invar);
LOOP infile.ReadReal(r);
IF infile.res#BinaryRider.done THEN EXIT END;
Out.RealFix(r,12,4);Out.Ln;END;
invar.Close; END redfil2.
1.0000
2.0000
3.0000
4.0000
5.0000
In the Out.RealFix statement the 12,4 part means the number is right justified in 12 spaces and 4 digits to the right of the decimal point will be shown. There are other ways to write real numbers; consult the oo2c reference manual.
Next, we write a file of integers, read the file, and use set variables to print the results out in binary.
MODULE ritfil3;
IMPORT Msg, Files, BinaryRider;
VAR resv:Msg.Msg;outvar:Files.File;
outfile:BinaryRider.Writer;
i:LONGINT;int:INTEGER;
BEGIN
outvar:=Files.New('demo3',{Files.write},resv);
outfile:=BinaryRider.ConnectWriter(outvar);
FOR i:=1 TO 11 DO
int:=SHORT(i); outfile.WriteInt(int); END;
outvar.Close; END ritfil3.
MODULE redfil3;
IMPORT Out, Msg, Files, BinaryRider;
VAR resv:Msg.Msg;invar:Files.File;
infile:BinaryRider.Reader;s:SET;
i:INTEGER;
BEGIN
invar:=Files.Old('demo3',{Files.read},resv);
infile:=BinaryRider.ConnectReader(invar);
LOOP infile.ReadSet(s);
IF infile.res#BinaryRider.done THEN EXIT END(*if*);
FOR i:=0 TO MAX(SET) DO
IF (MAX(SET)-i) IN s THEN Out.Int(1,1);
ELSE Out.Int(0,1);END(*if*);END(*for*);
Out.Ln; END(*loop*);
invar.Close; END redfil3.
00000000000000100000000000000001 00000000000001000000000000000011 00000000000001100000000000000101 00000000000010000000000000000111 00000000000010100000000000001001
The file was written as 16 bit INTEGER type from 1 to 11. The command "ls -l" shows 22 bytes in the file "demo3". There are 2 bytes per 16 bit word. 22 divided by 2 is 11, so there are 11 of the 16 bit words in the file. But the program redfil3 reads the file as 32 bit machine words. There are two 16 bit integers in each 32 bit word. Reading a set results in reading a machine word worth of bits from the file. There is no provision for reading other numbers of bits when reading sets. Since the last 16 bit word by itself was not a complete 32 bits, it was not read. Had the file been written with 32 bit LONGINT type it would have been easier to read the binary numbers. We can use sets to read the 16 bit numbers out of this file demo3 and write them out in a new file demo4 as 32 bit numbers as shown in the program below. If set s:={}, that means all bits are set to zero. s:=s+{7} means that bit 7 of set s is made to equal 1, not zero. With suitable modification this technique can extract any word size from an old file and write the same data, possibly truncated, to any different word size in a new file, even though the actual physical reading and writing takes place in machine words. This might be needed for burning ROM chips for hardware controllers.
MODULE rrfil4;
IMPORT Msg, Files, BinaryRider;
VAR resv1,resv2:Msg.Msg;invar,outvar:Files.File;
infile:BinaryRider.Reader;
outfile:BinaryRider.Writer;
s,s2:SET; i,j:INTEGER;
BEGIN
s2:={};
invar:=Files.Old('demo3',{Files.read},resv1);
infile:=BinaryRider.ConnectReader(invar);
outvar:=Files.New('demo4',{Files.write},resv2);
outfile:=BinaryRider.ConnectWriter(outvar);
LOOP infile.ReadSet(s);
IF infile.res#BinaryRider.done THEN EXIT END(*if*);
FOR i:=0 TO MAX(SET) DO
j:=i MOD 16;
IF i IN s THEN s2 := s2 + {j};END;
IF j=15 THEN outfile.WriteSet(s2);s2:={};END;
END(*for*); END(*loop*);
invar.Close; outvar.Close;END rrfil4.
If redfil3 is recompiled to read "demo4", it will output:
00000000000000000000000000000001 00000000000000000000000000000010 00000000000000000000000000000011 00000000000000000000000000000100 00000000000000000000000000000101 00000000000000000000000000000110 00000000000000000000000000000111 00000000000000000000000000001000 00000000000000000000000000001001 00000000000000000000000000001010
More complex text files could contain lines having a mix of numbers, words and characters in an order that might differ from line to line. The following very general program reads lines in a text file one at a time, finds "items" separated by one or more spaces on a line. In a real application you would test each item to see what kind it was, and interpret and respond to it appropriately. Here, we merely print out each item to show that we have properly found each item. Warning, there are two html ampersands in this program that will have to be manually changed to simple ampersands before the program will compile properly.
MODULE getitem;
IMPORT Out,Files,TextRider;
TYPE str=ARRAY 40 OF CHAR;
VAR f:Files.File;r:TextRider.Reader;
res:Files.Result;
str1,item:str;pos:LONGINT;
PROCEDURE anotheritem(str1:str;VAR i1:LONGINT;
VAR str2:str):BOOLEAN;
VAR i2:LONGINT;start,finish,space,eol:BOOLEAN;
(*i1 must be initialized to zero by calling program
before first call to this procedure*)
BEGIN
start:=FALSE;finish:=FALSE;space:=FALSE;
eol:=FALSE;i2:=0;
LOOP
IF (str1[i1]=00X) THEN eol:=TRUE;END;
(*the line below must be modified to look the way it looks
when seen in your browser*)
IF (eol & ~start) THEN EXIT END;
IF (str1[i1]=' ') THEN space:=TRUE;
ELSE space:=FALSE;END;
(*the line below must be modified to look the way it looks
when seen in your browser*)
IF ((space OR eol) & start) THEN finish:=TRUE;
EXIT;END;
IF ~space THEN start:=TRUE;str2[i2]:=str1[i1];
INC(i2);END;INC(i1);END(*loop*);
str2[i2]:=00X;
RETURN finish;END anotheritem;
BEGIN
f:=Files.Old("temp.txt",{Files.read},res);
r:=TextRider.ConnectReader(f);
LOOP r.ReadLine(str1);
IF r.res#Files.done THEN EXIT END;
Out.String(str1);Out.Ln;
pos:=0;
WHILE anotheritem(str1,pos,item) DO
Out.String(item);Out.Ln;
END(*while*); END(*loop*); f.Close;
END getitem.
The file "temp.txt":
product: hammer $20.00 5.83 lbs discontinued ref: memo 11-23-41 % A23.4 %
Output of the program:
product: hammer $20.00 product: hammer $20.00 5.83 lbs discontinued 5.83 lbs discontinued ref: memo 11-23-41 ref: memo 11-23-41 % A23.4 % % A23.4 %
Some file handling programs use standard input and standard output. A simple example is a program to add carriage returns to Linux files. If you type "man 7 ascii" you can see that the symbol for linefeed is "\n", and for carriage return is "\r". Create a tiny test file "test1":
xx yy zz
Then do "hexdump -c test1". You can see that the end of each line is "\n". But Windows needs both "\r" and "\n". The following program addcr.Mod will do this:
MODULE addcr; IMPORT In,Out; VAR str:ARRAY 256 OF CHAR; BEGIN In.Line(str); WHILE In.Done() DO Out.String(str);Out.Char(CHR(13));Out.Ln; In.Line(str); END; END addcr.
The program uses standard input and output:
bin/addcr < test1 > test2
The hexdump of test2 will show that "test2" is compatible with Windows. If as superuser you move addcr to /usr/local/bin, you will be able to use it as a built in system command.
Sometimes it is convenient to compile separate pieces of a program separately, and not have everything in one large file. The following example has files "mod1.Mod" and "mod2.Mod". Mod1 and mod2 are compiled separately. To compile mod1, type "oo2c mod1". To compile mod2 type "oo2c -M mod2". Only the main module needs to be compiled with the -M option. To execute both, type "bin/mod2". To show errors the scripts defined earlier in the "syntax" section are useful: "./ooc mod1" then "./oocm mod2". Separate compilation can involve several layers of modules. Whenever a module is changed, it must be recompiled and every module below it in the chain down to the executable module must be recompiled also.
Mod2 calls the routine "halveara" in mod1 and uses it. Halveara is declared in mod1 with an asterisk, "*", which makes it visible to any external program which uses mod1. Anything made visible with a minus sign, "-" is read only, and cannot be modified by the external program. Anything not explicitly made visible is invisible to the outside.
MODULE mod1; TYPE ara=ARRAY OF REAL; VAR half:REAL; PROCEDURE halveara*(n:LONGINT;VAR ar:ara); VAR i:LONGINT; BEGIN FOR i:=0 TO n DO ar[i]:=ar[i]*half;END; END halveara; BEGIN half:=0.5; END mod1. MODULE mod2; IMPORT m1:=mod1, Out; CONST m=3; TYPE ara2=ARRAY m+1 OF REAL; VAR br:ara2;i:LONGINT; BEGIN FOR i:= 0 TO m DO br[i]:=i;END; FOR i:= 0 TO m DO Out.RealFix(br[i],10,2); Out.Ln;END; m1.halveara(m,br);Out.Ln; FOR i:= 0 TO m DO Out.RealFix(br[i],10,2); Out.Ln;END; END mod2.
bin/mod2
0.00
1.00
2.00
3.00
0.00
0.50
1.00
1.50
Notice in mod2 that mod1 was imported with a name change: "m1:=mod1". This is desirable when the name of the external module is excessively long. It could have been imported without a name change simply as "mod1".
Notice that the part at the bottom of Mod1 happens automatically when mod1 is imported. If it did not the value of "half" would be a random number or zero when halvara was called.
While mod1 could have declared an array, it did not. It declared an array type without any declaring the size of the array. In the procedure halveara the array was declared as a VAR parameter. The memory locations corresponding to VAR parameters are not in the procedure that declares the VAR parameter, but in the procedure that calls the procedure that has the VAR parameter. Mod1 has no array, only instructions for operating on an array. Mod2 is the only one of the two modules that actually has an array in it. This way mod1 is general purpose, and could be used by some other mod3 that had an array of completely different size from the one in mod2. This kind of array parameter without dimension is called an "open array parameter", and is useful within a module as well as between modules as shown here.
An Oberon browser is included with oo2c. This allows you to see interface information from a separately compiled module without the necessity of reading the detailed code of the module. Thus if we type "oob mod1" we get:
MODULE mod1; TYPE [ara] = ARRAY OF REAL; PROCEDURE halveara (n: LONGINT; VAR ar: ara); END mod1.
C programmers depend on special programs known as debuggers to get their programs running. Oberon programmers should have less need for debuggers, because of the clarity of the language. However, they will still need basic debugging techniques. The following example illustrates the basic technique:
MODULE debug;
IMPORT Out;
VAR ar:ARRAY 3 OF LONGINT;
i:LONGINT;
BEGIN
FOR i:=1 TO 3 DO
(*the following line must be modified to look
the way it does in your browser*)
IF i>2 THEN Out.String('out of range i=');
Out.LongInt(i,4);Out.Ln; HALT(1);END;
ar[i]:=i;
Out.LongInt(ar[i],4);Out.Ln;
END;
END debug.
Without the IF statement the program "bombs off" with a run time error when it is executed. The two line IF statement was inserted to write out intermediate results and terminate the program just before it got to the fatal error. In a large program, many such statements might have to be tried at different locations to track down the problem.
The following example responds to user word commands to execute procedures "firstproc" or "secondproc" or terminate the program. The commands are "first", "second" or "q". This technique is handy for making user friendly programs:
MODULE intrct;
IMPORT In,Out;
CONST er=0;q=1;first=2;second=3;last=4;
TYPE str=ARRAY 10 OF CHAR;
VAR cmdara:ARRAY last+1 OF str;
command:LONGINT;
PROCEDURE firstproc;
BEGIN Out.String('executed first procedure');Out.Ln;
END firstproc;
PROCEDURE secondproc;
BEGIN Out.String('executed second procedure');
Out.Ln;
END secondproc;
PROCEDURE initvar;
BEGIN
cmdara[er]:='er';
cmdara[q]:='q';
cmdara[first]:='first';
cmdara[second]:='second';
cmdara[last]:='last';
END initvar;
PROCEDURE determine(VAR cmdvar:LONGINT);
CONST bell=7;
VAR i:LONGINT;str1:str;
BEGIN
Out.String('enter command');Out.Ln;
cmdvar:=er;
In.Line(str1);
FOR i:=q TO last DO
IF (cmdara[i]=str1)THEN cmdvar:=i;END;END;
IF cmdvar=er THEN
(*the line below must be modified to look the way it looks
when seen in your browser*)
Out.Char(CHR(bell));Out.String('<--<< error');
Out.Ln;END;
END determine;
BEGIN
initvar;
LOOP
determine(command);
IF command=q THEN EXIT;END;
CASE command OF
er:|
first:firstproc|
second:secondproc END;
END;END intrct.
A very important application of computing is to plot graphs, or create other graphical output. The book "PostScript by Example", by Henry McGilton and Mary Campione, Addison-Wesley 1992, shows clearly and simply how to use postscript to produce graphics. The following program shows the basics of graphics programming. You must have a window system such as X11, Gnome or KDE running for this program to work. It requires that you have Ghostscript installed on your system. The program produces a plot of a damped sine wave. Then type "quit" to clear it from the screen, and it will print out on your printer.
MODULE graf1;
IMPORT In,Out,Msg,Files,TextRider,
OS:ProcessManagement,Strings,RealStr,rm:=RealMath;
CONST move=1;draw=2;
TYPE str=ARRAY 80 OF CHAR;
VAR str1:str;outvar:Files.File;resw:Msg.Msg;
outfile:TextRider.Writer;
PROCEDURE initfile;
BEGIN
outvar:=Files.New('temp1',{Files.write},resw);
outfile:=TextRider.ConnectWriter(outvar);
outfile.WriteString('%!PS');outfile.WriteLn;
END initfile;
PROCEDURE moveto(x,y:REAL;md:LONGINT);
VAR str1,str2:str;
BEGIN
str1:='';str2:='';
RealStr.RealToFixed(x,2,str1);
Strings.Append(' ',str1);
RealStr.RealToFixed(y,2,str2);
Strings.Append(str2,str1);
IF md=move THEN Strings.Append(' moveto',str1);
ELSE Strings.Append(' lineto',str1);END;
outfile.WriteString(str1);outfile.WriteLn;
END moveto;
PROCEDURE width(w:REAL);
VAR str1:str;
BEGIN
str1:='';
RealStr.RealToFixed(w,2,str1);
Strings.Append(' setlinewidth',str1);
outfile.WriteString(str1);outfile.WriteLn;
outfile.WriteString('stroke');outfile.WriteLn;
END width;
PROCEDURE drawaxis;
BEGIN
moveto(157,584,move);moveto(157,396,draw);
moveto(446,396,draw);width(1);
END drawaxis;
PROCEDURE drawcurve;
CONST xorig=157;pi2=6.3821;ampl=94;cycle=58;
yorig=396;
VAR i:LONGINT;x,y:REAL;
BEGIN
moveto(xorig,yorig+ampl,move);
FOR i:=1 TO 100 DO
x:=2.88*i+xorig;
y:=rm.exp((x-xorig)*(-2.3/290))
*ampl*rm.sin((pi2/cycle)*(x-xorig))+ampl+yorig;
moveto(x,y,draw);END;
width(2);
END drawcurve;
PROCEDURE initfont;
BEGIN
outfile.WriteString('/Palatino-Roman findfont');
outfile.WriteLn;
outfile.WriteString('14 scalefont');
outfile.WriteLn;
outfile.WriteString('setfont');outfile.WriteLn;
END initfont;
PROCEDURE labelaxis;
CONST xorig=157;cycle=58;
yorig=396; charsize=14;
VAR i:LONGINT;ir:REAL;str1,str2:str;
BEGIN
FOR i:=1 TO 5 DO
moveto(xorig+i*cycle,yorig-charsize,move);
outfile.WriteString('(|) show');outfile.WriteLn;
moveto(xorig+i*cycle,yorig-2*charsize,move);
str1:='(';str2:='';
ir:=i;
RealStr.RealToFixed(ir,1,str2);
Strings.Append(str2,str1);
str2:=') show';Strings.Append(str2,str1);
outfile.WriteString(str1);outfile.WriteLn;
END;END labelaxis;
PROCEDURE endfile;
BEGIN
outfile.WriteString('showpage');
outfile.WriteLn;
outvar.Close;
END endfile;
PROCEDURE viewplot;
VAR i:LONGINT;
BEGIN
i:=ProcessManagement.system("gs -sDEVICE=x11 temp1");
END viewplot;
PROCEDURE printplot;
VAR i:LONGINT;
BEGIN
i:=ProcessManagement.system("lpr temp1");
(*lpr is assumed to send file through
Ghostscript to printer*)
Out.String('sent to printer');Out.Ln;
END printplot;
BEGIN
initfile;drawaxis;drawcurve;initfont;labelaxis;
endfile;
Out.String
('X11 must be running before you run this program');
Out.Ln;
Out.String
('hit return when ready to view plot');Out.Ln;
Out.String
('enter quit when finished viewing plot');Out.Ln;
In.Line(str1);
viewplot;printplot;
END graf1.

On my printer the x,y coordinates of the lower left corner of the page are 14,18 in PostScript units. The upper right corner is 591,774, and the middle of the page is 302,396. This is for letter paper, 8.5 inches by 11 inches. Your printer may be set up a little bit different from this.
To see other graphs produced with this same technique click here.
One application for graphics is publishing, especially on the internet. You can use the word processing system called Latex that is supplied with Linux systems to create fancy text. You can integrate plots like the one created by the program above in your text. You can use the Linux command "ps2pdf" to convert your PostScript document to a pdf document suitable for posting on the internet. An example of this, showing the graph produced by the above program is the PDF Example at this website. Another alternative is to convert it to a png file. Temp1.eps was read by the linux program xfig, and exported as a png file that can be read by the newer browsers. The result is shown above.
If you will not need to know how to do mathematical programming then skip to the next section.
Oberon does not have the built in mathematical functions of FORTRAN. The language does not even have elementary transcendental functions. However, any implementation will probably provide at least these, certainly oo2c does. This should not stop you from doing mathematical programming. You can provide your own math library without much difficulty. The book "Handbook of Mathematical Functions" by Abramowitz and Stegun contains formulas for all the math functions you could ever want. The following module provides basic complex arithmetic.
MODULE cxarith; IMPORT rm:=RealMath; TYPE complex* = RECORD r*,x*:REAL END; VAR one-,zero-,jone-,mone-:complex; PROCEDURE neg*(z1:complex;VAR z2:complex); BEGIN z2.r:=-z1.r;z2.x:=-z1.x END neg; PROCEDURE conj*(z1:complex;VAR z2:complex); BEGIN z2.r:=z1.r; z2.x:=-z1.x END conj; PROCEDURE add*(z1,z2:complex;VAR z3:complex); BEGIN z3.r:=z1.r+z2.r;z3.x:=z1.x+z2.x END add; PROCEDURE sub*(z1,z2:complex;VAR z3:complex); BEGIN z3.r:=z1.r-z2.r;z3.x:=z1.x-z2.x END sub; PROCEDURE mult*(z1,z2:complex;VAR z3:complex); BEGIN z3.r := z1.r*z2.r-z1.x*z2.x; z3.x := z1.r*z2.x+z2.r*z1.x END mult; PROCEDURE rmult*(r:REAL;z2:complex;VAR z3:complex); BEGIN z3.r:=r*z2.r;z3.x:=r*z2.x;END rmult; PROCEDURE div*(z1,z2:complex;VAR z3:complex); (*z3:=z1/z2*) VAR h:REAL; BEGIN (*the line below must be modified to look like it does in your browser*) IF (ABS(z2.r)>ABS(z2.x)) THEN h:=z2.x/z2.r;z2.r:=h*z2.x+z2.r; z3.r:=(z1.r+h*z1.x)/z2.r; z3.x:=(z1.x-h*z1.r)/z2.r ELSE h:=z2.r/z2.x;z2.x:=h*z2.r+z2.x; z3.r:=(h*z1.r+z1.x)/z2.x; z3.x:=(h*z1.x-z1.r)/z2.x END END div; PROCEDURE cxr*(r:REAL;VAR z:complex); BEGIN z.r:=r;z.x:=0;END cxr; PROCEDURE cxj*(r:REAL;VAR z:complex); BEGIN z.r:=0;z.x:=r;END cxj; PROCEDURE cpx*(r,x:REAL;VAR z:complex); BEGIN z.r:=r; z.x:=x END cpx; PROCEDURE abs*(z1:complex):REAL; VAR h:REAL; BEGIN z1.r:=ABS(z1.r);z1.x:=ABS(z1.x); (*the line below must be modified to look like it does in your browser*) IF z1.x>z1.r THEN h:=z1.r;z1.r:=z1.x;z1.x:=h END; IF z1.x=0.0 THEN RETURN z1.r ELSE RETURN z1.r*rm.sqrt(1.0+(z1.x/z1.r)*(z1.x/z1.r)) END END abs; PROCEDURE expj*(x:REAL;VAR z:complex); (* e^jx *) BEGIN z.r:=rm.cos(x); z.x:=rm.sin(x) END expj; PROCEDURE exp*(z1:complex;VAR z2:complex); VAR x:REAL; BEGIN x:=rm.exp(z1.r); z2.r:=x*rm.cos(z1.x);z2.x:=x*rm.sin(z1.x);END exp; PROCEDURE sqrt*(z1:complex;VAR z2:complex); VAR h:REAL; BEGIN h:=rm.sqrt((ABS(z1.r)+abs(z1))/2.0); IF z1.x#0.0 THEN z1.x:=z1.x/(2.0*h)END; (*the line below must be modified to look like it does in your browser*) IF z1.r>=0.0 THEN z1.r:=h ELSIF z1.x>=0.0 THEN z1.r:=z1.x;z1.x:=h ELSE z1.r:=-z1.x;z1.x:=-h END; (*the else part of the following if statement adopts the convention that zero to pi, rather thanzero to plus or minus half pi, is the principal root*) (*the line below must be modified to look like it does in your browser*) IF z1.x>=0.0 THEN z2.r:=z1.r;z2.x:=z1.x ELSE z2.r:=-z1.r;z2.x:=-z1.x;END; END sqrt; BEGIN one.r:=1.0;one.x:=0.0; zero.r:=0.0;zero.x:=0.0; jone.r:=0.0;jone.x:=1.0; mone.r:=-1.0;mone.x:=0.0 END cxarith.
Note that any other module which imports cxarith could shorten the name, as "IMPORT cx:=cxarith". This way, complex multiply would be called as "cx.mult(x,y,z);".
To see results produced with this complex arithmetic module click here.
Some people, like myself, may find all the capitilization required in Oberon to be inconvenient. There is an easy solution. Write the program in lower case. Create a separate file called "caps". The version of caps that you see below looks correct in your browser, but the embedded html code is not correct. For a useable version click on caps.txt. On linux systems change the name from caps.txt to caps. Note that there is a minor insurmountable problem: "char" becomes "Char" in the library, and "CHAR" in the language.
%s/\<import\>/IMPORT/g %s/\<module\>/MODULE/g %s/\<in\>/In/g %s/\<out\>/Out/g %s/\<ln\>/Ln/g %s/\<line\>/Line/g %s/\<string\>/String/g %s/\<procedure\>/PROCEDURE/g %s/\<const\>/CONST/g %s/\<type\>/TYPE/g %s/\<longint\>/LONGINT/g %s/\<entier\>/ENTIER/g %s/\<div\>/DIV/g %s/\<real\>/REAL/g %s/\<begin\>/BEGIN/g %s/\<end\>/END/g %s/\<var\>/VAR/g %s/\<for\>/FOR/g %s/\<do\>/DO/g %s/\<if\>/IF/g %s/\<then\>/THEN/g %s/\<else\>/ELSE/g %s/\<case\>/CASE/g %s/\<of\>/OF/g %s/\<to\>/TO/g %s/\<boolean\>/BOOLEAN/g %s/\<true\>/TRUE/g %s/\<false\>/FALSE/g %s/\<or\>/OR/g %s/\<array\>/ARRAY/g %s/\<char\>/CHAR/g %s/\<chr\>/CHR/g %s/\<repeat\>/REPEAT/g %s/\<loop\>/LOOP/g %s/\<halt\>/HALT/g %s/\<exit\>/EXIT/g %s/\<until\>/UNTIL/g %s/\<while\>/WHILE/g %s/\<abs\>/ABS/g
Use the vi editor to edit your Oberon program. When you type "vi" you will get either of two versions of the vi editor, depending on how your system is set up. The versions are nvi and vim. You need vim for this, so type "vim" rather than vi, if you are not sure which is the default on your system. Then in the vim editor, when in the edit mode, not the input mode, type ":so caps". You may need to hit the space bar several times before the process is finished. When done, all the proper key words will be capitalized automatically.
Managers and professional experts who don't write much code make a big fuss over the format of programs. This is nonsense. Usually it is best to use one line per statement. But sometimes more than one statement on a line enhances readability in a program for the same reason that it does in English prose. Skipping lines between procedures enhances readability for the same reason skipping lines between paragraphs does in English. Fancy indenting schemes help a lot only if you write procedures that are much longer than you should be writing. The real keys to readability are taking the time to concentrate hard on the names you call things and most importantly thinking and rethinking how you subdivide the problem into procedures. Write comments explaining the purpose of each procedure if the name of the procedure does not make it obvious.
If the same block of code is repeated in different places, extract it into a procedure that is called from different places. Elimination of repeated code means that there is less code to read to understand the program, and less code to change if changes must be made.
The programmer should be aware of how to use flowcharts, dataflow diagrams and other conceptual tools, but he should not be forced to use them where he does not feel they would do him any good. This would be analogous to requiring a mechanic to use a particular wrench for everything he does in fixing a car. The author wrote a 44 page program where dataflow diagrams and structure charts seemed to help only at the highest level. Flowcharts would have been worse than useless for all but one page of the program, and that page would never have been figured out without flowcharting. Informal essays describing each major section yet to be programmed were very useful to get the ball rolling, but proved to be less than 50% accurate after the section was completed, and needed to be rewritten to properly document the program. I do not have any experience in a team all working on the same program. In such a situation more use of formal procedures is probably appropriate.
Another area where irrational dogmatism has come raining down on the poor programmer is global variables versus parameter lists. Some authorities virtually rule out the use of global variables, claiming parameter lists should be used to pass every variable to every procedure or function. This is nonsense. Such a rule greatly discourages subdividing the program into small procedures to enhance readability and modifiability. If the program is about say, taxpayers, and a taxpayer record is used throughout the program, then it should be global to the whole program and should seldom appear in a parameter list. If a procedure will be called several times with different arguments, these should be passed through a parameter list. If the argument is the same every time, putting it in a parameter list may or may not significantly improve the readability of the program, and should be at the discretion of the programmer.
There are considerations for large modules that don't arise in small ones.
First, there may be a page or more of global type and variable declarations. One frequently needs to refer to these in writing and debugging the module, and it can be difficult to find one if they are all lumped together. The module can usually be considered to have major sections, or at least major topics, even if these topics are not lumped form sections. The TYPE and VAR declarations should each be subdivided into paragraphs labeled by comments according to section or topic, to make it easy to find what you are looking for and easy to understand the meaning of the module.
In a large program you cannot wait until the whole thing is finished to see if you are getting right answers to intermediate steps. It will save much debugging time to write otherwise unnecessary procedures whose sole purpose is to write out intermediate data in an understandable form to check the program as it is being written, well before it is finished.
Large programs are the primary justification for learning objects, which are provided in Oberon-2 but are not covered in this article.
Converting old Pascal programs to Oberon-2 is not hard. The following hints will be helpful:
array[100,100] --> ARRAY 100,100 if...then...; --> IF...THEN...END; if...then begin...end else begin...end; --> IF...THEN...ELSE...END; else if --> ELSIF <> --> # integer --> LONGINT while...do begin...end --> WHILE...DO...END for...to...do begin...end; --> FOR...TO...DO...END; case...of...:...;...:...;end; -->CASE...OF...:...|...:...END; function func(...):...; begin...func:=...;end; --> PROCEDURE func(...):...; BEGIN...RETURN...;END func; round -->ENTIER
The file "caps" in the previous section may be modified to help with conversion of Pascal to Oberon.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.