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


TOPS-10: Jobs

TOPS-10 is able to run multiple jobs (processes) per user, and has a flexible set of commands to control them, including the ability to pause a job and resume it on another terminal. It also has a batch job facility. But we'll start with a question on how to run jobs.

Why do I sometimes need to type R command and other times type command?

When you login to TOPS-10, you are in monitor mode - you are actually running shared code that is part of the system kernel. Some simple commands, like RESOURCES, are part of the monitor and can be executed directly. Other commands need to be loaded from disk first. To signal this, you type RUN command if the command is in your directory, or R command if it is a system command. R looks for EXE and SAV files to run in SYS:, so R PIP is the same as RUN SYS:PIP.EXE

A program loaded from disk remains in memory - your core image - even after the program exits. This image is only replaced when you load a new command. As a demonstration, type R PIP to load and run the PIP command. The prompt will change to *. Press Control-C and you will be back at the monitor level with the . prompt. Type RESOURCES and after that has finished, type START - you will find yourself back in PIP again.

One other complication is that the system administrator can declare an alias for some common commands. An example here is the SOS editor, which you can run with just SOS. Behind the scenes, the system does a R SOS for you to load and start SYS:SOS.EXE.

In practice you will remember what to type, but to check if a command is part of the monitor look at the Operating Systems Command Manual chapter 2. If the write up for a command says it "replaces core" then it is a disk loaded command that you either need to start via R or know that there is an alias declared.

Detaching and attaching jobs

You can log in more than one terminal under the same ID, and hence have more than one job running.

It is possible to pause execution of a job on one terminal and pick it up again on another. Let's say you are running this horribly inefficient way to print all prime numbers between 1 and 2 million:

	DO 1 J = 1000000,2000000
	DO 2 K = 2,J-1
	IF ((J/K)*K .EQ. J) GOTO 1
2	CONTINUE
	TYPE 100, J
1	CONTINUE
100	FORMAT(I)
	END

Run this with EXECUTE and press Control-C twice to interrupt it. Then type DETACH to detach the job

.exec primes.for
LINK:	Loading
[LNKXCT PRIMES Execution]

       1000003
       1000033
       1000037
       1000039
       1000081
       1000099^C^C

.det
From job 5

As you are detached, you are also logged out. You could LOGIN again and start a new job if you wished.

But let's start a new terminal. Login as normal and then type ATTACH with the job number from the previous DETACH. Then type CONT to continue execution from where you left off.

.login 100,100
JOB 13 KA603 TTY1
Password: 
[LGNJSP Other jobs same PPN:5,12]
1055	31-Jan-79	Tue

.at 5
From job 13

.cont


       1000117
       1000121
       1000133
...

You can also run jobs in the background - after you press Control-C type CCONT to continue the job in the background before DETACH. However, if the job does terminal input or output it will pause until it is attached to a terminal again.

Job information

You can get an idea of what jobs are running through SYSTAT. Giving a job ID as a parameter shows just one job.

.sys 5
 5   100,100	TTY1	SYSTAT	7+SPY	RN	       5 $

I have not found a way to kill a job that is running apart from attaching to it and then doing a Control-C.

Batch jobs

The closest equivalent to shell scripts is the batch job. Here you can prepare a file with a list of commands and send it for execution via SUBMIT. This places it on the batch job queue (which could also contain jobs submitted via punched cards) and it is run asynchronously. Here we create a new batch file TEST.JOB to run the DIR and SYS commands.

.sos test.job
Input: TEST.JOB
00100	dir
00200	sys
00300	$
*es

[DSKB:TEST.JOB]

.submit test.job

After you submit it you will see some activity on the operator's console when it is started. To see the results, check the line printer output (which is in host file units/printer.txt if you are using the TOPS-10 Quickstart). There will also be a log file created in your directory, by default with the extension .LOG.

.type test.log

13:24:26 BAJOB	BATCON version 13(1071) running TEST sequence 2 in strea
m 1 for DEMONSTRATIO
13:24:26 BAFIL	Input from DSKB3:TEST.JOB[100,100]
13:24:26 BAFIL	Output to  DSKB0:TEST.LOG[100,100]
13:24:26 BASUM	Job parameters
		Time:00:05:00	Unique:YES	Restart:NO
 
13:24:26 MONTR	
13:24:26 MONTR	.LOGIN 100/100
13:24:26 USER	JOB 14 KA603 TTY15
13:24:26 USER	[LGNJSP Other jobs same PPN:5,12,13]
13:24:26 USER	1324	31-Jan-79	Tue
13:24:27 MONTR	
13:24:27 MONTR	.dir
13:24:27 USER	
13:24:27 USER	CTEST	CBL	2  <057>   21-Apr-78
13:24:27 USER	HELLO	MAC	1  <057>   21-Apr-78
...

You can also interleave commands and data for the commands by preceding commands with . and data with *.

Further information

See the Operating Systems Command Manual on Bitsavers for full documentation of TOPS-10 commands. The Beginner's Guide to Multiprocessing Batch may be useful for batch jobs.

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 and I will add it here and update the main text.


TOPS-10: Getting data in and out

On a real PDP-10 running TOPS-10, you'd likely to have several peripherals to allow data to enter and leave the system: line printers, card punches and readers, paper tape and magnetic tape. One other alternative we've not seen before is DECtape which were magnetic tapes with a smaller form factor and a block structured, allowing random access to files. This meant they could be used as a slow substitute for hard disks, eg for user files.

/images/pdp-10/pdp-10-with-dectapes.jpg PDP-10 with several DECtapes. Source: Wikipedia/Jason Scott. License: CC0

On an emulated system your concerns are different: you want to get data to and from your host computer. Small amounts of text can be transferred using copy/paste in your terminal, but this is not practical for large amounts - especially for pasting, as the console was expected to be a typewriter and cannot accept text that fast.

Let's look at ways to do this using emulated devices under simh.

Device access

As a large computer running time-sharing for many users, one important concern for the operating system is how to mediate access to peripheral devices. There are essentially two options on TOPS-10

Spoolers allow several people to share a device. The operating system maintains a queue of jobs entered by different users, and processes them one at a time. These spooler jobs are run under the operator's ID via a system called OPSER, which you can see running on the operator's console after you boot the system. The line printer is an example of a spooled device.

Assignable devices allow you to request sole access to a device, giving it a logical device name you can use instead of the real physical device. This is useful for magnetic tape drives, where there may be several allocated on an as-needed basis to users.

The choice of how to share devices was up to each site and can be configured in the monitor; we'll just use how it is set up on the quickstart disk images without changing these.

Printing

The line printer is a good way to get text data out of the system. This is a spooled device available as device LPT:. To print a single file, use the PRINT command

.print hworld.for
Total of 1 block in 1 file in LPT request

You'll see a message on the operator's console saying this is being processed.

!11:34:52(L\7)
        Job HWORLD file DSKB1 :HWORLD.FOR   [64,64] for [64,64] started 
        D:HWORLD.FOR   [64,64] Done

Then look in the file units/printer.txt on the host system to find your output. You'll see the name of the file you requested to be printed in large letters (on a physical printer this would allow easy identification of jobs) followed by the file's text.

You can also use the device name where a file would be normally used. For example, to print all the Fortran files in your directory:

copy lpt:=*.for

Note when using the device, the files will not be printed until your job terminates, ie you log out.

In simh, where printer output goes on the host is controlled by the attach lpt command in the configuration file. On the quickstart, see common.ini:

at lpt  -n units/printer.txt

The -n switch will start a new file each time the system is booted. If you'd prefer to keep the contents of the file over each session and append to it, replace -n with -a.

Extracting files to your PC via virtual tape

If you want to extract several files from TOPS-10, here is a method using emulated tapes and the TOPS-10 backup program.

You will need to get the back10 utility by Johnny Eriksson running on your PC. Download the file back10.tar from the linked page, untar t an empty directory and run make to compile the binary back10. Place this on your PATH somewhere.

Next, on the simh console, we will create a blank tape file on your PC backup.tap and attach it to the emulated tape drive MTA1.

Press Control-E to enter simh command mode and then type the following at and set commands, then ~c` to resume TOPS-10.

Simulation stopped, PC: 000001 (SOJG 6,1)
sim> at mta1 backup.tap
%SIM-INFO: MTA1: creating new file
%SIM-INFO: MTA1: Tape Image 'backup.tap' scanned as SIMH format
sim> set mta1 write
sim> c

Switch to your TOPS-10 login. We will assign the tape to logical name TAP: and run BACKUP . The prompt will change to a /. We'll then use the following BACKUP commands

  • tape to select the tape drive
  • save to add files - below we add all Fortran files in our home directory
  • unload to finalise the tape

Press Control-C to get back to the command level. and finally deassign the logical tape name. The session will look something like this:

.assign mta1: tap:
MTA001 assigned

.r backup

/tape tap:
/save *.for
!100,100	DSKB

"Done

/unload

[MTA001: WRITE(C/H/S) = 10880/0/0]
/^C

.deassign tap:

You may get a message "Tape write locked–add write ring then type "GO"" - just type GO as suggested in this case.

On your host PC, use back10 with the -l switch to list the contents of the tape file

$ back10 -lf backup.tap
  21-Apr-1978 11:13:59  <057>  dskb:[100,100]ftest.for
  17-Jan-1979 14:40:59  <057>  dskb:[100,100]hworld.for

Use the -x option to extract files. Here I use -R and a wildcard to extract everything.

$ back10  -xf backup.tap  -R '.'  '*.*'
  dskb:[100,100]ftest.for
  dskb:[100,100]hworld.for

Adding the -b option will create a directory tree, ie create files named like dskb/100,100/hworld.for.

Uploading files

To upload files from your PC to TOPS-10, we can use the same technique bur in reverse. First, use back10 with the -c option to create a backup tape file. Here we create the tape transfer.tap containing a single file test.for. Note we provide destination disk and user information using the -U switch which will help simplify things when we extract the files later.

$ back10 -cf transfer.tap -U dskb:100,100 tesr.for

Switch back to the simh console, press Control-E and attach the new file.

sim> at mta1 transfer.tap
%SIM-INFO: MTA1: Tape Image 'transfer.tap' scanned as SIMH format
sim> c

Go to your TOPS-10 login and use BACKUP with the RESTORE command to get all the files in the archive:

assign mta1: tap:

.r backup

/tape tap:
/restore *.*
!100,100	DSKB

"Done

/^C

There are several other BACKUP commands available at the / prompt. Type HELP for more information. One is PRINT, which will print a listing of the tape on the line printer

Other methods

One potential way to interchange files is Kermit, which allows you to send and receive files interactively over your terminal connection. There is a version on the TOPS-10 v6.03 disk images that we are using, but it only works for download of files from TOPS-10 to your local machine, uploads do not work. A thread on the PiDP-10 group indicates that there is a newer version of Kermit that does work, but it is only compatible with TOPS-10 v7.

Further information

See the Operating Systems Command Manual on Bitsavers for full documentation of TOPS-10 commands such as PRINT and BACKUP.

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 and I will add it here and update the main text.

A draft version of this article was publushed in error on 8-Feb-2026.


TOPS-10: The SOS text editor

In this post we'll look at the simple but powerful SOS text editor on TOPS-10.

Starting SOS

Type sos file.ext: if this is a new file SOS will print INPUT and put you in a mode where you can type lines. If it is an existing file it will print EDIT and you will be in command mode.

Inputting text

SOS will automatically assign numbers to each line which you will see on the left. You can type each line normally, making corrections using Delete, Control-U like on the command line. When you have finished inserting text, press Escape; SOS will print $ and return you to command mode.

SOS command format

At the * prompt, SOS accepts commands which are generally single letters followed by which lines to work on.

Command Meaning
p Prints around 16 lines around the current line
p . Prints just the current line
p 500 Prints line 500
p 600:700 Prints lines 600-700
p ^:* Print from first line to last line

Let's load up and print our hello world program from before.

.sos hworld.for
Edit: HWORLD.FOR
*p
00100		DO 1 J=1,5
00200	1	WRITE(5, 2)
00300	2	FORMAT(' HELLO, WORLD')
00400		END
*

Quick navigation

Once you select a line (eg via p 500 above) you can quickly move up and down by pressing Escape to move up one line and Control-J to move down one line; SOS will print the line it has moved to.

Replace, Insert, Delete

The R, I and D commands replace, insert and delete lines. Each take a line specification like above. R will prompt you to enter the new line to replace it. I will insert a line after the one you specified, giving it an appropriate line number. An example of changing our hello world program to print a different message twice:

*r300
00300	2	FORMAT(' GOODBYE, WORLD')
1 Lines (00300/1) deleted
*i100
00150		WRITE(5,2)
*p^:*
00100		DO 1 J=1,5
00150		WRITE(5, 2)
00200	1	WRITE(5, 2)
00300	2	FORMAT(' GOODBYE, WORLD')
00400		END

Exiting SOS

At the * prompt, type E to save and exit, ES to save without line numbers and exit, and EQ to exit without saving changes. Some programs do not accept line numbers hence the ES option; even if you do remove them, if you edit the file in SOS again it will add them back.

Those are all the commands you need to do most editing tasks. But read on for additional features that make life easier. Alternatively, you can type H at the prompt to view the help file.

Find and replace

The F command finds the next line with the specified text. You type F, then the text to find, then Escape and finally Return and it will print out the matching line. Note that SOS prints $ when you type Escape.

*fgoodbye$
00400	2	FORMAT('GOODBYE, WORLD')

After the Escape, but before the Return, you could also type a line number range.

To find the next instance of the same text, just type F on its own again.

To replace part of text without retyping the whole line, use the S command. Type S, the old text to replace, Escape, the new text and then Escape. You then must add a line range before pressing Return, so to replace on the current line you could use ..

00400	2	FORMAT('GOODBYE, WORLD')
*sGOODBYE$HELLO$.
00400	2	FORMAT('HELLO, WORLD')

Alter mode

This is a special mode where you can edit a line using interactive commands. Type A and your line number (or .) and SOS will print the line number. You can then type Space to move forwards one character, Delete to move back one character. SOS will print character by character as you do this navigation so you can see where you are.

D will delete the next character; I will allow you to insert text until you press Escape. Commands take prefixes to say how many characters to operate on.

In the example below I wanted to change HELLO to GOODBYE. I pressed Space until I got to the start of the word then pressed 5D to delete HELLO. I then pressed I, typed GOODBYE, then Escape, then finally Return to accept the rest of the line.

*p400
00400	2	FORMAT(' HELLO, WORLD')
*a.
00400	2	FORMAT(' \\HELLO\\GOODBYE, WORLD')

Quit-and-go, renumbering files

Instead of saving and exiting with E, you can type G and it will save and then execute (compile, load, run) the file - more precisely, it will remember the last command you ran and execute that. But this allows rapid edit-compile-run-edit loops.

The N command will renumber your lines - useful if you have done a lot of inserts into existing text.

There's more, such as the ability to take editing commands from a file and to cut/copy text to move it around the file: see the Reference Guide linked below for details.

Summary of commands

Command Meaning
A Alter mode
C Copy text
D Delete lines
E Save and exit
ES Save without line numbers, exit
EQ Exit without saving
F Find
G Save and execute
H Help
I Insert
J Join lines
Jc Justify lines
K Delete page mark
L List to line printer
M Set page mark
N Renumber lines
P Prints lines
R Replace lines
S Substitute text
T Cut text
V Case inversion
W Save, but do not exit.
X Extend (append) to a line

Further information

See the SOS User's Guide and the SOS Reference Manual on Bitsavers.

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 and I will add it here and update the main text.


TOPS-10: Files and directories

In this post we'll look at how TOPS-10 handles files and directories, along with the commands to manipulate them.

Files

Files are named file.ext where file is 1-6 characters long and ext 0-3. The extension is used to identify the type of file, eg HELLO.FOR would be a Fortran source code file.

The * and ? wildcards operate like we are used to now, ? matching one character and * matching any number of characters. An example using the DIR command:

.dir hworld.*

HWORLD	FOR     1  <057>   12-Jan-79	DSKB:	[100,100]
HWORLD	REL     1  <057>   12-Jan-79
  Total of 2 blocks in 2 files on DSKB: [100,100]

Manipulating files

COPY, RENAME and DELETE do what you'd expect, but note that for the first two you put the new name first followed by an =. For example, to copy HWORLD.FOR to H2.FOR and then rename it to H#.FOR you'd do:

.copy h2.for=hworld.for

.rename h3.for=h2.for
Files renamed:
H2.FOR 

.dir h*.for

HWORLD	FOR     1  <057>   12-Jan-79	DSKB:	[100,100]
H3	FOR     1  <057>   15-Jan-79
  Total of 2 blocks in 2 files on DSKB: [100,100]

Many of these commands are implemented using the PIP utility, which I'll look at in a later post.

Users and directories

Users are identified by a project number and a programmer number. These are both octal values (ie they consist of the digits 0-7), and values for both lower than 7 are reserved by the operating system. If you followed the quick start you logged in as project 100, programmer 100.

This is tied together with the concept of the directory. There is a Master File Directory and below that a User File Directory for each valid project/programmer pair. These are enclosed in square brackets when referencing files, for example:

.dir [100,100]hworld.for

HWORLD	FOR     1  <057>   12-Jan-79	DSKB:	[100,100]

Project/programmer numbers can also be wirldcarded.

[1,1] is also the Master File Directory, so you can see a list of all User File Directories by:

.dir [1,1]

     1,1	1  <555>   21-Apr-78	DSKB:	[1,1]
     1,4	4  <775>   21-Apr-78
     3,3	0  <777>   21-Apr-78
...

You can create sub-directories using the CREDIR command. For example, to create a sub-directory test under my user directory I would run CREDIR and at the prompt give the response [,,test]. I could have typed [100,100,test] but as this is my current log on it allows me to omit it. Running this looks like the below:

.r credir

Create directory: [,,test]
  Created DSKB1:[100,100,TEST].SFD/PROTECTION:775
Create directory: ^C

I can then use this in commands.

.dir [,,test]

%WLDDEM Directory DSK:[100,100,TEST] is empty

It is possible to change your current directory, but it is a little more involved then typing cd on modern systems. Use the SETSRC program and give it the CP command followed by the desired directory at the * prompt. The TP command will list your current directory. Type Control-C to exit.

.r setsrc

*tp
[100,100]/NOSCAN
*cp [,,test]

*tp
[100,100,TEST]/NOSCAN
*^C

To delete a sub-directory, ensure it is empty and then use DELETE on the SFD file representing the directory. So for our example here, test, you would do DELETE TEST.SFD.

Devices

Physical devices, such as tape drives, disks and the terminal, are referred to by device names which are 1-3 characters long, an optional digit followed by a colon. Some examples:

Device Meaning
CDP: Card punch
CDR: Card reader
DTA000: DECTape unit #0
DTA001: DECTape unit #1
LPT: Line printer
SYS: System library
TTY: Your terminal

You can see a list of physical devices on the system by typing RESOURCES.

These can be referred to in commands. To type the contents of HWORLD.FOR to your terminal you'd normally do TYPE HWORLD.FOR but you could also do COPY TTY:=HWORLD.FOR.

There are also device names pointing to directories. An example is SYS:, the location on disk where system executable files are stored. We can see from the DIR command below that SYS~ points to user [1,4].

.dir sys:pip.exe

PIP	EXE    44  <155>    8-Nov-76	33B(260)	DSKB:	[1,4]

You can create logical device names. Say your program was reading from a DECTape where the unit may change depending on what is in use. Rather than hard-coding the physical name, you can set up a logical name with ASSIGN - for example ASSIGN DTA000: ABC: - and then use ABC: to refer to the tape. DEASSIGN will remove the logical name.

Protection

A file has a protection code which is 3 octal digits enclosed in angle brackets like <O57>. Each digit applies to a different class of user..

Digit Applies to
First You
Second Users with the same project number
Third All users

The values for each digit and their meaning:

Digit Meaning
7 No access
6 Execute
5 Read and execute
4 Append and everything in 5
3 Update and everything in 4
2 Write and everything in 3
1 Rename and everything in 2
0 Change protection and everything in 1

So a code of <057>, which is the default, means you can do anything, people in your project can read/execute, and all other users have no access.

Protection can be applied using the PROTECT command:

protect hworld.for<025>

Or directly at file creation time, eg via copy

copy world.for<000>=hworld.for

The default protection can be changed with SET DEFAULT PROTECTION.

Disk quota

Each regular user has an allowance of how much data they can store on disk. A higher quota is given for when you are logged in compared to logged out, to allow for creation of temporary files. The QUOLST command allows you to see this: the units are 128-word blocks, so 576 bytes per block.

.r quolst

User:	100,100
Str	used   left:(in) (out)	(sys)
DSKB:	  135	99865	99865	132307

On an emulated system today this is not a concern. On a real system with small disks and many users, the quota could be set quite low (I read that some undergraduates were limited to 5 blocks).

An alternate way to log out of the system is KJOB. This will give options for you to manage your files to get them under quota.

The quotas are set via the user administration utility REACT, which we'll look at later.

Further information

See the Operating Systems Command Manual on Bitsavers for full documentation of TOPS-10 commands.

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 and I will add it here and update the main text.


Next →