Oberon is Niklaus Wirth's successor to Pascal and Modula-2 — a small, strongly-typed systems language designed to be compilable by a single programmer in a single pass, with no preprocessor and (almost) no undefined behavior. Oberon-07 (2007) is Wirth's own later simplification of the language.
Wirth's Oberon-07, compiling itself, targeting a 1981 processor. Written entirely in Oberon, running on real DOS hardware — no C, no assembler front end, no host toolchain required at runtime.
This project implements a practical DOS dialect of Oberon-07: the core
language plus what real 16-bit systems programming needs — SYSTEM
intrinsics for port I/O and inline machine code, FAR/NEAR procedures,
typeless VAR parameters, and an INLINE mechanism for embedding raw
opcodes. See DOCS/OBERON07.EBN for the authoritative
grammar and every deviation from the standard.
There is no free, modern, actively-usable Pascal-family compiler that
targets (and works) 16-bit MS-DOS real mode. Borland stopped at Turbo Pascal 7 /
Borland Pascal 7 in the early '90s and never open-sourced it. FreePascal —
the natural place to look — has supported 16-bit i8086 code generation
since around 2014, but it was always the neglected back corner of the
project: undermaintained, thin on runtime support, and never treated as a
first-class target the way its 32/64-bit backends are. I had been waiting
since the late 1990s for a serious support 16-bit target from the Free
Pascal and it never statify my expectations.
So this project doesn't try to be that. It picks Wirth's other, smaller language — Oberon-07, Pascal's own successor — and builds a compiler for it from scratch, purpose-built for 16-bit DOS real mode, with no inherited 32-bit assumptions to work around. The compiler, linker, dependency scanner, archive manager, and test tools are all written in Oberon-07 and compile themselves, on the target architecture (8086 real mode), from a single checked-in bootstrap binary. Feed it its own source and it reproduces itself byte-for-byte — a full fixpoint, verified on every change.
That means:
- No hidden C runtime.
SYSTEM.MOD+ a smallSYSTEM.ASMare the entire foundation. - No 32-bit protected-mode escape hatch. Real segmented 8086 memory, real 64 KB segments, real far pointers.
- A compiler that could, in principle, have been built in 1987 — but with the discipline of zero-heap-leak, byte-reproducible modern engineering practice behind it.
- Self-hosting, byte-stable.
toc /ENTRY=Run TOC.MODrebuilds the whole compiler in a single process; two consecutive generations are byte-identical. SeeTESTS/test_selfhost.sh. - Zero heap leaks, guaranteed. Every compile and every link reports
0 leaked paragraphs— enforced byLeakGuardinstrumentation and regression tests, not just hoped for. - One-command driver.
toc.exeis the dep-scanner, incremental compiler, and smart linker behind a single command — no makefiles required to build an Oberon program. Under the hood it is three binaries (toc.exedrivestocc.exeto compile andtocl.exeto link), so no single DOS process ever has to hold both the compiler and the linker in 640 KB. - Real DOS constraints, handled properly. Per-module 64 KB code segments, far pointers (segment:offset), a large memory model, EMS-backed temp files, and a linker that streams instead of holding everything in RAM.
- 311-row regression manifest plus DOS-side executable and unit-test suites — every codegen change is checked against real compiled/linked/run output, not just "it compiled."
- Turbo Debugger-compatible debug info. Compile with
(*$D+*), link with/G, and get a Borland TDS/TDINFO v2.08 block appended straight onto the.EXE— inspect with the includedtdinfo.exe. - Linkable with external object files. The linker consumes standard RDOFF2
object files, so hand-written assembly modules assembled with NASM, YASM,
or MSA2 (
-f rdf) can be linked directly alongside Oberon-compiled.rdf/.ommodules — no C shim required. - Trubo Assembler (TASM).
SRC/TASM/is a TOC-hosted NASM-subset 8086 assembler (.asm→ RDOFF2.rdf), verified against real-NASM-built oracles for every hand-assembled.ASMfile in the standard library (SYSTEM.ASM,EMS.ASM,DETECT.ASM,WINCB.ASM,KMOUSE.ASM,SCREEN.ASM) — four fully byte-identical, two identical in code/data with only a benign header-record-ordering difference. Directives includeSECTION/GLOBAL/EXTERN/STRUC/DB/DW/TIMES/RESB.../EQUand the%define/%ifdef/%include/%macropreprocessor. It's the toolchain's tool of record for regenerating those files — seeDOCS/PORTING.MD§1.6 andDOCS/BUILD.MD'sregen-asmtarget. - Trubo Link (TLINK) — a second linker for tiny/small memory models.
SRC/TLINK/is one TOC-tool (tlink /T=com|sys|exe|texe) covering both tiny model (.COM/.SYS/texe, no relocation table) and small model (.EXEwith an MZ relocation table + a DS-setup startup stub) — memory modelstoc.exe's own embedded linker does not target.texeis a tiny-model.COM-equivalent wrapped in an MZ header instead of relying on DOS's.COM-load convention — seeDOCS/BUILD.MD's "Using tlink" section. - New Executable (NE) output (
/CP) — a 16-bit DPMI protected-mode target, still segmented.toc /CPemits a 16-bit DPMI-protected-mode program in an NE container, the same approach Borland Pascal's own/CPtarget used. SeeDOCS/NE-BP.MDandDOCS/PORTING.MD§3.5.
Hello, world:
MODULE Hello;
IMPORT Out;
PROCEDURE Run*;
BEGIN
Out.Open;
Out.String("Hello, world!"); Out.Ln
END Run;
END Hello.BIN\TOC.EXE /ENTRY=Run Hello.Mod
Hello.exeMODULE Example;
IMPORT Out;
TYPE
PNode = POINTER TO Node;
Node = RECORD val: INTEGER; next: PNode END;
VAR head: PNode;
PROCEDURE Push*(val: INTEGER);
VAR n: PNode;
BEGIN
NEW(n); n^.val := val; n^.next := head; head := n
END Push;
PROCEDURE Pop*(): INTEGER;
VAR v: INTEGER;
BEGIN
v := head^.val; head := head^.next;
RETURN v
END Pop;
PROCEDURE Run*;
VAR i: INTEGER;
BEGIN
Out.Open;
FOR i := 1 TO 5 DO Push(i) END;
WHILE head # NIL DO Out.Int(Pop(), 0); Out.Char(" ") END;
Out.Ln
END Run;
END Example.BIN\TOC.EXE /ENTRY=Run Example.Mod
Example.exe
5 4 3 2 1
See EXAMPLES/ for larger programs, including a Forth
interpreter.
| Area | Support |
|---|---|
| Types | INTEGER, LONGINT (32-bit), WORD (16-bit unsigned), BYTE, CHAR, BOOLEAN, SET, REAL/LONGREAL (x87), ARRAY (fixed/open/multi-dim), RECORD with single inheritance, POINTER, FAR/NEAR procedure variables |
| Control flow | IF/ELSIF/ELSE, WHILE/ELSIF, FOR/BY (negative step), REPEAT, CASE (INTEGER or CHAR, incl. ranges) |
| Procedures | FAR/NEAR, nested, EXTERNAL, INLINE (whole-body and statement forms), FORWARD, typeless VAR params, full actual-parameter type checking |
| Object model | POINTER TO RECORD extension, runtime IS type tests (compile-time folded where possible), NEW/DISPOSE with runtime type tags |
| Modules | Qualified imports, namespace isolation, $L/$M/$R directives, dead-code/unused-symbol warnings |
SYSTEM |
ADR, SEG, OFS, PTR, MOVE, FILL, PORTOUT/PORTIN, Intr, LSL/LSR/ASR/ROR/AND/IOR/XOR |
| Expressions | Constant folding, implicit INTEGER→REAL coercion, SET operators, IN, array bounds checks ($R+/$R-), SIZE/LEN |
| Codegen | Strength reduction (mul/div/mod by powers of two and small constants → shift/add), peephole cleanup, RDOFF2 object format with smart (dead-module-eliminating) linking |
Full detail lives in DOCS/OBERLANG.MD and
MANUAL.MD.
SCAN.MOD tokens (sym, id, ival, rval, sval, line)
|
PARSER.MOD module / declarations / statements / types (single-pass, no AST)
|
PEXPR.MOD expressions: designators, actual params, SYSTEM intrinsics
| | |
SYMS.MOD CGEN.MOD --> RDOFF.MOD IMPORT.MOD --> .rdf object file
|
LINK.MOD --> smart-linked MZ .EXE
toc.exe bundles all of the above plus dependency scanning behind one command.
toc /ENTRY=Run MyProg.Mod scans, compiles whatever's stale, and links — no
makefile needed for user programs. toc.exe itself is a small driver that
execs tocc.exe (compile) and then tocl.exe (link); all three ship together
and must stay in the same directory.
BOOT/{TOC,TOCC,TOCL}.EXE (checked-in seed binaries — never touched by `make`)
|-- SRC/LIB/*.MOD -> BIN/OBERON.OM (standard library archive)
|-- SRC/TOC/*.MOD -> BIN/TOC.EXE (driver)
BIN/TOCC.EXE (compile half)
BIN/TOCL.EXE (link half)
BIN/TOC.EXE
|-- SRC/TOOLS/TOLIB.MOD -> BIN/TOLIB.EXE (archive manager)
|-- SRC/TOOLS/RDFGREP.MOD -> BIN/RDFGREP.EXE (RDOFF/.om/.exe inspector)
|-- TESTS/TESTALL.MOD -> BIN/TESTALL.EXE (DOS-side test driver)
|-- SRC/TASM/*.MOD -> BIN/TASM.EXE (NASM-subset assembler)
BOOT/TASM.EXE (checked-in seed binary — never touched by `make`, installed
manually by the maintainer, same status as BOOT/TOC.EXE)
|-- SRC/LIB/SYSTEM.ASM, EMS.ASM, DETECT.ASM, WINCB.ASM, KMOUSE.ASM,
SCREEN.ASM -> SYSTEM.RDF, EMS.RDF, DETECT.RDF, WINCB.RDF, KMOUSE.RDF,
SCREEN.RDF (`make regen-asm`, a manual step — not part of
`make all`, see the circular-dependency note in DOCS/BUILD.MD)
Rebuilding SRC/TOC with BOOT/TOC.EXE produces toc1.exe; rebuilding the
same sources with toc1.exe produces toc2.exe. toc1.exe == toc2.exe,
byte-for-byte, every time. That's the self-hosting proof, and it's run as a
regression test, not just a one-time milestone.
Twenty-two modules, merged into one BIN/OBERON.OM archive:
| Module | Purpose |
|---|---|
SYSTEM |
Runtime: allocation, halt, FPU detection, INT calls, bitwise ops |
Out / Out87 |
Formatted stdout, including REAL/LONGREAL (x87) |
In |
Buffered stdin: char, integer, long integer, token |
Files |
Buffered file I/O — open, read, write, seek, close; EMS-backed temp files |
Strings |
Length, Equal, Copy, Append, Pos, Insert, Delete, Extract |
IO |
Raw rider-based I/O to any DOS file handle |
Dos |
Args, Exec, FindFirst, date/time, interrupts, interrupt vectors |
Time |
DOS packed date/time <-> DateTime record conversion (PackTime/UnpackTime) |
Crt |
ANSI-Terminal control: cursor, clear, colour |
Math |
FPU math: sin, cos, sqrt, exp, ln, abs, … |
EMS |
Expanded-memory page-frame management |
Test |
Unit-test framework: AddTest, RunTests, Assert* |
RdfLoad |
Runtime RDOFF2 loader — dynamic code loading from a running program |
LogErr |
Single error/diagnostic state for the whole toolchain (fail-fast, output.log/error.log) |
Rdoff |
RDOFF2 object file reader/writer |
Tar |
ustar archive reader/writer |
Detect |
Hardware/environment probing: EGA/VGA/mono, redirected stdio, FreeDOS, CPU class |
WinCB |
Windows 3.x clipboard access (WinOldAp INT 2Fh) |
KMouse |
Extended keyboard (101/102/122-key) + Microsoft Mouse (INT 33h) driver |
Screen |
Text-mode shadow-buffer driver: colour print/scroll, cursor, background spinner, VGA palette fades |
Overlay |
$O+ overlay manager: load/evict/patch overlaid code on demand, disk- or EMS-backed (/CD only) — see DOCS/OVR.MD |
Detect/WinCB/KMouse/Screen are ports of small Turbo Pascal UNITs —
see DOCS/PORTING.MD §16.1 for the porting conventions (companion NASM
.ASM file per module, %define/%undef stack-argument aliasing, PCHAR
→ SYSTEM.ADDRESS, CALL FAR for cross-module calls).
make test # 311-row manifest + TASM (116) & TLINK (14) regressions — codegen
# bytes, RDOFF structure, error messages, exit codes
make testall # + DOS-side executables (Section 17) and unit tests (Section 18)Every codegen or language change is checked three ways: the compile-time manifest (does it produce the right object bytes / the right diagnostic?), DOS-side executables (does the linked program actually behave correctly when run?), and the self-hosting fixpoint (does the compiler still reproduce itself?).
| File | Contents |
|---|---|
MANUAL.MD |
Language reference, ABI, library APIs, porting guide |
DOCS/OBERON07.EBN |
Authoritative grammar for this dialect |
DOCS/OBERLANG.MD |
Type sizes, calling convention, codegen patterns, SYSTEM intrinsics |
DOCS/IMPLRULE.MD |
DOS/portability rules, .def file format, emit conventions |
DOCS/BUILD.MD |
Build instructions, toc.exe flags, linker internals |
DOCS/TESTS.MD |
Test suite reference: running, adding, interpreting tests |
DOCS/RDOFF2.MD |
RDOFF2 object file format specification |
DOCS/TD-SPEC.MD |
Turbo Debugger TDS/TDINFO format notes |
DOCS/PORTING.MD |
Guide for porting code from C/Pascal |
DOCS/OVR.MD |
Overlay feature: $O+ directive, .OVR format, Overlay runtime module |
DOCS/HISTORY.MD |
Dated changelog of resolved bugs, refactors, and features |
Public domain. See LICENSE.TXT.