10.12. Input/Output

We now have a complete, working language, except for one minor embarassment: we have no way to get data in or out. We need some I/O.

Now, the convention these days, established in C and continued in Ada and Modula 2, is to leave I/O statements out of the language itself, and just include them in the subroutine library. That would be fine, except that so far we have no provision for subroutines. Anyhow, with this approach you run into the problem of variable-length argument lists. In Pascal, the I/O statements are built into the language because they are the only ones for which the argument list can have a variable number of entries. In C, we settle for kludges like scanf and printf, and must pass the argument count to the called procedure. In Ada and Modula 2 we must use the awkward (and slow!) approach of a separate call for each argument.

So I think I prefer the Pascal approach of building the I/O in, even though we don't need to.

As usual, for this we need some more code generation routines. These turn out to be the easiest of all, because all we do is to call library procedures to do the work:

{ Read Variable to Primary Register }
procedure ReadVar;
begin
   EmitLn('BSR READ');
   Store(Value);
end;

{ Write Variable from Primary Register }
procedure WriteVar;
begin
   EmitLn('BSR WRITE');
end;

The idea is that READ loads the value from input to the D0, and WRITE outputs it from there.

These two procedures represent our first encounter with a need for library procedures … the components of a Run Time Library (RTL). Of course, someone (namely us) has to write these routines, but they're not part of the compiler itself. I won't even bother showing the routines here, since these are obviously very much OS-dependent. I will simply say that for SK*DOS, they are particularly simple … almost trivial. One reason I won't show them here is that you can add all kinds of fanciness to the things, for example by prompting in READ for the inputs, and by giving the user a chance to reenter a bad input.

But that is really separate from compiler design, so for now I'll just assume that a library call TINYLIB.LIB exists. Since we now need it loaded, we need to add a statement to include it in procedure Header:

{ Write Header Info }
procedure Header;
begin
   WriteLn('WARMST', TAB, 'EQU $A01E');
   EmitLn('LIB TINYLIB');
end;

That takes care of that part. Now, we also need to recognize the read and write commands. We can do this by adding two more keywords to our list:

{ Definition of Keywords and Token Types }
const NKW =   11;
      NKW1 = 12;

const KWlist: array[1..NKW] of Symbol =
              ('IF', 'ELSE', 'ENDIF', 'WHILE', 'ENDWHILE',
               'READ',    'WRITE',    'VAR',    'BEGIN',   'END',
'PROGRAM');

const KWcode: string[NKW1] = 'xileweRWvbep';

Note

Note how I'm using upper case codes here to avoid conflict with the 'w' of WHILE.

Next, we need procedures for processing the read/write statement and its argument list:

{ Process a Read Statement }
procedure DoRead;
begin
   Match('(');
   GetName;
   ReadVar;
   while Look = ',' do begin
      Match(',');
      GetName;
      ReadVar;
   end;
   Match(')');
end;

{ Process a Write Statement }
procedure DoWrite;
begin
   Match('(');
   Expression;
   WriteVar;
   while Look = ',' do begin
      Match(',');
      Expression;
      WriteVar;
   end;
   Match(')');
end;

Finally, we must expand procedure Block to handle the new statement types:

{ Parse and Translate a Block of Statements }
procedure Block;
begin
   Scan;
   while not(Token in ['e', 'l']) do begin
      case Token of
       'i': DoIf;
       'w': DoWhile;
       'R': DoRead;
       'W': DoWrite;
      else Assignment;
      end;
      Scan;
   end;
end;

That's all there is to it. Now we have a language!