The pymdl book
2. Running pymdl
2.1 Starting
pymdl is a Python package under src/; nothing is installed. From the
repository root:
PYTHONPATH=src python -m pymdl.repl
With no arguments and a terminal on standard input, this starts MDL's listener (section 2.2). With file arguments, pymdl reads and evaluates the files in order, prints the values, and exits. With piped or redirected input, it reads standard input to the end before evaluating the forms. An evaluation error is reported and processing continues with the next form; a reader error stops processing the current input.
echo '<+ 1 2>' | PYTHONPATH=src python -m pymdl.repl # prints 3
PYTHONPATH=src python -m pymdl.repl setup.mud game.mud # runs the two files
By default, RANDOM starts with the same sequence in each session. Use
--random to seed it from host entropy for varied runs, or
--random=SEED to choose a reproducible sequence. Both take effect before
the initialization file or program runs.
2.2 The listener
At a terminal pymdl puts the tty in character mode and runs the listener the manual describes in 1.2: you type MDL, and the form is evaluated when you type ESC. The editing keys are the era's:
| key | does |
|---|---|
ESC (echoed as $) |
evaluate what has been typed, if the brackets balance |
! then ESC |
close every open bracket first, then evaluate |
| rubout, Backspace | delete the last character |
^@ |
clear the line |
^D |
retype the line |
^L |
clear the screen and retype |
^G |
the ITS quit character: interrupt a running program and enter a LISTEN level (chapter 19) |
Typed interrupt characters reach a running program, so <ON "CHAR" ...>
handlers (chapter 23) fire as they did. Under the Tenex personality the quit
character is ^A, as the manual says.
When standard input is not a terminal, there is no character mode. The
reader treats newlines as whitespace: a form may span lines, and several
forms may appear on one line. ESC is not needed to submit batch input.
Interactive READCHR has a separate detail that matters to programs such
as DOCTOR: the ESC used to submit a form remains available to that form
when it reads a character.
Measured, MDL 55, 2026-09-10. A
<READCHR>typed at the listener, or run by a file being FLOADed, is handed the ESC that activated the line before it. DOCTOR (indemo/) begins its session with aREADCHRcommented "FLUSH ALTMODE" for exactly that character. pymdl leaves the activating ESC pending for the first character read after a typed form (tests/test_fload_keeps_inchan.py).
2.3 Ending a session
<QUIT> ends the process. A batch session also ends after its input has
been processed. At the interactive listener, Ctrl+C clears the input being
typed or aborts the current evaluation and returns to listening; it does
not end the session. Outside that listener, Ctrl+C exits with status 130.
Chapter 25 explains <LOGOUT> and <VALRET>.
2.4 Files and the file root
MDL names files the way the operating systems of 1979 did, and pymdl keeps
that surface: a file specification is DEVICE:SNAME;NAME1 NAME2 on ITS or
DEVICE:<DIRECTORY>NAME.EXT on Tenex, with every part optional and
defaulting (manual 11.2). Under pymdl those names land in a directory
called the file root: the current directory when the listener starts,
or whatever a host program sets. The mapping is one rule with a few
defaults:
| you write | pymdl opens, under the root | because |
|---|---|---|
"MUDDLE;SCR" |
muddle/scr |
SNAME is a subdirectory; the ITS default second name > (the newest version) is the bare name |
"MUDDLE;DOCSCR >" |
muddle/docscr |
likewise |
"COMPIL;PASS1 MUD" |
compil/pass1.mud |
a second name is an extension |
"doctor.mud" |
doctor.mud |
a host-style name is taken as it is |
"DSK:MDL;MADADV INFO" |
mdl/madadv.info |
the device is ignored; there is one disk |
"TPL" under the Tenex personality |
tpl.mud, then tpl |
the Tenex default second name is MUD |
Names are lowercased on the way to the host. An absent SNAME defaults to
the SNM global, the "working directory" of the manual's Appendix 5, which
is empty by default and so names the root itself. NM1 and NM2, the
manual's default first and second names, work as described.
Two directory names are special. LIBMUD; and PS:<MDLLIB> -- the ITS
and Tenex names of the system library, both on the package system's default
search path -- resolve first under the file root, so a local library shadows
the installation as it did on the era, and then to the shipped library
src/pymdl/mdl/mdllib/ (chapter 30).
pymdl's own. The file root is pymdl's; the era had a disk. Programs that write files, as DOCTOR writes its transcript to
MUDDLE;DOCSCR, write under the root, so run them from a directory you mean them to write in.
2.5 MUDDLE INIT
When the listener starts it FLOADs MUDDLE INIT if the file exists --
muddle.init under the root -- exactly as the manual's 1.3 says the
interpreter does. Under the Tenex personality the name is MUDDLE.INIT. An
error in the file is reported and the listener comes up anyway.
2.6 A first session
The four programs in demo/ are era MDL kept byte for byte (demo/README.md
says where each came from). From that directory:
$ cd demo && PYTHONPATH=../src python -m pymdl.repl
<FLOAD "celest.mud">$
"DONE"
<DECL-CHECK <>>$
T
<SUNRISE>$
((126 9 10) (6 23 13) "EST")
<FLOAD "doctor.mud">$
JUST ONE MOMENT AND THE DOCTOR WILL BE WITH YOU.
HOW DO YOU DO. PLEASE TELL ME ABOUT YOUR PROBLEM.
PUSH THE "ESC" KEY EACH TIME YOU FINISH WHAT YOU WANT TO SAY.
I AM SAD$
IS IT BECAUSE YOU ARE SAD THAT YOU CAME TO ME?
MY MOTHER HATES ME$
TELL ME ABOUT YOUR MOTHER .
CELEST reports Boston's sunrise for the current day, so your result will
differ from this sample dated September 10, 2026. Its date uses years since
1900: 126 means 2026. Turn declaration checking off before calling SUNRISE,
as explained in the demo README. DOCTOR is the MDL ELIZA; its
last form starts the session as soon as the file has loaded, and every
sentence ends with ESC.
2.7 From Python
pymdl is also a library. A fresh interpreter, a string of MDL, and the printed form of the answer:
import pymdl
from pymdl import unparse
ip = pymdl.new_interpreter()
ip._file_root = "demo" # where FLOAD looks
print(unparse(pymdl.evaluate_str("<+ 1 2>", ip), ip)) # 3
new_interpreter takes os_name="tenex" for the other personality (chapter
3), and ip.input_lines is a list of lines a program's READ and READCHR
consume in place of a terminal, which is how the test suite drives DOCTOR
through a conversation. Output goes to ip.out, a file-like object;
io.StringIO() captures it.