ML/I
Peter Brown's general-purpose macro processor from 1966 Cambridge. It recognizes free-form macro calls in any text, and it was used to port itself to more than two dozen machines.
Created by Peter J. Brown
ML/I (Macro Language One, with a Roman-numeral “I” as in PL/I) is a general-purpose, stand-alone macro processor. Peter J. Brown created it in 1966 as part of his PhD research at the University Mathematical Laboratory, Cambridge. ML/I copies text from input to output and replaces macro calls as it goes. Its calls can take almost any shape: a macro can be a single word such as SUM, or a whole structure such as IF … THEN … ELSE … END spread over several lines, with nesting. ML/I needs no special character to mark a call, and it works on any text: program source in any language, prose, or data. It was also one of the earliest serious experiments in portable software. ML/I was written in an abstract language and moved to each new machine with its own macros. Sixty years later it is still maintained and still runs, from 1960s minicomputers to Raspberry Pi.
History & Origins
Cambridge, 1966
In the mid-1960s macro processors were a lively research area. Assembler macro systems were common. Christopher Strachey had published his General Purpose Macrogenerator (GPM) in The Computer Journal in October 1965. Calvin Mooers’s TRAC and William Waite’s language-independent macro processor (later the basis of STAGE2) appeared in Communications of the ACM in 1966 and 1967.
Brown surveyed this field for his dissertation, Macro processors and their use in implementing software. The survey part was later published as “A survey of macro processors” in Annual Review in Automatic Programming (vol. 6, 1969). He then designed ML/I to keep what worked and fix what did not. Bob Eager, ML/I’s long-time maintainer, wrote in the Computer Conservation Society’s journal Resurrection that ML/I was clearly modelled on GPM but meant for jobs that needed more power and freedom. He adds that the name was partly a jab at IBM’s PL/I.
The first ML/I User’s Manual is dated July 1966, and the first implementation ran that year on a stand-alone DEC PDP-7. Brown’s paper “The ML/I macro processor” was received by Communications of the ACM in January 1967 and published in the October 1967 issue (vol. 10, no. 10, pp. 618–623). It reports that ML/I had been implemented on the PDP-7 and the ICT Atlas 2 (the Cambridge Titan), where it took about three thousand words of storage. Brown completed the dissertation in 1968.
On the year: 1966 is the date given by the official ML/I site, and it matches the July 1966 user’s manual that the CACM paper cites. Brown’s own university web page links ML/I to his PhD thesis “in 1967”, probably counting from the published paper. The dissertation itself was finished in 1968.
Kent and beyond
Brown moved to the University of Kent at Canterbury in 1968. There he implemented ML/I on the ICL 4130, and he introduced a simpler way to port it (see LOWL below). A colleague, Rod Saunders, did a PDP-11 implementation. In 1971 Bob Eager, then an undergraduate, took ML/I to the Honeywell DDP-516 in his spare time. Over the next five decades he produced many more implementations, including the DECsystem-10, the ICL 2900 under VME/K and EMAS, VAX/VMS, IBM mainframes and the IBM PC.
In the early 1980s Eager hand-converted ML/I into BCPL, reorganizing the code rather than translating it mechanically. He then converted the BCPL version to C: the official history gives 1985, and his Resurrection article dates the main C source to 1984. The C version brought ML/I to a much wider audience, though it is less efficient than the assembly-level ports. At the same time he typed in and updated the User’s Manual, which had been at its Fourth Edition since 1970, to a Fifth Edition. In 2004 he started the official web site, ml1.org.uk, with a Sixth Edition of the manual rewritten in Texinfo.
Design Philosophy
Macros written for the input, not input written for the macros
Most macro processors expect their input in a fixed form. GPM wanted special warning characters around every call. Card-oriented systems such as STAGE2 matched one line against a template. ML/I reverses this. Any atom can start a macro, and the user describes the shape of the call as a delimiter structure. Eager’s FAQ compares ML/I with m4 this way: “macros can be written for the input, rather than input having to be written for the macros.” In the same FAQ he calls ML/I “paper tape oriented” and STAGE2 “card oriented”.
Atoms, not characters
ML/I matches atoms, not raw character strings. An atom is a single punctuation character, or a run of letters and digits with punctuation on both sides. A macro named DO therefore does not fire inside DOCUMENT. This makes ML/I suited to processing program source, where a simple search-and-replace damages identifiers.
Small and portable
Brown designed ML/I to be small and to move easily between machines. In 1967 it used about three thousand words of store. Eager’s site says that even an unoptimised modern PC build is typically little more than 15 KB as an executable file.
Key Features
Operation macros
ML/I starts out recognizing only about twenty built-in operation macros. Their names all begin with MC so that they do not clash with user macros. Everything else is copied through unchanged. The most important are:
| Operation macro | Purpose |
|---|---|
MCDEF | Define a substitution macro and its delimiter structure |
MCSKIP | Define a skip: text to delete, or to copy without scanning for macros |
MCINS | Define an insert marker, used to insert arguments and variables |
MCSET | Macro-time assignment and arithmetic |
MCGO | Macro-time conditional or unconditional “goto” |
MCNOTE | Print a diagnostic message |
MCSUB, MCLENG | System functions: substring and length |
A first session
These examples come from the official ML/I tutorial. Lines are shown as input, followed by the output.
| |
Delimiter structures and literal brackets
A macro can have several delimiters, and its arguments are the text between them. Here Promote, to and a newline (NL) make up the structure. %A1. and %A2. insert the two arguments:
| |
Output:
| |
The MCSKIP MT,<> line makes < and > literal brackets. The replacement text is stored unevaluated, and the newline inside the brackets becomes part of the replacement. When the macro is called, argument 1 is evaluated, so Robert becomes Bob. To insert an argument as written, without evaluating it, use %WA1..
Delimiter structures can include optional and repeated parts. The manual’s example is a SUM statement that takes any number of arguments separated by + or - and ends with ;. A macro like this cannot produce fixed output, so ML/I includes a small macro-time programming language, with variables, labels, MCSET assignment and MCGO jumps. The manual’s SUM macro uses a loop to generate assembly code for each argument. In outline:
| |
(%DT2. inserts the delimiter whose number is held in T2, and MCGO L0 means “return”.) Eager notes that ML/I’s age shows in “an emphasis on ‘goto’ statements.”
Skips, warning mode and startlines
- Skips protect comments and string literals from macro replacement. They can delete text, copy it unchanged (
T), keep their delimiters (D), and match nested skips (M). - Warning mode, described in the 1967 paper, lets a user define a warning marker. Only names preceded by the marker are then treated as macro calls, as in GPM.
- Startlines, added fairly early in ML/I’s history, make line-oriented formats such as assembly language much easier to handle.
Portability: L, LOWL and the self-implementing macro processor
ML/I was designed to implement other software, starting with itself. Brown wrote its logic in L, a high-level descriptive language designed for writing ML/I. On the PDP-7 the L source was first converted to assembly language by hand. Once ML/I was running, it did the conversion itself with a set of mapping macros. This process is called an L-map, and later PDP-7 versions were produced this way. Part III of the dissertation, “An exercise in machine independence”, describes it.
L-maps proved hard for implementors who were not already skilled with ML/I. At Kent, Brown therefore translated L, again using ML/I, into LOWL (“low-level L”), an assembly language for an abstract machine. Porting then meant writing simple macros to map LOWL instructions into the target’s assembly language (a LOWL-map). Brown used the same method for ALGEBRA, SCAN and UNRAVEL. ML/I is often discussed alongside Waite’s STAGE2 and its Mobile Programming System as an early example of abstract-machine portability.
The official site lists known implementations, including the following. It does not date most of them, so they are grouped here by machine family rather than by decade:
| Machine family | Examples |
|---|---|
| DEC | PDP-7, PDP-8, PDP-11, PDP-15, DECsystem-10, VAX/VMS |
| British machines | Cambridge Titan; ICT/ICL 1900, ICL 4130, ICL 2900 (EMAS); Elliott 900 series; GEC 4000 (OS4000) |
| Other minis and mainframes | Honeywell DDP-516; CDC 6000; IBM 370 (DOS and MVS); AT&T 3B2 (UNIX System V); Olivetti L1 |
| PCs and current systems | IBM PC (DOS, OS/2, Windows, FreeBSD, NetBSD, Linux), Apple (OS X on PowerPC, macOS on Intel), Raspberry Pi (Linux) |
Evolution
Eager has changed the core logic as little as possible, “to preserve the original ethos of ML/I”. Versions are named with three letters. The first letter gives the source the build came from (A for L/LOWL, B for BCPL, C for C), and the other two give the major and minor version. The recorded history runs from AGA, the earliest known version, through the AJB/BJB/CJB family to the CK series. The CK series added character (string) variables (CKA), logical | and & in macro-time expressions (CKD), and support for all 256 character codes (CKJ). The latest versions, CKM through CKP, are portability fixes for Microsoft compilers, gcc, clang and 64-bit systems. The Sixth Edition of the User’s Manual describes version CKP.
Current Relevance
ML/I is a niche tool, but it is not abandoned. The official site has ready-to-run binaries and sources for current platforms and was still being updated in 2025 (its home page is dated May 2025; the last dated news entry is June 2024). It also hosts the L and LOWL sources, a test suite, Brown’s complete dissertation, and an archive of papers on ML/I, GPM and STAGE2. ML/I has a category on Rosetta Code. There is no official Docker image. On a modern system, you download the C implementation from ml1.org.uk and build it.
Why It Matters
ML/I was one of the most powerful general-purpose macro processors of its time. It showed that macro calls could follow the user’s own notation instead of a fixed syntax, and that a macro processor could extend almost any language. It was also an early case of software built for portability. The L and LOWL approach, using ML/I to port ML/I, carried a program from a 1966 PDP-7 to machines its author could not have imagined, and Brown’s 1974 book turned that experience into general techniques for portable software. GPM is better known and m4 more widely used, but ML/I sits between them historically, and it is a good illustration of how much can be done with a macro processor.
Timeline
Notable Uses & Legacy
ML/I itself (self-porting via L and LOWL)
ML/I's logic was written in L, an abstract language designed for the purpose, and later in LOWL, an assembly language for an abstract machine. ML/I macros then mapped that source into each target machine's assembly language. This is how it reached machines from the PDP-7 and Titan to the ICL 4130, PDP-11, DECsystem-10 and ICL 2900.
Brown's LOWL software: ALGEBRA, SCAN and UNRAVEL
Brown wrote other programs in LOWL and ported them the same way as ML/I: ALGEBRA, a package for Boolean algebra and multi-valued logics; SCAN, a BASIC-like text-analysis language for non-technical users; and UNRAVEL, a language for interpreting memory dumps.
Tanenbaum's 'poor man's compiler-compiler'
In a 1976 IEEE Transactions on Software Engineering paper, Andrew S. Tanenbaum showed how a general-purpose macro processor, specifically ML/I, could serve as a cheap compiler-compiler. The official ML/I site also archives his Vrije Universiteit report on a programming language for writing operating systems.
Elliott 905 implementation
Harold Thimbleby, then a student at Queen Elizabeth College, London, wrote a LOWL-map implementation of ML/I for the Elliott 905, using Strachey's GPM for the translation step; the surviving paper tape holds MASIR (Elliott macro-assembler) source. He contributed it to the Elliott Computer Users Association in 1976. Andrew Herbert later found the tape among digitised Elliott paper tapes supplied by Terry Froggatt and reconstructed the lost tool chain, as described in Resurrection, issue 84 (Winter 2018/19).
Language extension and systematic text editing
Brown designed ML/I to let users add new statements and syntactic forms to an existing programming language. Its maintainer also lists program parameterization, data-format conversion, converting HTML-marked-up files, and a microprogram translator for a bit-slice computer among its uses.