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. In saying this it should be noted that there is a commerical product called Pascal-5 that is entirely different from Oberon-2. 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 wonder whether linux is a good operating system, note that Microsoft, which sells Windows, runs their Bing search engine on linux, not on Windows or Mac OS. If you wanted to write a program to sell to Windows users, it would be better to program in the Windows environment. But if you want to write a program to do useful work, the linux environment is much better. That is why Microsoft used linux for their search engine. 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 should work under MacIntosh OS X since MacOS is also a clone of unix like linux is, but I have no experience installing it under MacOS. 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.
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.
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.
In 2007 Wirth, in keeping with his philosophy of software engineering, proposed an improved version of oberon with minor changes called oberon07. Unfortunately, the only compiler available is for the ARM chip. Perhaps another version will someday become available. Certainly any new compiler written after this point in time should be in accordance with the new standard. Anyone who feels inclined to write a new compiler should consider Wirth's book "Compiler Construction".
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. It used to be hard to do this kind of installation, but now it is easy. Here is how I did it. Try it at your own risk. Backup your personal files onto a USB stick or blank CD, because they may not be safe during the installation procedure. Go to www.whatismyscreenresolution.com and write down your screen resolution.
Debian linux can be installed over the net starting with a "netinst" CD that you download and burn yourself. Or, you can get a set of CD's from a vendor listed on the Debian website and install it entirely from CD's. What follows is the installation procedure using the netinst CD. This works best if you have a high speed ethernet connection. Two alternatives for installation are presented, so you should read the whole thing before you start lest you get the two alternatives mixed up. If you have a new netbook computer, the ethernet chip may not be compatible with linux, in which case you need special instructions that you can find if you click here. If you have an older, tiny netbook, your ethernet chip will work but your disk space will be so small that you will need special instructions found at the end of this section.
The linux installation sofware is in an iso file. Windows does not have the capability to write an iso file to a CD in the correct manner. This capability can be added. An iso file could be written like any other file, or it can be written in a special way that iso files are intended to be written, which is what you want. If it is written like an ordinary file, you will see the iso file on the CD. If it is written correctly you will see the files that were contained in the iso file. Go to http://isorecorder.alexfeinman.com/isorecorder.htm and be very careful to click on the version for your particular version of windows. When asked to run or save click run. Pop up windows will ask if you really want to do this, you do. When installation is finished reboot to make sure it takes effect.
Your Windows files may be scattered all over the disk. You need to tidy up your disk and move all Windows files to a neat group at the beginning of the disk, so that the last half of the disk will be free to install linux. You must "defragment" your disk. To do that, when in Windows XP, click on "my computer", right click on C drive, click on "properties", "tools", "defragment now". Defragmentation may take a while.
Go to debian.org, download the free 180 Mb "netinst" iso file. If your PC runs 32 bit windows, you will need the "i386"version. If your PC runs 64 bit windows, you will need the "AMD64" version even if your PC has an Intel chip, not an AMD chip. Click on the appropriate link in the "small CDs" section at www.debian.org/distrib/netinst. Download the iso file to "my documents". Insert a blank CD into your machine to write the iso file onto. The following detailed proceedure was performed on Windows XP, but will probably be similar on other versions of Windows. Only minor changes were necessary in Windows 7. Go into "my documents" and right click on the iso file. In the popup window click on "copy image to CD". The "CD recording wizard" pops up. Under "source", "image file" should be checked. Click "next", then you will see "recording", then "closing disk", then the CD will eject. Push the button to put the CD back in, then click "start" "turn off computer" "restart". You should see the Debian install screen when the computer restarts. There is no risk to your system to go this far, as nothing is written to your disk until the installation process gets to the "partitioning" phase and you deliberately start the writing process.
The easiest way to install linux on an existing windows system is to reduce the size of the windows partition during the linux installation and install linux in the new free space. The first time you reboot after this, windows will insist on doing a file system check and reboot. This will require two reboots of your windows system to get windows working normally again. But suppose you are afraid that if you mess up, you might ruin windows, not get linux installed, and not have a working computer? This is not likely, but you should know how to install windows from scratch. The next paragraph describes the alternative of installing windows from scratch instead of simply reducing the size of the windows partition. You may feel safer knowing how to do this, so here it is.
The alternative is to do a fresh install of windows followed by a linux installation. If you want to do a fresh windows installation, find your windows CD and the associated product key number, which you must have to install windows. You will also need any device driver CD's that were supplied when you bought your computer. 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. When it says "press any key", do so quickly, or it will not do the installation, it will just boot back into windows. 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. After the basic installation insert the driver CD's to bring your system up to its full functionality. If you do not have a driver CD for some part of your system, you can probably find a free download on the net. Save it in "my documents", then double click on it to install the driver. Remember to activate your windows system after you finish installing it.
When you are ready for the linux installation put in the linux net install CD and shut down the computer. Windows should be fully up and running and you should do a normal shutdown. If Windows is only partly powered up when the power is turned off, the file system may be left in a condition where the following procedure will not work. The computer must be connected to the internet, as most of the software is not on the CD, and will be downloaded from the internet. You may not need driver CD's for linux, because most of the necessary drivers will be downloaded automatically. If you have a new model of graphics card, it will work with the default driver in linux, but for best results you may have to go to the manufacturers website to find a free linux driver to download for it. More about this later. Turn on the computer with the linux CD in the drive. During the installation process you will be asked questions. Use the up/down arrow keys to move the cursor vertically, the tab key to move horizontally, the enter key to accept a choice and the spacebar to select a choice. You will need to enter your name, and make up your user name root name and two passwords. Root user is also sometimes called superuser. 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 an empty partition. If you have done a fresh install of windows there is already an empty partition.
If the original windows installation still occupies the whole disk, you will need to reduce the windows partition. Windows will be in an "ntfs" partition. If there is a small one and a large one, Windows will be in the large one. When you get to the partitioning part of the linux installation, select "manual" partitioning, use the down arrow key to highlight the windows "ntfs" partition, hit "enter", select "resize partition", enter "50%" for the new size. Then "write previous changes to disk and continue"--yes. This will reduce the size of the ntfs partition and create a free partition of equal size. If you get an error message and the installer cannot resize the partition, you may have not done a normal shutdown of Windows before the installation.
Once you have a free partition after the ntfs partition, select "guided partitioning", then "use the largest continuous free space", and "all files in one partition", "write to disk". When you get to task selection, use the spacebar to select "gnome desktop" and "standard system". Just accept defaults for the rest of the installation.
Now both windows and linux are on the computer. When you start up the computer you have a few seconds to choose between linux, linux single user mode, 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. To do this, use the linux synaptic package manager to download the "grub-choose-default" package and run it in a terminal window. In linux click on "system", "administration", "printers" "edit" "+ new printer" to configure your printer.
If you reduced the size of the windows partition without re-installing windows, the first time you boot into windows it will want to do a file system check, which you should let it do. It should only do this one time. If it asks for a file system check every time you boot into windows, something is wrong. To fix it, put the windows CD in the drive and boot into the CD. DO NOT select "install", instead select "repair the system". This should fix the problem.
The blue disk in the upper left side of the linux screen activates your browser. After installation, go to the previously mention website and check your screen resolution. If it is less than before, linux did not automatically install the right driver for your video card. If this is the case, reboot into linux single user mode, a black screen with white text. When the bootup text stops scrolling, there may not be a prompt, but enter your root password, then you will see the "#" prompt. Enter "cd /etc/X11", then "cp xorg.conf xorg.conf.orig", then "cd /". Enter "Xorg -configure". Skip the offered chance to test, which is hard to get out of. Enter "cp /root/xorg.conf.new /etc/X11/xorg.conf". Note that there must be one or more spaces between "cp" and "/", and between "new" and "/". Then "shutdown -r now". Then reboot into the normal linux mode. Hopefully your screen resolution will be correct now. If not, you need to install the manufacturer's driver as described in the next paragraph.
Now a comment on graphics issues. If your grapics card is not of an entirely new type, linux probably already has a software driver for it, which will be loaded automatically. But if your graphics card is a new design, linux will load the default "vesa" driver for it. The default "vesa" driver in linux will work after a fashion with any graphics hardware. However, it assumes a sreen with a 3:4 height:width ratio. Many modern screens are wider than this compared with the height. This may result in part of the screen being chopped off or in a distorted pixel ratio. On a small netbook with a wide screen, the bottom fourth of the picture was chopped off, but the pixel ratio was fine. This is probably acceptible for most people, and nothing more needs to be done. On a desktop with a wide screen monitor, nothing was chopped off but the pixel ratio was distorted. This meant that a circle looked like an ellipse. Furthermore, on the desktop computer, "logout" and "shutdown" both hung with a black screen. To fix all this, the linux software driver found at the website of the manufacturer of the graphics card, Nvidia, needed to be installed. The driver was downloaded to the "Download" directory. The linux driver install was done with a DVI cable, not a VGA cable, connecting the monitor to the computer. Windows needed a DVI cable to work properly with a wide screen monitor. The driver installation required that the correct version of the kernel headers be downloaded. Do "ls /boot" to see the precise version number of vmlinuz that you are using, because you should select the kernel headers with the same number. Using the synaptic package manager get the "linux-headers-2.6.26-2-all", "g++", and "make". Then shutdown and reboot selecting the single user boot option, the second line at the initial boot. The single user option gives only a black screen with white letters, not the Gnome desktop. The Nvidia driver will not install itself while the Gnome desktop is running. When the scrolling stops, enter the root password and the "#" prompt will appear. Then "cd /home/(yourname)/D*/D* to get into the Download directory, "ls" to see the driver you downloaded. Then "sh N*" to install the Nvidia driver. When the installation is complete, enter "exit" to leave superuser mode and boot into the gnome desktop.
Now that you are finsished with the linux installation, you should go to debian.org, click on "debian packages", "view package lists", "view packages in the stable distribution", and look through all the free software available from debian.
If you have an old computer or a tiny netbook with small disk space there will not be enough disk space for both Windows and linux, and you will have to install linux alone to run oo2c. You will need a separate plug-in USB CD drive to install linux on a tiny netbook. For instance, on a computer with only 4Gb of disk space, a full linux installation with oo2c will occupy 95% of the available disk space as shown by the command "df". Therefore you will need to do a skimpy linux install to save space. Do not select "gnome desktop" when you get to that step. Continue the rest of the installation as described above. When the installation is complete, you will have only a black screen with white text. To fix this, as superuser "apt-get install gnome-core gdm synaptic". When this is finished, shut down as superuser by "shutdown -h now". Upon reboot you will have a blue screen with icons. Open a terminal window, as superuser enter "synaptic", and search for and get epiphany-browser, unzip, vim and ghostview, which will appear in the list simply as gv. Now you will have a browser. After installing oo2c as described below, df will show only 51% of disk space used, but you will not have wifi or openoffice, just ethernet internet capability. Other useful programs that do not take up much space that you might want to install are xfig, xfig-doc, imagemagick, mmv and htmldoc, which will bring the total up to 53%.
If you are new to linux, read the article how to use linux.
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 "gc: /usr/bin/gc", which shows that you do indeed have gc somewhere on your computer. If it just shows "gc:", then you do not have it. 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, to find it with "whereis" you must ask for simply "gc".
Make a directory called "prog". "cd prog" to get in the directory prog. In the directory "prog", "mkdir sym obj bin src" to make four new directories under prog. Go to the sourceforge website listed above and download the oo2c software, oo2c_32-2.1.11.tar.bz2. This is the 32 bit version, indicated by 32 in the name. If you have a 64 bit computer you will need the 64 bit version. It will probably initially download into ~/Desktop/Downloads directory, but you should move it into the directory "prog". "tar xvfj oo*" to unzip the software. You will now, in addition to the tar file, have a new directory oo2c_32-2.1.11 . You now have both files and directories in your prog directory. To see the difference, do "ls -aF". Now "cd oo*" to get into the directory oo2c_32-2.1.11 . If you are running MacOS you will need to read additional instructions found in this directory. In that directory do "./configure ", and some text will quickly scroll by. When this is done, you will need to use the "su" command and enter the password to become root or superuser, and the prompt symbol will change from "$" to "#". As superuser do "make install". Lots of text will scroll by over a period of minutes, and your computer fan may speed up. Next, still as superuser, do "ldconfig", then "exit" so you are no longer superuser. In addition to any software already there, oo2c files will be installed in /usr/local/bin and /usr/local/lib.
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 "prog", 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.
From this point on, many small programs will be presented as examples. In the beginning you will not understand why each program is written the way it is and not some other way. But you should at least be able to read each program to get some understanding of why it does what it does, and does not do something else. I will usually provide some explanation, but my explanations are only hints; the important thing is to read the program. Each program is to some extent self explanatory, and you must read each program carefully to understand as much as you can about how it works. Later, when you want to write your own program, you will have some notion of how to do it, and will know what details you need to look up in more detailed and tedious descriptions of the language.
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 words in the oo2c library have the first letter of every word capitalized. Thus, while LONGREAL is an Oberon word, and LongReal is a library word, you could still use longreal as a user defined word if you wanted to.
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 "prog" and the program is in the directory "prog/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, use the vi editor to 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. Do NOT make the mistake of typing "./ooc nothing.Mod", leave off the "Mod" when compiling. If you become superuser and move the ooc script to /usr/local/bin, then the leading "./" is no longer necessary and you can just enter "ooc nothing".
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. Note that we execute the program by "bin/nothing" because we are in the directory above "bin". If we were in the directory "bin" we would execute the program by "./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 next example in this section will be to compute the number of meters and the number of yards around the inside lane of a quarter mile oval running track. We use the facts that one meter is 100 centimeters, one inch is 2.54 centimeters, one foot is 12 inches, one yard is 3 feet, and one mile is 5280 feet. A nautical mile is different than a statute mile; here we are talking about a statute mile. We start with one centimeter, and build up our units of measure from there.
MODULE mile;
IMPORT Out;
VAR mi,qmi,cm,in,ft,yd,m:REAL;
BEGIN
cm:=1.0;
m:=100*cm;
in:=2.54*cm;
ft:=12*in;
yd:=3*ft;
mi:=5280*ft;
qmi:=mi/4.0;
Out.String('a quarter mile is:');Out.Ln;
Out.RealEng(qmi/m,20,6);Out.String(' meters');Out.Ln;
Out.RealEng(qmi/yd,20,6);Out.String(' yards');Out.Ln;
END mile.
a quarter mile is:
402.336 meters
440.000 yards
Now what have we done in this program? We decided what variables we needed to do the work, and we declared them before BEGIN. Then we described the work to be done with a series of statements, ending with clear output statements to present the results. In this example the work used numbers. In some other example it might use characters, bits, or a user defined type such as complex numbers, personnel records, coordinates on a diagram, lines to be drawn or whatever types you can devise using the built-in types as building blocks.
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 declarations before BEGIN are information needed to execute the statements after BEGIN, which is where execution of the program starts. Each statement ends with a semicolon ";". The names of variables and constants in this program are a, b, and c. These user defined names must have a letter, not a number, as their first character, but they can be many characters long. In this program the "=" symbol when used by itself represents a definition, a=3. Thus "a" is a constant and can never change its value. The mathematical constant pi=3.14159 would be a good candidate for this kind of treatment. The combination symbol ":=" means a variable is assigned a new value. As an example, if your bank account is "b" and you write a check for ten dollars thus reducing your bank account by ten dollars, you would represent this by the statement in English "b is b minus ten" or in Oberon, "b:=b-10.00;". In this case you would define your bank account to be a REAL variable, not LONGINT, so that it could contain decimal fractions of a dollar.
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 oo2c library 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". Add, subtract and multiply are represented by "+", "-" and "*" for both real and integer types. While raising a number to a REAL power is not directly provided in the language, it can be done with a function provided in the RealMath library module. 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. A CHAR variable takes on the value of an ascii character, such as "a", "2", "&", etc. A BOOLEAN variable takes on the values TRUE or FALSE.
Complicated statements can be simplified by the use of parentheses. Thus "x:=(a*b)/(c+d);" means that the computations in each pair of parentheses "(...)" will be done before the "/" outside of the parentheses. Parentheses can be nested "(...(...))" to handle even more complexity, in which case the inner parentheses is computed before the outer.
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 the primitive programming language 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 the programming language 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. It can take on any type of single value. Here we use LONGINT, but it could also be REAL, CHAR, BOOLEAN, etc. 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. To see a non-trivial example of the use of this technique, in the filter example article at this website click on the "zip file" link and unzip the file. To go to the other article click here.
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
Between DO and END(*FOR*) we put only one statement, but any number of statements could go there. 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. Even in the case of an ordinary variable that is not part of an array, if it is declared, but then used without being intitialized, the oo2c compiler will flag it with an "undefined variable" warning.
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 an example 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.
The input/output functions rely heavily on the oo2c library. The library is not nearly as self explanatory as the language. Therefore, it will not be obvious why everything is done the way it is, you just have to accept it and use it as an example if you ever need to do something similar yourself. The library manual provides additional explanation, and the source code for the library provides even more.
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.
Since there is no type checking when reading files this way, the bit pattern of any type of file can be displayed, whether integer, real, text or executable code.
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
Suppose you want to read a file of unsigned 8 bit integers. The Oberon type SHORTINT refers to 8 bit signed twos complement integers. You would read the file as SHORTINT, then using the function LONG assign the value to a 16 bit INTEGER variable, then say if the integer is less than zero it equals 256 plus itself. That would convert the twos complement value to an unsigned integer value. If, after performing appropriate arithmetic on the data you wished to write it out again as a file of unsigned 8 bit integers, you would need to perform the inverse operation using the function SHORT before writing out the data.
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. In a more typical case, you would know in advance what type each item would be, as in our earlier example "redfil1". In that case, you would use built in functions in the OOC library to read each type. 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.
Sometimes the compiler issues misleading error statements. If the symbols comma (,), semicolon (;), or colon (:) are interchanged or missing the compiler may complain about missing END words. Single character errors are difficult to see. You should deliberately sabotage a working program with several different single character errors, one at a time, to get a feel for what error messages might mean.
An important mistake to avoid is using the ".Mod" suffix when compiling. If you have a program test.Mod, "./oocm test" will compile it, but "./oocm test.Mod" will not, and will not give you any warning. You may think your corrections have been ignored by the compiler, but your corrections were never compiled because you added the Mod suffix by mistake when you compiled your corrections.
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:=(5*cycle/100)*i+xorig;
y:=rm.exp((x-xorig)*(-0.46/cycle))
*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
('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.

The program writes out the file "temp1" to disk, and then invokes the system commands "gs" and "lpr" to send "temp1" to the screen, then to the printer. System commands are really just programs that are located in places where they can be run just by typing their names, without the user knowing where they are located. In this program the ProcessManagemet statements invoke external programs just as you would typing at the keyboard. Thus the "gs" and "lpr" programs are used by this program the same way you would use them.
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. You will need the packages "gv" and "netpbm" installed on your system to do this. Using the command "gv temp1" you can move the cursor to find the lower left and upper right bounds of the figure. Then using the vi editor add the following line as the second line in temp1: "%%BoundingBox: 156 368 464 584". Now 464-156=308, the x-extent, and 584-368=216, the y-extent. Now "pstopnm -xborder=0 -yborder=0 -xmax=308 -ymax=216 -stdout temp1 > temp1.pnm". Then "pnmtopng temp1.pnm > temp1.png". This is discussed at length at http://mintaka.sdsu.edu/GF/bibliog/latex/PSconv.html. A faster way to determine the bounding box is to use the command ps2epsi, which produces a version of the postscript file with the epsi suffix, line 8 of which is the closest possible bounding box.
Note that the above example used the Oberon programming language to write a file in the Postscript graphics language to be displayed by the Ghostscript graphics viewer. Similarly, the Oberon programming language could write files in any other graphics language for display by the apropriate viewer. Other viewers for other graphics languages are available in linux. In Debian linux, the artistic graphics programs are listed under graphics, and the graphics programs for plotting mathematical functions are listed under mathematics in the list of debian packages. What follows is an example of the same program shown above re-written to use the gnuplot graphics program. We did not have to do as much detailed work in this example.
MODULE graf2;
IMPORT In,Out,Msg,Files,TextRider,
OS:ProcessManagement,rm:=RealMath;
TYPE str=ARRAY 80 OF CHAR;
VAR str1:str;outvar:Files.File;resw:Msg.Msg;
outfile:TextRider.Writer;
PROCEDURE initfile;
BEGIN
outvar:=Files.New('temp2',{Files.write},resw);
outfile:=TextRider.ConnectWriter(outvar);
END initfile;
PROCEDURE drawcurve;
CONST xorig=0.0;pi2=6.3821;ampl=1.62;cycle=1.0;
yorig=0.0;
VAR i:LONGINT;x,y:REAL;
BEGIN
FOR i:=0 TO 100 DO
x:=(5*cycle/100)*i+xorig;
y:=rm.exp((x-xorig)*(-0.46/cycle))
*ampl*rm.sin((pi2/cycle)*(x-xorig))+ampl+yorig;
outfile.WriteRealEng(x,10,4);outfile.WriteRealEng(y,10,4);
outfile.WriteLn;
END;
outvar.Close;
END drawcurve;
PROCEDURE cmdfile;
BEGIN
outvar:=Files.New('cmd',{Files.write},resw);
outfile:=TextRider.ConnectWriter(outvar);
outfile.WriteString('set yrange [0.0:3.5]');
outfile.WriteLn;
outfile.WriteString('set style line 1 lw 2 lt -1');
outfile.WriteLn;
outfile.WriteString('set terminal postscript portrait size 5.15,3.57');
outfile.WriteLn;
outfile.WriteString('set output "temp2.ps"');
outfile.WriteLn;
outfile.WriteString('plot "temp2" with lines ls 1');
outfile.WriteLn;
outvar.Close;
END cmdfile;
PROCEDURE viewplot;
VAR i:LONGINT;
BEGIN
cmdfile;
i:=ProcessManagement.system("gnuplot cmd");
i:=ProcessManagement.system("gs -sDEVICE=x11 temp2.ps");
END viewplot;
PROCEDURE printplot;
VAR i:LONGINT;
BEGIN
i:=ProcessManagement.system("lpr temp2.ps");
END printplot;
BEGIN
initfile;drawcurve;
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 graf2.

We have now seen two ways to plot the same graph. The first way is more general, and could have been used to draw a building or the artwork for a printed circuit board. The second way is more convenient to illustrate numerical data. The gnuplot program can draw curves, surfaces, or various kinds of charts.
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, in the module "RealMath". 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.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.