No description
  • Modula-2 95.4%
  • Shell 3.6%
  • AMPL 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Eric Streit 5e9258e50c README: coroutines are released (no branch qualifier)
Drop "(branch `single-pass-coroutines`)" now that it is merged to master.
2026-10-19 12:43:38 +02:00
gm2-port single-pass: SYSTEM coroutines (NEWPROCESS / TRANSFER) 2026-10-19 12:09:21 +02:00
selfhost selfhost: resolve binary/source defaults relative to the script 2026-10-19 09:02:57 +02:00
src single-pass: SYSTEM coroutines (NEWPROCESS / TRANSFER) 2026-10-19 12:09:21 +02:00
.gitignore Track the decode binary 2026-10-17 13:16:13 +02:00
ERRLIST.md ERRLIST catalog + README link 2026-10-15 15:51:03 +02:00
README.md README: coroutines are released (no branch qualifier) 2026-10-19 12:43:38 +02:00

Lilith single-pass Modula-2 compiler (GNU Modula-2 port)

Wirth/Gutknecht one-pass Modula-2 compiler for Lilith (1983–86, M3PL banner ETHZ M3L NW 2.8.86). This port builds and runs under Linux and emits the same 16-bit M-code .OBJ files that ../Interpreter executes — no Lilith hardware needed.

Background: src/singlepasscompiler.txt (Wirth 1.5.84 / rev. 15.11.85) and src/M2SinglePass.pdf. Original sources live in src/ (M2S, M3DL, M3TL, M3RL, M3GL, M3PL, plus M2.atg, M2SP.COM and the original decode.mod); the Linux port lives in gm2-port/ (see gm2-port/PORT-SUMMARY.md for every porting change).

Build

cd gm2-port && ./build.sh   # produces ./m3pl and ./decode (gm2 -fpim)

build.sh compiles the six modules separately and links via a module list (direct linking fails with duplicate main, one per module with initialization — same flow as CocoGm2).

Use

The compiler reads one source file per in> line and derives all output names from the module name, not the file name. Work in an empty directory; Medos filenames traditionally start with DK.:

mkdir -p /tmp/opencode/hello && cd /tmp/opencode/hello
cat > DK.HELLO.MOD <<'EOF'
MODULE HELLO; VAR x: CARDINAL; BEGIN x := 1 END HELLO.
EOF
printf 'HELLO.\n\033' | /path/to/m3pl
ETHZ M3L NW 2.8.86
in> HELLO.MOD
 + DK.HELLO.RFC
 + DK.HELLO.OBJ 11
in>
  • Type the module name without prefix/extension (HELLO. — the trailing dot triggers the default .MOD); ESC (or empty input / host EOF) quits the in> loop. Piped input works.
  • Batch mode: command-line arguments skip the prompt — m3pl HELLO TWO compiles both. HELLO, HELLO., HELLO.MOD and DK.HELLO.MOD all name DK.HELLO.MOD; another extension is used as-is. --version/--help print the banner/usage.
  • 11 is informational (code size). Errors go to DK.err.LST / DK.err.DAT with an error number; on failure the output files are deleted (errors detected). Every number is documented in ERRLIST.md.
  • Append /r to a name (FOO./r) to suppress range-check code for that unit (rngchk := FALSE).

Outputs per unit:

source outputs
DEFINITION MODULE DK.<name>.SBL (symbol file + 3-word key)
IMPLEMENTATION / program module DK.<name>.OBJ (M-code) + DK.<name>.RFC (debugger reference)

Definitions must be compiled before their users: the compiler reads the imported .SBL files (InRef) and records their 3-word keys in the .OBJ; the interpreter re-checks those keys at load (import key mismatch = stale .OBJ, recompile the imported module first). Multi-module example:

printf 'FOO.\nFOOIMP.\nBAR.\n\033' | /path/to/m3pl
printf 'DK.BAR.OBJ\n\033' | /path/to/mcode

Module map

module role
M2S scanner: GetSym, identifiers/numbers, error marking (DK.err.LST)
M3DL base structures (ObjPtr, StrPtr, WordSize = 16) + private heap
M3TL symbol table / scopes (Find, NewObj, NewScope, InitTableHandler)
M3RL symbol/reference file I/O (reads .SBL, writes .SBL/.RFC)
M3GL code generator: expression Items → M-code bytes (PutOp*, GenCall, …)
M3PL recursive-descent parser + in> main loop (one pass, no IR)

gm2-port/decode.mod (built as gm2-port/decode) disassembles an .OBJ to a .DEX text file (header/data/entries/code + imports/relocation); it is the reference for what the compiler emitted when the interpreter's /T trace disagrees with expectations. It also reads the multipass .o (16-bit big-endian) to a .DEX, and a multipass symbol file (.sy) to a grouped .SYD: module header and key, imports/exports, then const/type/var/proc sections with each type rendered (enumeration, subrange, pointer, set, array, record with CASE variants, procedure type), e.g.

type
  Structrec = RECORD size : CARDINAL; ...; CASE Structform OF 0 (size 6) | ... END END
proc
  Compare #2 (ARRAY OF CHAR; ARRAY OF CHAR) : INTEGER

Coroutines

SYSTEM also provides PROCESS (a word-sized handle type) and the standard procedures NEWPROCESS/TRANSFER (numbers 21/22, free slots in the standard table): NEWPROCESS(P, a, s, VAR q) emits CLX 0 2 and TRANSFER(old, new) emits TRA <mask>, exactly the encodings the multipass C18 uses, so mcode runs both. A coroutine's workspace is a plain array; its address and SIZE are passed to NEWPROCESS.

M3TL registers the type and procedures (in both gm2-port/, the host build, and src/, the self-hosted one) and M3GL generates the code. Interpreter/tests/DK.COR.MOD interleaves two routines (1 2 3 4 5 6) and is part of make test; Interpreter/examples/showcase runs the same larger program through this compiler and through mcc, and make showcase checks that both print identical output.

Dialect notes (1983 revisions, pre-existing — not port bugs)

  • DEFINITION MODULEs take no EXPORT clause (error 38); everything declared is exported.
  • TRUNC returns CARDINAL — TRUNC of a negative aborts at runtime (the compiler emits CHKS).
  • Unary minus / operators on LONGREAL constants are rejected at compile time (error 112, original GenNeg limitation) — use variables.
  • Single-character string literals ("7") type as CHAR; pad short patterns ("07") where a string is needed.
  • Real literals must fit IEEE-754 single range (else error 207); code generation assumes 16-bit words throughout.

Longer worked examples (libraries, I/O, tracing, test suites) are in ../Interpreter/TUTORIAL-LANGUAGE.md; the .OBJ container layout the interpreter accepts is documented in ../Interpreter/README.md.

Self-hosting (selfhost/)

selfhost/build-self.sh compiles the original src/ sources with m3pl itself (host-interface stubs DK.FileSystem/DK.Terminal/ DK.Heap/DK.Clock.MOD stand in for Lilith-only modules at type-check time; SYSTEM needs no stub — the compiler declares it builtin). Two mechanical patches are applied to the copies (never to src/): M3DL.Size (CODE 247B is Lilith-firmware magic — even the reference emulator crashes on SYS — so RETURN current - last, identical to its Available twin) and five lines in M3GL (16-byte names, 7-word imports, Real MIN/MAX literals, LSB-first MOD mask). Result: all six DK.M*.OBJ, which run under mcode (Heap/Clock/FileSystem synthesis, traps 70–81) and compile to byte-identical OBJs modulo timestamp keys. selfhost/test-self.sh replays T1/T3/T4/TS/ MSK/MODT/PV/HFS plus two negative tests through the self-compiled compiler as a regression test.

Known limits

  • Generated code is Lilith M-code; only the compiler itself runs on Linux. W fields are written as 4-byte CARDINALs, so .OBJs are readable by this port's decode/interpreter only, not by original Lilith tools.
  • M3DL.ResetHeap is a no-op under Storage (leaks per-scope memory; fine for small compiles).
  • Sources are PIM dialect (-fpim); -fiso additionally wants empty ELSEs on pre-existing non-exhaustive CASEs, no logic change.