TMod-c Language Report (Draft)
Status: Living draft — Wirth-style language report
(definition for experienced programmers).
Version alignment: Compiler 0.26.x
(see CURRENT.md for implementation status).
Last updated: 2 September 2026
Author: M. Scott Reynolds
Purpose: Define the TMod-c
programming language: informal rules and semantics, bottom-up.
Formal syntax: Complete EBNF is only
in syntax-ebnf.md.
Not this document: Build numbers, test status, session
diaries — see CURRENT.md, changelog.md,
docs/grok_report-*.md,
docs/project_bible.md.
Draft policy: Rules marked Implemented are intended to match the current compiler. Rules marked Planned are language intent; the compiler may not enforce them yet. When prose and EBNF disagree, fix both; prefer explicit update over silent drift.
Public name: TMod-c (TYPE
compiler). Mod-c / modc are legacy names
during transition. Target binary: tmodc;
system ABI module: tmodc.mc /
tmodc.h.
This report is not a programmer’s tutorial. It is a
concise reference for programmers, implementors, and manual writers —
the same role as Wirth’s Oberon report. What remains unsaid is mostly
left so intentionally. Examples show the look of the
language; they are not complete programs unless labelled as such. The
expected reader already programs in C (and may know
Pascal or Oberon). Comments marked (* C: … *) are a
lowering hint, not TMod-c syntax.
Table of contents
- Introduction and model
- Notation and vocabulary
- Compilation units and modules
- Declarations
- Types
- Expressions and designators
- Statements (§7.5 Design by contract)
- Procedures, functions, and parameters
- Storage, ownership, and memory
- Builtins and standard facilities
- C lowering and interop
- Complete syntax
- Document map
1. Introduction and model
TMod-c is a statically typed language in the Pascal / Modula-2 / Oberon family, with selected modern conveniences, that compiles to C11. It is aimed at readable, reviewable systems and compiler construction code — not at replacing C’s entire ecosystem.
A compilation unit is a finite sequence of lexical
symbols (identifiers, numbers, strings, operators, keywords,
comments). The compiler emits ordinary C; the linker sees a
.c / .h pair, never .mh.
Example (complete program):
program Hello()
import printf from "stdio.h"
begin
printf("Hello, TMod-c\n")
end
(* C: #include "Hello.h"; int main-shaped unit Hello(void) that calls printf. *)
1.1 Design principles
| Principle | Meaning |
|---|---|
| Clarity over cleverness | Assignments are statements only; control flow is explicit
(begin…end, required ELSE in
SWITCH, …). |
| Simple but not too simple | Small surface language; hard problems (ownership, modules) get short explicit rules rather than hidden runtime magic. |
| No mandatory GC | Default model is explicit allocation, defer, and arenas
— not a tracing collector. |
| No hidden allocations (policy) | Standard library and builtins that allocate must make who owns / who frees obvious. |
| Plan mutability | Mutable locals (VAR) are declared at the
beginning of a block; immutable bindings
(LET) may bind when the value is known. |
| Contracts, not comments | REQUIRE / ENSURE / loop
INVARIANT are executable Design by Contract (§7.5). They
lower to C assert. |
| Lower to ordinary C | Generated code links as normal C; no required heavy runtime for the core language. |
1.2 What TMod-c is not
- Not a beginner tutorial.
- Not classic Oberon System (no mandatory GC heap).
- Not “C with different braces” — modules,
EXPORT/.mh, and declaration style are first-class.
1.3 Source and product files
| Kind | Extension | Role |
|---|---|---|
| Author source | .mc |
Only files programmers edit for TMod-c code |
| Generated C | .c, .h |
Input to the host C compiler |
| Export table | .mh |
Machine-readable exports for modc / tmodc (not hand-authored) |
The PROGRAM or MODULE name must match the
output stem (Hello.mc → Hello.c /
Hello.h, unit Hello).
2. Notation and vocabulary
2.1 Syntax notation
Productions use Wirth EBNF as in syntax-ebnf.md:
"X"— terminalname— nonterminal[ X ]— optional{ X }— zero or moreX | Y— alternative( X )— grouping
Illustrative fragments in this report are non-normative if they disagree with the syntax document.
2.2 Keywords and identifiers
- Keywords are case-insensitive
(
IF,if,If). - Identifiers are case-sensitive
(
MyVar≠myvar). - Length Implemented (0.26.3.170) — an
identifier is at most 255 characters. Longer is a
compile-time error (lexer /
.mhreader). Mangled export / C namesOwner__namemay be 512; they are not source identifiers. - Full keyword list: EBNF production
keyword.
Examples:
x scan TModc getSymbol firstLetter
IF if If (* same keyword *)
Point point (* distinct identifiers *)
2.3 Numbers, strings, and comments
Integers are decimal digit sequences. Reals contain a decimal point.
Strings use "…". Character literals use
'x'.
Examples:
1987
12.3
4.567E8
"TMod-c"
"Don't worry"
'A'
Comments: // to end of line, or (* … *) /
(** … *). They may appear between symbols. They do not
affect meaning.
var n: integer = 1 // working set
(* block comment *)
2.4 Layout and end-of-statement Implemented (core)
- Prefer one statement per line.
;separates multiple statements on one line.- Indentation is not significant.
- Newlines end statements except for continuations (trailing
operators, open brackets, trailing commas in declaration lists,
IMPORT … FROM, explicit\, etc.).
Examples:
x := 1
y := 2; z := 3
s := a +
b (* continuation after trailing operator *)
3. Compilation units and modules
3.1 Programs and modules
A compilation unit is a PROGRAM or a
MODULE. Both have a declaration sequence
and a body. Exact productions: program-unit,
module-unit in the syntax document.
A program is the application entry. A module is a compilable collection of declarations (and an optional body) meant to be imported.
Examples:
program Demo(argc: integer, argv: ^char[])
begin
(* … *)
end Demo
module Counters()
export type TCounter = struct
n: integer
end
export procedure bump(ref c: TCounter)
begin
c.n := c.n + 1
end bump
begin
end Counters
(* C: void Demo(int argc, char **argv); void Counters(void); *)
Optional end tag may repeat the unit or procedure name
(end Demo, end Type::name).
3.2 Import and export Partly implemented
.mh/.hsplit:.mhcarries TMod-c export semantics;.his the C ABI. Generated C#includes.honly, never.mh.IMPORT … FROM:- Quoted string — foreign/C path or explicit path
(
"stdio.h","path.mh"). - Qualident — TMod-c module
(
fixtures.minimod→.mh+ paired.h). Implemented (0.24.5). - Import items: plain names, or
Type::namefor type-bound methods (0.25.2); lookup uses C manglingType__name.
- Quoted string — foreign/C path or explicit path
(
- Include-only:
IMPORT FROM sourcewith no item list —#includeonly, no TMod-c names. - Module entry (7a, 0.25.3.150): a named import from
an
.mhalso binds the@moduleunit-entry export. Include-only does not.PROGRAMnames are not bound. EXPORT— marks declarations written into the unit’s.mhtable.EXTERN— symbol implemented elsewhere (typically C); no body in this unit.- Calls (0.26.0): unresolved free
name(...)/Type::name(...)is an error. - Types (0.26.1): unknown
single-word type is an error. Multi-word C spellings
(
long long,unsigned int) remain legal without import. Bareunsignedis not a type. - C preprocessor:
#define/#iflines are copied into generated C; they do not bind TMod-c names.
Examples:
import printf, FILE from "stdio.h"
import size_t from "stddef.h"
import TPoint from "fixtures/tpoint.mh"
import mini_answer from fixtures.minimod
import Point::print from geometry
import from "extra.h" (* include-only: no names bound *)
export type TPoint = struct
x: integer
y: integer
end
export function mini_answer(): integer
begin
return 42
end
Quoted FROM "stdio.h" does not parse
the C header. The item list is the programmer’s claim that C will
provide those names after the include.
.mh export lines
(modc-mh/1, @format 1): one physical line per
export. Signatures, field lists, method-type RHS, and width tails
(integer 32) are described in §3.2 of earlier revisions and
in CURRENT.md. Parent fields on EXTENDS
require the parent type imported. Field call-through requires importing
the method type as well as the struct.
Example (generated .mh line, not
source):
export type TPoint complete struct (x: integer, y: integer)
export function mini_answer complete () : integer
export procedure Point__print complete instance Point (Point)
Deferred: project-root module search paths,
IMPORT *. Mutual TMod-c
IMPORT: later, after Makefile subdirectory
compilation (item 15). Today cycles can compile because C headers
include each other and .mh comes from a previous emit.
3.3 .mh
completeness vs source OPAQUE
In export tables,
export type Name opaque means “name only, no layout.”
In source, type Name = opaque defines an
opaque type (§5.2). Same English word; different
languages.
4. Declarations
Every identifier that is not a prelude builtin must be introduced by a declaration. A declaration also states whether the name is a constant, a type, a variable, or a procedure. The name may be used from the point of declaration to the end of the enclosing block. No identifier may denote more than one object in the same scope.
4.1 Declaration classes
Typical forms: TYPE, CONST,
LET, VAR, procedures/functions, methods
(Type::name), EXTERN variants.
Examples:
type Point = struct
x, y: integer
end
const N = 100
let title: string = "demo"
var i: integer
var p: Point
4.2
CONST, LET, and VAR Design
locked; codegen partial
| Form | Role | Binding |
|---|---|---|
CONST |
Compile-time named constant (Pascal/Oberon). May fold in array
bounds, case labels. Not a C const object. |
= |
LET |
Runtime immutable binding after initialization. | = |
VAR |
Mutable binding. | := for later assignment; optional = at
init |
Block placement (intent): VAR (and
typically CONST that define the working set) at the
beginning of the block. LET may appear
when the value becomes known. One binding; LET does not
rebind.
Examples:
const Limit = 256
const Mask = 2 * Limit - 1
const Name = "Oberon"
var i, j, k: integer
var x, y: real
var s: string = "ok"
let n = f() (* type inferred from initializer *)
let msg: string = "done"
(* n := 3 — illegal: LET is immutable *)
i := 0 (* VAR *)
(* C: CONST currently lowers to static const objects; intent is #define / enum / fold (item 19). LET / VAR are ordinary locals. *)
4.3 Grouped trailing types
A trailing type on a comma list applies to each name (Oberon / C grouping).
Examples:
var x, y, z: integer
var p, q: ^Point
4.4 FORWARD and
incomplete types
type T FORWARD— incomplete; completed later in this unit. Implemented (0.24.17.135).- Postfix
FORWARDon procedures/functions — mutual recursion. Implemented. - Declaration blocks
forward … end/extern … end— deferred.
Examples:
type Node forward
type Node = struct
key: integer
next: ^Node
end
procedure A(n: integer) forward
procedure B(n: integer)
begin
if n > 0 then A(n - 1) end
end
procedure A(n: integer)
begin
if n > 0 then B(n - 1) end
end
5. Types
A type determines the set of values a variable of that type may assume, and the operators that apply.
5.1 Type expressions vs type specifiers
- Full
type-expression: RHS ofTYPE(and optionalEXTERN TYPE … = …) — structs, enums, method types, opaque, arrays, … type-specifier: formals, fields,VAR/LETannotations — no bareOPAQUE.
Examples:
type Handle = opaque (* type-expression *)
var h: Handle (* type-specifier: named *)
(* var x: opaque — illegal *)
5.2 Opaque types Implemented (0.24.11)
type Handle = opaque
type address = ^opaque (* or POINTER TO opaque *)
OPAQUEonly on TYPE RHS.- Illegal:
var x: opaque,procedure f(p: ^opaque)— name the type first. - Distinct named opaques are not freely
interchangeable (
Handle≠File). - No user-facing type named
void. Prefer procedures with no result, and^opaque/ user aliases for Cvoid *. - C lowering (v1): named
opaqueand^opaquealiases both emit astypedef void *Name;.
5.3
Pointers and auto-dereference Types implemented (one
^); field / p[i] / instance on ^T
Implemented (0.26.5.176–178)
A type-specifier may include at most one pointer
constructor: ^T or POINTER TO T. That matches
the current parser. It is also the JPL Power of Ten rule
9 stance on declarations: do not make char ** /
T *** the everyday type language.
Need another level? Name the inner type, or use an
array of pointers (^char[]). Do not write
^^char.
| Form | Meaning |
|---|---|
p := q |
Always rebinds the pointer variable |
p.field |
Auto-deref if p is a pointer (same as
p^.field) — Done (0.26.5.176), Oberon-07
§8.1 |
p[i] |
Auto-deref if p is a pointer-to-array
(same as p^[i]) — Done (0.26.5.178). Do
not peel ^char[] argv[i] |
p^ |
Explicit pointee (one peel) |
@e |
Address-of (expression context only) |
Not used: @p := … as a special rebind
form. Rebind is bare p := …. Existing p^.field
remains valid (explicit peel; AST is postfix ^ then
.). Auto-deref does not insert a caret
node.
C char **argv:
argv: ^char[] (in-tree: modc,
ComputePi) or type pchar = ^char then
argv: ^pchar / array of pchar.
APIs that would be T ** in C: prefer
VAR / REF on T
(§8.2). The extra star is ABI, not a second pointer type in the source.
That is the intended way to avoid stacked ^.
Explicit deref in expressions: postfix
^ may be repeated (p^^) because a value of
type ^pchar is still a pointer, and C interop sometimes
peels twice. That is legal, not the preferred style.
Power of Ten rule 9 wants at most one dereference per
expression (Holzmann allows N=2 only with justification).
Prefer:
let q = p^
x := q^
over x := p^^. v1 does not make
p^^ a parse error. A later lint may warn. Field auto-deref
is one implied peel per . selector, not
sugar for ^^. Linked-list p.next.next is
multiple peels via fields (Oberon allows it; rule 9 is stricter on that
shape — review, not a grammar ban). Index auto-deref is the same rule
for [] when p is a pointer-to-array.
argv: ^char[] stays one index (s := argv[i] is
^char, not a peel of a pointer-to-array-of-char). Explicit
p^[i] emits parenthesized C (*(p))[i] so
[] does not bind tighter than unary *.
Rule 9 also objects to hiding a dereference in a
typedef. Named type pchar = ^char is still one visible
^ on that line; ^pchar is a second pointer
type with a name, not a silent **. Do not use typedefs to
pretend p^^ is a single star.
Examples:
type pNode = ^Node
var p, q: pNode
p := q (* rebind *)
p := nil
n := p^ (* pointee — one peel *)
k := p^.key (* explicit peel *)
k := p.key (* auto-deref; still one peel; C: (*(p)).key *)
q := @node (* address-of *)
type pchar = ^char
var argv: ^char[] (* C char ** — not ^^char *)
s := argv[i] (* one index; result is ^char — not peeled *)
var pv: ^Vector3 = @v (* Vector3 = array[3] of int *)
x := pv[i] (* auto-deref; C: (*(pv))[i] *)
x := pv^[i] (* explicit; same C *)
(* var bad: ^^char — not a type-specifier *)
(* C: Node *p; p = q; n = *p; k = (*p).key; q = &node; char **argv as ^char[] *)
5.4 Structured types
Arrays
An array has a fixed number of elements of one type. Indices are
signed (integer), from 0. The bound may be
a named CONST or integer const-expression
(0.24.8).
Examples:
const N = 32
type Vector = array[N] of real
type Matrix = array[10] of array[20] of real
var a: array[100] of integer
var buf: array[N] of char
Open / suffix forms (T[], integer[8])
follow the syntax document.
Enumerations Implemented (0.24.6–0.24.7)
type Color = enum
Red,
Green,
Blue
end
var c: Color = Green
(* C: typedef enum { Red, Green, Blue } Color; *)
Members are integer constants. Duplicate members are an error.
Cross-module import uses export enum in
.mh.
Sets of enumerations Planned (not 0.26.x)
A set is a bit vector over a small enum, not a hash
table. SET OF E is legal only when E is an
enumeration whose members are distinct constants in 0 ..
63. The value is one machine word (cardinal 64).
Julia/Python-style Set of arbitrary values is not this
type.
type NodeKindSet = set of NodeKind
const LOOP_KINDS: NodeKindSet =
{ NODE_FOR, NODE_WHILE, NODE_REPEAT_UNTIL, NODE_LOOP }
if new_parent^.kind in LOOP_KINDS then
child.enclosing_loop := new_parent
end
s := s + { NODE_FOR } (* include *)
s := s - { NODE_FOR } (* exclude *)
(* C: typedef uint64_t NodeKindSet; then
(s & (1ULL << x)) != 0, union |,
intersection &, difference & ~.
*)
- Constructor
{ a, b, … }is the same braces as an array literal; the type of the context (or of the elements) decides set vs array. Empty{}requires a type ascription. x IN s— membership;xhas typeE,shas typeSET OF E.s + tunion,s * tintersection,s - tdifference;==/!=. Do not overload|/&on sets (those remain integer operations).SET OF EandSET OF Fare distinct types. No implicit conversion tocardinal 64.- Not v1:
SET OF integer/char/ structs; more than 64 members;FOR x IN s; heap sets.
See CURRENT.md (Future plan: SET OF enum) for
implementation order.
Records (STRUCT)
A struct is a fixed number of named fields, possibly of different types. Nested aggregates are named types first, then used as a field type.
Examples:
type Date = struct
day, month, year: integer
end
type Person = struct
name: array[32] of char
age: integer
born: Date
end
var d: Date
d.day := 28
d.month := 8
d.year := 2026
PACKED is available on structs (see syntax). Field lists
may group trailing types (x, y: integer).
Method types
A TYPE whose RHS is PROCEDURE /
FUNCTION is a first-class procedure type (C function
pointer). See §8.3.
type TFn = function (n: integer): integer
var fp: TFn
5.4.0
Record extension (STRUCT … EXTENDS) — Oberon
model Implemented (0.24.13)
TMod-c treats EXTENDS as record / struct
extension, not as a class system. Single inheritance; base as
prefix. A future CLASS is out of
scope.
Intent (locked)
| Rule | Meaning |
|---|---|
| Single inheritance only | At most one EXTENDS base type |
| Base at offset 0 | Child layout begins with a full parent value |
| Flat field namespace | Parent and child fields form one set of names |
| No name clashes | A field name must be unique along the entire chain |
| Access | c.x whether x was declared on the child or
on a base |
Synthetic base |
On EXTENDS types only: reserved name for the
embedded parent (c.base). Plain structs may use a field
named base |
| Upcast (by value) | Child where parent is required — Done (0.24.16); C
.base × N |
Upcast (pointer / REF) |
^Child where ^Parent is required;
REF Parent + Child actual — Done
(0.26.5.175) |
AS ancestor |
g as Parent → .base × N, not
(Parent)g — Done (0.26.5.175) |
p^.field |
Parent fields through postfix ^ — Done
(0.26.5.175) |
p.field auto-deref |
Same as p^.field when p is ^T
— Done (0.26.5.176). C (*(p)).field then
.base × N |
| No vtable / no hidden RTTI | Compile-time layout only. No IS / type guards on
structs |
C lowering — option A (chosen)
Named embedded base member (portable C11). Not GNU anonymous structs; not flattened parent fields (option B).
typedef struct Parent { /* … */ } Parent;
typedef struct Child {
Parent base; /* always first — offset 0 */
/* child-only fields */
} Child;| Layer | Behavior |
|---|---|
| TMod-c source | c.x for any unique field on the chain |
| Generated C | Parent fields via .base (c.base.x);
multi-level g.x → g.base.base.x |
| Upcast codegen | Insert .base × N (not a blind struct cast) |
Example
type Parent = struct
name: array[20] of char
end
type Child = struct extends Parent
age: integer
end
var c: Child
c.name := … (* parent field; C: c.base.name *)
c.age := … (* child field; C: c.age *)
use_parent(c) (* auto-upcast by value; C: use_parent(((c).base)) *)
use_parent(c.base) (* equivalent explicit parent value *)
p := @c (* ^Child *)
use_parent_ptr(p) (* ^Child where ^Parent; C: &((*(p)).base) *)
q := c as Parent (* C: ((c).base) — not (Parent)c *)
k := p^.name (* parent field; C: (*(p)).base.name *)
k := p.name (* auto-deref; same peel; C: (*(p)).base.name *)
(* type Bad = struct extends Parent; name: integer end — clash *)
(* type Bad2 = struct extends Parent; base: integer end — 'base' reserved *)
p[i] auto-deref for pointer-to-array and instance method
on ^T — Done (0.26.5.178) (§5.3 /
§8.3).
5.4.1 Union types Implemented (0.24.12)
C-compatible overlapping members — sibling of
STRUCT, not EXTENDS.
Not language-level tagged unions.
type Value = union
i: integer
f: real
p: ^char
end
var u: Value
u.i := 3
u.f := 1.5 (* overlays u.i — C-like; no active-member check in v1 *)
No EXTENDS on unions. Nested anonymous unions deferred;
name an inner type first.
5.5 Built-in type names
Reserved lowercase prelude names: bool,
byte, char, integer,
cardinal, real, string (see
§10).
Of these, only integer,
cardinal,
real, and
string may be bound once per
compilation unit:
- by a top-level
TYPEalias, or - by importing that type name from another module (e.g. a pack such as
tmodc.lp64).
A second binding of the same name in the same unit is an error. Binding affects only that compilation unit. If a name is never bound, the compiler default applies (§10.1).
bool, byte, and char are
not rebindable. Width forms (integer N, …)
are not rebindable names; they are type constructors on
the RHS / in type positions. Closed N: 8/16/32/64 for integers;
32/64 for real. integer 128 is illegal until
the closed set is extended.
Status: Phase A (0.24.18); widths (0.26.5); import-as-bind and
.mh width tails (0.26.5.174). Arch packs and build-time
defaults TBD.
Examples:
type integer = integer 64 (* this unit: integer is int64_t *)
type i32 = integer 32 (* alias; width form is not a bindable name *)
var x: integer
var w: integer 32
(* type integer = long
type integer = int — second bind: error *)
import integer from "fixtures/lp64.mh" (* import is the one bind *)
6. Expressions and designators
Expressions denote rules of computation. Parentheses group. Assignments are not expressions.
6.1 Assignments are not expressions Implemented
:= and compound assignments are statements
only. This avoids C-style “assignment in condition.”
Examples:
i := 0
(* if (i := f()) then … — not TMod-c *)
if i == 0 then … end
6.2 Designators
designator = qualident { selector } with selectors
., [], ^.
Address-of is a prefix expression (@), not
part of a designator — so @n := … is not an
assignment target.
If A is an array, A[E] is the element at
index E (E is integer). If
r is a struct, r.f is field f. If
p is a pointer, p^ is the pointee;
p^.f and auto-deref p.f are a field of the
pointee, including EXTENDS parent fields
(p^.f Done 0.26.5.175; p.f Done
0.26.5.176). Auto-deref p[i] when p
is a pointer-to-array — Done (0.26.5.178) (§5.3); not
^char[] argv[i]. Instance
p.method when p is ^T —
Done (0.26.5.178) (§8.3). Postfix ^ may
chain (p^^) — legal, not preferred; see §5.3 / Power of Ten
rule 9.
Examples (see types in §5):
i (* integer *)
a[i] (* element *)
d.day (* field *)
p^.key (* pointer then field — one peel *)
p.key (* auto-deref; same as p^.key *)
pv[i] (* auto-deref pointer-to-array; same as pv^[i] *)
pp.getX() (* instance on ^Point; same peel as pp^.getX() *)
t.left^.key
t.left.key (* auto-deref through a pointer field *)
argv[i] (* ^char[] — one index; not a pointer-to-array peel *)
@i (* address-of; expression, not a designator target *)
6.3 Operators
Precedence is C-like: conditional ? :, then
or, and, equality (==,
!= / <>), relational, shifts, additive,
multiplicative (*, /, DIV,
MOD, %), power **
(right-associative), unary (not, ~,
+, -, ^, @).
Logical and / or are sequential
(short-circuit), like C && / ||.
Examples:
1987 (* integer *)
i DIV 3
not p or q
(i + j) * (i - j)
(0 <= i) and (i < 100)
t.key == 0
x < 0 ? -x : x
n as integer 64
CAST(integer, c)
IN is Planned as set membership
(x IN s for SET OF enum; §5). It is not a type
guard. IS is dropped (no RTTI / no
IS on structs).
6.4 Casts
Prefer postfix AS.
CAST(type, expr) is also accepted.
r := i as real
w := n as integer 32
7. Statements
Statements denote actions. A statement may be empty. Structured statements contain statement sequences.
7.1 Control structures
IF, SWITCH (no fallthrough;
required ELSE), WHILE,
REPEAT, FOR, LOOP,
BREAK, CONTINUE, RETURN,
DEFER, ASSERT.
WHILE … BY: not planned; use
defer at loop head for an end-of-iteration step.
If
if ch >= "A" and ch <= "Z" then
ReadIdentifier
elsif ch >= "0" and ch <= "9" then
ReadNumber
else
ReadOther
end
Switch no fallthrough
switch c of
case Red: name := "red"
case Green: name := "green"
case Blue: name := "blue"
else: name := "?"
end
(* C: switch without fallthrough; ELSE is mandatory in the grammar. *)
While / repeat / for / loop
while j > 0 do
j := j DIV 2
inc(i)
end
repeat
k := k + 1
until k >= n
for i := 1 to n do
sum := sum + a[i]
end
for i := n downto 1 by 1 do
(* … *)
end
loop
if done then break end
end
Today FOR assigns a predeclared var i
(for i := …). Loop-local FOR
(for i = …) is a later breaking change
(JPL rule 6); see CURRENT.md. Bounds and BY
are evaluated once at entry. The control variable must not be assigned
in the body.
7.2 Calls and discarded results Implemented (Policy A — 0.24.15)
| Form | Intent |
|---|---|
proc() |
Call with no useful result (procedure, or untyped foreign import) |
x := f() |
Use function result |
(f()) |
Call and intentionally discard result |
A bare call as a statement is an error when the result type is
known. Unknown result (procedure, untyped
printf) may be called bare. C may lower discards as
(void) f(...).
Examples:
printf("n=%d\n", n) (* unknown C result: bare OK *)
x := f()
(f()) (* discard known result *)
(* f() — error if f is a typed function *)
7.3 DEFER
Implemented (core)
DEFER statement schedules work on scope
exit, LIFO (last deferred runs first). Primary tool for pairing
acquire with release.
Runs on every language-level exit of that scope:
falling off the end, RETURN,
BREAK, and
CONTINUE (including early
RETURN from a function). Nested scopes run their own defers
when they exit.
Does not run if the process aborts
or calls C exit (or _Exit,
abort, a failing assert / REQUIRE
/ ENSURE that aborts). Like C, TMod-c has no
exception processing — there is no
try/finally, no unwind past exit.
Resource cleanup across a hard abort is the operating system’s job.
Examples:
procedure load(path: string)
begin
var f: ^FILE = fopen(path, "r")
if f == nil then return end
defer fclose(f)
(* … use f; RETURN still closes *)
end
for i := 1 to n do
defer printf("iter %d done\n", i)
if i == 3 then break end (* defer for this iteration still runs *)
if i == 1 then continue end
end
(* C: not a destructor; compiler inserts the deferred statement before each in-scope RETURN/BREAK/CONTINUE and at the closing brace. exit(1) skips it. *)
7.4 INC / DEC /
ASSERT
inc(i)
inc(i, 2)
dec(n)
assert i >= 0
ASSERT lowers to C assert.
INC/DEC on a formal require
VAR (bare name), like :=.
ASSERT is a statement (anywhere a
statement is allowed). REQUIRE / ENSURE /
INVARIANT are contract clauses with fixed
positions (§7.5), not general statements.
7.5 Design by contract (DbC) Implemented (core)
TMod-c supports a small Eiffel-style contract:
executable boolean expressions that document and check what a block or
loop owes its caller and itself. They are not comments. A false contract
is a failed C assert (abort in a typical host build).
| Clause | Role | Where it may appear |
|---|---|---|
REQUIRE expression |
Precondition — must hold on entry | Immediately after BEGIN, before the statement
sequence |
ENSURE expression |
Postcondition — must hold on exit
(including RETURN) |
After the statement sequence and after the block’s
RETURN (if any); immediately before END.
Not before RETURN. |
INVARIANT expression |
Loop invariant — must hold at the invariant point of each iteration | Immediately inside WHILE / FOR /
REPEAT / LOOP, before the body statements |
Keywords are case-insensitive (require,
REQUIRE). Parentheses around the expression are optional
(require p <> nil or
require (p <> nil)).
Not class/object invariants on STRUCT.
Not a separate proof language. Side effects in a
contract expression are allowed by the grammar and
discouraged (same as Holzmann: assertions should be
observational).
ASSERT vs DbC: ASSERT is
an ad-hoc check in the middle of a body (Power of Ten rule 5: many
assertions).
REQUIRE/ENSURE/INVARIANT name
entry, exit, and loop
obligations so a reviewer can see the contract without reading the whole
body.
C lowering: each clause becomes
assert(expr); /* precondition */ (or
postcondition / invariant). A compiler flag
(dbc_off) omits them. <assert.h> is in
the generated prelude.
Nil: nil belongs to pointer
types. require p <> nil is for
p: ^T. A REF n: integer is the object, not a
nullable pointer — do not write require n <> nil to
mean “the caller passed a real variable” (see §8.2 and
parameter-passing.md).
Examples:
procedure bump(ref c: TCounter, n: integer)
begin
require n >= 0
c.n := c.n + n
ensure c.n >= n
end bump
recursive function Fact(n: integer): integer
begin
require n >= 0
if n <= 1 then
return 1
end
return n * Fact(n - 1)
ensure n >= 0
end Fact
procedure load(path: string)
begin
require path <> nil
var f: ^FILE = fopen(path, "r")
if f == nil then return end
defer fclose(f)
(* … *)
ensure true (* or omit ENSURE *)
end
ENSURE is evaluated on the way out of the
block (codegen re-emits postconditions at return
sites). In source it follows RETURN:
locked when 0.25 closed / 0.26 opened — there is no ENSURE
then RETURN. There is no special
Result / old syntax in v1: a function
postcondition can mention formals and (if still in scope) locals, not a
built-in result name. Assign to a local, RETURN that local,
then ENSURE it.
function clamp(n, lo, hi: integer): integer
begin
require lo <= hi
var r: integer = n
if r < lo then r := lo end
if r > hi then r := hi end
return r
ensure (r >= lo) and (r <= hi)
end clamp
Loop invariant — checked at the start of each iteration (and thus
before the body for WHILE / FOR /
LOOP; at the corresponding point for
REPEAT):
i := 0
while i < n do
invariant i >= 0
invariant i <= n
inc(i)
end
Several REQUIRE / ENSURE /
INVARIANT lines may appear; each is a separate
assert. Nested BEGIN…END blocks
may have their own contracts.
begin
require p <> nil
require n > 0
(* statements *)
ensure p^.n >= 0
end
(* C: assert(p != nil); /* precondition */ … assert(p->n >= 0); /* postcondition */ *)
8. Procedures, functions, and parameters
8.1 Procedure vs function
PROCEDURE— no result type.FUNCTION— has a result type; the body mustRETURNa value.
Avoid “returns void” as a type.
RECURSIVE (enforced 0.25.4): required
on a procedure/function that directly calls itself. The
completing declaration (the one with the body) must carry the tag.
Mutual recursion is not checked.
Examples:
procedure Swap(var a: integer, var b: integer)
begin
var temp: integer = a
a := b
b := temp
end Swap
recursive function Fact(n: integer): integer
begin
if n <= 1 then
return 1
end
return n * Fact(n - 1)
end Fact
(* C: void Swap(integer *a, integer *b); integer Fact(integer n); *)
8.2 Parameter modes Implemented (0.25.0 / item 20)
Formals may be prefixed with VAR,
REF, or
CONST. Parameter
CONST is a passing-mode / deep
read-only qualifier — not a compile-time CONST
declaration.
| Mode | Bare name as LHS of := / INC /
DEC |
Through designator (p.x, a[i],
p^) |
|---|---|---|
VAR p: T |
Allowed | Allowed |
REF p: T |
Forbidden | Allowed |
Bare p: T |
Forbidden | Selectors if used; whole-name assign illegal |
CONST p: T |
Forbidden | Forbidden (deep freeze) |
Scalar REF n: integer: no fields →
effectively read-only for updates. Use VAR
to change the caller’s integer.
VAR / REF vs stacked
pointers: C out-parameters and “pointer to the caller’s object”
are VAR p: T / REF p: T, not
^^T. That is how TMod-c keeps APIs to one ^ in
source (Power of Ten rule 9 on types; §5.3). Field auto-deref on
^T (p.x) is Done (0.26.5.176)
and does not replace VAR/REF.
Instance method on a pointer receiver is still Planned.
Bare formals are by-value in C (expression actuals
allowed). They share non-VAR body
discipline with REF but are not
full REF at the ABI. See parameter-passing.md.
Examples:
procedure Swap(var a: integer, var b: integer)
(* … *)
procedure Scale(ref p: Point, n: integer) (* mutate p.x; cannot p := … *)
procedure Show(const p: Point) (* no writes through p *)
procedure Id(n: integer): integer (* bare: by-value; n := illegal *)
Swap(x, y) (* auto-& → Swap(&x, &y) *)
Scale(pt, 2)
Show(pt)
k := Id(a + b) (* expression actual: needs bare, not VAR/REF on T *)
| Formal | Typical C |
|---|---|
VAR / REF on non-pointer
T |
T *p |
REF p: ^T |
T *p |
VAR p: ^T |
T **p |
Bare p: T |
T p |
Expression actuals (a + b, g()) are
illegal for VAR/REF on
non-pointer T (no address). REF p: ^T does not
auto-&.
8.3
Type-qualified methods Implemented (0.23.9–0.25.3; 0.26.4;
^T instance 0.26.5.178; Type::m(p)
0.26.5.179)
Methods are ordinary procedure / function
declarations whose name is
Type::identifier. They are
not declared inside STRUCT bodies. C
mangling: Type::name →
Type__name.
Classification uses the type of the first formal
only. The formal’s name is free (need not be self).
| Kind | First formal | Call |
|---|---|---|
| Instance | Type is the owner Type (not ^Type) |
Type::m(obj, …) and obj.m(…);
p.m(…) when p is ^Type
(0.26.5.178); Type::m(p) when
p is ^Type (0.26.5.179) |
| Static / factory | No formals, or first type ≠ Type |
Type::m(…) only |
Method-typed fields are Oberon procedure variables:
obj.field(args) → (obj.field)(args) —
no auto-receiver. If the same identifier is both an
instance method and a method-typed field, obj.name(...) is
a compile-time error; use
Type::name(obj, …).
Examples:
function Point::getX(self: Point): integer
begin
return self.x
end Point::getX
function Point::new(x: integer, y: integer): Point
begin
var p: Point
p.x := x
p.y := y
return p
end Point::new
procedure Point::print(self: Point)
begin
printf("(%d, %d)\n", self.getX(), self.y)
end Point::print
type THandler = procedure (n: integer)
type Box = struct
h: THandler
n: integer
end
var p: Point = Point::new(5, 6)
Point::print(p)
p.print()
p.getX()
(* p.new(1, 2) — illegal: new is static *)
var pp: ^Point = @p
n := pp.getX() (* C: Point__getX((*pp)) *)
pp.print() (* C: Point__print((*pp)) *)
Point::print(pp) (* same peel; C: Point__print((*pp)) *)
Point::print(pp^) (* explicit caret; still one peel *)
(* TLexer::next(@p.lexer) — @ already an address; do not wrap (*&) *)
var box: Box
box.h := say
box.h(42) (* (box.h)(42) — no auto-self *)
No vtables. Cross-module: import Type::name;
.mh carries instance/static and
formals; VAR/REF auto-&
applies. REF/VAR first formal on a
^T receiver: auto-& outside the peel
(Type__scale(&(*pp), n)). Type::m(@x) is
not peeled (@ is already the address). Deferred:
parent-chain method lookup.
9. Storage, ownership, and memory
9.1 No mandatory garbage collector
Safety comes from types, explicit pointers, structured
defer, allocator discipline, and (planned)
bounds/LEN — not a tracing collector.
9.2 Ownership modes (API contracts)
| Mode | Who allocates | Who frees |
|---|---|---|
| Borrow | Caller (or static) | Caller |
| Transfer | Callee | Caller (create / new / copy +
defer) |
| Arena / region | From arena a |
Arena end / reset |
| Optional RC | Shared heap | Last release — not the default |
9.3 defer and leaks
On successful acquisition of an owned resource, schedule release with
defer on the next lines. One owner, one free path on
success. RETURN / BREAK /
CONTINUE still run it; exit / abort do not
(§7.3).
9.4 Arenas
Phased work: one compilation unit, one request, one parse. Initialize / reset at phase start; bulk free at phase end; copy out only what must outlive the arena.
9.5 Block VAR vs heap
- Block
VAR: mutable locals (frame). - Arena / heap: dynamic structures; ownership per
§9.2.
DeclaringVARup front does not replace explicit heap policy.
10. Builtins and standard facilities
10.1 Builtin types Partially implemented
Everyday portable names answer “what exactly is this?” — not C’s rank ladder.
| Name | Role | Default lowering (unbound unit) |
|---|---|---|
integer |
Everyday signed | int |
cardinal |
Everyday unsigned | unsigned int |
real |
Everyday floating | float |
string |
Immutable text | const char * |
bool, byte, char |
Fixed small types | bool; byte is uint8_t;
char is host char. Not rebindable |
Unit binding: at most one bind per unit via
TYPE or named .mh import. Unbound defaults as
above. import integer as i64 does not bind
integer. Include-only and foreign .h imports
do not count.
Width forms: exact C11 intN_t /
uintN_t / float / double.
.mh type-spec may write integer 32. Alias RHS
in the export table is TBD. Arch packs TBD.
Not C rank: bare integer always has a
known unit binding or a documented default.
Examples:
var i: integer = 1 (* typedef int integer; unless rebound *)
var u: cardinal
var r: real
var s: string = "ok"
var w: integer 32 (* int32_t *)
assert sizeof(w) == 4
10.2 LEN and
SIZEOF
LEN(designator) — element or string byte length
(codegen TBD).
SIZEOF(...) — size in bytes; type
integer; C
((integer)sizeof(...)) — not size_t
(0.25.4).
Lengths and indexes are signed.
cardinal is for bits / wrap / C unsigned ABI.
size_t is an imported C name.
Examples:
n := sizeof(integer)
n := sizeof(p)
(* n := len(buf) — when emitted, same signed integer type *)
10.3 Standard library
Richer facilities (StringBuffer, containers, …) are
stdlib, not core syntax.
11. C lowering and interop
Generated C is C11, intended for pedantic/warning-heavy flags.
- Automatic includes (0.26.1):
<stdint.h>,<stdbool.h>,<assert.h>only. Prelude:typedef uint8_t byte;,#define nil ((void *)0),#define NIL nil, plus the portable four typedefs unless rebound. - Prelude location (0.26.5): prelude and unit
prototype live in the generated
.h. Generated.cbegins with#include "UnitName.h".-Cwrites a companion.h. - Multi-TU hand-written C:
#include "tmodc.h"(plannedtmodc.mc→tmodc.h). Per-unit binds stay on that unit’s.h. - Foreign functions:
IMPORT/EXTERN. Unresolved free calls and unknown types are errors. - Preprocessor lines in
.mcare emitted as-is; they do not create TMod-c symbols.
Example — source:
program Sum()
type integer = integer 32
var a: integer = 1
begin
a := a + 1
end
Sketch of generated header (shape, not a golden file):
#ifndef MODC_PROGRAM_Sum_H
#define MODC_PROGRAM_Sum_H
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
#define nil ((void *)0)
typedef uint8_t byte;
typedef const char* string;
typedef uint32_t cardinal;
typedef float real;
typedef int32_t integer; /* unit bind; default typedef skipped */
void Sum(void);
#endif12. Complete syntax
Normative formal grammar:
→ syntax-ebnf.md
Do not maintain a second full EBNF elsewhere.
13. Document map
| Document | Role |
|---|---|
language-report.md
(this file) |
What the language is — prose, rules, intent, examples |
syntax-ebnf.md |
Complete formal EBNF only |
parameter-passing.md |
Parameter Passing Rules |
../CURRENT.md |
What the compiler is doing this week; TODOs; version |
../changelog.md |
Version-oriented change list |
grok_report-*.md,
project_bible.md |
Session history — not daily language reference |
grammar.md is a stub pointing here. Filenames use
hyphens (language-report.md,
syntax-ebnf.md).
Revision notes (report only)
| Date | Change |
|---|---|
| 18 July 2026 | Initial draft report; SSOT split from monolithic
grammar.md. Incorporates OPAQUE, discard,
Oberon auto-deref intent, ownership modes, no mandatory GC,
VAR/LET placement. |
| 21 July 2026 | union-type EBNF drafted (sibling of
struct-type; no EXTENDS). Implementation
TBD. |
| 21 July 2026 | STRUCT EXTENDS: Oberon record model +
C lowering option A. |
| 21 July 2026 | OPAQUE types implemented (item
13a). |
| 23 July 2026 | Bare UNION implemented (item
13c). |
| 29 July 2026 | Discard enforcement (Policy A / item 13b). |
| 1 August 2026 | EXTENDS by-value auto-upcast (0.24.16). |
| 3 August 2026 | Type-qualified methods §8.3 (0.24.17 / item 21). |
| 6 August 2026 | §5.5 / §10.1 portable rebind Phase A (0.24.18). |
| 6 August 2026 | §8.2 parameter modes implemented (0.25.0 / item 20). |
| 7 August 2026 | §8.2 actuals: bare stays by-value. §8.3 callable fields (0.25.1). |
| 11–12 August 2026 | Cross-module methods, formal signatures, 7a module-entry bind. |
| 14 August 2026 | RECURSIVE; SIZEOF as
integer; lean C prelude. |
| 18 August 2026 | Free-call and unknown-type enforcement (0.26.0 / 0.26.1). |
| 24–26 August 2026 | Parser split note; ident ≤ 255; .mh field lists and
method-type RHS. |
| 27 August 2026 | Width forms; C prelude in generated .h. |
| 28 August 2026 | Import-as-bind; .mh width tails. Examples
throughout (Oberon-report pattern; C-facing gloss). |
| 28 August 2026 | §5.3 / §6.2 / §8.2 pointer depth: one
^ per type-specifier (named inner type or
^T[]); postfix p^^ legal, not preferred (Po10
rule 9); VAR/REF instead of ^^T.
Field auto-deref landed 0.26.5.176. |
| 28 August 2026 | §7.5 Design by contract: REQUIRE /
ENSURE / loop INVARIANT; examples; C
assert; vs ASSERT. |
| 28 August 2026 | §7.3 DEFER: runs on
RETURN / BREAK / CONTINUE; not on
exit / abort; no exceptions. |
| 2 September 2026 | §5 sets of enumerations Planned:
SET OF enum (0..63), { … } literals,
IN + * -.
IS dropped; IN is membership only. |
| 2 September 2026 | §5.4.0 EXTENDS polish (0.26.5.175):
p^.field parent fields; as ancestor →
.base; pointer / REF upcast. |
| 3 September 2026 | §5.3 / §6.2 field auto-deref (0.26.5.176):
p.field = p^.field when p is
^T; C (*(p)).field. p[i] /
instance on ^T closed in 178. |
| 4 September 2026 | §5.3 / §6.2 / §8.3 p[i] and instance on
^T (0.26.5.178): pointer-to-array peel (not
^char[]); p.method when p is
^T; caret (*(expr)) so p^[i] is
not *p[i]. Type::m(p) closed in
179. |
| 4 September 2026 | §8.3 Type::m(p) peel (0.26.5.179):
first actual ^Owner peels like p.m(); do not
peel @x or p^. |
End of TMod-c Language Report (draft).