Reconstructing OPL: Joseph Weizenbaum's Online Programming Language

Joseph Weizenbaum joined MIT in 1963 and is most famous for creating ELIZA, the world's first chatbot. One of his other projects was OPL, the Online Programming Language, which used his SLIP list processing library to implement an interpreted, interactive language that could take advantage of the new facilities provided by time-sharing operating systems like CTSS. First conceived in 1963, it was a contemporary of such languages as JOSS, BASIC and interpreted LISP. It was even combined with a later version of ELIZA to provide probably the first embedded scripting language.

Based on a printout of source code found among Weizenbaum's papers archived by MIT Libraries, I have reconstructed the language so it can live again for the first time in nearly sixty years on a IBM 7094 emulator running CTSS. I'm calling this a reconstruction as although the main logic of the interpreter was in the printout, the I/O routines and some utility functions were missing. I have written these in MAD (Michigan Algorithm Decoder), which is the source language of OPL. Around 18% of the project is new code.

Source code and further details on Github.

/images/ctss/opl.png OPL printout and code execution under emulation. Source: MIT Libraries and Rupert Lane. License: MIT

The origins of OPL

The first reference to OPL is in OPL-I An Open Ended Programming System Within CTSS. a Project MAC technical report written by Weizenbaum in April 1964. He notes the shared observation that new time-sharing operating systems like CTSS allow programmers to attack problems in new ways that traditional high-level languages like Fortran do not allow.

The whole point of time-sharing is to enlarge the opportunities for carrying out truly significant man-machine dialogue — not to merely reduce turn-around-time.

with a longer term goal

It is to be expected that in many problem areas the computer will begin to help man by doing only the most obvious in mechanical parts of his problem but that, as the man-machine dialogue extends over a long period of time, more and more of the previously fussy issues over which man retained authority will become clear and finally be turned over to the computer.

(which rings true today with the advent of LLM based coding agents).

But Weizenbaum does draw the line somewhere - "The goal is to give to the computer those tasks which it can best do and leave to man that which requires (or seems to require) his judgement." - the fundamental difference between humanity and machines would be something he increasingly feel strongly about as he became more disillusioned with AI in the 1970s and wrote his book Computer Power and Human Reason

But turning to the details on the language itself. OPL-I, as it was called then,

permits the user to augment both his program and his data base during widely separated successive sessions at his terminal. Facilities are provided which make it possible for the user to operate on his already established data base both by means of built-in operators and in terms of operators (functions) which the user has previously defined in the language of the system.

The OPL system is written in a high level language and uses the SLIP list processing library for its internal workings; the same SLIP functions are exposed to the user for their programs. Users can build up their own lists and functions that operate on them, and can run small fragments of code to interrogate their data. These queries are discarded after running them, but the data and functions remain.

Persistence over sessions is afforded by a facility in CTSS which is not really available on modern operating systems. At any time while running OPL, the user can press the Interrupt key on their typewriter console and it will return them to the CTSS command level. The user can then type SAVE and a complete record of their core memory - the OPL program and the user functions and data - is stored to a disk file. At any point later they can RESTORE that core image file and pick up where they were before they pressed Interrupt, without the need to load in data and code again.

As a concrete example, Weizenbaum gives code for a function that calculates the mean of a list of numbers.

(DEFINE)
(MEAN (L)
          (S = SEQRDR(L))  (SUM = 0.0)  ((COUNT = 0.0)
BEGIN     (C = SEQLR(S,F))
          IF(F)MORE,MORE,DONE
MORE      (COUNT = (COUNT + 1.0))
          (SUM = (SUM + C))  GOTO BEGIN
DONE      ( (SUM/COUNT))  )

Here, the SLIP primitive SEQRDR is used to start an iterator over the list L. Each call to SEQLR brings in the next element, travelling left to right over the list's contents. The flag F denotes what has been read: this will be 1 at the end of the list, so the Fortran II style IF statement is used to branch to the label DONE at this point, where the mean is returned. This could be called by something like:

        (X = MEAN(SET))

Weizenbaum gives an example of a use for OPL. An organisation can define its management hierarchy as a set of lists arranged into a tree, and write functions to do tasks such as compute the budget at an arbitrary level based on money allocated to sub-organisations, "What-if" scenarios could be run by temporarily changing the data.

He also looks ahead to a potential multi-terminal version of OPL (which as far as I can tell was never built) where several people could play a business game, sharing the same data and seeing the impact of changes performed by one player on the other terminals.

Weizenbaum compares OPL to LISP and IPL-V, and states "OPL-I is of a character quite similar to the LISP program mode and of about equivalent power." IPL-V was falling out of use at this time, and LISP was growing its number of users. The AI lab at MIT was using LISP on the PDP-1 and PDP-6, and I would speculate that there was an interchange of ideas between the groups.

The marriage of OPL and ELIZA

The next sighting of OPL is a talk Weizenbaum gave at UCLA in the spring of 1965, reproduced in a 1966 paper On-Line User Languages. This does not add much technical detail, but Weizenbaum puts OPL in the context of wider interpreter development at MIT. He views the command line environment in CTSS as an ad hoc interpreter to solve problems, and mentions other CTSS interpreters such as COGO.

But the really interesting news came in the Project MAC progress report for 1966-7. Weizenbaum had turned OPL into an evaluator that could be called by ELIZA to do arbitrary computation:

The difficulty with the early ELIZA system alluded to above was that the system could do no computation in any significant sense. […] In order to enhance the ability of ELIZA in this direction it was necessary to design and build an evaluator, that is, an interpretive program, to which computational and logical tasks could be given for execution. Such an evaluator was built.

The composition of this evaluator proved to be an interesting task with ramifications extending beyond its immediate utility as a subsystem to ELIZA. An effort was made to design the program in a way such that it would prove a useful tool for the teaching of a number of deep issues in the field of programming languages. The resulting program is essentially an interpreter version of SLIP with approximately the power of LISP but with very considerably simpler syntactic conventions than LISP.

Note the source code for this combination of ELIZA and OPL has not been found. What we do have is the stand alone version of OPL from 1967.

But there are several documents that show ELIZA+OPL being used. Paul Hayward's 1967 bachelor's thesis Flexible Discussion Under Student Control in the ELIZA Computer Program describes creating a system to have a computer interactively discuss a physics problem with a student, and gives his scripts at the end of the document. Two papers by Edwin Taylor from the Science Teaching Group at MIT: The ELIZA Program: Conversational Tutorial and Automated Tutoring and Its Discontents describe the experience of building teaching scripts using ELIZA+OPL.

Of most interest is Hayward's 1968 Eliza Scriptwriter's Manual which contains a description of how ELIZA and OPL work together, including an appendix on the OPL language and its functions. This is the sole documentation we have on OPL.

The source code

The printout of source code we have is for a standalone version of OPL without its ELIZA embeddings. The document contains

  • Three pages of what looks like a larger manuscript about OPL, describing how it uses description lists to store symbol tables.
  • A listing of the file DI MAD. This is the main entry point for the stand alone program. It accepts commands from the user's terminal and sends them to EVAL for evaluation. It has some debug facilities the user can turn on or off, such as printing how much free space is left.
  • A listing for the file EVAL MAD. This is an external function that takes a list expression as input and returns the result of its evaluation. It contains a parser and run time support for OPL's statements.
  • A listing of a CTSS ARCHIV file called FAP FAP that contains several FAP assembly language files that support OPL. An example of these is OBEY FAP which has a jump table that matches OPL function names with their entry points. This had to be done in assembly language as MAD lacks function pointers.

In total there are 48 pages or 2654 lines of code in the printout.

The printout is dated "06/30" and I think the year is 1967 based on the internal dates on the files in the FAP FAP archive. The user ID printed on top of the printout is T0109 2531 which from other printouts we know is Weizenbaum's.

Having studied his 1965 ELIZA code for that reconstruction, it's instructive to compare the two.

Like the 1965 code, Weizenbaum uses playful names for variables and functions: APRIL, JULY, VIRGIN, OBEY, RID. One distinctive trait is seen in his use of EQUIVALENCE statements. These are similar to C's union, where one location in memory can be accessed by two variable names with different types. Weizenbaum often uses a German and an English word with similar meanings to name these, as in this example:

           EQUIVALENCE (PLATZ,THERE)

Based on the non-contiguous line numbers and non-code file separators in ELIZA, it was likely originally prepared on punched cards and then loaded into CTSS for use under time-sharing. The 1967 code has perfectly regular line numbers, probably indicating it was entered online using an editor such as ED, which manages line numbers automatically as you edit a file. The 1967 code also has more comments and more blank lines, which is something you'd avoid on punched cards as it would make the deck physically larger.

Weizenbaum also uses more CTSS subroutines rather than relying solely on facilities in the MAD language. For example, he uses CTSS's PRMESA to print a message on the typewriter without a terminating carraige return.

The reconstruction

The first task was to OCR the PDF to extract machine readable text. Luckily recent LLMs do an amazing job on OCR, avoiding the hallucinations common even in mid 2025, and making around one mistake per page.

Then, the code was loaded into CTSS and compiled. This shook out the last of the OCR issues, some of them subtle. IBM in their wisdom used = to define decimal constants in assembly code and =O for octal constants, so =020 means decimal 20 but =O20 means octal 20, ie decimal 16, so this needed careful checking. No syntax errors were detected in the code, but when it came time to link/load (using the L command below) there was a problem:

l di eval (libe) opllib
W 1126.2
 NEED BOT    BRKEY  CNTSPC INITAS INLSTL IRARDR LEMPTY
      LIST   MTLIST NTHTOP ONELIN POPTOP RDLONL READER
      REMOVE SDBC   TODAY  TOP    LSLCPY LSSCPY LSTMRK
      MANY   MRKLST NAMTST NTHBOT NULSTL NULSTR OEPRNT
      POPBOT TXTPRT VCTLST NUCELL RCELL  ATEND  ATOMIC
      CONCAT TIME   REST   HIRANK CONS   REPLAC CALLS
      ADDKEY WASKEY NTOP   NBOT   MAX    MIN    FIRST
      SECOND INLSTR DSKLST DSKCLS LSTEQL NODLST LINLST
      STRLST YMATCH ASSMBL GETLIN FNDKEY
R .316+.033

61 functions were missing.

23 of these were SLIP primitives like BOT or LIST. We had a complete SLIP library on hand from the ELIZA reconstruction and its interface had been fairly stable since its first publication, so we could drop in the ELIZA-SLIP library easily.

Input/Output

More problematic were the input/output routines that were missing. The key ones here were RDLONL to read lines of input from the terminal and convert to a list, and TXTPRT to print a list to the terminal. We had similar code from ELIZA's TREAD and TXTPRT but they would not work directly with OPL. ELIZA really just cares about text but OPL needs support for numbers and symbols needed to express code in the language. So I chose to write these from scratch in MAD.

String handling on the IBM 7094 is deeply unfamiliar to modern programmers. The machine pre-dated ASCII and the 8-bit character: instead it uses 6-bit BCD characters, packed 6 to a 36-bit word. Even worse, there is no built-in facility to manipulate data at the character level, either in the MAD language or in assembly. Instead, characters need to be masked and bit-shifted to or from a word. Input and output also only occurs at a word level, so to output A B you have to prepare a word of 6 blanks, poke in the characters at bits 1-6 and 13-18, and then send the encoded word "A B " to the CTSS output subroutine.

By seeing what the EVAL function was expecting, it was possible to build working code for these functions. But lacking detailed examples of OPL code running, we do not know exactly how this looked. For example, we can see EVAL trying to output a list of two floating point numbers separated by a comma. To what precision should the numbers be printed? Should there be a space before or after the comma? Although the functionality of OPL is working as it did in 1967. how it looks on the terminal may be slightly different.

Data types

The next problem was data types. Each element of a SLIP list - called the datum - is a single 36-bit word and OPL allows text, symbols like *, integer and floating point numbers to be stored here. However, there is no unified way to tag what type the datum represents. Text data uses the SLIP indicator, which is two unused bits on the pointer to the next word in the list. Symbols are encoded as text but with the indicator turned off and padded with spaces, for example " *". Numbers also have no indicator, and Weizenbaum appeared to use a heuristic

           WHENEVER 77777K6 .A. DATUM .E. 0
              (treat datum as integer)
           OTHERWISE
              (treat datum as floating point)
           END OF CONDITIONAL

which basically means see if none of the bits marked as "x" in the word ...xxxxxxxxxxxxxxx.................. are set. FP numbers have the IBM 7094 equivalent of the exponent set here (note this is not the IEEE 754 floating point representation used today), whereas integers will not have these bits set unless they are very large.

The overlapping nature of this type detection was not documented in the code and had to be worked out by trial and error when developing the I/O routines: it's probable that there are still some mistakes in the reconstruction here.

Lambda function support

The printout had several instances where someone, presumably Weizenbaum, had made hand-written annotations.

/images/ctss/opl-annotation.png Example of an annotation to the OPL code.Source: MIT Libraries. License: MIT

These seem to concentrate around lines of code enabling lambda functions and the ability to create and call new anonymous functions. This feature does not appear in any of the OPL documentation and seems not to be fully working. I chose not to apply any of the hand written changes to the source code and left it as is.

There may be a connection here to the work Weizenbaum did on the funargs problem for his 1968 paper, though that only mentions the problem in the context of LISP. Joel Moses' 1970 paper The function of FUNCTION in LISP does provide some historical background that explicitly mentions OPL:

Joseph Weizenbaum got sufficiently interested in Landin's work that he implemented a subset of [the ISWIM lambda calculus] based on his SLIP-OPL system. The tree structure of the resulting stack was made quite vivid to me when Weizenbaum encountered difficulty in implementing the backward pointers necessary in the general case. The reason for this difficulty is that one could not then garbage collect the circular SLIP structures which are created in a straight-forward implementation. This is what motivated Weizenbaum to finally introduce classical garbage collection into SLIP [in 1969].

This would be a good topic for further research.

OPL utility functions

The above took care of most of the missing functions, but there were still several functions not used by the interpreter directly, but available to be called by the user's OPL program. Many of these were trivial to implement - eg MAX and MIN - so I have included implementations for around 15 of these. 18 functions remain unimplemented at this point, mainly either because the documentation is not clear on what they should do or if they are used by ELIZA but we lack the main ELIZA+OPL code so they would not be useful on stand-alone OPL.

Using OPL

See the GitHub repo for details on setting up the emulator and compiling the code.

Once done, you start OPL by typing R OPL. It responds with a greeting, the current date/time and a prompt of a line number. You can then enter one or more lines of text, ending your input with two carriage returns in a row.

In the below, my input is in lower case, OPL's is in capitals. Simple variable assignment and calculation:

r opl
W 1818.1
EXECUTION.
OPL AT YOUR SERVICE
DATE  02/16  TIME 1818.1
     1      a = 21, b = 2, c = a*b, type(c)

C               42

List manipulation

     2      x = '(10.0 20.0 30.0),
newtop(5.0, x),
popbot(x),
txtprt(x, 0)

5 10 20

Reproducing the mean example from the original paper

     3      define(mean(lst) =
      count = 0, total = 0,
      r = seqrdr(lst),
*loop item = seqlr(r),
      if item .e. 'nil then goto end :
      count = count + 1, total = total + item,
      goto loop, 
*end  total / count)

     4      type(mean(x))

MEAN(X)               11.66667

The logic is mostly the same, but note that parentheses are not needed and statements are separated by commas. seqlr does not return a flag, instead taking the value 'nil at the end of the list. Goto targets are now prefixed by *, and the IF statement is now a Fortran 66 style logical IF rather than the Fortran II three-way branch version. IF also needs to be terminated with a : as the THEN clause could have multiple statements separated by commas.

Sessions can be saved by pressing Interrupt (Control-Backslash under emulation) and then using the SAVE and RESTORE commands.

     5       QUIT,
R .133+.066

save myopl
W 1825.5
R .000+.033

r myopl
W 1825.6

     5      type(a, b, c)

A               21
B                2
C               42

The interpreter has some built in meta commands which start with a .. For example, .clock on enables display of how long an expression takes to complete evaluation.

     1      define(fac(n) = if n .le. 1 then 1 else n * fac(n - 1) )

     2      .clock on

     2      type(fac(10))

FAC(10)                3628800
TIME      66 MILLISECONDS

OPL will also detect missing definitions of variables and functions, and prompt for them to be supplied when needed.

     1      a1 = 10, a3 = 20, type(a1 + a2 + a3)

A1 + A2 + A3      IDENTIFIER  A2     UNDEFINED
      12

          42

It's important to mention that this is a snapshot of Weizenbaum's work, not a finished product. For example, undefined math results are not handled correctly and missing keywords, like DO in the IF statement below, can cause the program to crash.

     1      type(1/0)

1 / 0            0
     2      for j = 1 step 1 until j .e. 5 type(j)

J                1
 PROTECTION MODE VIOLATION AT 20163.
 INS.=050000063400, RI.=000000000000, PI.=053422000000
R .050+.050

I've put some more code examples in the samples directory of the Github repo, along with some details on known issues.

OPL's impact

I think it's fair to say that OPL did not have much of a direct impact. Weizenbaum never published OPL and it did not spread outside of MIT. A lot of different approaches were tried in computer aided instruction at this time, and the ELIZA+OPL scripting approach did not take off: more successful were ventures elsewhere such as the PLATO system and the Logo language. Weizenbaum himself spent less time on programming, instead working on his book Computer Power and Human Reason and getting involved in the political turmoil of the early 1970s.

It's always difficult to identify firsts in computer science history, as many people have similar ideas at the same time. I think the idea of a library (SLIP) having bindings in both a compiled (MAD) and an interpreted (OPL) language is a first, and embedding a scripting language like OPL in a domain specific language like ELIZA is also novel for its time. Certainly the ideas developed by Project MAC and CTSS were shared across the community and as a collection of ideas it would show its influence on later generations of time-sharing operating systems and interactive languages.

OPL remains interesting as one of the last examples of Weizenbaum's technical achievements, and it's interesting to think what could have happened had he developed it further.

Acknowledgements

Thanks to Anthony Hay, Arthur Schwarz, David Berry, Jeff Shrager, Mark Marino and everyone on RetroAI/Team ELIZA for their help and advice.

Thanks to MIT Libraries for preserving and scanning this code, and making it publicly available.

Questions, corrections, comments

I welcome any questions or comments, and also especially any corrections if I have got something wrong. Please email me at rupert@timereshared.com