HOW TO PROGRAM A COMPUTER

Using the oo2c Oberon-2 Compiler

by Donald Daniel

2001

revised Sep 2023

Updated for version 2 of oo2c

www.waltzballs.org

CONTENTS

If the lines of text are too long you can fix the problem with these instructions.

up one level

INTRODUCTION

First let us be clear about what we mean by programming. Programming is not installing software that already exists. Programming is not replacing the old version of Windows on your computer with a new version of Windows. Programming is writing new software, not configuring existing software. If you adjust the width of your browser or the margins in your word processor, you are not programming it, you are configuring it. Similarly, if you configure a very complex database program so it will work a certain way, you are not programming it, even though the job is complex and many people who do this call themselves programmers. The people who created the complex database program, the word processor or the browser were programmers. The programming language taught here could have been used to create a database program, a word processor or a browser. You may never need to create large programs like these. But in many lines of work it is useful to create small programs to perform simple tasks for which there is no available program.

Programming a computer is having a written conversation with the computer to explain to the computer what you want the computer to do for you. The conversation is in a special computer language of very few words that the computer can understand. The language is mostly English words. The conversation is mostly one way. The only part the computer will play in the conversation is to let you know that you that you have miss-spelled a word or your wording or punctuation makes no sense and that it cannot understand what you meant. It is important that you be able to understand the language also. Some computer languages are easier for people to understand than others. The language taught here is the easiest that I know of for people to understand. It is also important that the language be able to express whatever you want to express. If a person only knew the words "eat, drink, water, food", the person might not starve do death but would not be able to accomplish anything else. Some computer languages are too simple to be useful. This language can express anything a computer can do.

There are hundreds of computer programming languages. Most of them have never been used for anything. Presumably most were created as a project to get a Ph.D. degree in computer science. Tricky obscure hieroglyphic languages that are widely used presumably provide employment insurance for a computer science priesthood, but do not provide an opportunity for retraining unemployed workers from other fields. The language taught here is the work of decades of work by Niklaus Wirth and his associates to create a language that is both user friendly and very capable.

For a quick example of what programming is all about, jump ahead to the example of the quarter mile running track.

On television, the author of the book "Finding the next Steve Jobs" said that he "would rather hire a self taught programmer than a PhD in computer science". So this self taught course may be your big chance. A full time professional programmer might be expected to know five different programming languages. But he should start with an easy language like the one taught here. An amateur programmer will only need one language, and an easy one would be best. But not too easy. The original version of BASIC was too easy, it was useless for anything more elaborate than counting from one to ten.

Some languages are interpreted, some compiled. Interpreted languages work immediately after the program is written. Compiled languages must be run through a program called a compiler which will create an executable version of the program which will work. But the compilation step takes only a few seconds. If a program does even a complicated thing only once, it does not need to run fast, and an interpreted language will be satisfactory. But if a program needs to do whatever it does a very large number of times, it needs to be fast. A compiled program might run typically 100 times faster than an interpreted program. But computers are so fast today that even if your program runs 100 times slower than the computer, that is still pretty fast. The language I use, oberon-2, is compiled and very fast.

The most popular compiled language is probably the version of C called C++. But it is not easy. When writing a program it is easy to make mistakes that are hard to find and it is hard to read. The language taught here is compiled, not tricky, easy to write and easy to read. But it is not popular and widely used because the industry standardized on C before it came along. The most popular interpreted language is probably python, and it is easier than C for people to read, but not as easy as the language taught here.

In the 1950's important early programming languages were invented that were very different from each other: Fortran, Lisp, Cobol and Algol. Fortran was for formula translation of mathematical formulas. It had more math functions than other languages. Lisp was for list processing. Cobol was for financial data processing. It was to duplicate the functionality of mechanical accounting machines such as the IBM 407 which weighed 2620 pounds or 1.3 tons. And cobol could not do much else. To see more on the IBM 407 see my: computer history. I have seen a lapel pin that said "we can stamp out cobol in our lifetime". Now the computer science profession feels that general purpose languages are a better idea than specialized languages. The only one of the bunch that was intended to be a general purpose programming language was Algol.

As new concepts were invented in computer science, new versions of old languages were introduced, and new languages were introduced. In 1967 the language simula introduced the programming feature known as objects, but it was a long while before other languages incorporated the concept. In 1970 Pascal was a modernized language based on Algol. In 1972 the C programming language was introduced. But there were several obscure languages developed for special systems of different kinds.

A large well funded effort was undertaken to develop a new general purpose language to replace the many languages in use. It was to be called Ada. It was to be based on pascal. The reason it was based on pascal was because pascal was more readable and user friendly than C. They wanted a language as readable as pascal but more capable than pascal or C. But the implementation was grotesque. The specifications for the language were defined by a committee representing several different languages. The committee could not agree. For instance, it had many different ways of declaring the same array, which was preposterous. Different programmers would learn different ways and could not read each other's programs that were also written in ada.

The first compiler I used in 1963 did not run on an operating system. But it is more convenient if a compiler runs on an operating system. Just as a house is built on land, a programming language runs on an operating system. There is no need for the operating system to be written in the same programming language as a programming language that runs on the operating system.

The source code of the operating system Unix was licensed to university computer science departments. This was their one chance to study the source code of an operating system. Unix was written in C. So if students were to study how the operating system was written, they needed to know C. Ada was a failure, so the industry standardized on C, and the improved version of C, C++. After that, Oberon, and soon after, Oberon-2, came out. But C++ was already the standard. Ada was such a humiliating embarrassment to the industry that nobody wanted to be seen using anything related to pascal, which oberon-2 is. But this was a small minded over reaction. Ada was very large and bloated but oberon-2 is very small and lean. Oberon-2 is as capable as the improved C, C++, but smaller and leaner, and much, much more readable and less tricky. Oberon-2 achieves the readability and user friendly goals that were the original motivation for the development of Ada.

The linux operating system was written from scratch as a free open source imitation of Unix. It has replaced Unix and been improved beyond Unix. It is written in C++ but you do not need to know anything about C++ to use linux. The oberon-2 compiler used in this course runs on a version of linux called Ubuntu linux.

All of the software used here can be downloaded free from the internet and costs absolutely nothing. The only background you need to do everything presented here is the education of a typical 18 year old. Only a small portion, trigonometric functions and complex numbers, requires that much education. So many instructions are presented here that you will make mistakes and have to do some of the work over again. That is normal. If you are determined and persistent you will succeed. If you cannot stand to do anything where you will make mistakes, you should consider janitorial work instead. If you cannot run a mile that means your body is out of shape. If this course seems too difficult in the beginning, that means your mind is out of shape. Your mind can get out of shape just like your body can. By the time you master this material your mind will be in much better shape. If you are a novice, you cannot expect to understand everything the first time you read through this article. But if you keep experimenting with trying to write different kinds of small trivial programs just to see if you can get them to work everything will eventually become clear to you.

A computer memory is a single long list of "binary words". Each word has an address that specifies where it is in the list. If each word consists of 32 binary bits, the computer is called a 32 bit computer. If each word is 64 binary bits, it is a 64 bit computer. You will need to know whether your computer is a 32 bit or a 64 bit computer so you can select the right version of software to install on your computer. What we have just described is the way software uses memory, which we can call the logical arrangement of memory. Sometimes the physical arrangement of the memory is different than the logical arrangement of the memory. The logical arrangement of the memory is what determines the right software to install. For instance one 64 bit computer had words arranged physically as 256 bit words. The central processing unit, also called the CPU or the microprocessor, will be described as 32 bit or 64 bit according to the logical arrangement of memory.

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 commercial 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. It has automatic garbage collection and inheritance. This introductory course does not cover objects, garbage collection or inheritance. Objects are covered in the advanced book "Object-Oriented Programming in Oberon-2" by Hanspeter Mossenbock published by Springer-Verlag. 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 sophisticated user who needed them for his particular machine. An example would be type conversions which violate type compatibility rules.

Even though this course does not cover any advanced capabilities of oberon-2 except procedure types, it is very useful. With only the concepts covered in this course I have written a text formatter, a two-port frequency domain modelling package for electric circuits, a nodal frequency domain modelling package for electric circuits, a time domain pole-zero filter design and test package, a printed circuit board auto-router, a program to filter hits that are of no interest from a website access log, a line and curve drawing package and pattern programs for clothes, and miscellaneous smaller programs.

This article is based on the oo2c implementation of Oberon-2. It runs on Intel chips, AMD chips, and other chips that support the operating systems that it runs under. It is available in both 32 bit and 64 bit versions. It runs under unix or linux, and possibly under MacOS and other unix based operating systems, but I have only tried it on linux. It does not work on all versions of linux. For instance, it works on Ubuntu linux but not on Red Hat linux. It has always worked on Debian linux, but not with the recent version 10 release which probably has a bug in it. A separate very short article in this website explains for the beginner how to use linux. Instructions are given in a later section to add linux to the windows that is already on your hard drive. 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. A much higher percentage of linux users do programming than the percentage of Windows users who do programming. Linux is an environment that appeals to programmers much more than Windows. The linux environment is much better. That is why Microsoft used linux for their search engine. Decisions were made in the design of windows that makes it easier for a non-programmer to use. Decisions were made in the design of linux that makes it easier for a programmer to use. Windows and most of the software that is used on windows costs money. Linux and most of the software used on linux is free. If you have a windows computer, a bit later a description will be given of how to install linux for free on your windows computer. Linux can be loaded on most, but not all, computers that run windows. 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. I have only used it on linux. 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 which executes about as fast as any other native code. For other implementations of oberon see www.modulaware.com/zel/oberon/osci.htm . For comparisons with C see http://www.modulaware.com/zel/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. Instruction is available free online. A textbook for the original Oberon language is at https://inf.ethz.ch/personal/wirth/Oberon/PIO.pdf. This text only describes 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 in a previous paragraph. Another very valuable book by Wirth is "Algorithms + Data Structures = Programs". It is available at http://people.inf.ethz.ch/wirth/AD.pdf 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, except for "cxarith.Mod" which is issued under the GNU public license.

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.

Anyone who feels inclined to write a new compiler should consider Wirth's book "Compiler Construction". The oo2c compiler uses the additional compiler technology described in "Optimizing Compilers for Structured Programming Languages" by Marc Michael Brandis, ETH Zurich dissertation number th11024.ps. It can be downloaded free from ftp://ftp.inf.ethz.ch/doc/diss/th11024.ps.gz.

In 2007 Wirth, in keeping with his philosophy of software engineering, proposed an improved version of oberon with minor changes called oberon07. Unfortunately, a compiler with an extensive library does not, and may never, exist. So for now practical general purpose programming is better done on Oberon-2.

INSTALLING LINUX IN ADDITION TO WINDOWS

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. It is free to install it yourself, but if you are afraid to try you can pay your local computer repair shop to do it for you. Just make sure they install Ubuntu linux, not some other kind of linux.

When your turn on your computer, the software picks itself up by its own bootstraps. This is called booting up. It boots into windows, apple-os, or linux, depending on which operating system happens to be installed on your computer. It resides on your hard drive or solid state drive, whichever kind you have. But the operating sytem itself does not respond to the fact that you have turned your computer on. What does respond to the fact that you have turned your computer on is the BIOS, which stands for basic input output system. The BIOS does not reside on your hard drive. It resides in a chip on your motherboard. It is a tiny program that operates automatically when you turn the computer on. It is specific to your computer hardware, and not to which operating system software that is installed on your computer. There are different BIOS chips. One of its functions is to tell the central processing unit, the CPU, where to find the software that will boot the system up. It will tell the CPU to boot up from the hard drive. But the BIOS can be configured by the user to tell the CPU to try to boot up from a USB port or an optical drive before it tries to boot up from the hard drive. Some brand new computers have the BIOS already configured this way. But if your computer is not this way, you or your computer repair shop will have to configure it that way before you can install linux. This is because you will put the linux installation program on a CD or on a USB stick in order to boot into the installation program to install linux. If your BIOS is not already configured to allow booting from an optical drive or a USB stick, you will have to search the internet to find out how to get into your BIOS program and change it. It will not hurt anything to try to boot into the linux install program to see if it works. If it does not, only then should you try to find out how to re-configure the BIOS. Even if your BIOS is configured to allow booting from USB, depending on which BIOS you have, it may not happen automatically. You may have to repeatedly tap a special key to make a display to appear that asks you where you want to boot from. Search the internet for configuring your particular BIOS.

To find how to configure your BIOS, download the user manual for your computer from the manufacturer's website, or search for the website of the manufacturer of the BIOS. You may be able to see which BIOS you have when your computer first starts to boot up. To see a detailed example of configuring a particular BIOS click here.

Here is how I installed linux. Try it at your own risk. If you make a mistake, just start over and try again. Backup your personal files onto a USB stick or blank CD, because they may not be safe during the installation procedure. I have never had personal files damaged, but if you made the wrong mistake it would happen.

I use a "composition book", which is a notebook 9.75 inches by 7.5 inches with pages blank except for horizontal lines to write on. Something like this should be available in the school supplies section of your local grocery store. During this procedure write down everything that you do as you do it. If something goes wrong in this complicated procedure you will know where you need to do better the next time you try it. After the installation of linux, document every additional piece of software that you install. This will serve as a logbook for your computer. Leave the first 10 pages of the book blank for a table of contents that you will add later. Put a page number in the corner of each page as you fill the book up. It is a good idea to write the date the first time you write in the book each day.

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. This will not happen instantly, it will take a while to finish. You can use windows help or search the net to see how to defragment your particular version of windows.

Go to www.whatismyscreenresolution.com and write down your screen resolution. This number will be useful at the end to check whether you did the installation correctly.

Ubuntu linux can be installed over the net starting with an iso file.

Go to ubuntu.com, download the free iso file. The iso file will take a very long time to download because it is 2GB. You will save, not open or run the file. Make note of where it is downloaded on your computer so you can find it.

The iso file you have downloaded must be written to a USB stick in a special way with special software. Go to rufus.akeo.ie. download "rufus 3.5" or whatever is the latest version. Put an empty USB stick in. Click on "rufus". "do you want this app to make changes to your device?". Yes. Rufus window pops up. In boot selection select "disk or ISO image". Click on the SELECT box. A window opens up showing the netinst file. Click on it then "open". Click "start". It will take a while to write your netinst file to the USB stick.

To install linux, you put the USB stick in the computer and restart the computer. If your BIOS is set properly, it may automatically boot into the USB stick, or you may have to tap a special key to invoke a display that will ask you where you want to boot from. On one of my computers I have to tap the "esc" key during boot to boot from the USB stick. On the other computer I have to tap the "F12" key during boot. Yours will probably be different.

When a display appears on the screen giving you choices of where to boot, you will have 2 choices that are identical except that one ends in "partition 2". You do not want that one, it is to run ubuntu from the USB stick. Use the up and down arrow keys to highlight the other one, then the enter key to select it. Then you will be given more choices, select "install ubuntu".

During the installation you will be asked if you want to install ubuntu in addition to windows, or ubuntu by itself so windows will no longer be present. Make your choice. At some point you will be asked for your name and other information. After you fill in the blanks go back to the "your computers name" part. You do not want a long name, since it will appear on the text lines in your terminal later on. You cannot leave the space blank, but you can make it only one letter, which will be convenient later on. After this, linux will be downloaded over the net. This will take a long time.

Once linux is installed, log in and click on "Activities" in upper left corner of screen. A column of icons will appear. Click on the icon with 9 dots to see more icons. To the right of the icons will be large dots. Click on each dot to see a different array of icons. The terminal icon is a gray rectangle but so is the system monitor icon. Right click on the terminal icon, not the system monitor icon. Click on "add to favorites".

If you want to save a copy of this article on your computer and read it in your browser, you will have to add that capability to your browser. Click on the triple bar icon in the upper right corner of the Firefox browser that comes with linux. Click on "customize" at the bottom of the pop up window. On the screen should be an "open a file" icon. Right click on the icon to add it to the toolbar.

INSTALLING THE COMPILER

If you are new to linux, read the article how to use linux terminal. Take considerable time learning to be comfortable with linux before attempting to install the oo2c compiler. In particular read how to install extra software.

All of the following will be done in linux, not in windows.

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". To install each of these in a terminal window enter "sudo apt install xxx" where xxx is the package you wish to install.

It is probably best to make a workspace directory. In an active terminal window enter "mkdir wkspc". Now you will have a workspace directory called wkspc. In the wkspc directory enter "mkdir back scratch prog". The directory back will contain important stuff that you will periodically save on a USB stick for backup purposes. The directory scratch will be a place to do messy experimental work that is only temporary in nature. The directory prog is where you will program using your oberon-2 compiler. "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 http://sourceforge.net/projects/ooc/. The sourceforge site is a nightmare of fancy web design, just to be fancy. It is difficult to find what you want in this site. Do not click on the big "download" rectangle. Find and click on "files". Notice "oo2c", where the compiler is, and "OOCref" where the documentation is. Click on "oo2c". Click on "2.2.11". Click on "oo2c_64-2.1.11.tar.bz2" if you have a 64 bin computer, or the other one if you have a 32 bit computer. Be careful to pick the right version depending on whether you have a 32 bit or a 64 bit computer. When finished, the file will be in your linux "Downloads" directory.

You should make a copy of the software you have downloaded in the directory "prog". In the directory prog "tar xvfj oo*" to unzip the software. You will now, in addition to the tar file, have a new directory oo2c_64-2.1.11 . You now have both files and directories in your prog directory. To see the difference, do "ls -aF". Next "rm *bz2" to get rid of the tar file. Now "cd oo*" to get into the directory oo2c_64-2.1.11 . If you are running MacOS you will need to read additional instructions found in this directory in the file README.MACOS. In the directory oo2c_64-2.1.11 do "./configure ". Be sure to include the period "." before the slash "/". Some text will quickly scroll by. Next enter "sudo make install". You will be asked to enter your normal password. Lots of text will scroll by over a period of minutes, and your computer fan may speed up. Some warning messages may scroll by, ignore them. 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_64-2.0.17, because it documents the changes from version 1 to version 2. Inside that directory important documentation is found in lib/oocdoc/html and in doc/from-v1-to-v2.

Go back to the sourceforge site and click on the "OOCref" then "2000229" then "OOCref-000229.tar.gz" to download the the version 1 manual. Move it to wkspc/prog and unpack it with with "tar xvzf OO*". You now have a new directory "OOCref". In that directory the OOCref manual is two consecutive lists of files, info files and html files. You will only be able to read the html files with your browser. Enter "rm *info" to get rid of the info files. Enter "man oo2c" to see the man page for oo2c.

You will put your program source text files in "src". Use the vi editor to create and modify your programs. You will compile the program from the directory "prog". When you compile the program a warning message may appear from the gcc compiler that can be ignored.

SYNTAX

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 the more detailed and tedious descriptions of the language that are linked to in the introduction.

If you have never read programs before, it will be distressing and distasteful to read them. In the beginning you may be able to make sense of only half of what you read. But you will never be a programmer if you never become comfortable reading programs, and you will never become comfortable reading programs unless you force yourself to read some programs. You should read in detail every example in this article except "getitem", "graf2", "obelisk", "beep" and "cxarith", which you only need to read if you are going to do something similar. The rest of the examples illustrate things that every programmer should know.

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 from this article is to save the html source file to disk complete with embedded html commands. To remove the html stuff enter "unhtml prog.html > prog.text". The text version will not have programs contaminated by html stuff. Use the vi editor on your Linux system to copy the programs out of the text version 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.

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 executable. 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". This can be done with the command "sudo mv ooc /usr/local/bin".

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 exactly 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? The statement "IMPORT Out" was needed because we were going to put out results. We decided what variables we needed to do the work, and we declared them before BEGIN. We arbitrarily chose names for the variables that made it easy to remember what they represented. Then we described the work to be done with a series of statements, ending with clear output statements. Then, not shown here, but as described in previous paragraphs, we run the program through the oo2c compiler to create the executable version of the program, then execute it to get 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.

It should be clear from the example of the quarter mile track that you cannot program the computer to do anything unless you know how to do the thing yourself. Often, you will not know everything about how to do the thing you want to do when you start, and will learn and figure things out as you write the program. Some people relish the challenge. One said "what good is a project if you don't learn something new?" To help you figure things out you can search the internet, there are free courses online and books in libraries. For more information click here. Or perhaps an employer will explain how to do something that he wants you to write a program to accomplish.

What kind of programs can you write? Anything a computer can do. Do simple calculations like the previous example. Input a person's measurements and print out patterns for custom clothing. Simulate the sounds of musical instruments. More different things than any one person can imagine.

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;
(*this is a comment*)
Out.LongInt(c,4);Out.Ln;
END tiny.

   5 

Note that anything enclosed within the pair (* *) is a comment for human readers, and is ignored by the compiler. 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 ";". Any place where one statement can go, any number of statements can be put there, one after the other. Notice that there is no semicolon between END and tiny. 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. If you get confused in writing a large program and try to change the value of a constant, the compiler will warn you with a compilation error. Another reason for declaring a constant is to make the meaning of the program clear. Suppose you want to add a safety factor of the number 1 to a varable x after you compute the value of x. You could just put x+1 in the program. But it would make the meaning of the program clearer to use x+sf where sf means a safety factor and is described as such with a comment where it is declared as a constant. You need to get into the habit of writing clear programs. Ten years after you wrote an important program you do not want to read it and say to yourself "what was I thinking?". If you were going to insert the same constant at more than one place in the program, it would be foolish to insert it as a number. If you ever wanted to change it you would have to remember every place in the program where you used it. Some commonly used constants, such as pi=3.14159 are in the RealMath library. To avoid mistakes from miss-typing a commonly used constant, you should always use the one in the library. First IMPORT rm:=RealMath, then you would declare CONST pi=rm.pi. It is present in more decimal digits than you would probably be willing to type. This will be illustrated in some examples later on.

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 becomes b minus ten" or in Oberon, "b:=b-10.00;". The right side of this expression means "copy the contents of the memory location called b and subtract 10.00 from the copy"; the left side means "put the result in the memory location called b". 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 library, 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.

Computers represent numbers internally as binary numbers. Decimal numbers use the digits 0,1,2,3,4,5,6,7,8,9. Binary numbers only use the digits 0,1. Even though computer hardware uses binary numbers, computer programs are usually have input and output expressed in decimal numbers. The software translates between decimal numbers for humans and binary numbers for the computer. The decimal number 111 means ten to the second power, 100, plus ten to the first power, 10, plus ten to the zeroth power, 1, or one hundred and eleven. The binary number 111 means two to the second power, 4, plus two to the first power, 2, plus two to the zeroth power, 1, or seven. The binary number 111 is expressed as three binary bits, and is the largest possible 3 bit number. The largest 7 bit binary number is 2 to the 7th power minus one, or decimal 127, and the largest 15 bit binary number is decimal 32767. An 8 bit binary number is usually a 7 bit number plus a sign bit, in "two's complement" form. A 16 bit binary number is usually a 15 bit number plus a sign bit.

An integer number has no fractional part, like decimal 37. A real number can have a fractional part like decimal 37.64332. If a real number happens to have no fractional part, like 37.000, it is still represented in the software as a real number. An integer number is represented differently from a real number. An integer is stored as a signed number. A real is stored as a signed number and a signed exponent. A real is stored as binary but in decimal an example would be 2.775 times an exponent term of ten to the minus seventh power. This could be written as 2.775E-7. A real 32 bit number would typically be 24 bits representing a 23 bit signed number, and 8 bits representing a 7 bit signed exponent. The number 2 raised to the power 127 is decimal 1.7... times ten to the 38th power. The exponent can be from 10 to the plus 38th to ten to the minus 38th. The binary number 1.11111... is nearly decimal 2.0. The binary number 1.0000... is decimal 1.0. The 23 bits are the fractional part of the number. The "1" to the left of the decimal point is implied, and not actually stored. This scheme can represent any number from 10 to the plus 38 to 10 to the minus 38th. It cannot represent the number 0, which is handled differently as a special case, where the implied "1" is not implied. The maximum value of a 32 bit real number is 3.40282347E+38. If you want to see a website that will convert any decimal number to its floating point binary equivalent see http://babbage.cs.qc.cuny.edu/IEEE-754/ .

For the sake of completeness, we should mention ultra-reliable memory, called error correcting code memory or ECC memory. This is available only on 64 bit computers. Presently it is only available on a class of computers called workstations. It is not standard on workstations, but is available at extra cost. ECC memory uses 72 bit words in hardware that look like 64 bit words in software. Instead of being stored as a 64 bit word, the data is stored as a 72 bit code that represents the 64 bit word. If only one of any of the 72 bits fails, hardware logic automatically corrects the error and the software sees only a correct 64 bit word. The error correcting logic hardware is not in the memory, it is between the 72 bit memory and the 64 bit central processing unit or CPU. Memory bits are so tiny there is a remote possibility that a memory bit could fail temporarily because of a charged cosmic ray particle. Or it could fail permanently because of a hardware failure.

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. Math functions for LONGREAL variables are in the LRealMath library module. The math libraries include common math functions and math constants, such as pi. The function ENTIER applied to a real number produces the integer part of the real number, the part to the left of the decimal point. The function ABS gives the absolute value of any real or integer type. Integer "i" can be converted to real "r" by "r:=i". SET variables are a group of bits equal in number either to the size of a machine word or to the size of word written or read by the hardware to the hard disk. In oo2c the SET size is 32 bits for both 64 bit and for 32 bit computers, presumably because the same the same hardware interface to the hard drive is typically used in both cases. A CHAR variable takes on the value of a character, such as "a", "2", "&", etc. CHAR variables are represented in binary by the 8 bit ascii character code. The functions ORD and CHR convert between ascii characters and integers. 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. Sometimes parentheses are not needed. Thus a+b*c is the same as a+(b*c) because without parentheses multiplication and division happen before addition and subtraction. But if what you wanted was (a+b)*c, you will have to use parentheses.

Another issue is when you must use spaces between characters. The compiler will have no problem with a statement like x:=a+b*c; This has no spaces. But every variable or constant name is bounded by a symbol such as :=, +, * or ;. To the compiler this is just as clear as if they were bounded by spaces: x := a + b * c ;. But you would not want to leave out a space between a reserved word like THEN and a variable name. THENx would confuse the compiler because it would try to interpret it as a single word, not as a reserved word and a variable name.

We end this section with one final example of a simple calculation. I used to run on a one mile track, and knew how fast I could run a mile. Now where I live the most convenient place to run is 1.44 miles, but I would still like to know how many minutes per mile I run. At the end of my run, my stopwatch tells how many minutes and seconds it took to run 1.44 miles. The ENTIER function was described in a previous paragraph. There are 60 seconds in a minute. The following program tells how many minutes and seconds it took me to run one mile:

MODULE run;
IMPORT In,Out;
CONST dist=1.44;
VAR minin,secin,sectot,secpmi,minpmi:REAL;
minout,secout:LONGINT;
BEGIN
Out.String('enter minin, secin');Out.Ln;
In.Real(minin);In.Real(secin);
sectot:=60*minin+secin;
secpmi:=sectot/dist;
minpmi:=secpmi/60;
minout:=ENTIER(minpmi);
secout:=ENTIER(secpmi-minout*60);
Out.String('run time per mile');Out.LongInt(minout,3);
Out.String(' minutes ');Out.LongInt(secout,3);
Out.String(' seconds');Out.Ln;
END run.

enter minin, secin
14 45
run time per mile 10 minutes  14 seconds

The line "14 45" was entered from the keyboard in response to the line before it that asked for data that the program needed to do its job.

STRUCTURE

The human mind can only be comfortable with small programs. Large programs must be built up from many small programs to be comprehensible. This section explains how these small programs communicate with each other to form a large program. An important feature of modern computer languages is an emphasis on hierarchical structure of the program as achieved by internal procedures and functions. These procedures and functions are almost small programs within the program. 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, functions 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.

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. Here the function procedure had a parameter list in parentheses. Even if there is no parameter list, a function procedure, unlike other procedures, requires the parentheses.

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

This is our first example of a user defined type, defined in the TYPE section. The built-in types, such as REAL, INTEGER, BOOLEAN, are pre-defined and do not need a TYPE section. Here we have defined a procedure type which we have named "pt".

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

We will end this section with a program that uses some of the ideas introduced in this section, and computes something interesting. This is the first time we use the RealMath library module, which we use here to get exponents "exp", natural logarithm "ln" and square roots "sqrt". We use the simplified actuator disk formula for the thrust of an airplane propeller that can be found in textbooks. We calculate the thrust of one propeller of the type found on a real airplane of the 1940's and 1950's. This program is included only to show a simple handy application of programming. You do not need to look up the formulas used to appreciate the interesting results. When we run the program we input the data that the propeller is 19 feet in diameter, the engine is running at a full power of 3500 horsepower, the speed of the plane is zero because it has not started its takeoff roll, and the altitude of the airport is 3200 feet above sea level. The result that the program gives is that the thrust of the propeller is 16360.15 pounds. This would be 7.5 tons of thrust just from fanning thin air. Neat huh? The speed of the air blown out the back of the propeller is 80.24 miles per hour.

MODULE prop2;
(*momentum theory Dreier Introduction to Helicopter
and Tiltroter Flight Simulation p.149-151*)
IMPORT rm:=RealMath,In,Out;
VAR ro,a,d,rad,p,t,v0,w,q,r,s1,s2,x1,x2,x3,alt,sigma:REAL;
str:ARRAY 40 OF CHAR;

PROCEDURE pwr(x,y:REAL):REAL;
BEGIN RETURN rm.exp(y*rm.ln(x));END pwr;

PROCEDURE dens(alt:REAL;VAR sigma:REAL);
(*computes sigma as fraction of sealevel dens*)
BEGIN
IF alt<35332.0 THEN
sigma:=pwr((1.0-6.879E-6*alt),4.2358)
ELSE sigma:=0.3058*rm.exp(-4.778E-5*(alt-35332.0));END;END dens;

BEGIN
Out.String('enter d,p,v,alt');Out.Ln;
In.Real(d);In.Real(p);In.Real(v0);In.Real(alt);
In.Identifier(str);
Out.String('dia(ft)=');Out.RealFix(d,4,1);
Out.String(' hp=');Out.RealFix(p,6,1);
Out.String('  plane v(mph)=');Out.RealFix(v0,6,1);
Out.String(' alt=');Out.RealFix(alt,4,1);Out.Ln;
d:=d*0.3048;p:=p*745.7;v0:=v0*0.447;
(*convert to meter kilogram second units*)
dens(alt,sigma);
Out.String('density=');Out.Real(sigma,0,0);Out.Ln;
ro:=1.18*sigma;
rad:=d/2.0;a:=rm.pi*rad*rad;
(*cubic solution Abramowitz and Stegun p17*)
q:=-(1/9)*v0*v0;
r:=(1/27)*v0*v0*v0+0.25*p/(ro*a);
x1:=q*q*q; x2:=r*r;
Out.String('q^3+r^2=');Out.Real(x1+x2,0,0);Out.Ln;
x3:=rm.sqrt(x1+x2);
s1:=pwr((r+x3),1/3);
IF (r-x3)<0.001 THEN s2:=0.0 ELSE
s2:=pwr((r-x3),1/3);END;
w:=s1+s2-2*v0/3;
t:=ro*a*(v0+w)*2*w;
t:=t/4.448;(*newtons to pounds*)
Out.String('thrust(lbs)=');
Out.RealFix(t,8,2);
Out.String('  induced velocity at disk w(mph)=');
Out.RealFix(((w)/0.447),8,2);Out.Ln;
Out.String('ideal efficiency=');
Out.RealFix((1/(1+w/v0)),5,2);
Out.Ln;
END prop2.

bin/prop2
enter d,p,v,alt
19 3500 0 3200
dia(ft)=19.0 hp=3500.0  plane v(mph)=   0.0 alt=3200.0
density=9.10024703E-1
q^3+r^2=5.32133600E+8
thrust(lbs)=16360.15  induced velocity at disk w(mph)=   80.24
ideal efficiency= 0.00

Note that the symbol "^" was used to indicate an exponent in the printed output of the program, but that would not have been legal for computation in the program.

CONTROL

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". These repetitive statements end when a BOOLEAN value becomes TRUE. This can happen with a boolean variable that is set to TRUE or with an expression that evaluates to a boolean value, such as "i=5" or "x>3.2". ">" means "greater than", "<" means "less than" and "#" means "not equal to". "OR" and "&" can be used to combine boolean relations. ~(a>b) means not (a>b).

It is possible to write a program with a repetitive statement that goes on forever, and never ends. If this was a mistake and not your intention, you can halt the program by holding down the "ctrl" key and tapping the "c" key. This is called hitting "control-c" or "ctrl-c". An example of a bad program with a never ending loop follows. You should run it and stop it with control-c.

MODULE inf;
BEGIN
LOOP END;
END inf.

An example of a good program using REPEAT follows:

MODULE repeat1;
IMPORT Out;
CONST a=10;
VAR b:LONGINT;

PROCEDURE thick():BOOLEAN;
BEGIN
IF b>a THEN RETURN TRUE ELSE RETURN FALSE; END(*if*);
END thick;

PROCEDURE stirring;
BEGIN
b:=b+1; END stirring;

BEGIN
b:=0;
REPEAT stirring UNTIL thick();
Out.String('b=');Out.LongInt(b,3);Out.Ln;
END repeat1.

b= 11

The next example shows WHILE:

MODULE while1;
IMPORT Out;
VAR r:REAL;
BEGIN
r:=1.0;
WHILE r < 1.5 DO r:=1.1*r;END;
Out.String('r=');Out.Real(r,5,0);Out.Ln;
END while1.

r=1.61051011

The next example shows LOOP:

MODULE loop1;
IMPORT Out;
VAR i:LONGINT;
BEGIN
i:=0;
LOOP i:=i+1; 
IF i>7 THEN EXIT END(*IF*);
i:=i*2;END(*LOOP*);
Out.String('i=');Out.LongInt(i,2);Out.Ln;
END loop1.

i=15

Wirth recommends using LOOP only if you have more than one EXIT. Otherwise, use REPEAT or WHILE.

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.

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 next example in this section illustrates the MOD and DIV operators, and the ABS function. The DIV and MOD operators work only on integer types. The ABS function works on both integer types and real types.

MODULE mod3;
IMPORT Out;
VAR i:LONGINT;
BEGIN
FOR i:=-9 TO 9 DO 
Out.LongInt(i,2);END(*for*);Out.Ln;
FOR i:=-9 TO 9 DO
Out.LongInt((i MOD 3),2);END(*for*);Out.Ln;
FOR i:=-9 TO 9 DO
Out.LongInt((i DIV 3),2);END(*for*);Out.Ln;
FOR i:=-9 TO 9 DO
Out.LongInt(ABS(i),2);END(*for*);Out.Ln;
END mod3.

-9-8-7-6-5-4-3-2-1 0 1 2 3 4 5 6 7 8 9
 0 1 2 0 1 2 0 1 2 0 1 2 0 1 2 0 1 2 0
-3-3-3-2-2-2-1-1-1 0 0 0 1 1 1 2 2 2 3
 9 8 7 6 5 4 3 2 1 0 1 2 3 4 5 6 7 8 9

The MOD function only works for integers. But you might want a real version of it. Suppose you have a vector from the center of a circle to the periphery. There are 360 degrees of angle in a circle. If some variable multiplied the angle of the vector by a large number, the vector would spin around the circle many times. But you might want to know the orientation of the vector after this had happened. A real version of the MOD function would do this. The following example shows the use of such a function.

MODULE rmodtest;
IMPORT Out;
CONST rev=360;
VAR r,d:REAL;

PROCEDURE rmod(r1,r2:REAL):REAL;
BEGIN
RETURN r2*(r1/r2-ENTIER(r1/r2));
END rmod;

BEGIN
r:=25*rev-30;
d:=rmod(r,rev);
Out.RealFix(d,8,2);Out.Ln;
END rmodtest.

  330.00

We have assumed positive angles. In nearly every mathematical or geometrical application angles should always be expressed as positive to avoid complexity or wrong answers.

A warning is needed about the floating point mod function. In the example only 25 revolutions are used. If 1.0E4 revolutions are used a small error will appear, the answer will be 330.12. With more revolutions the error would be larger. That is the nature of floating point arithmetic. We cannot get good accuracy in a tiny difference between two very large numbers.

The situation can be improved by resorting to the LONGREAL data type at the critical part of the calculation. We will again get the correct answer of 330.00.

MODULE rmodtest;
IMPORT Out;
CONST rev=360;
VAR r,d:REAL;

PROCEDURE rmod(r1,r2:REAL):REAL;
VAR l1,l2,l3:LONGREAL;
x:REAL;
BEGIN
l1:=LONG(r1);l2:=LONG(r2);
l3:=l2*(l1/l2-ENTIER(l1/l2));
x:=SHORT(l3);
RETURN x;
END rmod;

BEGIN
r:=1.0E4*rev-30;
d:=rmod(r,rev);
Out.RealFix(d,8,2);Out.Ln;
END rmodtest.

This raises the question of why you would not just replace all reals with longreals. If you have 32 bit computer large arrays of longreals would take twice as much memory as large arrays of reals. If the hardware adder and hardware multiplier in your 32 bit machine were also 32 bits, computation would take longer if you used longreal. But if you have a 64 bit computer there would probably be no penalty on storage or speed to use only longreals. If you are going to use math functions with longreals you must import the LRealMath library, the RealMath library will not work.

In the above example we only used a small number of real numbers and needed longreals to get an answer with real accuracy. The more typical need for longreals to get real accuracy is when a large number of numbers must be combined. The most well known such application would be, say, solving a system of 100 equations in 100 unknowns. There are even rare applications where longreal accuracy is needed in the final result.

DATA STRUCTURES

The following is a short example using an array:

MODULE array1;
IMPORT Out;
TYPE ara=ARRAY 5 OF LONGINT;
VAR i:LONGINT;a:ara;
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 initialized, 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. An example of a large two dimensional array of real would be declared as "ara=ARRAY 100,100 OF REAL" and an element accessed as "a[57,34]".

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 built-in and/or user defined 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.

Records can contain pointers, allowing the construction of data structures called linked lists, binary trees etc. Another use of pointers is to form objects, for object oriented programming. An object is a record with procedure fields that is accessed through a pointer. If you are going to program a graphical user interface or an operating system you will want to use objects. Pointers are not covered in this introductory course. I have written many programs and never needed a pointer. They are not needed for most mundane, ordinary programs where you just want the computer to do something that would be a lot more work to do without the computer.

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.

The next example is taken from a working program to show the complexity that is sometimes desired in data structures. Other examples have been as simple as possible to make the point, but here the whole point is the uninhibited, arbitrary, apparently random complexity that is allowed when you need it. You may never write a program that needs a data structure this complex, but the language gives you the permission and the ability to do so.

The final record "fltrrec", combines reals, integers, booleans, and arrays of records. You will not understand the motivation behind it, but you should be able to see how the pieces fit together. The reason for the asterisks "*" will be explained later in the section on separate compilation.

IMPORT cx:=cxarith;
CONST maxpoles=20; 
TYPE
polerec*=RECORD sigma*, omega*, gain*:REAL;
xk*,yk*,ykm1*,onemeps*,rt1meps*,eps*,gaincx:cx.complex;
first*:BOOLEAN; END;
zerorec*=RECORD sigma*,omega*,gain*:REAL;xk*,yk*,xkm1*,
rt1meps*,norm*,gaincx:cx.complex;first*:BOOLEAN;END;
para*=ARRAY maxpoles+1 OF polerec;
zara*=ARRAY maxpoles+1 OF zerorec;
fltrrec*=RECORD fc*,bw*,f1*,db*,gain*:REAL;n*,nz*,nzo*:LONGINT;
        firstb*:BOOLEAN;p*:para;z*:zara END;

Notice that "fltrrec" uses "para" and "zara". The type "para" uses "polerec" and "zara" uses "zerorec". The variable in "fltrrec" that is of type "zara" is the variable "z". A procedure in the program that uses fltrrec would have a variable in the parameter list declared as "fr:fltrrec". The variable "fr" is declared as being of type "fltrrec". A statement from the program that sets a variable "sigma" to zero is:

fr.z[i+fr.nz].sigma:=0.0;

There are two different variables named "sigma". You should be able to see that the one selected here is from "zerorec", not from "polerec". The "[i+fr.nz]" array index is unusual, "[i]" would be more typical, but this example serves to show what is possible.

If we had two variables of type "fltrrec", like fr1 and fr2, we could copy all of the data from fr1 to fr2 with "fr2:=fr1;".

Suppose an array type is declared with a dimension: TYPE ara=ARRAY n OF REAL;. Then variables VAR a,b:ara; would be arrays of that type. A procedure could be declared with a parameter of that type: PROCEDURE dostuff(c:ara);. This procedure could be called to work on either array a or array b: dostuff(a); or dostuff(b);. But this constrains dostuff to work only on arrays of size n. Suppose we would like dostuff to be able to work on different arrays of any size. We can do this if we declare dostuff with an open array parameter: PROCEDURE dostuff(c:ARRAY OF REAL;i:LONGINT);. Now we can declare arrays of different dimension and use them with dostuff. VAR a:ARRAY 5 OF REAL; b:ARRAY 10 OF REAL;. Now we can call dostuff(a,5) or dostuff(b,10) and dostuff can work on arrays of different sizes.

Suppose we declare TYPE coord=RECORD x,y OF REAL END;. We could declare VAR ara:ARRAY 1000 OF coord; Then PROCEDURE workcoord(cd:LONGINT); could be called as workcoord(536); to work on coordinate 536. But suppose we would like to break the large array up into smaller arrays: VAR ara1:ARRAY 73 OF coord; ara2:ARRAY 46 OF coord;, etc. Then we could redefine workcoord: PROCEDURE workcoord(cd:coord);. Then we could call workcoord(ara1[32]); or workcoord(ara2[22]); and workcoord could work on elements of different arrays of different sizes.

INPUT/OUTPUT

This section is about input and output. The language does not have any input and output. The input and output functions are in the library. The library was mostly written using the language. Input and output are only a small part of most programs. Much more careful thought was given to the design of the language than the library. This section explains a very small part of the library. Only the most basic and frequently used parts of the library. If you want to understand the rest of the library you will have to study the library manual and experiment with other functions.

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 word read from the keyboard:

MODULE testwd;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR str1:str;
BEGIN
Out.String('enter abc');Out.Ln;
In.Identifier(str1);
IF str1='abc' THEN Out.String('right word');Out.Ln;
ELSE Out.String('wrong word'); Out.Ln; END;
END testwd.

bin/testwd
enter abc
xyz
wrong word

bin/testwd
enter abc
abc
right word

You should should modify the above "testwd" program to eliminate "Out.Ln" after "enter abc". The program will not work. This is a common mistake of beginners.

Read real numbers from the keyboard:

MODULE readreal;
IMPORT In,Out;
VAR x,y:REAL;
BEGIN
Out.String('enter x,y, real numbers');Out.Ln;
In.Real(x);In.Real(y);
Out.Real(x,5,6);Out.Ln;
Out.Real(y,5,6);Out.Ln;
END readreal.

bin/readreal
enter x,y, real numbers
8.5 -3.2
8.50000
-3.20000

We can run the program again to show how large and small numbers are printed in floating point format:

bin/readreal
enter x,y, real numbers
1000 0.001
1.00000E+3
1.00000E-3

There are other ways to write out real numbers. You should have downloaded the "OOCref" document. In the table of contents of that document go to the "Standard I/O" section to see ways to write real numbers. You should replace Out.Real with Out.RealEng and Out.RealFix to see other formats to write out the numbers.

The math libraries include math constants such as pi and other constants. This gives us an opportunity to write out a very long number.

MODULE pidemo;
IMPORT rm:=LRealMath,Out;
BEGIN
Out.LongReal(rm.pi,0,0);Out.Ln;
END pidemo.

3.1415926535897931

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.

bin/getname
enter filename
myfile.txt
fileid=myfile.txt

We had to use In.Line, not In.Identifier, because myfile.txt contains a period, which is not legal in an identifier. An identifier must be made of only letters and numbers, and the first character must be a letter.

At your keyboard enter "man ascii" to see decimal equivalents of ascii characters. The next program serves to demonstrate the functions CHR and ORD. A character is read, converted to the ascii decimal equivalent, then the ascii decimal equivalent is converted back into the character. The character is printed out between two "xxx" so that invisible characters can be seen in the example. Note that after "In.Char" an "In.Line" is needed to read to the end of the line. This is so the next character read will be the first character on the next line, not the linefeed character created when we press "enter". The first example with an apparently blank line has the space character as the first character of the line. The space character can be seen as a space between the two "xxx" terms. The second blank line example has the linefeed character as the first character of the line. This was accomplished by hitting "enter" twice in a row. The linefeed character is made apparent by the fact that the two "xxx" terms are on successive lines.

MODULE char;
IMPORT In,Out;
VAR i:LONGINT;ch:CHAR;str:ARRAY 20 OF CHAR;
BEGIN
REPEAT
In.Char(ch);In.Line(str);
i:=ORD(ch);
Out.LongInt(i,3);Out.Ln;
Out.String('xxx');
Out.Char(CHR(i));
Out.String('xxx');
Out.Ln;
UNTIL ch='7';
END char.

q
113
xxxqxxx
#
 35
xxx#xxx
 
 32
xxx xxx


 10
xxx
xxx
7
 55
xxx7xxx

The preceeding example required an In.Line(str) statement to clear the linefeed character left in the input buffer after enterning the data. Sometimes this is optional, sometimes it is necessary. If a following input statement would not work correctly because the linefeed character was still in the input buffer, the statement to clear the linefeed character would be necessary. The following program works correctly if the linefeed character is cleared, but not if the linefeed character is not cleared. Sometimes such an error will result in a catastrophic failure of the program with an error message printed on the screen.

MODULE badread;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR i,j:LONGINT;str1,dummy:str;
BEGIN
Out.String('enter i, j integers'); Out.Ln;
In.LongInt(i); In.LongInt(j);
In.Line(dummy);
Out.LongInt(i,6); Out.LongInt(j,6); Out.Ln;
Out.String('enter the letter y');Out.Ln;
In.Line(str1);
IF str1='y' THEN 
Out.String('it worked');Out.Ln;
ELSE Out.String('it did not work');Out.Ln;END;
END badread.

bin/badread
enter i, j integers
3  4
     3     4
enter the letter y
y
it worked

If there were spaces before or after the y it would not work. But if we comment out "In.Line(dummy);", recompile and re-execute, then it does not work properly, as follows:

bin/badread
enter i, j integers
3  4
     3     4
enter the letter y
it did not work

The In.LongInt statements will not be confused by linefeed characters preceeding the integer. If you hit the "enter" key between the two integers, the program will work fine. But the In.Line statement will not work properly if there is still a linefeed character in the input buffer. But you cannot just insert an In.Line(dummy) statement before the In.Line(str1) statement to clear any stray linefeed character that you may have forgot about. If there were no linefeed character, the program would stop executing until you hit the "enter" key. An In.Char statement would clear a linefeed if only a linefeed was in the buffer. But there might be space characters before the linefeed character, so it is more reliable to clear the linefeed character with the In.Line statement.

If we create a program "badread2" where we only change In.Line to In.Identifier, the problem goes away. We do not need the In.Line(dummy) statement. Furthermore we can preceed and follow the y with spaces and it will still work.

MODULE badread2;
IMPORT In,Out;
TYPE str=ARRAY 40 OF CHAR;
VAR i,j:LONGINT;str1,dummy:str;
BEGIN
Out.String('enter i, j integers'); Out.Ln;
In.LongInt(i); In.LongInt(j);
(*In.Line(dummy);*)
Out.LongInt(i,6); Out.LongInt(j,6); Out.Ln;
Out.String('enter the letter y');Out.Ln;
In.Identifier(str1);
IF str1='y' THEN 
Out.String('it worked');Out.Ln;
ELSE Out.String('it did not work');Out.Ln;END;
END badread2.

enter i, j integers
3   4
     3     4
enter the letter y
  y
it worked

Here we only read one identifier on a line. But we could have read more, with successive In.Identifier commands, and it would have still worked.

If you just want the user to give the program words of instruction, use In.Identifier. If you must use In.Line, make sure there is not a linefeed character in the input buffer before you use In.Line.

The next example asks you to enter the filename then reads the file and puts the results out to the screen. The file "sq" is as follows:

10 12

The program and its execution are as follows:

MODULE square;
IMPORT In,Out,Msg,Files,TextRider;
TYPE str=ARRAY 40 OF CHAR;
VAR filename:str;resv:Msg.Msg;invar:Files.File;
infile:TextRider.Reader;
height,width,area:REAL;

BEGIN
Out.String('enter filename');Out.Ln;
In.Identifier(filename);
invar:=Files.Old(filename,{Files.read},resv);
infile:=TextRider.ConnectReader(invar);
infile.ReadReal(height);
infile.ReadReal(width);
infile.ReadLn;
area:=height*width;
Out.String('area is ');Out.RealFix(area,6,2);Out.Ln;
END square.

enter filename
sq
area is 120.00

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 "getname" above could be substituted for 'demo1.txt' with quotes in the following example, if it were desired for the user to enter filenames.

These programs deal with mixed text and numbers. Note that the text must be enclosed in quotes to be read properly

MODULE ritfil1;
IMPORT Msg, Files, TextRider;
VAR resv:Msg.Msg;outvar:Files.File;
outfile:TextRider.Writer;
a,b:LONGINT;c,d:REAL;
BEGIN
a:=5;b:=6;c:=2.3;d:=-2.1;
outvar:=Files.New('demo1.txt',{Files.write},resv);
outfile:=TextRider.ConnectWriter(outvar);
outfile.WriteLInt(a,3); outfile.WriteString('" some text"');
outfile.WriteRealFix(c,5,1); outfile.WriteLn;
outfile.WriteLInt(b,3); outfile.WriteString('" more text"');
outfile.WriteRealFix(d,5,1); 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. This is the way it will appear in the editor:

  5" some text"  2.3
  6" more text" -2.1

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. Note that str2 is only used to read to the end of line so the next line can be read.

MODULE redfil1;
IMPORT Out, Msg, Files, TextRider;
VAR resv:Msg.Msg;invar:Files.File;
infile:TextRider.Reader;
str1,str2:ARRAY 40 OF CHAR;
x:LONGINT;y:REAL;
BEGIN
invar:=Files.Old('demo1.txt',{Files.read},resv);
infile:=TextRider.ConnectReader(invar);
WHILE infile.res=TextRider.done DO 
infile.ReadLInt(x); infile.ReadString(str1); 
infile.ReadReal(y); infile.ReadLine(str2);
IF infile.res=TextRider.done THEN 
Out.LongInt(x,3); Out.String(str1);
Out.RealFix(y,5,1); Out.Ln;
END(*IF*); END(*WHILE*);
invar.Close; END redfil1.

  5 some text  2.3
  6 more text -2.1

Reading of the text field with ReadString required it to be in quotes. We would not need quotes if we had used ReadIdentifier. But each identifier would have had to be read separately. The identifiers would have had to be legal identifiers, the first character could not be a number. ReadString had no such restriction on what it could read. But when "some text" was written out with two Out.String statements it would have been run together: "sometext". To prevent this an additional statement Out.String(' ') would put a space between the two text strings.

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);
WHILE infile.res=BinaryRider.done DO 
infile.ReadReal(r);
IF infile.res=BinaryRider.done THEN 
Out.RealFix(r,12,4);Out.Ln;
END(*if*);END(*while*);
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);
WHILE infile.res=BinaryRider.done DO
infile.ReadSet(s);
IF infile.res=BinaryRider.done THEN 
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(*if*);END(*while*);
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);
WHILE infile.res=BinaryRider.done DO 
infile.ReadSet(s);
IF infile.res=BinaryRider.done THEN 
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(*if*);END(*while*);
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. Unsigned integers are all positive, and could be from 0 to 255. The Oberon type SHORTINT refers to 8 bit signed twos complement integers with a maximum value of 127. You would read the file as SHORTINT, then using the function LONG assign the value to a 16 bit INTEGER variable. Then do the appropriate conversion to get the negative numbers to the correct positive values.

In our earlier example "redfil1" we knew the format of the data in the file before we read it. In that case, we used built in functions in the OOC library to read each type. But what if we want to read a file of ascii data whose format we do not know? More complex text files could contain lines having a mix of numbers, words and characters in an order that might differ unpredictably 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. It is not likely that you will face a problem like this, but it serves as an example of low level I/O programming. In this program 00X is the nul ascii character. 00X is three ascii characters, but the compiler interprets a hexadecimal number followed by capital "X" as a single ascii character. 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.

Another simple filter program will convert ordinary text files with a plain font to postscript files with a fancy font. The program is very primitive and will only handle one page of output at a time. If you have the "zip" package installed on your computer, find a font in the "font.zip" format, download it and unzip it and hopefully it will be in the "font.ttf" format or some other format that ghostscript can use. Do "gs -h" to find where ghostscript looks for fonts, and put a fancy font you downloaded from the internet where ghostscript will find it. In this example the fancy font is "OldeEnglish.ttf". The fonts have adjustable size, the size specified is "font=24", a large font size. You can view the postscript file with "gv", if it installed on your computer, or you can print it out. In Ubuntu linux click on "applications" "system tools" "file browser" and you can see how it will appear on a sheet of paper before you print it out. You can convert the postscript to pdf with "ps2pdf". The advantage of pdf over postscript is that the font is actually in the pdf file, not elsewhere on the computer. If you email a pdf file or post it on the web other people will see the fancy font. Another way to post a fancy font on the web is as a png file referenced by an html file, which is the way the example is shown here. The program is shown here followed by input and output:

MODULE tx2ps;
(*ascii text to postscript with fancy font.
use: bin/tx2ps < file.txt > file.ps *)
IMPORT In,Out;
CONST xorg=72; yorg=720; font=24;
VAR str:ARRAY 256 OF CHAR;i,j:LONGINT;
BEGIN
i:=0;
Out.String("%!PS");Out.Ln;
Out.LongInt(xorg,4);Out.LongInt(yorg,6);
Out.String(" moveto");Out.Ln;
Out.String("/OldeEnglish.ttf");
Out.LongInt(font,4);Out.String(" selectfont");Out.Ln;
In.Line(str);
WHILE In.Done() DO
Out.Char("(");
Out.String(str);Out.String(") show");Out.Ln;
i:=i+1;j:=yorg-i*font;
Out.LongInt(xorg,4);
Out.LongInt(j,6);Out.String(" moveto");Out.Ln;
In.Line(str);
END;
Out.String("showpage");Out.Ln;
END tx2ps.

This is Fancy Text

fnctx.png

You will sometimes need for your program to issue commands to the linux operating system. The "ProcessManagment.system" command will do this. You can enter the command in your program between double quotes "xyz" as an argument of the above command, but you may need the program to compute the command as a string variable, which will require a more complex procedure. The "Strings.Append", "IntStr.IntToStr" and "Object.NewLatin1" commands are useful for this. If at the keyboard you enter the command "echo cat324" the result will be that "cat324" will be printed on the screen. We now give a trivial example of computing a string variable to issue the command "echo cat324", which will result in "cat324" being printed on the screen.

MODULE sys;
IMPORT IntStr,Strings,Object,OS:ProcessManagement;
TYPE str=ARRAY 40 OF CHAR;
VAR str1,str2:str;
str3:STRING;i,j:LONGINT;
BEGIN
str1:="echo ";
str2:="cat";
Strings.Append(str2,str1);
i:=324;
IntStr.IntToStr(i,str2);
Strings.Append(str2,str1);
str3:=Object.NewLatin1(str1);
j:=ProcessManagement.system(str3);
END sys.

cat324

This technique will allow you to program complicated tasks with Oberon that would otherwise have to be programmed with bash. Oberon is much easier to program than bash. Some commands are very long. If your array is not long enough, you will get a compilation error "expression not compatible with type".

When using Strings.Append to make filenames do not create filenames with leading or trailing spaces in the name. A filename with a leading space will appear in the list when you issue the command "ls". But a program expecting to read the name without a leading space will say the file is not there. You will not be able to remove the file with simple ordinary use of the "rm" command. To remove one such file "rm -v " will probably work. To remove a directory full of such files issue "rm -- *\ *".

SEPARATE COMPILATION

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.

Separate compilation can provide the environment needed by many of the examples in the book by Erik Nikitin. He assumes a special operating system that allows keyboard commands to accomplish what requires separate compilation under linux. For instance his first example "OfeHello" on page 7 of his book can be made to work by the following case of separate compilation:

MODULE nikitin;
IMPORT OfeHello;
BEGIN
OfeHello.Do;
END nikitin.

DEBUGGING

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.

If you comment out the IF statement, the runtime error will state that the error is at position 255 in this case. To find where that is use the ooef command as follows:

ooef debug.Mod 255

In file debug.Mod:
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;
#-^
# 11: 
Out.LongInt(ar[i],4);Out.Ln;
END;
END debug.

The up arrow "^" points to where position 255 is. The number "11" under the up arrow tells how many lines down in the program. Look at the program in the vi editor. Make sure you are in the editor's edit mode, not the input mode. If you are at the beginning of the program, enter 11 then hit the "enter" key. The cursor will drop down 11 lines and you can see the line where the error is.

In the case of a large program, you may not remember every detail of how it works. This is not usually necessary to debug it. If you remember the main idea of how it works, you can probably think of things to try to debug it.

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.

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.

In some cases it would be appropriate to declare a variable called "dummy" that is an array of characters. Then declare a procedure called "debug" that just writes out the values of variables that might help figure out a problem. The last statement in the procedure "debug" would be "In.Line(dummy);". Then you could put calls to "debug" at various places in your program. Then compile and run your program. When you finish reading the values of the variables each time, hit the "enter" key and go on to the next call to "debug".

Another technique to debug is to use the linux command "script". Put extra lines in your program that write out to the screen the value of variables at a certain point in your program. Compile the program. Enter the command "script". Then run your program. Then hold down the "ctrl" key and type the "d" key at the same time. This is called entering "ctrl-d". That will write out a file called "typescript". Use the vi editor to view typescript, and you will see the results of running your program.

A problem that could create an error is illustrated by the "MODULE badread" example in the input/output section. A left over linefeed character may cause a problem with a statement much later in the program. Try clearing the linefeed character after an input statement if you get a runtime error that has no other explanation.

Make sure you are not using numerical array elements that have not ben initialized.

INTERACTIVE PROGRAMS

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.Identifier(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.

The first case of "er:|" is required so that if a mistake is made the program will not quit with an error message. Instead, the user will be warned that he has made an error and can try again.

You have now learned enough about programming to understand the source code of a simple text formatter, given here. You will notice that various parts of the program could have been written in different ways.

GRAPHICS OUTPUT

If you want to program line drawings of patterns or objects, see the article software tools for line drawings.

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 as does the book "Mathematical Illustrations" by Bill Casselman. The latter book is available in hard copy from Cambridge University Press or free online at www.math.ubc.ca/~cass/graphics/manual/. 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,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);
BEGIN
outfile.WriteRealFix(x,8,2);
outfile.WriteRealFix(y,8,2);
IF md=move THEN 
outfile.WriteString(' moveto');
ELSE 
outfile.WriteString(' lineto'); END;
outfile.WriteLn;
END moveto;

PROCEDURE width(w:REAL);
BEGIN
outfile.WriteRealFix(w,8,2);
outfile.WriteString(' setlinewidth'); 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.28318;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('/Latin-Modern-Roman 14 selectfont');
outfile.WriteLn;
END initfont;

PROCEDURE labelaxis;
CONST xorig=157;cycle=58;
yorig=396; charsize=14;
VAR i:LONGINT;ir:REAL;
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);
outfile.WriteString('(');
ir:=i;
outfile.WriteRealFix(ir,8,1);
outfile.WriteString(') show');
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.

your browser is too old

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. There are 72 postscript units per inch, and 2.54 centimeters per inch. You can use real numbers to specify locations to fractions of a postscript unit.

The above program shows the use of the postscript commands "x y moveto" and "x y lineto". For drawing arcs use "xc yc r sa ea arc". xc and yc are the coordinates of the center of the arc, r is the radius of the arc, and sa and ea are the start and end angles of the arc in degrees. Another useful command is the bezier curve for connecting two straight lines with a curve with the "curveto" command. The format is "x1 y1 x2 y2 x3 y3 curveto". The first straight starts where you are and ends at x1 y1. The second straight line starts at x2 y2 and goes to x3 y3. The curve starts at the beginning of the first straight line and ends at the end of the second straight line. It is tangent to each straight line at either end of the curve. The straight lines are not drawn, only the curve is drawn.

If you want to take oversize graphics like clothing patterns or architectural designs to a blueprint shop or graphics shop to be printed you will want to convert them from postscript to pdf format. For letter sized drawings the linux command "ps2pdf" will work. For large size drawings a different way is needed. You may have to install free missing commands on your computer. The above program produces the file "temp1" in postscript format. "pstoedit -f fig temp1 temp1.fig" will convert it to fig format. "fig2dev -L pdf -b 20 temp1.fig temp1.pdf" will convert it to pdf. You can see a part of your large figure on the screen with the linux command "gv" and scroll around the figure, if your computer has enough memory to handle the size of graphics you have plotted. Even if your computer does not have enough memory to display large graphics it will probably have no trouble creating the large pdf files. You can now transfer "temp1.pdf" to a USB stick and take it to a copy shop or graphics services shop that has a very large printer and have it printed. This procedure produces a pdf file that spans the smallest rectangle that can hold your image. Large scale printers typically do not print the quarter inch (6mm) nearest the edge of the pdf file. So you will have to draw a thin rectangle beyond the edges of your graphics for all of your graphics to be printed.

An example of the output of a program that took a man's measurements and drew the pattern for an old fashioned tailcoat follows. It is shown here in a small size. It would be printed in a large size by a large printer at a graphics shop. The program that produced it is too large to include here. The program was written following the instructions in a tailoring book that told how to draw the pattern by hand. It is drawn entirely with straight lines and arcs. No computed curves like the one in "graf1" above were used. If drawn by hand a string compass, ruler and drafting triangle would be used, but to draw it in a computer program geometry and trigonometry was required. The advantage of having a computer program to draw it is that the program works for any measurements:

your browser is too old

Sometimes you want your text output to be greek letters or math symbols instead of ordinary text. The procedure "initfont" selects "Latin-Modern-Roman", which is ordinary text. Create a similar procedure "initsfont" which selects "Symbol", which is greek letters and math symbols. Each call to either procedure switches to that kind of text. You can have both kinds of text mixed in the same word, if you want to. The size of text selected here is "14". If you want slightly larger text, select "16". When using the Symbol font, you will specify "s" and greek sigma will appear; "w" will result in omega, etc. Some symbols in the Symbol font are beyond the range of the keyboard, and must be specified by an octal number. For example, if you search the web for the symbol font, you can find that the decimal number for the first character of the square root sign is 214. If you install the wcalc command, "wcalc -o 214" gives the octal equivalent 0326. In your postscript file "(\326) show" will produce the character. The top of the square root sign can be completed by the underscore character from the keyboard. If you are going to include something complicated like an equation in your graphics, it would be too cumbersome to debug it in your program. Instead, write the postscript for the equation by hand using the vi editor, and view it each step of the way with the "gv" command. You will need the postscript commands "save", "scale", "restore" and "rmoveto". Once you have the postscript for the equation debugged, it will be easy to add commands to generate it in your program.

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. Whereas postscript shows your drawing somewhere on a whole page, the following bash script will convert only the drawing to png:

# convert ps to png 
namechange ()
{
oldname=$1
newname=${oldname%.ps}.png
echo "$newname"
pstopnm -stdout "$oldname" \
| pnmcrop | pnmscale 0.5 \
| pnmtopng > "$newname"
}
for filename in "$@" ; do
namechange "$filename"
done

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 appropriate viewer. Other viewers for other graphics languages are available in linux. In Ubuntu 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 ubuntu 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.28318;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.

your browser is too old

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.

Next we give an example of graphics programming to create a geometrical object, in this case a modern obelisk like would be used to decorate a campus or a park. It would be made of metal plates welded together, preferably heliarc welded titanium to prevent corrosion. We use the linux graphics program "geomview". Our program will write a file in the format that geomview can read, and call geomview to display the object. We can change the input data and change the proportions of the object. Our object is built in pieces that are added one at a time to finish the final object. Theoretically the object could have been created in one piece, but errors would be too difficult to correct that way. Geomview allows you to rotate the object any way you want to view it from any angle, but you must use the left and the middle mouse buttons for different movements. As per the instructions the vertices of each face are enumerated in the program in a counterclockwise direction as seen from the exterior of the object. To see the structure without the fins, comment out the call to fins at the end of the program, recompile ignoring the compiler warning, and run.

MODULE obelisk;
IMPORT  Msg, Files, TextRider,
OS:ProcessManagement;
VAR h1,h2,h3,h4,w0,w1,w2,w3,w4: REAL;
i,vert:LONGINT;
resv:Msg.Msg;outvar:Files.File;
outfile:TextRider.Writer;
infile:TextRider.Reader;invar:Files.File;

PROCEDURE square(h,w:REAL;norm:BOOLEAN);
VAR w2:REAL;sq:ARRAY 4,3 OF REAL;
i,j:LONGINT;
BEGIN 
w2:=w/2;
IF norm THEN
sq[0,0]:=w2;sq[0,1]:=w2;sq[0,2]:=h;
sq[1,0]:=-w2;sq[1,1]:=w2;sq[1,2]:=h;
sq[2,0]:=-w2;sq[2,1]:=-w2;sq[2,2]:=h;
sq[3,0]:=w2;sq[3,1]:=-w2;sq[3,2]:=h;
ELSE
sq[0,0]:=w2;sq[0,1]:=0;sq[0,2]:=h;
sq[1,0]:=0;sq[1,1]:=w2;sq[1,2]:=h;
sq[2,0]:=-w2;sq[2,1]:=0;sq[2,2]:=h;
sq[3,0]:=0;sq[3,1]:=-w2;sq[3,2]:=h;END;
FOR i:=0 TO 3 DO 
FOR j:=0 TO 2 DO
outfile.WriteRealFix(sq[i,j],6,2);END;
outfile.WriteString(' # ');outfile.WriteLInt(vert,3);
outfile.WriteLn;INC(vert);END;
END square;

PROCEDURE block(hl,wl,hu,wu:REAL);
BEGIN
vert:=0;
outfile.WriteString('{ OFF');outfile.WriteLn;
outfile.WriteString('8 6 0');outfile.WriteLn;
square(hl,wl,TRUE);
square(hu,wu,TRUE);
outfile.WriteString('4 3 2 1 0');outfile.WriteLn;(*bottom*)
outfile.WriteString('4 0 1 5 4');outfile.WriteLn;(*sides*)
outfile.WriteString('4 1 2 6 5');outfile.WriteLn;
outfile.WriteString('4 2 3 7 6');outfile.WriteLn;
outfile.WriteString('4 3 0 4 7');outfile.WriteLn;
outfile.WriteString('4 4 5 6 7');outfile.WriteLn;(*top*)
outfile.WriteString(' }');outfile.WriteLn;
END block;

PROCEDURE fins;
BEGIN
vert:=0;
outfile.WriteString('{ OFF');outfile.WriteLn;
outfile.WriteString('12 8 0');outfile.WriteLn;
square(h2,w2,TRUE);
square(h3,w3,FALSE);
square(h4,w4,TRUE);
outfile.WriteString('3 0 8 4');outfile.WriteLn;
outfile.WriteString('3 0 5 8');outfile.WriteLn;
outfile.WriteString('3 1 9 5');outfile.WriteLn;
outfile.WriteString('3 1 6 9');outfile.WriteLn;
outfile.WriteString('3 2 10 6');outfile.WriteLn;
outfile.WriteString('3 2 7 10');outfile.WriteLn;
outfile.WriteString('3 3 11 7');outfile.WriteLn;
outfile.WriteString('3 3 4 11');outfile.WriteLn;
outfile.WriteString(' }');outfile.WriteLn;
END fins;

BEGIN
invar:=Files.Old('obin',{Files.read},resv);
infile:=TextRider.ConnectReader(invar);
infile.ReadReal(h1); infile.ReadReal(h2); infile.ReadReal(h3); 
infile.ReadReal(h4); infile.ReadReal(w0); infile.ReadReal(w1);
infile.ReadReal(w4); invar.Close;
w2:=(w1-w4)*((h4-h2)/(h4-h1))+w4; w3:=w4; 
outvar:=Files.New('obelisk.txt',{Files.write},resv);
outfile:=TextRider.ConnectWriter(outvar);
outfile.WriteString('{ LIST ');outfile.WriteLn;
outfile.WriteString('# h1,h2,h3,h4=');
outfile.WriteRealFix(h1,7,3); outfile.WriteRealFix(h2,7,3);
outfile.WriteRealFix(h3,7,3); outfile.WriteRealFix(h4,7,3);
outfile.WriteLn;
outfile.WriteString('# w0,w1,w4=');
outfile.WriteRealFix(w0,7,3); outfile.WriteRealFix(w1,7,3);
outfile.WriteRealFix(w4,7,3); outfile.WriteLn;
outfile.WriteString('# w2,w3='); 
outfile.WriteRealFix(w2,7,3); outfile.WriteRealFix(w3,7,3); 
outfile.WriteLn;
outfile.WriteString('# base ');outfile.WriteLn;
block(0,w0,h1,w1); 
outfile.WriteString('# side');outfile.WriteLn;
block(h1,w1,h2,w2); 
outfile.WriteString('# bevel');outfile.WriteLn;
block(h2,w2,h3,w3);
outfile.WriteString('# shaft');outfile.WriteLn;
block(h3,w3,h4,w4);
outfile.WriteString('# fins');outfile.WriteLn;
fins;
outfile.WriteString(' }');outfile.WriteLn;
outvar.Close; 
i:=ProcessManagement.system("geomview obelisk.txt");
END obelisk.

the input data file is:

2 5 7 11 5.717 8.55 3

The above example illustrates the advantage of the combination of a custom program with a drawing program over a drawing program alone. Certain practical and aesthetic rules can be enforced while changing the proportions for the most satisfactory result. In this example, all horizontal cross-sections of the inner structure are square. The shaft is of constant cross-section. The corner of the sides and the outer edge of the fin are always on the same straight line. The inner vertices of adjacent fins meet at the base of the shaft. Any or all of the input data parameters can be changed without violating any of these rules.

The default behavior of geomview is spectacular, but not easily useable. To tame it and make it more useable, create the following file named ".geomview". The leading period in the name is essential. Put the file in your home directory:

(progn
(ui-motion inertia off)
(ui-motion constrain on)
(ui-motion own-coordinates on)
)

The leading period in the name ".geomview" means that when you do "ls" you will not see it. If you do "ls -aF" in your home directory you will see it and other configuration files.

The picture was produced for this web page in the following manner. In geomview, click "file", "save", "commands" "ppm software snapshot". Then enter "mypicture.ppm" in the selection window, then click "ok". Then get out of geomview and use the netpbm command "pnmtopng mypicture.ppm > mypicture.png". Then if you have imagemagick installed, "display mypicture.png" will display the result.

If you want to program games, you will want to use OpenGL. Install the program glxgears and run it to see that OpenGL is working properly on your system. The program glxinfo will give the status of your OpenGL software. The GLUT package is helpful for developing OpenGL programs. There are books available on OpenGL programming.

AUDIO OUTPUT

If you want to create waveforms for audio, you will want the waveform to average to zero. This means no direct current, or DC component. But the waveform you create may have a DC component. This DC component can be filtered out with a high pass filter that is 3dB down at 10Hz. It will be flat above 10Hz. It will fall at 6dB per octave below 10Hz. The following program tests a procedure that is a high pass filter.

MODULE hptest;
(*Test unity gain highpass filter procedure.*) 
IMPORT Out,rm:=RealMath;
CONST rate=48.0E3;fhp=10.0;
VAR t,y,xkm1,ykm1,gf,est:REAL;
i,j:LONGINT; first:BOOLEAN;

PROCEDURE highpass(t,fhp,xk:REAL;VAR yk:REAL);
(*Variables only used inside procedure must be declared
globally: first,est,gf,xkm1,ykm1. first must be initialized
globally. With filter 3dB point fhp at 10Hz, the result
of the filter is that the DC component reduced to a tenth,
20dB down, at 0.04 seconds*)
BEGIN
IF first THEN est:=rm.exp(-2*rm.pi*fhp*t);
gf:=(1+est)/(1-est);first:=FALSE; 
xkm1:=0.0;ykm1:=0.0; END(*IF*);
yk:=(xk-xkm1)/2;xkm1:=xk; xk:=yk;
yk:=est*ykm1+(1-est)*xk;ykm1:=yk; yk:=gf*yk;
END highpass;

BEGIN
t:=1.0/rate; first:=TRUE; j:=0;
FOR i:=0 TO 3000 DO highpass(t,fhp,1.0,y);
IF (i MOD 300)=0 THEN 
Out.LongInt(j,3); Out.Real(y,8,3);Out.Ln;
j:=j+1; END; END; END hptest.

  0 9.99E-1
  1 6.75E-1
  2 4.56E-1
  3 3.08E-1
  4 2.08E-1
  5 1.40E-1
  6 9.47E-2
  7 6.40E-2
  8 4.32E-2
  9 2.92E-2
 10 1.97E-2

The program feeds a DC signal in the form of a steady stream of the number one to the filter. The first output example is example number 0. There are 300 samples of input data between each example. By example number 6 the output is less than a tenth of the input. This is a reduction of 20dB since the inputs are analogous to voltage, not power. 6 times 300 is 1800 samples of input. 1800/48000 is 0.0375 seconds, or about 0.04 seconds. That is how long you will have to generate your waveform before you can feel that the DC component is not a problem.

We give here a trivial example of audio programming. We create a short beep file at an audio frequency of 1 kHz at a sample rate of 48kHz. The file will be 16 bit twos complement integer arithmetic. The largest number in 16 bit signed numbers is 2 to the 15th power or 32768. To make the beep 20dB quieter than the maximum, we want a number about one tenth of the maximum. The maximum value of a sine wave is one. Therefore we multiply our sine wave by 3000. We have only one channel. Our sine wave has no DC component, so we do not need the high pass filter, but we include it just to show how it is used.

MODULE beep;
IMPORT Msg,Files,BinaryRider,rm:=RealMath,
OS:ProcessManagement; 
CONST rate=48.0E3;lf=20.0;seconds=2.0;
VAR t,a,b,freq,xkm1,ykm1,est,gf:REAL;i,e:LONGINT;
ai:INTEGER;first:BOOLEAN;resv:Msg.Msg;outvar:Files.File;
outfile:BinaryRider.Writer;

PROCEDURE highpass(t,fhp,xk:REAL;VAR yk:REAL);
BEGIN
IF first THEN est:=rm.exp(-2*rm.pi*fhp*t);
gf:=(1+est)/(1-est);first:=FALSE; 
xkm1:=0.0;ykm1:=0.0; END(*IF*);
yk:=(xk-xkm1)/2;xkm1:=xk; xk:=yk;
yk:=est*ykm1+(1-est)*xk;ykm1:=yk; yk:=gf*yk;
END highpass;

BEGIN
t:=1.0/rate;first:=TRUE;
outvar:=Files.New('beep.s16',{Files.write},resv);
outfile:=BinaryRider.ConnectWriter(outvar);
e:=ENTIER(rate*seconds);freq:=1000;
FOR i:=1 TO e DO
a:=3000*rm.sin(2*rm.pi*freq*i/rate);
highpass(t,lf,a,b);
ai:=SHORT(ENTIER(b));
IF i > 2000 THEN
outfile.WriteInt(ai);END;END;
i:=ProcessManagement.system("sox -r 48k beep.s16  beep.wav");
i:=ProcessManagement.system("play beep.wav");
outvar.Close;
END beep.

We named our raw sound file beep.s16. The s16 suffix to the file name tells the sox program that it is a 16 bit raw sound file. We then called the sox program to convert it to a wav file, which simply added a header to the file. The header tells sound programs how to interpret the file. Some modern CD players have a USB port that can read and play wav files.

The above program produces two files, beep.s16 and beep.wav. If the outvar.Close statement were moved to just before the two process management statements, beep.wav would have been produced and played, but would not have been written to the hard drive.

The sox program can do many things, including conversion between sample rates. If you install the program "audacity" and adjust it to show a very small segment of time, you can see the shape of your waveform.

There are other audio formats such as MP3, AAC and ogg vorbis. There are programs to convert between formats. There are programs to burn audio files onto audio CD's.

If you want to go further in audio programming, the book "the physics of musical instruments" by N.H.Fletcher and T.D.Rossing will be helpful. Also the articles on digital filtering in this website, click here. An example of audio programming with digital filters is at this link.

MATHEMATICAL PROGRAMMING

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 library module "RealMath". You can read about the available functions in the OOCref manual in the "mathematical functions" section. You can add your own math functions without much difficulty. The book "Handbook of Mathematical Functions" by Abramowitz and Stegun contains formulas for all the math functions you could ever want. It also includes numerical tables of functions so that you can check that your math functions are producing the right answers. Novice programmers give too much importance to built in math functions. More important is how the language facilitates writing complex programs in a comprehensible manner.

In addition to mathematical functions, you may need applied math formulas. The best book I know of for this is "Handbook of Mathematics and Computational Science" by John W. Harris and Horst Stocker.

The following module provides basic complex arithmetic.

MODULE cxarith;
(*written 1980 donald daniel.  Originally written in pascal and
translated to Oberon-2 in 2000. This program is free software:
you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software
Foundation, version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General
Public License along with this program.  If not, see
http://www.gnu.org/licenses/.  *)
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
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);
 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;
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*)
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);".

PROGRAMMING HINTS

Some people, like myself, may find all the capitalization 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, data flow 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 data flow 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.

LARGE MODULES

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.

HOW TO CONVERT PASCAL TO OBERON

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.

up one level

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.