Software Tools in Modula-2

[Note:] This is a work in progress. Please check back for updates.

In a previous document, I relate my experience of building the programs of Software Tools in Pascal [KP81], using Free Pascal.

An ETH project existed to build similar tools primitives on Medos-2 [K83] as a kind of portable library for Modula-2 called HOST[KU87]. Like the UCSD example in Software Tools in Pascal, as well as the classic Mac OS implementation of Wirth's command line in MacMETH, it required building a custom interpreter environment.

Following the example of Kernighan and Plauger, I have decided to pattern this project on the final language report, in this case from Wirth's Programming in Modula-2 Fourth Edition (PIM4), using a subset compatible with the ISO/IEC 10514-1:1996 standard. The ISO standard library offers primitives equivalent to those in Software Tools. The command line interfaces of cmd.exe in Windows and /bin/sh on any Unix system (e.g. GNU/Linux, macOS) will be assumed to be available, instead of building a custom environment. Unfortunately, the ISO/IEC standard is a dialect, in some ways similar to PIM3, but also with elements from PIM4 and Algorithms and Data Structures[W86].

Compiler Examples

Some years ago, I began working with m2c (which claims to be a PIM4 compiler), but found so many bugs in it that I spent more time patching it than doing anything productive. Plus, m2c is an incomplete (and buggy) implementation of Modula-2: to use it requires working with further restrictions. The Mill Hill & Canterbury (MHC) Modula-2 compiler works with Java, but seems equally buggy.

Mocka is a curious creature depending on the version used: 9905 (RPM), 0605m, 0608m (RPM), and 1208m, (the m is for Professor Christian Maurer's editions, who also provides the Murus libraries. See Mueller's recent ports to 64-bit Mint.). With 1208m, the m2.tgz package should be extracted as the root (ID 0) user to /usr/local/ or /opt/, and the correct variables set in the /etc/profile.d/m2.sh file. Instead of the .mi and .md previously required extensions, .mod and .def are used. The first time the m2 command is run on a file, it will create a $HOME/m2/ directory with bin/, src/, and out/ directories. Place binaries in bin/. Source files should be authored directly in src/. Object code and symbol files will go in out/. Unfortunately, this is a PIM3, 32-bit compiler.

Another tempting compiler to use was the Amsterdam Compiler Kit (ACK), which is explicitly mentioned in the appendix of Software Tools in Pascal. I put together an RPM for Red Hat Enterprise Linux 7, and created a $HOME/src/ack/ directory. In that directory, I copied my hello.m2 file as hello.mod and ran the following command:

$ ack -Rm2-3 -o bin/hello hello.mod

I found that the current 6.1pre1 release did not build for me, so I had to download the source from the git repository. The author graciously provided a pre-packaged tape archive for me to use. One thing I disliked about this compiler was the lack of a separate compilation facility, requiring the modules to be rebuilt for each program. It is also a 32-bit, PIM3 compiler.

GNU Modula-2 (gm2) provides a PIM4 compiler which supports 64-bit, perhaps one of the few, but it is not yet part of the mainstream GCC. For the Unix system macOS, the only known compiler is p1. It is outrageously priced for the 64-bit version that runs on modern macOS (500 Euros). I have books that work with Logitech Modula-2 for PC DOS, which was replaced with Stony Brook's Modula-2, now ADW for Windows (accessible as freeware).

This project is compiled and used mainly with ADW, but modules were also tested with other compilers mentioned above.

Modula-2 syntax and standard modules

Modula-2 follows the Mesa language design: case sensitive upper-case statements should only be used for reserved words/objects of the compiler. This avoids portability problems between compilers. In Software Tools (both versions), upper-case symbolic constants are used so they stand out. If the stand out convention is followed, hiding these objects in a module, and calling it unqualified (e.g. ST.ENDFILE), will avoid reserved object collisions in Mesa/Modula-2 style.

Unfortunately, different compilers interpreted the Modula-2 Report in differing ways as to how IMPORT was used. (See Modula-2: A Complete Guide[K88], pg. 39.) For instance, if FROM foo IMPORT bar is used, a separate IMPORT foo is needed in ADW to use foo.bar.

In Modula-2 there are no built-in I/O statements. Instead, I/O is considered to be device and implementation specific, provided by compiler specific modules, and thus are not portable. Wirth indicated that the modules InOut, RealInOut, LineDrawing, MathLib0, and Streams (or an equivalent thereof) can be considered as standard modules available in all implementations of Modula. Except that Streams was later combined into FileSystem, LineDrawing was rarely included with third party compilers, nor was InOut implemented exactly the same. (In Oberon, Texts and Files remain separate.) After HOST, the Modular Input/Output Services Library (MINOS[O88]) was suggested by M. Odersky. Thus what is standard is a suggestion for what a Modula-2 compiler would be negligent not to have. As the report states, Such facilities must therefore not be included as elements of the language itself, but appear as (so-called low level) modules which are components of most programs written. Such a collection of standard modules is therefore an essential part of a Modula-2 implementation.... For portability, it is therefore the programmer's job to use modules to abstract the underlying operating environment in such a way that is not system specific.

Note that Wirth's InOut module, under Medos-2 and MacMETH, ties the standard in and out types to the interactive keyboard and monitor display, and is thus unbuffered. InOut switches between the use of Terminal and Streams (or FileSystem), based on context, and only allows a single file input, and a single output file, to be accessed at a time. Low level modules described in PIM are different between the RT-11 and Medos-2 implementations, and though Streams (or FileSystem) should be considered fundamental in concept, they are not guaranteed. InOut will work for building getc and putc initially, but as with the Fortran examples in Software Tools, requires built-in buffering to work. Though custom modules using SYSTEM, and FileSystem or external libraries might be effective for file access, the ISO standard library can be used. For more details on Wirth's library, as well as differences with Pascal, see K. N. King's Modula-2: A Complete Guide[K88]. Also see An Introduction to Modula-2 for Pascal Programmers [JW83].

One approach for module primitives is to follow the UCB examples, and make a Globals, Prims, and Utils set of modules. A simpler form without Utils, perhaps renaming Globals to Tools following the UCSD example, or even a unified module called ST, could be designed. It's also possible the opposite approach could be taken, starting with a module called GetPut. I experimented with all of these, and preferred the latter as a long term approach of using Kernighan's C-like primitives with Modula-2, yet in learning the ISO libraries as used in ADW and gm2, it seemed that those modules were just as suitable and standard, though for Modula-2 overall may not be as portable as those designed by Kernighan and Plauger.

Chapter 1

Chapter 1 covers some basic input and output filters.

getc and putc

Several reasons are given for getc. The first is to hide the details of what is unique to any particular system: its input and output devices. Hiding not only the details of how to pick the standard in and out devices, but how lines and files are handled in terms of markers and functions that identify these details is incredibly useful. The general answer to needing these is first explained in the authors' book The Elements of Programming Style[KP78]. The first point is to isolate the details of I/O into one place that is recognized as being non-portable, including different character sets. This is explained with both PL/1 and Pascal.

Under Fortran 66, there is no character type. (There is in Fortran 77.) There is only the Holerith string type. Integers had to be passed around and converted to Holeriths. PL/1 had a character type, but one of the exercises asks why it wasn't used, and both books suggest this will be explained (which it does in several places). Passing of integer and character around makes less sense at first with Pascal, until an underlying key piece is explained: most of the compilers available to Kernighan and Plauger are written in C. The char type of C, to be portable, needs to be abstracted to distinguish between signed and unsigned integers, not only differences in character set. This becomes especially needful when using a negative integer (e.g. -1) as an end-of-file sentinel. There's low level nuggets like this scattered through both books. This lends to solving problems of efficiency, which also merit having a separate abstraction. Compiler authors are dealing with complex software. Writing for small systems often required tricks to make a program usable. With modern computing this is mostly unnecessary, except with HPC needs.

Where the getc function is to collect I/O handling into one place, a Modula-2 style would be better expressed with a boolean that identifies end-of-file, though only piece left in the Pascal version requiring an INTEGER-based character type. The while loop approach with an integer return is an artifact of C-based compilers (though the Pascal and Modula-2 CHAR could still potentially be signed, this was rare if non-existant with Modula-2). The approach of handling portability that Kernighan suggests is testing on different platforms (i.e. architecture and OS) with different compilers, or at least do so when needed.

PRIMITIVE
   getc: get a character from standard input.
USAGE
   PROCEDURE getc (VAR c: character): character;
FUNCTION
   getc reads at most one character from the standard input. If there are no more
   characters available, getc returns ENDFILE; if the input is at end-of-line, it returns
   NEWLINE and advances to the beginning of the next line; otherwise it returns the next input
   character.
RETURNS
   getc returns the value of type character corresponding to the character read from the
   standard input, or one of the special values NEWLINE or ENDFILE as specified above. The
   return value is also written in the argument c.
PRIMITIVE
   putc: put a character on standard output.
USAGE
   PROCEDURE putc (c: character);
FUNCTION
   putc writes the character c to the standard output STDOUT; if the value of the argument c
   is NEWLINE, an appropriate end-of-line condition is generated.
RETURNS
   Nothing.

copy

Assuming ST with the custom character type, the copyprog program looked like this:

MODULE copyprog; (*DEE 2013-12-11*)
(* Software Tools in Pascal, Exercise 1-1. *)
  FROM ST IMPORT ENDFILE, character, getc, putc;

 (* copy: copy input to output *)
  PROCEDURE copy;
    VAR c: character;
  BEGIN
    WHILE (getc(c) # ENDFILE)  DO
      putc(c)
    END
  END copy;

BEGIN copy
END copyprog.

The ST module can be found at the end of this document, where getc, putc, character, and ENDFILE have been added. Using the ISO standard library, I did the following, looking very much like Pascal's copytext:

MODULE copytext; (*DEE 2020-05-02*)
  FROM SIOResult IMPORT ReadResult, ReadResults;
  FROM STextIO IMPORT ReadChar, SkipLine, WriteChar, WriteLn;
  VAR ch: CHAR;

BEGIN ReadChar(ch);
  WHILE ReadResult() # endOfInput DO
    WHILE ReadResult() # endOfLine DO
      WriteChar(ch); ReadChar(ch)
    END;
    SkipLine; WriteLn; ReadChar(ch)
  END
END copytext.

Where Unix (e.g. macOS, GNU/Linux) already have these tools and manuals, I decided to only document the Windows binary from cmd.exe:

PROGRAM
	copy - copy characters from standard input to standard output
USAGE
	copyprog
DESCRIPTION
	copyprog copies its input to its output unchanged. It is useful
	for copying from a text terminal to a file, from a file to a
	file, from a text terminal to a text terminal, or from a file
	to a text terminal (to display it).

	This program is also from the Pascal User Manual and Report, Second
	Edition (second 1978, and fourth printings), by Kathleen Jensen and
	Niklaus Emil Wirth, pg. 164. Also see pg. 200 of the Fourth Edition.

	On Unix (e.g. macOS, OpenBSD) and GNU/Linux, use the cat command to
	do the same. On Windows, see the more command, which is similar.
EXAMPLE
	C:\Users\David> copyprog
	hello, world
	hello, world
	^Z

	C:\Users\David> echo "hello, world" > hello.txt
	C:\Users\David> copyprog <hello.txt
	hello, world

This accomplishes exercise 1-1 of Software Tools in Pascal but in Modula-2. Exercise 1-2 is accomplished with the classic dmr hello, world output. The next step is identifying how character reading is done.

charcount

Here's the manual for charcount:

PROGRAM
	charcount: count character in input
USAGE
	charcount
DESCRIPTION
	charcount counts the characters in its input and writes the total as a
	single line of text to the output. Since each line of text is internally
	delimited by the EOL charactor or characters, the total count is the number
	of lines plus the number of characters within each line.
EXAMPLE
	C:\Users\david> charcount
	hello, world
	^Z
	13

Note the use of the INC standard procedure, instead of the typical expression nc := nc + 1:

MODULE charcount; (*DEE 2015-11-25*)
  FROM ST IMPORT ENDFILE, NEWLINE, character, getc, putc, putdec;

  (* charcount: count characters in standard input *)
  PROCEDURE charcount;
    VAR nc: CARDINAL; c: character;
  BEGIN nc := 0;
    WHILE (getc(c) # ENDFILE) DO INC(nc) END;
    putdec(nc, 1); putc(NEWLINE)
  END charcount;

BEGIN charcount
END charcount.

The putdec procedure is not introduced until the end of the second chapter. It is necessary for the program to work when using the character type. Under Pascal, the standard and generalized write command automatically does the integer and char conversions in the same way. BLANK is decimal character 32. It and putdec have been added to the ST module.

Here's the program with the ISO standard library:

MODULE charcount; (*DEE 2020-05-02*)
  FROM SIOResult IMPORT ReadResult, ReadResults;
  FROM STextIO IMPORT ReadChar, SkipLine, WriteLn;
  FROM SWholeIO IMPORT WriteInt;
  VAR ch: CHAR; nc: CARDINAL;
BEGIN nc := 0; ReadChar(ch);
  WHILE ReadResult() # endOfInput DO
    INC(nc);
    WHILE ReadResult() # endOfLine DO
      ReadChar(ch); INC(nc)
    END;
    SkipLine; ReadChar(ch)
  END;
  WriteInt(nc, 1); WriteLn
END charcount.

linecount

PROGRAM
	linecount: count lines in input
USAGE
	linecount
FUNCTION
	linecount counts the newlines in its input and writes the total as a line of
	text to its output.
EXAMPLE
	C:\Users\david> linecount
	hello, world
	^Z
	1

Here's the version with the character type. Notice that the module is imported without qualification. This allows clarity in the object uppercase naming as to its being part of a module, not a built-in object. It also can be better practice in bigger programs to avoid qualifying module objects and functions.

MODULE linecount; (*DEE 2015-11-25*)
  IMPORT ST;

  (* linecount: count lines in standard input *)
  PROCEDURE linecount;
    VAR nl: CARDINAL; c: ST.character;
  BEGIN nl := 0;
    WHILE (ST.getc(c) # ST.ENDFILE) DO
      IF (c = ST.NEWLINE) THEN INC(nl) END
    END;
    ST.putdec(nl, 1);
    ST.putc(NEWLINE)
  END linecount;

BEGIN linecount
END linecount.

Here's the source using the ISO/IEC 10514-1:1996 modules:

MODULE linecount; (*DEE 2020-05-02*)
  FROM SIOResult IMPORT ReadResult, ReadResults;
  FROM STextIO IMPORT ReadChar, SkipLine, WriteLn;
  FROM SWholeIO IMPORT WriteInt;
  VAR ch: CHAR; nl: CARDINAL;
BEGIN ReadChar(ch);
  WHILE ReadResult() # endOfInput DO
    WHILE ReadResult() # endOfLine DO ReadChar(ch) END;
    SkipLine; INC(nl); ReadChar(ch)
  END;
  WriteInt(nl, 1); WriteLn
END linecount.

For Exercise 1-3, I started with a blank text file, and a file with characters without a newline, to demonstrate that the program behaved cleanly, but missed counting the line as a natural consequence of the definition of a line being tied to the newline representation.

wordcount

PROGRAM
   wordcount: count words in input
USAGE
   wordcount
FUNCTION
   wordcount counts the words in its input and writes the total as a line
   of text to the output. A "word" is a maximal sequence of characters not
   containing a blank, tab, or newline.
EXAMPLE
   wordcount
   A single line of input.
   <ENDFILE>
   5
BUGS
   The definition of "word" is simplistic.

Here's the translation of the program to Modula-2. It introduces the BLANK and TAB character objects. Though the NOT term could also be used, the ~ operator often had a different gliff in earlier times to represent a negation (¬), but now has a dual meaning with its ~ tilde gliph. I decided to use this as it is what is used with its successor language Oberon, and is not unusual in other programming languages.

MODULE wordcount; (*DEE 2015-11-25*)
  IMPORT ST;

  (* wordcount: count words in standard input *)
  PROCEDURE wordcount;
    VAR nw: CARDINAL; c: ST.character; inword: BOOLEAN;
  BEGIN nw := 0; inword := FALSE;
    WHILE (ST.getc(c) # ST.ENDFILE) DO
      IF (c = ST.BLANK) OR (c = ST.NEWLINE) OR (c = ST.TAB) THEN
        inword := FALSE
      ELSIF ~inword THEN
        inword := TRUE; INC(nw)
      END
    END;
    ST.putdec(nw, 1); ST.putc(ST.NEWLINE)
  END wordcount;

BEGIN wordcount
END wordcount.

Here's the ISO library version:

MODULE wordcount; (*DEE 2020-05-31*)
  FROM SIOResult IMPORT ReadResult, ReadResults;
  FROM STextIO IMPORT ReadChar, SkipLine, WriteLn;
  FROM SWholeIO IMPORT WriteCard;
  VAR nw: CARDINAL;

  PROCEDURE wordcount;
    CONST BLANK = 40C; TAB = 11C;
    VAR c: CHAR; inword: BOOLEAN;
  BEGIN nw := 0; inword := FALSE;
    ReadChar(c);
    WHILE ReadResult() # endOfInput DO
      IF ReadResult() = endOfLine THEN
        SkipLine; inword := FALSE;
      ELSIF (c = BLANK) OR (c = TAB) THEN
        inword := FALSE;
      ELSIF ~inword THEN inword := TRUE; INC(nw)
      END;
      ReadChar(c)
    END;
    WriteCard(nw, 1); WriteLn
  END wordcount;

BEGIN wordcount
END wordcount.

wordcount exercise

Exercise 1-4 asks for the combination of the counting commands into one, following the wc command of early Unix. It becomes obvious when reading the source text that adding a count for lines and characters is trivial, since we're reading characters and identifying lines anyway. The Unix order of output is lines, words, and characters, biggest to smallest. One might argue that the word count should come first since this is the name of the program, and that lines should follow (since the representation could be more than one character, e.g. CRLF), then characters.

The final exercise question is whether the combination is better. The convienance of the addition, since it will only require adding the nl and nc objects from the other programs, and the use of the INC procedure to count, justifies the combination, though perhaps the combined output is no longer intuitive (though perhaps guessable). However, there's a bigger picture here that is part of the Unix philosophy that constantly begs the question regarding having a program do one thing and do it well.

A negative example is the cat command. Its purpose is to copy its input to its output, which when copy is combined with file handling (concat) becomes a simple file merging tool. We will see on page 67 that messages that are sent to the user should be kept distinct from the purposeful output of the program. For instance, we don't want an error message following data down a pipeline (introduced on page 59). The Berkeley Unix developers added flags, the Unix name for optional arguments (sometimes called shell or command parameters) to the cat command that modified the output behavior. See exercise 2-29 at the end of chapter 2's work with the translit command. This showed up in Edition 8 of research Unix as the vis command, a mneumonic for visualize, and found its way into the author's book The Unix Programming Environment as an example of when adding features to a program is not the right solution, requiring a new program. As Kernighan's co-author (Rob Pike) famously said, cat came back from Berkeley waving flags.

Here's the modified manual for the new program behavior of the exercise:

PROGRAM
   wordcount: count words in input
USAGE
   wordcount
FUNCTION
   wordcount counts the newline terminators, words, and single byte characters
   in its input and writes the totals (in that order) as a line of text to the
   output. A word is a maximal sequence of characters not containing a blank,
   tab, or newline. ENDFILE given in the examples is Ctrl+d in Unix (i.e. the
   EOT character), and Ctrl+z (i.e. SUB) then Enter in Windows cmd.exe.
EXAMPLE
   wordcount
   hello, world
   <ENDFILE>
   1       2        13
BUGS
   The definition of a word is simplistic. Since the newline representation is
   typically characters, a multi-character representation will only get a single
   character count.

Here's the program modified to match the behavior, first the ST library version, then the ISO library version.

MODULE wordcount; (*DEE 2015-11-25/2016-08-23*)
  IMPORT ST;

  (*See Exercise 1-4 in Software Tools.*)
  PROCEDURE wordcount;
    VAR nc, nl, nw: INTEGER; c: ST.character; inword: BOOLEAN;
  BEGIN nl := 0; nw := 0; nc := 0; inword := FALSE;
    WHILE (ST.getc(c) # ST.ENDFILE) DO
      IF (c = ST.BLANK) OR (c = ST.NEWLINE) OR (c = ST.TAB) THEN
        inword := FALSE;
        IF (c = ST.NEWLINE) THEN INC(nl) END
      ELSIF ~inword THEN inword := TRUE; INC(nw)
      END;
      INC(nc)
    END;
    ST.putdec(nl,8); ST.putdec(nw,8); ST.putdec(nc,8);
    ST.putc(ST.NEWLINE)
  END wordcount;

BEGIN wordcount
END wordcount.
MODULE wordcount; (*DEE 2020-05-31*)
  FROM SIOResult IMPORT ReadResult, ReadResults;
  FROM STextIO IMPORT ReadChar, SkipLine, WriteLn;
  FROM SWholeIO IMPORT WriteCard;
  VAR nc, nl, nw: CARDINAL;

  PROCEDURE wordcount;
    CONST BLANK = 40C; TAB = 11C;
    VAR c: CHAR; inword: BOOLEAN;
  BEGIN nl := 0; nw := 0; nc := 0; inword := FALSE;
    ReadChar(c);
    WHILE ReadResult() # endOfInput DO
      IF ReadResult() = endOfLine THEN
        SkipLine; inword := FALSE; INC(nl)
      ELSIF (c = BLANK) OR (c = TAB) THEN
        inword := FALSE;
      ELSIF ~inword THEN inword := TRUE; INC(nw)
      END;
      INC(nc); ReadChar(c)
    END;
    WriteCard(nl, 8); WriteCard(nw, 8); WriteCard(nc, 8);
    WriteLn
  END wordcount;

BEGIN wordcount;
END wordcount.

This never did quite satisfy me. First, it seemed better to run wordcount, then print the results, instead of handling output within the procedure. Yet the behavior of the new program was not compatible with the behavior of the Software Tools wordcount. Once arguments are introduced (see crypt in the Ratfor book, and echo in the Pascal version, an exercise in the Ratfor book), the individual commands can be emulated with options, and the default behavior adjusted. Using the PIM3 style of ADW, I had to use the VAL procedure to do a bit of casting to compensate for the INTEGER based character type (PIM4 can assume the INTEGER as default on larger systems, i.e. 32-bit and bigger, with CARDINAL as a compatible subrange):

    ELSIF (cmd[2] = VAL(INTEGER, ORD('w'))) THEN wordcount;
      ST.putdec(nw,1)

With PIM4 it looked like this:

    ELSIF (cmd[2] = 'w') THEN wordcount; ST.putdec(nw,1)

I wrote a simple help function, so that when the incorrect argument was selected, a usage summary was printed. Here's the ADW version:

  PROCEDURE help(CONST s: ARRAY OF CHAR);
  BEGIN WriteString(ErrChan(), 'usage: wordcount [ -l | -w | -c ]');
    WriteLn(ErrChan());
    HALT(1)
  END help;

Finally, I made a compatible wordcount program, defaulting to only the word count. This caused a dilemma of how to combine the options. I added an -a flag, since I didn't have a Unix getops function available to do this cleanly. At this point, I converted the program to use the ISO library for the cleaner, PIM4 style code, and landed with a usable wordcount tool on Windows.

detab

PROGRAM
   detab convert tabs to blanks
USAGE
   detab
FUNCTION
   detab copies its input to its output, expanding horizontal tabs to blanks
   along the way, so that the output is visually the same as the input, but
   contains not tab characters. Tab stop are assumed to be set every four
   columns (i.e., 1, 5, 9, ...), so that eac tab character is replaced by from
   one to four blanks.
EXAMPLE
   Using -> as a visible tab:
      detab
      ->col 1->2->34->rest
        col 1   2       34      rest
BUGS
   detab is naive about vertical motions and non-printing characters.
MODULE detab; (*DEE 2014-01-20*)
  FROM Tabs IMPORT tabpos, tabstops, settabs;
  IMPORT ST;

  (* detab - convert tabs to equivalent number of blanks. *)
  PROCEDURE detab;
    VAR c: ST.character; col: INTEGER;
  BEGIN settabs(tabstops); (* Set initial tab stops. *)
    col := 1;
    WHILE (ST.getc(c) # ST.ENDFILE) DO
      IF (c = ST.TAB) THEN
        REPEAT ST.putc(ST.BLANK);
          INC(col)
        UNTIL (tabpos(col, tabstops))
      ELSIF (c = ST.NEWLINE) THEN
        ST.putc(ST.NEWLINE);
        col := 1
      ELSIF (c = ST.BACKSPACE) THEN
        ST.putc(c);
        IF (col >= 1) THEN DEC(col) END
      ELSE ST.putc(c); INC(col)
      END;
    END
  END detab;

BEGIN detab
END detab.

Chapter 2

Chapter 2 continues working on filters, but begins looking at more complicated tools, and introduces command arguments.

getarg: crypt

In Software Tools in Pascal, the echo example replaces crypt as shown in Software Tools [KP76]. There are two ways to do this in Modula-2. The more efficient, though less portable approach, would be to use Modula-2's BITSET type and the VAL function (to cast between CHAR and INTEGER). If your compiler already has a built-in XOR function, that is better. In the end, I settled on a portable function in (PIM4) standard Modula-2.

MODULE crypt; (*DEE 2014-03-04/2015-12-05*)
  (* Software Tools crypt, but in Modula-2 *)
  FROM ST IMPORT ENDFILE, character, string, error, getarg, getc, putc, length;
  
  (* Thanks to Peter De Wachter. See Software Tools exercise 2-18. *)
  (* c := ((NOT b) AND a) OR ((NOT a) AND b); *)
  PROCEDURE xor(a, b: character): character;
    VAR c, k: CARDINAL;
  BEGIN c := 0; k := 1;
    WHILE (a # 0) OR (b # 0) DO
      IF (a MOD 2) # (b MOD 2) THEN
        c := c + k
      END;
      a := a DIV 2; b := b DIV 2; k := k * 2
    END;
    RETURN c
  END xor;

  (* Encrypt/decrypt using bitwise exclusive-or cipher. *)
  PROCEDURE crypt;
    CONST MAXKEY = 256;
    VAR c: character; key: string; i, keylen: CARDINAL;
  BEGIN
    IF getarg(1, key, MAXKEY) THEN
      keylen := length(key); i := 0;
      WHILE (getc(c) # ENDFILE) DO
        putc(xor(c, key[i]));
        i := (i MOD keylen)
      END
    ELSE error('usage: crypt key')
    END
  END crypt;

BEGIN crypt
END crypt.
PROGRAM
	crypt - encrypt and decrypt standard input and print to standard output
USAGE
	crypt key
DESCRIPTION
	crypt uses a key to encrypt characters coming from the standard input and
	prints the result to standard output. This half addition as a symmetric key
	encryption: what you encrypt with the key is decrypted with the key. It also
	means you can double encrypt by encrypting with one key, then reencrypting
	with a second key
EXAMPLE
	C:\>echo hello, world | crypt pass | crypt pass
	hello, world

Software Tools in Modula-2 ST module primitives

DEFINITION MODULE ST;
  (*DEE 2014-01-22/2015-11-25/2015-12-08/2015-15-25/2017-10-07/2018-05-28*)
  (* ST (ADW): basic I/O primitives, global constants, types, and variables. *)
  (* An adaption of the primitives from Software Tools in Pascal. *)
  IMPORT SeqFile;
  CONST

  (* Standard file descriptors. Subscripts in open, etc. *)
    (* These are not to be changed. *)
    STDIN = 1;
    STDOUT = 2;
    STDERR = 3;

  (* Status values for open files, and other I/O related stuff. *)
    IOERROR = 0;
    IOAVAIL = 1;
    IOREAD = 2;
    IOWRITE = 3;
    MAXOPEN = 10; (* Maximum number of open files. *)

  (* Universal manifest constants. *)
    ENDFILE = -1;
    EOF = ENDFILE;
    ENDSTR = 0;
    MAXSTR = 256;

  (* ASCII character set in decimal. *)
    BACKSPACE = 8;
    TAB = 9;
    NEWLINE = 10;
    BLANK = 32;
    EXCLAM = 33; (* ! *)
    DQUOTE = 34; (* " *)
    SHARP = 35; (* # *)
    HASH = SHARP;
    DOLLAR = 36;
    PERCENT = 37;
    AMPER = 38; (* & *)
    SQUOTE = 39; (* ' *)
    ACUTE = SQUOTE;
    LPAREN = 40; (* ( *)
    RPAREN = 41; (* ) *)
    STAR = 42; (* * *)
    PLUS = 43;
    COMMA = 44;
    MINUS = 45; (* - *)
    DASH = MINUS;
    PERIOD = 46;
    SLASH = 47; (* / *)
    COLON = 58;
    SEMICOL = 59;
    LESS = 60; (* < *)
    EQUALS = 61;
    GREATER = 62; (* > *)
    QUESTION = 63; (* ? *)
    ATSIGN = 64; (* @ *)
    ESCAPE = ATSIGN;
    LBRACK = 91; (* [ *)
    BACKSLASH = 92; (* \ *)
    RBRACK = 93; (* ] *)
    CARET = 94; (* ^ *)
    UNDERLINE = 95;
    GRAVE = 96; (* ` *)
    LETA = 97; (* Lower case... *)
    LETB = 98; LETC = 99; LETD = 100; LETE = 101; LETF = 102; LETG = 103; LETH = 104;
    LETI = 105; LETJ = 106; LETK = 107; LETL = 108; LETM = 109; LETN = 110; LETO = 111;
    LETP = 112; LETQ = 113; LETR = 114; LETS = 115; LETT = 116; LETU = 117; LETV = 118;
    LETW = 119; LETX = 120; LETY = 121; LETZ = 122;
    LBRACE = 123; (* Left brace. *)
    BAR = 124; (* | *)
    PIPE = BAR;
    RBRACE = 125; (* Right brace. *)
    TILDE = 126; (* ~ *)

  TYPE
    character = [-1..127]; (* Byte-sized. ASCII + other stuff. *)
    string = ARRAY [1..MAXSTR] OF character;
    filedesc = [IOERROR..MAXOPEN];
    ioblock = RECORD (* To keep track of open files. *)
      filevar: SeqFile.ChanId;
      mode: [IOERROR..IOWRITE];
    END;

  VAR
    openlist: ARRAY [1..MAXOPEN] OF ioblock; (* Open files. *)

  (* Primitives *)
  PROCEDURE getc(VAR c: character): character;
  PROCEDURE putc(c: character);
  PROCEDURE getarg(n: INTEGER; VAR s: string; maxsize: INTEGER): BOOLEAN;
  PROCEDURE nargs(): INTEGER;
  PROCEDURE message(CONST s: ARRAY OF CHAR);
  PROCEDURE error(CONST s: ARRAY OF CHAR);

  (* Utilities *)
  PROCEDURE addstr(c: character; VAR outset: string;
    VAR j: INTEGER; maxset: INTEGER): BOOLEAN;
  PROCEDURE esc (VAR s: string; VAR i: INTEGER): character;
  PROCEDURE equal (VAR str1, str2: string): BOOLEAN;
  PROCEDURE index (VAR s: string; c: character): INTEGER;
  PROCEDURE isdigit (c: character): BOOLEAN;
  PROCEDURE ispunct (p: character): BOOLEAN;
  PROCEDURE islower(c: character): BOOLEAN;
  PROCEDURE isupper(c: character): BOOLEAN;
  PROCEDURE isletter (n: character): BOOLEAN;
  PROCEDURE isalphanum (n: character): BOOLEAN;
  PROCEDURE itoc(n: INTEGER; VAR s: string; i: INTEGER): INTEGER;
  PROCEDURE length(VAR s: string): INTEGER;
  PROCEDURE max(x, y: INTEGER): INTEGER;
  PROCEDURE min (x, y: INTEGER): INTEGER;
  PROCEDURE ctoi(VAR s: string; VAR i: INTEGER): INTEGER;
  PROCEDURE putdec (n, w: INTEGER);
  PROCEDURE cxor(x, y: character): character; (* Bitwise Xor *)
  PROCEDURE xor(x, y: INTEGER): INTEGER; (* Bitwise Xor *)
END ST.
IMPLEMENTATION MODULE ST; (*DEE 2014-01-22/2015-11-25/2016-01-27/2017-10-07.*)
  IMPORT IOChan, ProgramArgs, SIOResult, StdChans, STextIO, TextIO;
  VAR n: INTEGER; argstr: ARRAY [0..MAXSTR] OF CHAR;

  (* Primitives *)
  PROCEDURE getc(VAR c: character): character;
    VAR ch: CHAR;
  BEGIN STextIO.ReadChar(ch);
    IF SIOResult.ReadResult() = SIOResult.endOfInput THEN
      c := ENDFILE
    ELSIF SIOResult.ReadResult() = SIOResult.endOfLine THEN
      STextIO.SkipLine; c := NEWLINE
    ELSE
      c := ORD(ch)
    END;
    RETURN c
  END getc;

  PROCEDURE putc(c: character);
  BEGIN
    IF (c = NEWLINE) THEN
      STextIO.WriteLn
    ELSE
      STextIO.WriteChar(CHR(c))
    END
  END putc;

  (*getarg: get n-th command line argument into s.
    which returns the 0th to paramcount-1th argument in s.*)
  PROCEDURE getarg(n: INTEGER; VAR s: string; maxsize: INTEGER): BOOLEAN;
    VAR arg: ARRAY [1..MAXSTR] OF CHAR; argn, i, t, lnb: INTEGER;
  BEGIN lnb := 0; argn := nargs(); t := 0;
    IF (n >= 0) AND (n <= argn) THEN
      WHILE (t < n) DO
        ProgramArgs.NextArg();
        TextIO.ReadToken(ProgramArgs.ArgChan(), arg);
        INC(t)
      END;
      IOChan.Reset(ProgramArgs.ArgChan());
      FOR i := 1 TO maxsize-1 DO
        s[i] := ORD(arg[i]);
        IF (arg[i] # ' ') THEN
          lnb := i
        END
      END;
      RETURN TRUE
    ELSE
      RETURN FALSE
    END;
    s[lnb+1] := ENDSTR
  END getarg;

  PROCEDURE nargs(): INTEGER;
  BEGIN n := 0;
    WHILE ProgramArgs.IsArgPresent() DO
      TextIO.ReadToken(ProgramArgs.ArgChan(), argstr);
      ProgramArgs.NextArg();
      INC(n)
    END;
    IOChan.Reset(ProgramArgs.ArgChan());
    RETURN n
  END nargs;

  PROCEDURE message(CONST s: ARRAY OF CHAR);
  BEGIN TextIO.WriteString(StdChans.ErrChan(), s);
    putc(NEWLINE)
  END message;

  PROCEDURE error(CONST s: ARRAY OF CHAR);
  BEGIN message(s); HALT(1)
  END error;

  (* Utilities *)
  PROCEDURE addstr(c: character; VAR outset: string;
    VAR j: INTEGER; maxset: INTEGER): BOOLEAN;
  BEGIN
    IF (j > maxset) THEN RETURN FALSE
    ELSE outset[j] := c; INC(j); RETURN TRUE
    END
  END addstr;

  (* Software Tools in Pascal, exercise 2-22. *)
  PROCEDURE esc (VAR s: string; VAR i: INTEGER): character;
    CONST CR = 13; LF = 10; FF = 12; VT = 11;
    VAR B, F, L, N, R, S, T, V: character;
  BEGIN
    IF (s[i] # ESCAPE) THEN RETURN s[i]
    ELSIF (s[i+1] = ENDSTR) THEN (*@ not special at end*)
      RETURN ESCAPE
    ELSE INC(i);
      B := ORD('b'); F := ORD('f'); L := ORD('l'); N := ORD('n'); R := ORD('r');
      S := ORD('s'); T := ORD('t'); V := ORD('v');
      IF (s[i] = N) THEN RETURN NEWLINE
      ELSIF (s[i] = T) THEN RETURN TAB
      ELSIF (s[i] = S) THEN RETURN BLANK
      ELSIF (s[i] = F) THEN RETURN FF
      ELSIF (s[i] = V) THEN RETURN VT
      ELSIF (s[i] = B) THEN RETURN BACKSPACE
      ELSIF (s[i] = R) THEN RETURN CR
      ELSIF (s[i] = L) THEN RETURN LF
      (* What about @000 syntax? See vis. *)
      ELSE RETURN s[i]
      END
    END
  END esc;

  PROCEDURE equal (VAR str1, str2: string): BOOLEAN;
    VAR i: INTEGER;
  BEGIN i := 1;
    WHILE (str1[i] = str2[i]) AND (str1[i] # ENDSTR) DO
      INC(i)
    END;
    RETURN (str1[i] = str2[i])
  END equal;

  PROCEDURE index (VAR s: string; c: character): INTEGER;
    VAR i : INTEGER;
  BEGIN i := 1;
    WHILE (s[i] # c) AND (s[i] # ENDSTR) DO INC(i) END;
    IF (s[i] = ENDSTR) THEN RETURN 0 ELSE RETURN i END
  END index;

  PROCEDURE isdigit (c: character): BOOLEAN;
    VAR a, b: INTEGER;
  BEGIN a := ORD('0'); b := ORD('9');
    RETURN (c >= a) AND (c <= b)
  END isdigit;

  PROCEDURE ispunct (p: character): BOOLEAN;
    VAR a, b, c, d, e, f, g, h: INTEGER;
  BEGIN a := ORD('!'); b := ORD('/'); c := ORD(':'); d := ORD('@');
    e := ORD('['); f := ORD('`'); g := ORD('{'); h := ORD('~');
    RETURN (a <= p) AND (p <= b) OR (c <= p) AND (p <= d)
      OR (e <= p) AND (p <= f) OR (g <= p) AND (p <= h);
  END ispunct;

  PROCEDURE islower(c: character): BOOLEAN;
    VAR a, z: INTEGER;
  BEGIN a := ORD('a'); z := ORD('z');
    RETURN (c >= a) AND (c <= z)
  END islower;

  PROCEDURE isupper(c: character): BOOLEAN;
    VAR A, Z: INTEGER;
  BEGIN A := ORD('A'); Z := ORD('Z');
    RETURN (c >= A) AND (c <= Z)
  END isupper;

  PROCEDURE isletter (n: character): BOOLEAN;
  BEGIN RETURN islower(n) OR isupper(n)
  END isletter;

  PROCEDURE isalphanum (n: character): BOOLEAN;
  BEGIN RETURN isletter(n) OR isdigit(n);
  END isalphanum;

  PROCEDURE itoc(n: INTEGER; VAR s: string; i: INTEGER): INTEGER;
    VAR b: INTEGER;
  BEGIN
    IF (n < 0) THEN s[i] := ORD('-'); RETURN itoc(-n, s, i+1)
    ELSE IF (n >= 10) THEN i := itoc(n / 10, s, i) END; (* PIM DIV := / *)
      b := ORD('0'); s[i] := (n MOD 10) + b;
      s[i+1] := ENDSTR; RETURN i + 1
    END
  END itoc;

  PROCEDURE length(VAR s: string): INTEGER;
    VAR n: INTEGER;
  BEGIN n := 1;
    WHILE (s[n] # ENDSTR) DO INC(n) END;
    RETURN n - 1
  END length;

  PROCEDURE max(x, y: INTEGER): INTEGER;
  BEGIN IF (x > y) THEN RETURN x ELSE RETURN y END
  END max;

  PROCEDURE min (x, y: INTEGER): INTEGER;
  BEGIN IF (x < y) THEN RETURN x ELSE RETURN y END
  END min;

  PROCEDURE ctoi(VAR s: string; VAR i: INTEGER): INTEGER;
    VAR o, n, sign: INTEGER;
  BEGIN
    WHILE (s[i] = BLANK) OR (s[i] = TAB) DO INC(i) END;
    IF (s[i] = MINUS) THEN sign := -1 ELSE sign := 1 END;
    IF (s[i] = PLUS) OR (s[i] = MINUS) THEN INC(i) END;
    n := 0; o := ORD('0');
    WHILE (isdigit(s[i])) DO
      n := (10 * n) + s[i] - o;
      INC(i)
    END;
    RETURN sign * n
  END ctoi;

  PROCEDURE putdec (n, w: INTEGER);
    VAR i, nd: INTEGER; s: string;
  BEGIN nd := itoc(n, s, 1);
    FOR i := nd TO w DO putc(BLANK) END;
    FOR i := 1 TO nd-1 DO putc(s[i]) END
  END putdec;

  (* Thanks to Peter De Wachter. See Software Tools exercise 2-18. *)
  (* c := ((NOT b) AND a) OR ((NOT a) AND b); *)
  PROCEDURE cxor(a, b: character): character;
    VAR c, k: CARDINAL;
  BEGIN c := 0; k := 1;
    WHILE (a # 0) OR (b # 0) DO
      IF (a MOD 2) # (b MOD 2) THEN
        c := c + k
      END;
      a := a DIV 2; b := b DIV 2; k := k * 2
    END;
    RETURN c
  END cxor;

  PROCEDURE xor(x, y: INTEGER): INTEGER;
    VAR c: INTEGER;
  BEGIN c := x BXOR y; (* ADW specific function BXOR *)
    RETURN c
  END xor;

END ST.

References

[JW83]
Lee Jacobson, Bebo White, An Introduction to Modula-2 for Pascal Programmers, PUG Pascal News, Number 26.
[K83]
Svend Erik Knudsen, Medos-2: A Modula-2 Oriented Operating System for the Personal Computer Lilith, Diss. ETH No. 7346, 1983.
[K88]
K. N. King, Modula-2: A Complete Guide, D. C. Heath and Company
[Ker81]
B. W. Kernighan, Why Pascal is Not My Favorite Programming Language, AT&T Bell Laboratories, Computing Science Technical Report No. 100, 2 April 1981
[KP76]
B. W. Kernighan, P. J. Plauger, Software Tools, Addison-Wesley, 1976
[KP78]
B. W. Kernighan, P. J. Plauger, Software Tools, McGraw-Hill, 1978
[KP81]
B. W. Kernighan, P. J. Plauger, Software Tools in Pascal, Addison-Wesley, 1981
[KR88]
B. W. Kernighan, D. M. Ritchie, The C Programming Language, Second Edition, Prentice Hall Software Series, 1988
[KU87]
Michel Kiener, Alfred Ultsch, HOST: An Abstract Machine for Modula-2 Programs, ETH-3161-01, February 1987
[O88]
Martin Odersky, MINOS: A New Approach to the Design of an Input/Output Library for Modula-2, ETH 86, May 1988
[W86]
Niklaus Emil Wirth, Algorithms and Data Structure, Prentice-Hall, 1986
[W88]
Niklaus Emil Wirth, Programming in Modula-2, Fourth Edition, Springer-Verlag.
[W91]
Niklaus Emil Wirth, Programmieren in Modula-2, 2nd Edition, Springer-Verlag.

©2017-2020 David Egan Evans.