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

  1. Introduction and model
  2. Notation and vocabulary
  3. Compilation units and modules
  4. Declarations
  5. Types
  6. Expressions and designators
  7. Statements (§7.5 Design by contract)
  8. Procedures, functions, and parameters
  9. Storage, ownership, and memory
  10. Builtins and standard facilities
  11. C lowering and interop
  12. Complete syntax
  13. 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 (beginend, 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

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.mcHello.c / Hello.h, unit Hello).


2. Notation and vocabulary

2.1 Syntax notation

Productions use Wirth EBNF as in syntax-ebnf.md:

Illustrative fragments in this report are non-normative if they disagree with the syntax document.

2.2 Keywords and identifiers

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)

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

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

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

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 *)

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 & ~. *)

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 (STRUCTEXTENDS) — 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)gDone (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 ^TDone (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.xg.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 ^TDone (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:

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 ^TDone (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 BEGINEND 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

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::nameType__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


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.

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);
#endif

12. 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).