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 (in demo/) begins its session with a READCHR commented "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.