Blog › ICP guides

Modula-2 developer on retainer: DEFINITION MODULE dependency, separate compilation, coroutines, and Modula-2 on monthly retainer

October 2, 2026 · ~13 min read

A Modula-2 developer was maintaining an embedded systems control program at an industrial automation company. The program tracked machine state using a RECORD type defined in a DEFINITION MODULE called MachineState. The developer added a new BOOLEAN field called haltRequested between the existing rpm : INTEGER field and the temperature : REAL field in the exported RECORD type.

The developer recompiled the IMPLEMENTATION MODULE that provided MachineState. The client module that read machine state was not recompiled because the build script only rebuilt modules with changed source files, not modules that imported a recompiled DEFINITION MODULE. The client module used the old field layout: it read temperature at the byte offset that now held haltRequested (a BOOLEAN, 1 byte padded to 4 bytes), and read nothing meaningful at what was now the temperature offset.

Wrong temperature readings produced: 5 wrong machine state reads → 0 after a full recompile of all modules importing MachineState.

The Modula-2 module system divides every compilation unit into a DEFINITION MODULE — which declares the exported interface (type names, variable names, procedure signatures) — and an IMPLEMENTATION MODULE, which provides the body. Client code imports only what the DEFINITION MODULE exports, via explicit FROM ModuleName IMPORT symbol; or IMPORT ModuleName. Opaque types let a DEFINITION MODULE export a type name without its internal representation, enforcing abstraction at the language level. The separate compilation model means object files can be out of date without the compiler knowing, unless the build tool tracks all reverse dependencies — specifically the compiled interface (.sym) files that encode the exported layout. A build script that watches only source file modification times cannot detect that a client module must be recompiled because a DEFINITION MODULE it imports has changed its RECORD layout.

Modula-2 overview: Pascal’s systems-programming successor (1978)

Niklaus Wirth designed Modula-2 at ETH Zürich as a systems programming language to replace Pascal in his Lilith workstation project. Modula-2 introduced the MODULE construct as the fundamental unit of compilation and encapsulation: a DEFINITION MODULE declares the exported interface (type names, variable names, procedure signatures); the IMPLEMENTATION MODULE provides the body. Client code imports only what the DEFINITION MODULE exports, via explicit FROM ModuleName IMPORT symbol; or IMPORT ModuleName. This explicit interface/implementation separation predated Java interfaces and C++ header/source separation by over a decade as a language-level feature rather than a convention.

Modula-2 added coroutines via NEWPROCESS and TRANSFER for concurrent embedded programming. The SYSTEM module provides low-level access: the ADDRESS type (an unsigned integer that can hold any pointer), ADR(v) to get the address of a variable, TSIZE(T) for the byte size of a type at compile time, BITSET for bit manipulation, and CAST(T, v) to reinterpret a value as a different type. SYSTEM imports are per-module and must be explicit; a module that does not import SYSTEM cannot perform pointer arithmetic or low-level memory operations at all.

Modula-2 was the basis for the ISO Modula-2 standard (ISO/IEC 10514-1:1996) and influenced Modula-3 (a type-safe successor developed at DEC SRC and Olivetti that added garbage collection and objects) as well as Oberon (Wirth’s later minimal language). GNU Modula-2 (gm2) is the current active compiler, integrated into GCC. ADW Modula-2 provides a Windows IDE. p2m2 / m2c provides source-to-C transpilation for portability.

Modula-2 retainer work today exists at industrial automation firms, embedded systems consultancies, and research institutions that still run control programs originally written in the 1980s and 1990s for Lilith workstations or early embedded targets. These programs control manufacturing equipment, coordinate sensor arrays, and manage state machines in long-lived industrial installations. The programs work; the organizations that depend on them have no incentive to replace them; and the maintenance burden falls on consultants who understand both the Modula-2 module system semantics and the embedded domain. GNU Modula-2 provides a current compiler targeting modern platforms; ISO Modula-2-compliant programs can often be recompiled with minimal changes.

DEFINITION MODULE interface stability and the stale-object problem

When a RECORD type is exported from a DEFINITION MODULE, the compiler generates object code for each importing client module based on the field offsets at the time the client was compiled. These offsets are encoded in the client’s object file. If a new field is inserted before an existing field, all subsequent fields shift in byte offset. Any client module compiled against the old DEFINITION MODULE uses the old offsets: it reads the wrong bytes for each field it accesses past the insertion point.

The bug is silent in the absence of runtime checking: the field reads succeed (no segfault, no exception in a language without bounds checking on record field access) but return wrong values. The type system cannot detect the mismatch at runtime because the stale object file already has the field access compiled as a fixed byte offset from the record base address. The mismatch only becomes visible when the wrong value is used in a calculation or comparison that produces an observable wrong result.

The fix is always a full dependent recompile — not just the changed module and its IMPLEMENTATION MODULE, but every module that directly or transitively imports the changed DEFINITION MODULE. Modula-2 build tools that track .sym file timestamps (the compiled interface representation that encodes the exported type layout) detect this automatically: if a client module’s .sym dependency is newer than the client’s object file, the client must be recompiled. Make-based builds that only track source file modification times do not detect this dependency and silently leave stale objects in place.

The work log entry for a stale-object bug must name the DEFINITION MODULE where the change was made, the field added or reordered, the client module that was not recompiled, the wrong field that was being read (because the client used the old offset), and the wrong value count before and after the full dependent recompile. A log entry that says “fixed field layout bug, 3h” is not auditable. A log entry that says “MachineState.DEF: haltRequested BOOLEAN inserted between rpm and temperature; ControlLoop.MOD not recompiled; temperature field read returning byte value of haltRequested; 5 wrong machine state reads; full recompile of 7 importing modules; 0 wrong reads after; 3h” is auditable.

Coroutines via TRANSFER and NEWPROCESS; opaque types and the SYSTEM module

Modula-2 provides coroutines through two procedures in the SYSTEM module: NEWPROCESS(p, workspace, size, c) creates a new coroutine c backed by procedure p, using the memory region at workspace of size bytes as the coroutine’s stack and context area. TRANSFER(from, to) switches execution from the current coroutine to coroutine to: it saves the current CPU registers into the from workspace, restores registers from the to workspace, and resumes execution at the saved program counter in to. This is cooperative concurrency: no preemption occurs. Only an explicit TRANSFER call hands control to another coroutine.

Coroutines are particularly useful for embedded state machines that alternate between sensor polling and control output. A sensor coroutine reads hardware registers and stores readings into a shared buffer; a control coroutine reads the buffer and drives actuators. Each coroutine runs to a TRANSFER call, yields, and resumes where it left off. The workspace size passed to NEWPROCESS must accommodate the full depth of the call stack within the coroutine, including all nested procedure calls and their local variables. Workspace-too-small bugs produce stack overflow that overwrites adjacent memory — but the corruption is observed in the coroutine switched to, not the one that caused the overflow, because the overflow damages the workspace of the next coroutine in memory. Diagnosis requires calculating the maximum stack depth of all procedures called within the coroutine and increasing the workspace size accordingly.

Opaque types provide abstraction enforcement at the language level: a DEFINITION MODULE can export a type name without its representation. Client modules can declare variables of the opaque type and pass them to the defining module’s procedures, but cannot access fields directly. This enforces encapsulation without requiring object-oriented machinery. The SYSTEM module provides ADDRESS (an unsigned integer type that can hold any pointer), ADR(v) to get the address of variable v, TSIZE(T) to get the byte size of type T at compile time, MOVE(from, to, nBytes) for bulk memory copy, and CAST(T, v) to reinterpret v as type T. Low-level retainer work often involves diagnosing CAST misuse: a value is cast to a type with a different size or alignment, and the reinterpretation reads or writes beyond the intended region, producing silent wrong values or occasional crashes depending on what lives at the reinterpreted address.

Typical Modula-2 retainer work and what it looks like in a work log

Stale-object field offset bugs from DEFINITION MODULE changes are the most common invisible retainer task: no compile error, no runtime trap, just wrong values. The work is entirely diagnostic — tracing the import graph, identifying which client modules were not rebuilt, verifying that the observed wrong values correspond to the old field offset, and executing a full dependent recompile. The three-hour session produces no new code artifact the client can inspect; the fix is a recompile command. Work log entry: “MachineState.DEF: added haltRequested : BOOLEAN between rpm and temperature; ControlLoop.MOD and StatusDisplay.MOD not rebuilt; both read temperature at old offset (now haltRequested byte); 5 wrong machine state reads; traced import graph (7 modules total importing MachineState.DEF); full recompile; 0 wrong reads after; updated Makefile to depend on .sym file timestamps; 3h.”

TRANSFER coroutine workspace overflow bugs are the second category. A workspace allocated too small for the coroutine’s maximum stack depth allows TRANSFER to overwrite adjacent memory. The state corruption appears in the coroutine switched to, not the one that switched away, because the overflow damaged the adjacent workspace. Diagnosis requires computing the maximum stack frame depth, identifying the deepest nested procedure call inside the coroutine, summing all local variable sizes and saved registers, and increasing the workspace allocation accordingly. Work log entry: “SensorCoroutine workspace 512 bytes too small; ReadSensorArray called ConvertRaw called ScaleValue; stack depth at deepest call 680 bytes; ControlCoroutine workspace (adjacent in memory) overwritten; control output corrupted after 12 state transitions; increased workspace to 1024 bytes; corruption count: 1 per 12 transitions → 0 after; 4h.”

SYSTEM module CAST misuse is the third category. A value is cast to a type with different alignment or size; the reinterpretation reads or writes beyond the target. Depending on what lives at the reinterpreted address, the result is a silent wrong value (if the adjacent bytes are valid data of the wrong meaning) or an occasional crash (if the adjacent bytes are a pointer or control structure). Work log entry: “PacketHeader CASTd from CARDINAL (2 bytes, 16-bit target) to HeaderRec (4 bytes); bytes 3–4 read from adjacent packet data field; length field always wrong for packets adjacent to status packets; 8 wrong packet length reads per 100 packets; removed CAST and used explicit field construction; 0 wrong reads after; 2h.”

Track Modula-2 developer retainer hours without the status emails

When a 3-hour session diagnoses a stale-object field offset bug — tracing a full recompile dependency graph for a DEFINITION MODULE change in a 20-module embedded control system, finding that two client modules were not rebuilt, confirming the wrong byte offset reads, and executing the full dependent recompile — the work log needs to name the DEFINITION MODULE, the new field, the importing client modules not recompiled, the wrong field reads, and the fix. HourTab gives your Modula-2 retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the Modula-2 mechanism. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks Modula-2 developer retainer hours

Modula-2 retainer work is invisible by the same mechanism that makes separate compilation powerful: the compiler trusts that imported interfaces have not changed unless the build tool explicitly checks. A 20-module embedded control system that has run correctly for years may have a latent stale-object bug that never fires until a DEFINITION MODULE is updated for a new hardware variant. The build script recompiles the IMPLEMENTATION MODULE and the two modules whose sources changed; the eight other modules that import the changed DEFINITION MODULE are silently left with stale object files. The program runs; it reads wrong sensor values; the operator notices anomalous machine behavior six production cycles later.

The connection between a BOOLEAN field insertion in a DEFINITION MODULE in 2026 and wrong temperature readings in the same production run requires understanding Modula-2 separate compilation semantics and the specific field layout shift that the insertion caused. The work log needs to name the mechanism: which DEFINITION MODULE changed, which field was added, which client modules were not recompiled, what wrong field was being read at what offset, what the observable symptom was (wrong value, wrong count, wrong control output), and what the fix was. A log entry that says “recompile fix, 3h” is not auditable.

HourTab gives Modula-2 developers a public retainer-hours URL they send to clients — industrial automation firms maintaining embedded control systems, defense contractors running Modula-2 mission software, and research institutions operating long-lived instrumentation programs. For Modula-2 retainers, each work log entry should name the Modula-2 mechanism: which DEFINITION MODULE, which field change, which importing modules, which offset mismatch. Comparative context: Modula-2 retainer work has conceptual overlap with other environments where module interface stability requires explicit attention — Ada (where package specs and bodies have analogous separate compilation with explicit with clauses and recompilation dependencies); ALGOL (Modula-2’s ancestor via Pascal, where block structure and procedure parameter semantics require analogous careful tracking); and Oberon (Wirth’s successor to Modula-2, where the module system is simplified but the separate compilation dependency problem is identical). Modula-2 is uniquely positioned as the language that introduced first-class module-level interface/implementation separation and whose separate compilation model underlies both correct embedded systems design and the most silent field-offset bugs in industrial control software.

FAQ: Modula-2 developer retainers

What does a Modula-2 developer on retainer typically do?

A Modula-2 developer on monthly retainer covers DEFINITION MODULE interface stability work (identifying when a RECORD field addition or reordering leaves importing client modules using stale field offsets; performing full dependent recompiles; updating build scripts to track .sym file timestamps); separate-compilation dependency management (tracing the transitive import graph to find every module that must be rebuilt after a DEFINITION MODULE change); TRANSFER coroutine workspace diagnosis (detecting workspace-too-small bugs where NEWPROCESS is called with an undersized workspace and TRANSFER overwrites adjacent memory); SYSTEM module maintenance (reviewing CAST usage for type size and alignment mismatches; auditing ADR() and TSIZE() calls in embedded control code); and toolchain maintenance for GNU Modula-2 (gm2) and ADW Modula-2.

What Modula-2 work is most commonly underlogged?

Stale-object field offset diagnosis is the most underlogged: a DEFINITION MODULE RECORD change (adding, removing, or reordering a field) shifts the byte offsets of all subsequent fields in every importing client module not rebuilt; the client reads the wrong bytes for each field; no exception, no compile error, just wrong values; fix is a full dependent recompile; 3 to 6 hours invisible per occurrence. TRANSFER coroutine workspace overflow: workspace allocated too small for the maximum call depth; TRANSFER overwrites the adjacent coroutine’s workspace; corruption appears in the switched-to coroutine; fix is increasing the NEWPROCESS workspace size; 2 to 5 hours. SYSTEM CAST misuse: a value cast to a type with a different size reads or writes beyond the target; silent wrong value or occasional crash; fix is removing the CAST and using explicit field construction or a correctly-sized type; 2 to 4 hours.

What are typical Modula-2 developer retainer rates?

Entry-level Modula-2 developers with 1 to 2 years covering basic module system maintenance, DEFINITION MODULE interface work, and separate compilation build scripts typically bill at $65 to $120 per hour. Mid-level Modula-2 programmers with 2 to 4 years covering stale-object diagnosis, TRANSFER coroutine debugging, and embedded control systems maintenance typically bill at $100 to $180 per hour. Senior Modula-2 developers with 4 or more years covering complex DEFINITION MODULE dependency tracing in large multi-module embedded systems, SYSTEM module CAST audits, and GNU Modula-2 toolchain maintenance typically bill at $145 to $270 per hour. Monthly retainer ranges: $800 to $1,800 per month for advisory engagements (8 to 15 hours per month); $1,800 to $5,000 per month for active embedded control system maintenance.

What should a Modula-2 developer retainer agreement include?

A retainer agreement should specify: module system scope (which DEFINITION MODULEs and IMPLEMENTATION MODULEs are in scope; whether the retainer covers the full import graph or only top-level modules); separate compilation scope (whether build script maintenance is in scope; whether the retainer covers updating makefiles or build tools to track .sym file timestamps rather than source file modification times only); coroutine scope (whether TRANSFER and NEWPROCESS usage is in scope; workspace sizing and overflow diagnosis); SYSTEM module scope (whether CAST, ADR, TSIZE, and MOVE usage audits are in scope; which embedded platforms are covered); toolchain scope (GNU Modula-2 / gm2 on Linux; ADW Modula-2 on Windows; ISO Modula-2 standard compliance); and hour logging format (the DEFINITION MODULE that changed, the field added or moved, the importing client modules not recompiled, the wrong field reads observed, the full recompile scope, and the wrong-value count before and after).

How should Modula-2 developer retainer hours be logged?

Log each Modula-2 retainer session with: the DEFINITION MODULE where the interface change occurred (e.g., MachineState.DEF); the field added or reordered (e.g., haltRequested BOOLEAN inserted between rpm and temperature); the importing client modules not recompiled (e.g., ControlLoop.MOD, StatusDisplay.MOD); the wrong field being read by the stale client (e.g., temperature field returning the byte value of haltRequested); the wrong value count before the full recompile (e.g., 5 wrong machine state reads); the fix applied (e.g., full recompile of all 7 modules importing MachineState.DEF); and the wrong value count after (e.g., 0). For TRANSFER coroutine bugs: the workspace size passed to NEWPROCESS, the procedure, the maximum stack depth, the overwritten memory, and the corruption observed. For SYSTEM CAST bugs: the source type, the target type, their sizes, the misaligned or out-of-bounds access, and the fix.