Blog › ICP guides

Oberon developer on retainer: procedure types, module system, type extension, open arrays, and Oberon-07 systems programming on monthly retainer

December 9, 2026 · ~15 min read

An Oberon program implementing a plugin dispatch system declared a procedure type Filter = PROCEDURE(x: INTEGER): INTEGER and a variable f: Filter to hold a dynamically selected filter function. The developer wrote code to invoke the filter: result := f x. In Oberon’s syntax, procedure variable calls require explicit parentheses even when passing a single argument: result := f(x). The syntax f x is not a procedure call — in Oberon’s grammar, a designator followed by an expression without parentheses is not valid as a call. The compiler rejected f x with a syntax error. The developer added parentheses: result := f(x). The compiler accepted it. Two compilation errors per procedure variable call site (one for the call syntax, one for the missing closing parenthesis) → 0. The second common error in the same session: the developer declared a record type Node = RECORD key: INTEGER END and an extension SortedNode = RECORD (Node) priority: INTEGER END. A procedure parameter VAR n: Node accepted a SortedNode variable passed by address with VAR — Oberon allows subtype polymorphism via type extension for VAR parameters, where a RECORD(T) extension variable is accepted where a T VAR parameter is expected. But the developer also had a RETURN n statement in a function declared with return type Node — Oberon does not allow returning a record by value from a function declared with a base type; record return types are exact, not polymorphic. The developer restructured to return a pointer POINTER TO Node instead. The Oberon developer on retainer diagnosed both: the procedure call syntax (parentheses required for all procedure calls including via procedure variables) and the record type extension polymorphism rules (VAR parameter accepts extensions; return types do not).

The work log entry read “fixed dispatch system, 7h.” It names the result and duration. It cannot explain why Oberon requires parentheses for procedure calls even when passing a single argument — Oberon’s minimalist syntax was designed to make compilation unambiguous without lookahead; the grammar rule for a call statement is designator "(" [ExprList] ")", requiring explicit parentheses even for a single argument and even for parameterless calls on procedure variables (f() to call a parameterless procedure variable f: PROCEDURE); the philosophy is that the call site should be unambiguous in the source text. It cannot explain why RETURN n fails for a type extension — Oberon’s value-return semantics for records requires that the return type of the function matches exactly the declared return type; when a function declares PROCEDURE ... : Node, the return value must be a Node record, not a SortedNode extension; this is because Oberon record return involves copying the record value, and copying a SortedNode into a Node return slot would truncate the extension fields; the fix is to return POINTER TO Node, which supports full type extension polymorphism via pointer. It cannot explain when to use POINTER TO T versus T as a parameter or return type — T (record value) copies the record at the parameter or return site, truncating any extension; VAR T passes by reference and accepts extensions at the call site; POINTER TO T passes a pointer and accepts pointers to extensions via type guard p(POINTER TO SortedNode) or type test p IS SortedNode. The 7 hours of procedure syntax diagnosis, type extension polymorphism analysis, and pointer vs value design are invisible in the diff.

Oberon type system: basic types, record type extension, pointer types, procedure types, and open arrays

Oberon’s type system is intentionally minimal: basic types (INTEGER, LONGINT, REAL, LONGREAL, BOOLEAN, CHAR, BYTE, SET), array types (ARRAY n OF T, fixed-length; ARRAY OF T, open array for formal parameters), record types (RECORD ... END with optional base type for extension), pointer types (POINTER TO T), and procedure types (PROCEDURE(params): return). The minimalism is a design choice: Wirth’s Oberon project aimed to show that a safe systems programming language could be simpler than Modula-2 and C while retaining the expressiveness needed for writing operating systems. The type system supports type-safe extensibility via record extension — RECORD(BaseType) ... END defines a type that has all the fields of BaseType plus additional fields — and via pointer polymorphism. Type extension is Oberon’s substitute for object-oriented inheritance: an extension record has all the fields of the base, and a POINTER TO Extension can be used where POINTER TO Base is expected, with type guards and tests for dynamic dispatch.

Procedure types are first-class in Oberon: Filter = PROCEDURE(x: INTEGER): INTEGER declares a procedure type, and f: Filter declares a variable that holds a procedure of that type. Assigning a compatible procedure to f requires that the procedure signature matches exactly: same parameter types in the same order, same return type. Calling a procedure variable requires parentheses: result := f(x) for a procedure with one argument, f() for a parameterless procedure variable. This is a common source of retainer work for developers coming from C (where function pointer calls do not require parentheses beyond the standard call syntax) or from languages that allow keyword-based dispatch without parentheses. Open arrays (ARRAY OF T in formal parameter position) allow procedures to accept arrays of any length: PROCEDURE Process(a: ARRAY OF INTEGER) accepts integer arrays of any declared length. Inside the procedure, LEN(a) returns the length of the actual argument. Open arrays must be formal parameters — they cannot be declared as variables (use ARRAY n OF T with a fixed size for variables). Two-dimensional open arrays require explicit index bounds: ARRAY OF ARRAY OF INTEGER or ARRAY n OF ARRAY OF INTEGER. Also see Standard ML developer retainer for a related treatment of first-class function types and type-safe polymorphism in a Wirth-school-adjacent language.

Oberon’s dynamic dispatch model uses type guards and type tests on pointers. Given p: POINTER TO Node, p IS SortedNode tests whether p actually points to a SortedNode extension (or further extension); p(SortedNode) is a type guard that returns a POINTER TO SortedNode and traps at runtime if the test fails. The pattern for dynamic dispatch in Oberon is: store procedure pointers in extension records (handler: PROCEDURE(self: POINTER TO Node)) and use type tests to select the appropriate procedure. Oberon-07 (the 2007 revision by Wirth) simplified the original Oberon language by removing features considered unnecessary: FOR loops, WITH guards, and some type coercions. Oberon-07 programs cannot use the WITH statement (extended type guard with block scope) from original Oberon; they use IF p IS SortedNode THEN p(SortedNode).priority END instead. Oberon was designed by Niklaus Wirth at ETH Zürich in 1988 as the language for the Oberon operating system (also designed by Wirth). The operating system and language were designed together, each informing the other’s constraints. Oberon’s retainer ecosystem includes embedded systems (Oberon compilers for resource-constrained targets), academic computing (ETH Zürich teaching, Oberon System 3 maintenance), and legacy industrial systems. Its closest retainer neighbors are Modula-2 (Oberon’s predecessor) and Pascal (Wirth’s earlier language), and Ada (similar focus on type-safe systems programming), but Oberon’s minimalist grammar (no FOR in Oberon-07, explicit parentheses for all calls, type extension without class hierarchies) makes the retainer work distinct in call syntax diagnosis, type guard design, and module system management.

Oberon module system: MODULE, IMPORT, DEFINITION modules, separate compilation, and initialization code

Oberon’s compilation unit is the module. A module is declared with MODULE Name; and ends with END Name.. Every module has an optional import list IMPORT A, B, C; that declares the modules it depends on. All exported identifiers in a module are marked with an asterisk: VAR x*: INTEGER exports x; PROCEDURE Process*() exports Process. Unexported identifiers are private to the module. The export marker is the only access control mechanism — there is no public/private/protected hierarchy. Clients access exported identifiers via qualified names: A.x, A.Process(). Circular imports are not allowed: if module A imports B, B cannot import A. The module system enforces a directed acyclic dependency graph at the language level, which Wirth considered essential for managing complexity in large systems. See also Standard ML developer retainer for how ML-school module systems (structures and functors) solve the same dependency management problem with a different formalism.

Oberon modules have initialization code: the BEGIN section at the end of a module (before END Name.) executes when the module is first imported. This is Oberon’s equivalent of a module-level initializer: BEGIN x := 0; OpenLog() initializes the module state when the module is loaded. The initialization order follows the import graph: if A imports B, B is initialized before A. Circular initialization is impossible because circular imports are banned. DEFINITION modules (in some Oberon dialects, including Oberon-2) separate the interface from the implementation, like a header file: the DEFINITION module declares the exported types and procedures without bodies, and the MODULE implementation provides the bodies. In Oberon-07, there are no separate DEFINITION modules — the export marker on declarations serves the same purpose, and the compiler extracts the interface automatically. Retainer work involving Oberon modules typically covers import order (initialization order when B depends on A’s initialized state), circular dependency refactoring (introducing an intermediary module to break the cycle), and the BEGIN initialization vs procedure call distinction (initialization code in BEGIN runs once at module load, not at program start — a common source of “initialized correctly first time, wrong second time” bugs in programs that reinitialize module state by hand).

How HourTab tracks Oberon developer retainer hours

Oberon retainer work carries the invisible-hours problem specific to minimalist systems languages: the difference between f x and f(x) is two characters in the source, but diagnosing the syntax error requires understanding Oberon’s grammar rule that all calls require explicit parentheses, and the fix is trivial once the rule is known but non-obvious to developers familiar with Pascal (where function calls also require parentheses) who write f(x) for functions but forget that procedure variables require the same syntax. The type extension polymorphism issue — RETURN n for a value return of a base type when the actual value is an extension — is more subtle: the return compiles correctly with the exact base type, fails only when the value at the call site is an extension, and the fix (use pointer return type) changes the calling convention. The retainer work is the syntax diagnosis (call syntax rule, why parentheses are required), the type extension polymorphism audit (which parameters and return types should be pointer types to support extensions), the module dependency analysis (what imports what, in what initialization order), and the type guard design (type tests and guards for dynamic dispatch without class hierarchies).

HourTab gives Oberon developers a public retainer-hours URL they send to clients — typically organizations running Oberon-based embedded systems that require ongoing call syntax and type extension discipline, academic computing groups maintaining Oberon System or Oberon-07 programs, and legacy industrial systems on Oberon or Modula-2 codebases. For Oberon retainers, each work log entry should name the mechanism (call syntax: f(x) parentheses required for procedure variable calls; parameterless call: f() required for PROCEDURE variables; type extension: POINTER TO T vs T for polymorphic parameters and return types; type guard: p IS Extension test and p(Extension) guard; module initialization: BEGIN section execution order; open array: LEN(a) for parameter length), the specific procedure type, call site, and before/after compilation error count, and the pointer-vs-value design rationale. Oberon retainers are often compared to Modula-2 developer retainers (Oberon’s predecessor, more verbose module system, no type extension), but Oberon’s explicit-parentheses call syntax for procedure variables, type extension without class hierarchies, and minimalist module system (export markers rather than visibility keywords) make the work distinct. HourTab’s work log makes the call syntax diagnosis, pointer vs value parameter analysis, and module dependency work visible to clients who would otherwise see only the symptom — compilation errors or runtime type guard traps — and not understand why the fix required understanding that RETURN n truncates extension fields while RETURN p (pointer return) preserves them, and why choosing the right parameter mode (VAR T, T, or POINTER TO T) at each procedure signature is the work that ensures the Oberon codebase remains extensible as record types gain new extension layers over the lifetime of the system.

Track Oberon developer retainer hours without the status emails

HourTab gives Oberon developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your call syntax diagnosis, type extension polymorphism audit, and module initialization work becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: Oberon developer retainers

What does an Oberon developer on retainer typically do?

An Oberon developer on monthly retainer covers Oberon procedure call syntax (procedure variable calls require explicit parentheses: f(x) for a single argument, f() for a parameterless procedure variable; designator without parentheses is not a call in Oberon’s grammar), Oberon type extension and record polymorphism (RECORD(BaseType) extension; VAR parameter accepts extensions; return types do not; POINTER TO T for polymorphic return and pointer parameters), Oberon module system (MODULE declaration; IMPORT list; export marker asterisk; qualified access A.Proc(); acyclic dependency requirement; BEGIN initialization code), open array parameters (ARRAY OF T in formal parameters; LEN(a) for runtime length; fixed ARRAY n OF T for variable declarations), and Oberon-07 vs original Oberon distinctions (FOR loops and WITH guards removed in Oberon-07; type guard syntax via IF p IS Extension THEN p(Extension).field END).

What Oberon work is most commonly underlogged in a retainer?

Procedure call syntax diagnosis (procedure variable f: Filter called as f x rejected; f(x) required; parameterless f() required; 2 compilation errors per call site → 0; 3–6 hrs invisible); type extension polymorphism audit (RETURN n for value return of base type with extension value fails; redesign to POINTER TO Node for polymorphic return; VAR parameter accepts extensions, value and return parameters do not; 2 compilation errors per conflated return site → 0; 4–8 hrs invisible); module initialization ordering (BEGIN section runs once at module load; initialization order follows import graph; re-initializing module state by hand causes “initialized correctly first time, wrong second time” bugs; 5–9 hrs invisible); type guard design (p IS SortedNode test; p(SortedNode) guard with runtime trap; procedure pointer in extension record for dynamic dispatch without class hierarchies; 4–7 hrs invisible).

What are typical Oberon developer retainer rates?

Entry-level Oberon developers (1–2 years, Oberon basic syntax, module system, basic types) bill at $60–$110/hr. Mid-level Oberon systems programmers (2–4 years, procedure types, type extension and pointer model, module initialization, open array parameters) bill at $100–$175/hr. Senior Oberon architects (4–8 years, large module system design, embedded Oberon targets, type extension dispatch architecture, Oberon-07 vs original Oberon distinctions) bill at $145–$255/hr. Monthly retainer ranges: $2,000–$4,500/mo advisory (15–25 hrs), $5,500–$15,000/mo for full Oberon systems engineering engagements.

What should an Oberon developer retainer agreement include?

An Oberon developer retainer agreement should specify: procedure type scope (Filter = PROCEDURE(x: INTEGER): INTEGER; procedure variable call syntax f(x) and f(); parameterless call requirement); type extension scope (RECORD(BaseType) field extension; VAR parameter polymorphism; POINTER TO T for polymorphic return and pointer parameters; type guard p(Extension) and type test p IS Extension); module system scope (MODULE/IMPORT/export asterisk; qualified access; BEGIN initialization; acyclic dependency; Oberon-07 vs original Oberon compatibility); open array scope (ARRAY OF T in formal parameters; LEN(a); two-dimensional open arrays); and hour logging format (advisory category, specific procedure type, call site, before/after compilation error count, and whether fix required call syntax correction, type redesign to pointer, module refactoring, or open array parameter change).

How should Oberon developer retainer hours be logged?

Log each Oberon retainer session with: advisory category (call syntax: f(x) parentheses required for procedure variable calls; parameterless call: f() required for PROCEDURE variable; type extension: POINTER TO T vs T for polymorphic parameters and return types; type guard: p IS Extension test and p(Extension) guard; module initialization: BEGIN section execution order; open array: LEN(a) for parameter length; Oberon-07 compatibility: FOR loop and WITH guard absence); the specific procedure type, call site, and before/after compilation error count (procedure type: Filter = PROCEDURE(x: INTEGER): INTEGER; variable: f: Filter; call: f x rejected; fix: f(x); compilation errors: 2 → 0); and the before/after metric. Include whether fix required call syntax correction, pointer return type redesign, module dependency refactoring, or Oberon-07 compatibility update.