TMod-c — High-level EBNF (merged with Atomic Pudding 2018 and prior influences + DbC )

Last updated: 14 July 2026

Version: 0.24.5

Author: M. Scott Reynolds

Purpose: High-level overview of TMod-c syntax (public name; Mod-c is the legacy name during transition). Simplified and incomplete — for reference during implementation.

File extensions:

Status: evolving design — simplified, safety-oriented, heavily influenced by Pascal/Modula-2/Oberon-07/C + modern touches

Hello World Example

program hello()

import printf from "stdio.h"

begin
    printf("Hello from Mod-c!\n")
end

Naming and style guide

Conventions for the language, compiler, source layout, and generated code. Evolving — update as stdlib and codegen mature.

Item Convention Example
Language / project name TMod-c (public); Mod-c (legacy, transitional) documentation, comments, user messages
Compiler executable tmodc (target); modc alias during migration bin/modc.0.24.5 today → bin/tmodc.0.24
System ABI module tmodc module → tmodc.h width types, nil, shared C interop prelude
Project web identity tmodc.com (primary), tmodc.org, tmodc.net public site, docs
Source files .mc arena.mc, StringBuffer.mc
Target language C (capital C) “compiles to C11” — ISO C, not the Mod-c brand
Builtin types lowercase, reserved bool, byte, char, integer, cardinal, real, string
Library / user structs T prefix when needed TStringBuffer, TSymbol, TType
Library modules (simple) lowercase file and MODULE name symtab.mc, symbol.mc, arena.mc
Library modules (named facility) PascalCase file and MODULE name StringBuffer.mc
Type-qualified methods Type::name in source → Type__name in C TStringBuffer::appendTStringBuffer__append
Module free procedures (lowercase) module_verb symtab_define, arena_init

Module name vs structure name: When the module name would match the primary exported type (e.g. module StringBuffer and struct StringBuffer), use T on the struct (TStringBuffer) so the type name does not clash with the module or with TStringBuffer__... symbols in generated C. Prefer lowercase module names (symtab, not tsymtab) for internal/compiler modules unless a prefix aids discovery.

Type-qualified vs module exports: Operations tied to a struct type use Type::method syntax (see Type-qualified methods). Flat module utilities with no receiver type remain ordinary procedure / function declarations (symtab_define, arena_init). Oberon-style (recv: T) name before the identifier is not in the grammar.

Keyword collisions: If a lowercase name clashes with a case-insensitive keyword (type, string, len, …), keep a disambiguator in the filename/module (ttype.mc) or use T on the struct. The builtin LEN reserves the spelling len / Len / LEN for identifiers (Pascal heritage).

PROGRAM entry point: Generated C wraps each PROGRAM in a separate main() that may perform toolchain pre-initialization before calling the program procedure.

Output naming: With -C / -H and no explicit path, output files default to {PROGRAM\|MODULE name}.c / .h in the source directory (override with -d directory, -b basename stem).

Library vs language: Builtin types and their closed builtin operations live in the grammar; facilities such as StringBuffer, concat, and format live in the stdlib (shipped with the compiler, not grammar keywords).

TMod-c rebrand (July 2026): Documentation and user-facing naming adopt TMod-c and tmodc. The T prefix matches Pascal-style type naming (TName, TList, …) and the planned system module tmodc (Oberon SYSTEM-like). Repo directory, Makefile targets, and modc binary name stay in place until a deliberate cutover. The tmodc.h header name is intentional — not a temporary shim.

tmodc module vs language builtins: Width typedefs (int8uint64), nil, and other shared C ABI typedefs are moving into tmodc.mctmodc.h. Everyday language builtins (integer, cardinal, real, bool, char, string, …) remain in the semantic prelude for now. Standalone .c: compiler may inline the prelude (current behaviour). Multi-TU builds: generated C #includes tmodc.h instead of duplicating the prelude per unit.


Modules, imports, exports, and foreign symbols

TMod-c uses a single author-facing source type (.mc). The compiler generates everything else:

Artifact Consumer Purpose
.c / .h C compiler Implementation and C ABI
.mh tmodc / modc (semantic analysis) Exported symbol table for TMod-c-to-TMod-c imports
tmodc.h C compiler (multi-TU) Shared ABI prelude (width types, nil, …)

Users do not author .mh files. They are build outputs (like object files), produced when a module is compiled and contains EXPORT declarations.

There are no separate definition modules, implementation modules, or abstract module kinds. DEFINITION is not a Mod-c keyword and should be removed from the reserved keyword list.

Design goals

Builtin types (language)

Only a small set of types is built into the language. They require no IMPORT and are always in the semantic prelude:

Builtin Role
bool Boolean
byte 8-bit unsigned octet
char Character / C char interop
integer Signed everyday integer (Oberon-like; lowers to C int)
cardinal Unsigned everyday integer (lowers to C unsigned int)
real Floating-point
string Immutable builtin string (see Builtin type: string)

integer / cardinal are not pointer-sized. On typical LP64 platforms they are 32-bit (int / unsigned int). Use them for everyday arithmetic, loop indices, and bit flags. For C size_t, pointer-sized values, or explicit 64-bit widths, use IMPORT / EXTERN (e.g. size_t from "stddef.h") or width-qualified integer N / cardinal N (planned).

Types such as size_t, FILE, void, C long, intptr_t, etc. are not builtins. Use IMPORT and/or EXTERN. Prefer integer / cardinal over raw C int / unsigned int in Mod-c source.

void: not a builtin. For C void (e.g. foreign APIs), use:

import void from "stddef.h"    (* optional: name in scope + include *)
extern type void

PROCEDURE declarations have no result type; only FUNCTION uses : return-type.

Pointer syntax (^T, POINTER TO T) is part of the type grammar, not a separate builtin type name.

Flags without SET

Mod-c does not provide Pascal/Modula-style SET OF types. Experience from Oberon (Wirth: general set types were rarely used; Oberon kept only a single integer SET) and Mod-c’s C/Unix interop goals both point the same way: bit flags are ordinary integers, not a separate type kind. Use named constants (often from a future ENUM), a width-qualified cardinal (e.g. cardinal 32 when implemented), and or / and / not for combine, test, and clear — the same idiom as C bitmasks (flags |= MASK, flags & MASK). Packed binary layouts and unknown record formats are handled by width-qualified integers and packed struct, not by a builtin set algebra. A stdlib BitSet module remains possible later; a language-level SET OF is deferred / unlikely unless requirements change.

IMPORT — bring names into scope

Syntax (implemented)
import-stmt   = "IMPORT" [ import-item { "," import-item } ] "FROM" import-source EOS ;
import-source = string-literal | qualident ;
import-item   = identifier ;
qualident     = identifier { "." identifier } ;

Two import worlds — the spelling of import-source selects behaviour:

FROM operand Meaning Example
String literal Foreign / C, or an explicit file path (including "foo.mh") import printf from "stdio.h"
Qualident (unquoted) Mod-c module — always .mh symtab + paired .h #include import symtab_define from symtab

Examples:

(* foreign / C — quoted string *)
import from "stdio.h"
import printf from "stdio.h"
import size_t, NULL from "stddef.h"
import
    CodegenTarget,
    TARGET_C,
    TARGET_HEADER
from "codegen_common.h"

(* Mod-c module — qualident (preferred) or explicit quoted .mh *)
import symtab_define from symtab
import generate_header from codegen_header
import mini_answer, MiniCounter from fixtures.minimod
import symtab_define from "symtab.mh"              (* still valid — explicit path *)

(* include-only — no import-item list *)
import from symtab                                 (* #include "symtab.h" *)
import from "stdio.h"

Layout: FROM import-source is the anchor that ends the import list. Newlines and whitespace between IMPORT and FROM are layout only; \ is not required between items. EOS (; or newline) follows the FROM operand.

Include-only import: IMPORT FROM import-source with no import-item list emits #include for the associated path (per rules below) and registers no names. Symbols such as printf or stderr still require a named IMPORT … FROM or an EXTERN declaration. This is not a wildcard import (IMPORT * remains deferred).

Semantic rules:

  1. Each import-item (when present) registers one spelling in the current scope (case-sensitive identifier rules). An empty list registers nothing.
  2. import-source selects behaviour:
import-source modc reads file? Symtab Codegen (generated .c / .h)
Qualident (unquoted) Yes — parse qualident → path with / + .mh Load kinds/types from export file #include paired .h (dots → slashes, append .h). Never #include .mh in C output.
Quoted string ending in .mh Yes — parse export table at resolved path Load kinds/types from export file #include associated .h (same stem/path rule). Never #include .mh in C output.
Quoted .h (or other non-.mh) No — do not parse C Register each imported name as foreign / minimal #include "path" exactly as written in the string literal
  1. For .h imports, modc does not extract declarations from the C header. The import list documents what the unit uses and registers names for semantic checks. No parameter/type checking unless the name is refined with EXTERN or imported from a .mh with a complete signature.

  2. Multiple imports of the same FROM path should merge includes (one #include per distinct path).

  3. Codegen include form: always #include "path" using the resolved path string. The build system (Makefile) supplies -I / -isystem so "stdio.h" resolves to system headers when not found beside the compiling unit. modc does not choose <> vs "".

  4. Case sensitivity: path strings and identifiers are case-sensitive in the language grammar (even on case-insensitive host filesystems). "Stdio.h" and "stdio.h" are distinct spellings.

Path resolution (today):

Import forms (implemented vs deferred):

Form Symtab Generated C include Status
import x from M.N Register x from M/N.mh export table #include "M/N.h" Implemented
import from M.N None (include-only) #include "M/N.h" Implemented
import x from "path.mh" Register x from resolved .mh Paired .h Implemented
import * from M.N All exports from M/N.mh Paired .h Deferred

String literals remain valid for odd paths, tests, and explicit spelling: import x from "codegen/codegen_header.mh" works alongside import x from codegen.codegen_header.

Still deferred (project layout and bulk import)

Source layout (future): .mc files live under src/ (optionally in subdirectories). The build mirrors src/build/ and passes module/include search paths to modc and the C compiler. Until then, place .mh / .h files where the relative qualident path from the importer can find them (e.g. tests/fixtures/minimod.mh for import … from fixtures.minimod in tests/).

EXTERN — refine foreign symbols

EXTERN declares that a symbol is implemented elsewhere (typically C). It may appear in the current unit or in a shared “foreign definitions” module whose exports become a .mh for others.

extern-type-decl = "EXTERN" "TYPE" identifier [ "=" type-expression ] EOS ;
extern-var-decl  = "EXTERN" "VAR" var-item { "," var-item } EOS ;
extern-proc-decl = "EXTERN" [ "EXPORT" ] "PROCEDURE" identifier formal-parameters EOS ;
extern-func-decl = "EXTERN" [ "EXPORT" ] "FUNCTION" identifier formal-parameters EOS ;

Examples:

extern type void
extern type FILE
extern type size_t
extern var errno: integer
extern procedure printf(fmt: ^char, ...): integer
extern function strlen(s: ^char): size_t

Opacity: extern type FILE — modc knows FILE is a type but not its layout (opaque foreign type).

Minimal extern: export extern function printf in a definitions module — .mh records printf as function, incomplete. Importers know the name is callable; arity/type checking stays limited until a full prototype exists.

Interaction with IMPORT:

EXPORT — generate .mh for other Mod-c units

EXPORT marks declarations that other Mod-c units may import via .mh.

module symtab()

export procedure symtab_define(...)
export type TSymbolTable = struct ... end

When modc compiles a module with exports, it writes:

.mh file format (v1)

Format name: modc-mh/1
Encoding: UTF-8 text, one record per line; # starts an end-of-line comment.
Rationale: C11 has no standard JSON/configuration library; line-oriented text is trivial to emit and parse without third-party dependencies. JSON may be added later as modc-mh/2.

Example:

# modc-mh/1
@module symtab
@format 1
export procedure symtab_define complete
export function symtab_lookup complete
export type TSymbolTable opaque
export extern function printf incomplete

Line kinds:

Prefix Meaning
@module name Required once: exporting module identifier
@format 1 Required once: format version
export procedure name complete \| incomplete Exported procedure
export function name complete \| incomplete Exported function
export type name opaque \| complete Exported type
export enum name complete Exported enum type (members also listed as export const)
export var name complete \| incomplete Exported variable
export const name complete \| incomplete Exported constant

Completeness:

Flag Semantics
complete Full signature/type available for checking
incomplete Name and kind only
opaque Type name only, no layout

v1 limitation: signature blobs may be added in v1.1; v1 requires at least name + kind + completeness.

Reading: IMPORT x FROM qualident or IMPORT x FROM "path.mh" — modc parses the resolved .mh. IMPORT x FROM "path.h" — modc does not parse the header.

Writing: Emitted when compiling a module with any EXPORT; normal Makefile dependency.

Implementation order (compiler)

  1. Semantic prelude — builtin types: bool, byte, char, integer, cardinal, real, string only.
  2. IMPORT … FROM — symtab + #include "path"; remove hard-coded stdio/stdlib mapping in codegen.
  3. EXTERN TYPE | VAR | PROCEDURE | FUNCTION — parse, symtab refine, codegen.
  4. .mh writer on EXPORT compile.
  5. .mh reader on IMPORT … FROM Mod-c module sources (qualident or "*.mh").
  6. Migrate sources to explicit import … from "….h" blocks.

Type-qualified methods (receivers)

Mod-c uses type-qualified method syntax (C++-style :: on definitions and calls). Methods are not declared inside STRUCT bodies; they are ordinary procedure / function declarations whose name is Type::identifier. A module may declare Type:: methods for a type defined elsewhere (extension-style), unlike Oberon-07 where bound procedures are tied to the type’s declaration unit.

Inspired by: C++ / Rust Type::name for qualification and stable ABI; Oberon . for instance calls (planned). Lua obj:method() is not used — postfix : clashes with switch / case / else labels and ternary ? :.

Unlike C++, there is no inheritance, ADL, or template method tables.

Struct layout vs methods

Concept Where declared Notes
Struct fields TYPE T = STRUCT ... END Unique field names per struct (no duplicate spellings, regardless of type). No C++-style data member + function homonyms.
Type-bound methods procedure / function with Type::name Independent of struct definition; may live in any module that sees Type.
Callable fields struct fields with procedure/function type e.g. vtable-style append: PAppend in StringBuffer.mc

Structs and Type:: methods are independent. Layout comes from STRUCT; behavior is attached via Type:: declarations.

Declaration (EBNF)

method-prototype = [ "RECURSIVE" ] ( "PROCEDURE" | "FUNCTION" )
    qualified-method-name formal-parameters EOS ;

qualified-method-name = type-identifier "::" identifier ;

(return-type for FUNCTION is part of formal-parameters, as for ordinary procedures.)

Examples:

(* Instance method — explicit self in parameter list *)
function TStringBuffer::append(self: TStringBuffer, data: string): TStringBuffer
begin
    ...
    return self
end

(* Type-level / static — no self parameter *)
function TStringBuffer::new(): TStringBuffer
begin
    ...
end

(* Procedure (no return value) *)
procedure TStringBuffer::clear(self: TStringBuffer)
begin
    ...
end

Rules:

Removed: Oberon-style procedure (sb: TStringBuffer) append(...) and Lua-style obj:method(...) are not in the grammar.

Generated C (stable mangling)

:: in a qualified method name maps to __ (double underscore) in linker-visible C identifiers:

Mod-c Generated C (conceptual)
function TStringBuffer::append(self: TStringBuffer, data: string): TStringBuffer TStringBuffer TStringBuffer__append(TStringBuffer self, string data)
function TStringBuffer::new(): TStringBuffer TStringBuffer TStringBuffer__new(void)

Rationale: predictable C symbols; libraries callable from hand-written C; :: does not appear in C. Double underscore after the type prefix is distinct from C reserved leading __ patterns.

Calls (phase 1: Type:: only)

Form Syntax Lowering
Type-qualified Type::name(args...) Type__name(args...)no implicit receiver
Explicit instance Type::name(self, args...) Same callee; self is the first ordinary argument

Examples:

var sb: TStringBuffer = TStringBuffer::new()
TStringBuffer::append(sb, "hello")
printf("%d\n", Counter::get(c))

Phase 1 implementation: parser, semantics, and codegen support Type::name(...) calls only.

Deferred — instance . calls: obj.method(args) (Oberon/C++ style) will resolve obj’s static type T, look up T::method, and lower to T__method(obj, args...). Not required for phase 1.

Field access: . remains field / member selection. A postfix .identifier followed by ( is either a call through a function-pointer field or (when implemented) a type-bound method — see Ambiguity below.

Ambiguity (callable field vs type-bound method)

If struct type T has a callable field foo (procedure pointer) and a visible T::foo type-bound method exists, then:

obj.foo(...)   (* ERROR: ambiguous — deferred until dot-call resolution is implemented *)

The compiler shall issue a compile-time error (not a silent pick, not a warning). Use an explicit Type::foo(obj, ...) call to select the type-bound method, or rename the field or method.

Override priority (Type:: wins over field) is not defined in phase 1; may be reconsidered much later with an explicit opt-in.

Builtins

Builtin types may define compiler-provided qualified methods:

Free procedures (no Type::)

Module-level procedures without a type qualifier remain the default for flat utilities:

procedure symtab_define(...)

Mangling: modulename_verb (or export rules per module) — not Type__name.

Implementation status (non-normative)

Feature Status
Type::name declarations → Type__name C symbols Implemented (0.23.9)
Type::name(...) calls → Type__name(...) Implemented (0.23.9)
semantic_verify_method_self (self param matches Type::) Implemented (0.23.9)
obj.method(...) dot instance sugar Deferred
Ambiguity error (field vs T::method) Deferred with dot calls
string::cstring(s) builtin lowering Planned (grammar only)

Full Language Specification

(*
 * Mod-c - High-level EBNF (merged with Atomic Pudding 2018 influences + DbC)
 * Purpose: High-level overview for implementation reference
 * Status: Evolving — detailed but still high-level in some areas
 * Phase 1 restrictions:
 * - Full type-expressions only in TYPE defs; elsewhere use type-specifier
 * - VAR declarations only at top level and in definition sequence; LET allowed anywhere
 * - const-expression restricted to compile-time values (enforced in semantics)
 *
 * Builtin types (lowercase reserved type-identifiers — see "Builtin types" section):
 *   bool, byte, char, integer, cardinal, real, string
 * Users cannot declare TYPE string = ... or otherwise shadow a builtin name.
 *)

modc-unit = program-unit | module-unit ;


(**** Top-level units **********************************************)

program-unit = "PROGRAM" module-name formal-parameters EOS
    { import-stmt }
    declaration-sequence 
    block ;

module-unit = "MODULE" module-name formal-parameters EOS
    { import-stmt }
    declaration-sequence 
    block ;

module-name = identifier ;


(**** Imports ******************************************************)

(* See "Modules, imports, exports, and foreign symbols". *)

import-stmt   = "IMPORT" [ import-item { "," import-item } ] "FROM" import-source EOS ;
import-source = string-literal | qualident ;

import-item = identifier ;


(**** Top Level Declarations *************************************************)

declaration-sequence = {  type-decl 
                        | const-decl 
                        | let-decl 
                        | var-decl 
                        | method-decl
                        | extern-type-decl
                        | extern-var-decl
                        | extern-proc-decl
                        | extern-func-decl } ;

type-decl   = [ "EXPORT" ] type-def ;
const-decl  = [ "EXPORT" ] const-def ;
let-decl    = [ "EXPORT" ] let-def ;
var-decl    = [ "EXPORT" ] [ "EXTERN" ] var-def     (* top level. *)
            | [ "STATIC" ] var-def ;                (* block level *)

(* Multi-item definitions *)
(*
    The EBNF uses a deliberately simple per-item form:

        var-item    = identifier [ ":" type-specifier ] [ "=" ( initializer | expression ) ] ;
        const-item  = identifier [ ":" type-specifier ] "=" const-expression ;
        let-item    = identifier [ ":" type-specifier ] "=" ( initializer | expression ) ;

    A comma list under a single VAR/CONST/LET is treated as a *group* for
    trailing type propagation. This is a semantic rule (implemented via
    right-to-left post processing in the parser), not a more complex set of
    grammar productions.

    Classic per-name style (type right after the name):
        var f: float = 3.14
        const c: integer = 42
        let l: string = "hello"

    Grouped trailing type style (Oberon/C-like; rightmost type propagates left):
        var a, b, c: integer
        var a = 0, b, c: integer        (* a infers from init; b and c get the group type *)
        const a = 1, b: integer = 2     (* a inferred, b gets explicit type + value *)

    Pure inference groups (no ":" types anywhere in the list):
        var p = 1, q = 2

    Rules:
    - CONST and LET always require "=" on every item (enforced in parse_item).
    - For VAR: every item must have either an explicit type or an initializer
      (or be covered by a trailing type from later in the same list).
    - The rightmost explicit type in a comma list propagates leftward to any
      preceeding bare names (items that have neither their own type nor an
      initializer).
    - An item with "=" but no explicit ":" type is left with type=NULL in the
      AST for later inference. It does not consume or block a type coming from 
      the right.
    - A bare name that has neither its own type/initializer nor a type to its
      right is an error.
    - Example error: `var x = 1, y;` - y has neither "=" nor a later type.
*)

type-def    = "TYPE" type-item { "," type-item } EOS ;
const-def   = "CONST" const-item { "," const-item } EOS ;
let-def     = "LET" let-item { "," let-item } EOS ;         (* may be used anywhere *)
var-def     = "VAR" var-item { "," var-item } EOS ;

type-item   = identifier "=" type-expression 
            | identifier "FORWARD" ;
const-item  = identifier [ ":" type-specifier ] "=" const-expression ;
let-item    = identifier [ ":" type-specifier ] "=" ( initializer | expression ) ;
var-item    = identifier [ ":" type-specifier ] [ "=" ( initializer | expression ) ] ;

#### CONST, LET, and immutability (design)

Mod-c uses **three different “constant / immutable” ideas**. Do not conflate them.

| Form | Role | When value is known | Typical C lowering (intent) |
|------|------|---------------------|-----------------------------|
| **`CONST` declaration** (`const-def`) | **Compile-time named constant** (Pascal/Oberon `CONST`) | Compile time (`const-expression`) | `#define`, `enum` member, or literal folded at use sites — **not** a substitute for C `const` objects |
| **`LET` declaration** (`let-def`) | **Runtime immutable binding** | Run time (initializer evaluated once) | `const` / `static const` object, or `const` pointer as appropriate |
| **`VAR` declaration** | Mutable binding | Run time | ordinary variable |
| **Bare / `REF` parameter** | Actual argument; **binding read-only**, **members mutable** | Run time | `T*` / reference + auto-deref |
| **`VAR` parameter** | Mutable alias; rebinding and member mutation allowed | Run time | `T*` |
| **`CONST` parameter** | **Deep read-only** actual argument; binding **and** members immutable | Run time | `const T*` (or `const` qualification on pointee) |
| **`const` in `type-specifier`** (`const ^T`) | **Type qualifier** on the pointee / view | — | C `const` on referenced type |

**`CONST` declarations are not “read-only variables”.** They name values the compiler may substitute in `case` labels, array bounds (`array[N] of T`), `FOR BY`, and other `const-expression` positions. C’s `const int x = 0` is a **read-only object** and is **not** an integer constant expression — hence `case x:` fails in C. Mod-c’s design follows Pascal, not C, here.

**`LET` owns runtime immutability** for locals and bindings that are fixed after initialization but not compile-time constants (e.g. `let fp: ^FILE = fopen(...)`).

**Formal parameters:** the **parameter name** is already **non-rebindable** unless `VAR` is specified (bare and `REF` default). You do **not** need parameter `CONST` merely to prevent `p := something` inside the callee — that is the default. Use parameter **`CONST`** only when the callee must not mutate **through** the parameter (fields / pointee), e.g. `CONST buf: ^char` for read-only scan of a buffer. Do **not** use parameter `CONST` for compile-time literals; use a **`CONST` declaration** or a literal.

**Implementation status (0.23.9):** `CONST` declarations are lowered to C `static const` objects today — a **codegen gap** relative to this design. Until const-expression enforcement and proper lowering land, use C `enum` in hand-written `.h` files for discriminant sets in `switch`/`case` (see `mh_exportkind.h`), and numeric literals in `array[N]` with a nearby `CONST` for documentation.

initializer = "{" [ expression { "," expression } ] "}" ;


(**** Types ********************************************************)

(* Full type expressions used on right-hand side of TYPE declarations *)
type-expression = type-specifier
    | type-name
    | struct-type
    | enum-type
    | method-type
    | qualident "<" type-specifier { "," type-specifier } ">" ; (* future generics *)

type-specifier = [ "^" | "POINTER" "TO" ] type-identifier [ array-suffix ] ;

array-suffix = "[" [ const-expression ] "]"     (* fixed size *)
            | "[" "]" ;                         (* open array T[] *)

type-name = type-identifier { type-identifier } ;  (* "int", "long long", etc. *)

type-identifier = identifier ;   (* lowercase builtins: bool, byte, char, integer, cardinal, real, string *)

struct-type = [ "PACKED" ] "STRUCT" [ "EXTENDS" type-identifier ] EOS { field-decl EOS } "END" ;

enum-type = "ENUM" [ EOS ] enum-decl { "," enum-decl } [ "," ] [ EOS ] "END" ;

enum-decl  = identifier [ "=" const-expression ] ;

method-type = ( "PROCEDURE" | "FUNCTION" ) formal-parameters ;

field-decl  = identifier ":" type-specifier [ "=" const-expression ] ;

return-type = [ "CONST" ] [ "^" | "POINTER" "TO" ] type-identifier ;

(* Placeholder for full template/generic definition support *)
template-decl = "TEMPLATE" "<" template-parameter { "," template-parameter } ">"
                ( type-decl | struct-decl | method-prototype ) ;

template-parameter = type-identifier [ ":" type-specifier ] ;   (* e.g. T: Type or just T *)


(**** Methods ******************************************************)

method-decl = [ "EXPORT" ] [ "EXTERN" ] method-prototype ;

(* Foreign symbols — see "Modules, imports, exports, and foreign symbols" *)
extern-type-decl = "EXTERN" "TYPE" identifier [ "=" type-expression ] EOS ;
extern-var-decl  = "EXTERN" "VAR" var-item { "," var-item } EOS ;
extern-proc-decl = "EXTERN" [ "EXPORT" ] "PROCEDURE" identifier formal-parameters EOS ;
extern-func-decl = "EXTERN" [ "EXPORT" ] "FUNCTION" identifier formal-parameters EOS ;

(* Type-qualified methods — see "Type-qualified methods" section *)
method-prototype = [ "RECURSIVE" ] ( "PROCEDURE" | "FUNCTION" )
    qualified-method-name formal-parameters [ "FORWARD" ] EOS ;

qualified-method-name = type-identifier "::" identifier ;

method-def = method-prototype
    block;

formal-parameters = "(" [ parameter-def { "," parameter-def } ] ")" [ ":" return-type ] ;

parameter-def = [ "VAR" | "REF" | "CONST" ] identifier ":" type-specifier ;
(*
   Parameter Passing Semantics (0.9.13)

   NOTE: Parameter CONST is a *passing-mode / deep-immunity* qualifier.
   It is NOT the same as a CONST declaration (compile-time constant).
   See "CONST, LET, and immutability (design)" above.

   - The parameter *binding* itself is immutable by default.
     Only VAR allows reassignment or rebinding of the parameter name.
   - Member fields are mutable via dot notation unless CONST is specified.
   - When no keyword is given, the parameter defaults to REF semantics
     (reference + auto-deref on dot + member mutation allowed).

   Bare / REF p: T   → Reference semantics. Auto-deref on dot.
                       Can modify members. Cannot rebind the parameter.
   VAR p: T          → Full mutability + rebinding (if ^T). Can modify members.
   CONST p: T        → Deep read-only: cannot rebind; cannot mutate members/pointee.

   REF and CONST accept both values and pointers.
   The compiler takes the address when a value is passed to REF or CONST.
*)


(**** Blocks & Statements ******************************************)

block = 
    "BEGIN"
        { require-clause }
        statement-sequence
        { ensure-clause }
        [ return-statement ]
    "END" [ identifier ] ;

(* Design by Contract clauses *)
require-clause      = "REQUIRE" expression EOS ;
ensure-clause       = "ENSURE" expression EOS ;
invariant-clause    = "INVARIANT" expression EOS ; (* allowed only in loop bodies *)

statement-sequence = definition-sequence { statement EOS } [ return-statement ] ;

definition-sequence = { const-def | let-def | var-def } ;       (* Future: | method-def *)

(****** Statements ****************************************************)

statement = 
    block
    | if-then-statement
    | switch-statement
    | while-statement
    | repeat-statement
    | for-statement
    | loop-statement
    | assignment-stmt
    | call-stmt
    | break-statement
    | continue-statement
    | expr-statement
    | defer-stmt
    | debug-stmt
    | assert-stmt
    | let-stmt
    | inc-dec-stmt ;

let-stmt = let-def ;

if-then-statement =
    "IF" expression "THEN" statement-sequence
    { "ELSIF" expression "THEN" statement-sequence }
    [ "ELSE" statement-sequence ]
    "END" ;

(* No fallthrough between case labels.
   ELSE is required (unlike C's optional default) to make the semantics
   explicit: this is not a C switch.
   Multiple discrete labels per case are supported with comma.
   Range support (e.g. 1..10) can be added in a later phase by extending
   the case-labels production.
*)
switch-statement =
    "SWITCH" expression "OF"
        { "CASE" case-labels ":" statement-sequence }
        "ELSE" ":" statement-sequence
    "END" ;

case-labels = const-expression { "," const-expression } ;

(* Example:
    switch c of
        case 'a': ...
        case 'b', 'c': ...
        else: ...
    end
*)

while-statement =
    "WHILE" expression "DO" 
        { invariant-clause } 
        statement-sequence 
    "END" ;

(* **Superseded — `WHILE` with step (not planned; July 2026).**
   Use `defer` at the top of the loop body for an end-of-iteration step:

        var n: integer = 1
        while n < 10 do
            defer n += 1
            printf("n=%d\n", n)
        end

   Same pattern applies to `LOOP` … `END`. See `tests/test_defer.mc`.

   (Original proposal retained for history:)

   Optional ergonomics for counted-style loops without folding init/test/step into one
   C-style header. Inspired by Zig's `while (cond) : (step)` continuing expression;
   kept separate from `FOR` so bounds are not re-tested every iteration.

   Surface syntax (proposal):

        while-statement-with-step =
            "WHILE" expression "BY" assignment-stmt "DO"
                { invariant-clause }
                statement-sequence
            "END" ;

   Example:

        while i < n by i := i + 1 do
            ...
        end

   Rules (planned):
   - `BY` clause is a single assignment statement (typically `ident := expr` or compound `+=`).
   - Evaluated once per iteration, after the body, before the condition is tested again.
   - Does **not** replace `FOR` (Pascal bounds, read-only control var, end evaluated once).
   - Use `WHILE` alone when the condition must change arbitrarily; use `FOR` for fixed ranges.
*)

repeat-statement =
    "REPEAT"
        { invariant-clause }
        statement-sequence 
    "UNTIL" expression ;

(* bounds/step evaluated once; identifier read-only in body (semantics + codegen 0.23.4) *)
for-statement =
    "FOR" identifier ":=" expression ( "TO" | "DOWNTO" ) expression [ "BY" const-expression ] "DO" 
        { invariant-clause }
        statement-sequence 
    "END" ; 

(* **FOR implementation notes (0.23.4):**
   - Semantics: control variable must not be assigned, `INC`/`DEC` target in loop body.
   - Codegen: end expression stored in `const long long __for_end_l{line}` (evaluated once);
     compare uses `(long long)(control)` until expression types exist.
   - `BY` step is a compile-time constant in the parser (`for_stmt.step`); not re-evaluated.
   - For re-tested conditions or mutating the index, use `WHILE`, `REPEAT`/`UNTIL`, or `LOOP`.
*)

loop-statement =
    "LOOP" 
        { invariant-clause } 
        statement-sequence 
    "END" ;

(* Note: Assignment is deliberately a statement only (not an expression).
   This follows Pascal/Modula-2/Oberon heritage and avoids the classic
   "=" vs "==" / side-effect-in-condition bugs. Both ":=" and compound 
   forms are statements only. See assignment-stmt below.
*)

compound-assignment
    = "*=" | "/=" | "%=" | "+=" | "-=" | "<<=" | ">>=" | "&=" | "|=" ;

assignment-stmt = designator ( ":=" | compound-assignment ) expression ;

call-stmt = designator "(" [ argument-list ] ")" ;  (* mandatory () on calls *)
expr-statement = "(" expression ")" ;               (* explicit discard of results *)
return-statement = "RETURN" [ expression ] ;        (* expression required in FUNCTION *)
break-statement    = "BREAK" ;
continue-statement = "CONTINUE" ;
inc-dec-stmt = "INC" "(" designator [ "," expression ] ")" 
             | "DEC" "(" designator [ "," expression ] ")" ;
defer-stmt = "DEFER" statement ;    
debug-stmt = "DEBUG" statement ;
assert-stmt = "ASSERT" expression [ "," assert-msg ]
assert-msg = const-expression ;

(************** Designators ****************************)

designator = qualident { selector } ;

selector = "." identifier       (* field access *)
    | "[" expression "]"        (* array/index *)
    | "^"                       (* Pascal-style dereference *)
    | "(" type-identifier ")" ; (* Type guard *)

(*** INC / DEC - both statement and expression forms ***)
inc-dec-expr = "INC" "(" designator [ "," expression ] ")"
             | "DEC" "(" designator [ "," expression ] ")" ;

qualident = identifier { "." identifier } ;
identifier = (letter | "_") , { letter | digit | "_" } ;

(************** Expressions ****************************)

(* Expression precedence chain - right-associative where appropriate *)

expression = conditional-expression;

conditional-expression = logical-or-expression
    | logical-or-expression "?" expression ":" conditional-expression ;

logical-or-expression = logical-and-expression { "OR" logical-and-expression } ;

logical-and-expression = equality-expression { "AND" equality-expression } ;

equality-expression = relational-expression { equality-op relational-expression } ;
equality-op = "==" | "!=" | "<>" ;          (* "<>" is the same as "!=" *)

relational-expression = shift-expression { relational-op shift-expression } ;
relational-op = "<" | ">" | "<=" | ">=" | "IN" | "IS" ;

shift-expression = additive-expression { shift-op additive-expression } ;
shift-op = "<<" | ">>" ;

additive-expression = multiplicative-expression { additive-op multiplicative-expression } ;
additive-op = "+" | "-" | "|"  | "XOR";

multiplicative-expression = power-expression { mult-op power-expression } ;
mult-op = "*" | "/" | "%" | "&" | "DIV" | "MOD" ;

power-expression = unary-expression 
    | unary-expression "**" power-expression; (* right-associative *)

unary-expression = inc-dec-expr
    | cast-expression
    | prefix-op unary-expression ;

cast-expression = "CAST" "(" type-specifier "," expression ")"
    | postfix-expression "AS" type-specifier  (* postfix cast *)
    | postfix-expression ;

prefix-op = "NOT" | "!" | "~" | "+" | "-" | "^" | "@" ;

postfix-expression = primary-expression 
    { "[" expression "]" 
    | "(" [ argument-list ] ")" 
    | "." identifier 
    | instance-method-call    (* deferred — see "Type-qualified methods" *)
    | inc-dec-expr ;

(* Instance method: obj.Method() — deferred; Oberon/C++ style *)
instance-method-call = "." identifier "(" [ argument-list ] ")" ;

(* Type-qualified call: Type::Method() — phase 1 *)
type-qualified-call = type-identifier "::" identifier "(" [ argument-list ] ")" ;

argument-list = expression { "," expression } ;

primary-expression = identifier
    | literal
    | "(" expression ")"
    | array-literal         (* { expr {, expr} } *)
    | designator 
    | range-expression
    | len-expression
    | type-qualified-call ;

range-expression = additive-expression ".." additive-expression ; (* inclusive *)

array-literal = "{" [ expression { "," expression } ] [","] "}" ;

literal = integer-literal | real-literal | char-literal | string-literal
    | "TRUE" | "FALSE" | "NIL" ;

integer-literal = decimal-integer | hexadecimal-integer | octal-integer | binary-integer ;
decimal-integer     = digit { [ seperator ] digit } ;*)
hexadecimal-integer = "0x" hex-digit { [ seperator ] hex-digit } ;
octal-integer       = "0o" octal-digit { [ seperator ] octal-digit } ;
binary-integer      = "0b" binary-digit { [ seperator ] binary-digit } ;

hex-digit    = digit | "A".."F" | "a".."f" ;
octal-digit  = "0".."7" ;
binary-digit = "0" | "1" ;
seperator    = "_" ;        (* optional thousands/grouping seperator *)

real-literal = decimal-part [ "." decimal-part ] [ exponent-part ] ;
decimal-part = digit { [ seperator ] digit } ;
exponent-part = ("E" | "e") [ "+" | "-" ] digit { [ seperator ] digit } ;

char-literal   = "'" ( character | escape-sequence ) "'" ;
(* string-literal type is builtin string; see "Builtin type: string" *)
string-literal = '"' { character except '"' | escape-sequence } '"' ;

letter = "a".."z" | "A".."Z" ;
digit  = "0".."9" ;

escape-sequence = "\\" | "\'" | "\"" | "\?" | "\a" | "\b" | "\f" | "\n" | "\r" | "\t" | "\v" 
    | "\x" hex-digit hex-digit
    | "\" octal-digit [ octal-digit [ octal-digit ] ] ;

character = (* any printable char except " or \ or control chars *) ;

const-expression = expression ; (* Expressions that can be computed at compile time. *)

sizeof-expession = "SIZEOF" "(" ( type-specifier | designator ) ")" ;

(* LEN — generic element/byte count; see "Builtin LEN" section *)
len-expression = "LEN" "(" designator ")" ;


(******* Keywords ********************************************************************************)

(* Keywords are case-insensitive: "IF", "if", "If", "iF" are all recognized as the same keyword. *)
(* Identifiers are case-sensitive: "MyVar", "myvar", "MYVAR" are distinct. *)

(* Keywords are case-insensitive. *)
keyword = "ALIAS" | "AND" | "ARRAY" | "AS" | "ASSERT" | "BEGIN" | "BREAK" | "BY" | "CASE" | "CAST"
        | "CLASS" | "CONST" | "CONTINUE" | "DEBUG" | "DEC" | "DEFAULT" | "DEFER" | "DIV"
        | "DO" | "DOWNTO" | "ELSE" | "ELSIF" | "END" | "ENSURE" | "ENUM" | "EXPORT" | "EXTENDS" | "EXTERN"
        | "FALSE" | "FOR" | "FORWARD" | "FROM" | "FUNCTION" | "HEADER" | "IF" | "IMPORT" | "IN" | "INC"
        | "INVARIANT" | "IS" | "LEN" | "LET" | "LOOP" | "MOD" | "MODULE" | "NIL" | "NOT" | "OBJECT" | "OF"
        | "OR" | "PACKED" | "POINTER" | "PROCEDURE" | "PROGRAM" | "RECORD" | "RECURSIVE" | "REF" | "REPEAT"
        | "REQUIRE" | "RETURN" | "SIZEOF" | "STATIC" | "STRUCT" | "SWITCH" | "THEN" | "TO" | "TRUE" | "TYPE" 
        | "UNION" | "UNTIL" | "VAR" | "WHILE" | "XOR" | "TEMPLATE" ;


(**** End-of-Statement ****************************)

(* Layout convention (for authors):
   - Prefer one statement per line.
   - Multiple statements on one line require ';'.
   - Indentation is NOT significant (unlike Python).
*)

EOS = ";" | newline-when-eos ;

newline-when-eos = NEWLINE unless any of:

  (1) Explicit continuation: '\' is the last non-whitespace before NEWLINE
      (lexer suppresses the line break).

  (2) Bracketed expression: inside (), [], {} delimiter pairs in expressions.

  (3) Trailing continuator: the last non-comment token on the line cannot
      complete the current phrase. Includes:
        :=  ,  .  ::  +  -  *  /  ^  @  (  [  {
        and  or  xor  =  <>  <=  >=  <  >
        keywords that grammatically require a following phrase
        (if, elsif, while, repeat, for, return, require, ensure, then, else, …).

  (4) Anchor-not-yet-closed: statement forms with a fixed closing marker
      before EOS — notably import-stmt before FROM import-source.
      Comma-separated declaration lists (VAR, CONST, LET, TYPE) also continue
      on the next line when the previous line ended with ','.

';' always ends a statement (except inside strings and comments).

import-stmt EOS placement: IMPORT … FROM import-source EOS
  (EOS is after the import-source operand, not after each import-item).


(******* Comments *****************************************************)

comment = { single-line-comment | c-style-comment | pascal-style-comment } ;

single-line-comment = "//" ... newline
c-style-comment = "/*" ... "*/"
pascal-style-comment = "(*" ... { pascal-style-comment } ... "*)"    


(***************************************************************************)

End-of-statement rules

Mod-c follows a Lua-inspired rule: the lexer emits NEWLINE; the parser decides whether it ends the current statement. The goal is one statement per line without Python-style significant indentation.

Mechanism Role
Newline (default) Ends the current statement
; Ends a statement; allows two or more statements on one line
, at end of line Continues a comma-separated list (VAR, CONST, LET, TYPE, IMPORT items) on the next line
FROM import-source Ends the IMPORT item list; EOS comes after the string literal or qualident
\ before newline Explicit “same statement continues” when no other rule applies
( ) [ ] { } Newlines inside bracketed expressions do not end the statement

Open-ended declarations (VAR, CONST, LET, TYPE) have no anchor keyword. They end at EOS after the last item. Split items across lines using a trailing comma or \.

Anchor-terminated statements (IMPORT … FROM import-source) treat newlines before the anchor as layout only. This matches intuitive reading: the FROM operand marks the real end.

Parser implementation (three zones):

Zone Mechanism
Expression operands skip_layout_breaks at start of parse_unary (trailing or, and, +, …)
Required keyword after phrase skip_layout_breaks inside consume (then, do, of, until, …)
Between statements checkEOS / matchEOSmatch does not skip layout

Expression continuators — when the last token on a line cannot complete the current expression (or, and, +, -, :=, (, …), a following newline is layout only; the next operand is parsed on the next line.

RETURN (exception): A newline immediately after RETURN with no expression on the same line ends the return statement. The parser does not skip layout before the optional return expression. This prevents a following statement (e.g. debug printf) from being parsed as the return value. For a multiline return value, use parentheses: return ( a + b ).

Unreachable statements: In a statement-sequence or BEGIN block body, any statement after RETURN, BREAK, or CONTINUE (before END, UNTIL, ELSIF, …) is unreachable and a fatal parse error. Detection is per sequence (early exit inside a nested if arm does not mark the enclosing loop body unreachable).

Builtin types

Mod-c uses lowercase names for types the language recognizes natively. These appear in type-specifier like any type-identifier, but are reserved: users cannot redeclare them with TYPE (e.g. TYPE string = ... is forbidden).

Builtin Role (summary) Generated C (typical)
integer Signed everyday integer (Oberon-like; not pointer-sized) typedef int integer
cardinal Unsigned everyday integer typedef unsigned int cardinal
real Floating-point typedef double_t real
bool Logical type C _Bool / bool
byte 8-bit octet typedef char byte
char Character C char
string Immutable text (see below) typedef char *string (literal phase; struct planned)

Generated C prelude (codegen_c / tmodc.h): width typedefs from <stdint.h> for explicit layouts:

int8, int16, int32, int64, uint8, uint16, uint32, uint64 — mapped from int8_tuint64_t. Today a standalone compilation unit may inline these in generated .c; multi-TU builds should #include "tmodc.h" (from the tmodc system module) to avoid duplicate typedefs. These are not TMod-c builtin spellings in source (use width-qualified integer N / cardinal N when implemented, or IMPORT C typedefs).

printf / C varargs: Mod-c integer / cardinal match %d / %u on typical LP64 targets. Foreign APIs using size_t or long still require correct C types and format specifiers at the boundary.

Deferred: width-qualified integer N / cardinal N; bits(n) syntax.

Library types use distinct names (StringBuffer, TType, etc.) and are not part of this list.

Related C-interop type: ^char (or a typedef such as pchar) is a pointer to a NUL-terminated C string. It is separate from builtin string. There is no implicit conversion between string and ^char; use string::cstring(s) (borrow) or a stdlib copy helper when a persistent C pointer is required.


Builtin type: string

string is an immutable builtin type. It is part of the language grammar and receives a small closed set of builtin operations. Richer text processing (StringBuffer, format, split, trim, etc.) lives in the standard library — shipped with the compiler but not defined in this grammar.

Representation (implementation / codegen — not user-writable syntax)

The compiler lowers string to a structure conceptually equivalent to:

/* conceptual — fields are not directly accessible in Mod-c source */
struct {
    integer length;   /* signed; byte count; invariant: length >= 0 */
    char    data[];   /* UTF-8 payload; data[length] == '\0' always */
};

Invariants (enforced in semantics and/or codegen):

Users do not declare or access length / data in source. The type is opaque at the language level.

Literals

string-literal (see EBNF) constructs a builtin string. The empty literal "" is valid.

let msg: string = "hello"
const GREETING: string = "hello"    (* const-expression: literal only in phase 1 *)

Declarations and assignment

Builtin operators (closed set)

These use existing expression productions; semantics restrict them to string:

Form Semantics
a == b, a <> b Content equality / inequality (<> same as !=).
s[i] Indexing: s is string, i is integer → type char. Bounds-checked in semantics. No s[i] := ... (immutable).

Not builtin on string (stdlib or future language revision):

Builtin LEN on string

Byte count uses the standalone builtin LEN(designator), parallel to SIZEOF(...) — not a receiver method. On string, LEN(s) returns the byte length (integer, O(1)). See Builtin LEN (below) for generic rules (fixed arrays, etc.).

Builtin type-qualified operations (closed set)

Phase 1 defines one builtin qualified method on string:

Call Result Semantics
string::cstring(s) const ^char string::cstring(self: string); borrow data. Valid for the lifetime of s. For pointers that must outlive s, use a stdlib copy or arena-pinned helper.

No other string::... builtins are defined in phase 1.

Conversions

From → To Rule
string-literalstring Implicit (literal typing).
string^char Only via string::cstring(s) (explicit borrow).
^charstring Not builtin — stdlib (string_from_z, etc.).
charstring Not builtin in phase 1 — stdlib or future builtin.
stringinteger / real Not builtin — stdlib parsers.
unrelated types No implicit conversion (see Overload Resolution).

NIL

Whether NIL is a valid value for string is implementation-defined in phase 1. If permitted, string::cstring(s) on NIL is an error. Prefer non-nil "" for empty strings.

Standard library (not grammar)

The following are explicitly outside the language grammar:

Stdlib modules may IMPORT normally and return builtin string from functions such as to_string().

Example

procedure greet(msg: string)
begin
    require LEN(msg) > 0
    printf("%s\n", string::cstring(msg))
end

Builtin LEN

LEN is a generic language builtin (case-insensitive keyword + primary expression), not a library function and not a receiver method. Syntax parallels SIZEOF and INC / DEC (Pascal heritage — few reserved spellings, case-insensitive).

len-expression = "LEN" "(" designator ")" ;

LEN vs SIZEOF

Builtin Question answered
LEN(designator) How many elements (or string bytes)? Logical length for bounds and APIs.
SIZEOF(designator) How many bytes in memory? C sizeof semantics.

For a fixed array array[N] of T: LEN(a) is N (element count); SIZEOF(a) is N * sizeof(T) (bytes).

Unlike C, users should not rely on SIZEOF(a)/sizeof(T) in source — use LEN for element count.

Semantics by operand type

Operand LEN(...) result Phase
string Byte length (integer); UTF-8 code units; O(1) from internal storage 1
Fixed array[N] of T Element count N (integer, compile-time constant from type) 1
Open array of T (interim) Undefined — lowers to ^T; user tracks length separately interim
Open / dynamic Mod-c array (managed) Element count at runtime from implementation metadata later
^T / C pointer / foreign pointer Undefined — no element count; not a Mod-c array

Arrays and length metadata (design)

Mod-c does not decay fixed arrays to pointers (unlike C). A value of type array[N] of T remains an array for typing, bounds checking, and LEN.

Open arrays (interim — phase 0)

Until managed dynamic arrays (descriptor + New / NIL release) are implemented, an open array declaration is a pointer sugar in codegen — same practical model as ^T, with manual C heap lifetime.

Syntax (interim): var n: array of integer (and equivalent open forms such as array[] of T — spelling to align in implementation).

Codegen: lowers to T* in generated C (e.g. integer* n), identical to var n: ^integer for C purposes.

User allocation / release (manual — not Mod-c managed):

var n: array of integer
n := malloc(100 * SIZEOF(integer)) as ^integer   (* cast / API spelling TBD *)
...
free(n)
n := NIL    (* pointer = NULL only; does NOT call free — user must free first *)

Interim rules:

Feature Interim array of T
LEN(n) Undefined / error — no compiler-known length; keep a separate integer count
Bounds checks on n[i] No (unchecked pointer indexing, if permitted)
n := NIL Sets pointer to NIL; no automatic free
Scope exit No automatic release — user must free or leak
C interop Pass n directly as T*; pass length as a separate argument
vs ^T Same C lowering; array of T documents “buffer, length elsewhere” intent

This phase is intentionally not GC and not compiler-owned storage. It allows Mod-c code to use malloc/free while the full descriptor + New / assign-NIL / scope-finalize model is developed.

Later: the same Mod-c surface syntax may lower to an owning descriptor; interim pointer lowering will be superseded — document breaking-change notes in release notes when switching.

Future bounds-checked indexing (a[i]) requires the implementation to know the element count:

C interop: Values arriving as ^T or ^char from C have unknown length to Mod-c. They are pointers, not arrays. Use an explicit length parameter (len: integer), a string value, or a stdlib helper — not LEN(p).

Allocation (later, not grammar keywords): Dynamic arrays (array[] of T / open forms) are created via a small standard API (e.g. Oberon/Pascal-style New(T, n) or type-qualified Type::new(n)), lowering internally to heap allocation (malloc or arena). Details TBD.

Release — assign NIL to VAR (planned): Owning dynamic arrays are held in VAR bindings. Assigning NIL releases heap storage held by the previous value (finalize-on-assignment):

var n: array[] of integer
n := New(integer, 100)
...
n := NIL    (* frees prior allocation; n becomes empty / nil array *)

Semantics (implementation):

Mod-c is not GC: release is deterministic on assignment and scope end, not automatic collection.

C interop (dynamic, later): While allocated, pass payload to C via a type-qualified borrow helper (e.g. TArray::data(n)^T) plus LEN(n) — analogous to string::cstring(s) on string. @n is the descriptor address, not generally the element pointer.

Examples

let msg: string = "hello"
let n: integer = LEN(msg)          (* 5 *)

var buf: array[10] of char
let count: integer = LEN(buf)      (* 10 — element count *)

Future

Open and dynamic arrays will use the same LEN(designator) form; semantics and descriptors extend without new syntax. Bounds checks on a[i] use LEN(a) or the compile-time bound for fixed arrays.


Overload Resolution

Mod-c supports function overloading based on both function name and parameter types.
Overload resolution follows a strict, predictable order designed for clarity and safety.

1. Name Lookup

The compiler first looks for any visible function, procedure, or conversion whose name exactly matches the called name (case-sensitive, except for control-flow keywords).

If no function with the given name exists in the current scope (including imported modules), a compilation error is raised:

No function named ‘foo’ in scope.

2. Signature Matching (among candidates with the exact name)

Among all candidates with the matching name, the compiler selects the best match according to the following priority order:

  1. Exact Match (highest priority)
    All argument types match the parameter types exactly.
  2. Safe Widening (only if no exact match exists)
    Allowed only within the same type family and only in the widening direction (smaller → larger bit width). Narrowing conversions (larger → smaller) are never performed implicitly and will produce a warning or error.
  3. No other implicit conversions
    There are no implicit conversions between unrelated types (integer ↔︎ real, real ↔︎ string, integer ↔︎ bool, etc.).

If more than one candidate matches at the same priority level, the call is ambiguous and a compilation error is raised.

3. Type Conversions via Function Calls

Conversions can be performed by calling a function whose name is the target type (resolved after semantic analysis).

4. Type-qualified method calls (Type::name)

Methods are declared as Type::name(...) (see Type-qualified methods). Resolution uses the same overload rules as other calls, keyed on the mangled C name Type__name internally.

Deferred: obj.method(args) (dot instance sugar). When implemented, resolves via static type of obj to T::method and lowers to T__method(obj, args...). Ambiguity error if both a callable field and T::method exist (see Type-qualified methods).

Field access uses . without (; type-bound calls use Type:: in phase 1.

5. Assignment and Narrowing

Implicit narrowing on assignment (smaller := larger) is not allowed.

6. Error Messages

The compiler shall produce clear, helpful error messages, including:

Example: No matching function for call to print(real) Candidates: print(string), print(integer) Note: implicit conversion from real to integer is not allowed. Use integer(floor(r)) or define print(real).

INC / DEC

INC and DEC are first-class expressions in Mod-c (a deliberate extension over classic Pascal/Modula-2).

Additional Notes (for your internal reference)

Cast / Conversion Operator AS

ASSERT

C11 only has assert() defined. I will need to build my own custom assert function that emulates the C11 version, in order to handle assert(, hint).

DIV

DIV always performs integer division. Both operands are cast to currently (long) before / is applied.

_ Digit Seperator

The digit seperator _ is used for convience to group digits into thousands or other grouping. It is ignored in processing.

✅ Mod-c Parameter Passing Model — Official Summary (0.9.13)

Design Goals


1. Core Rules

Declaration Binding mutable? Members mutable? Auto-deref on dot Call site syntax C codegen equivalent
x: T (bare, default) No Yes Yes Foo(x) T* x (REF semantics)
VAR x: T Yes Yes Yes Foo(VAR x) T* x
REF x: T No Yes Yes Foo(REF x) T* x
CONST x: T No No Yes Foo(x) or Foo(CONST x) const T* x

Key distinction:


2. Formal Grammar

parameter-def = [ "VAR" | "REF" | "CONST" ] identifier ":" type-specifier ;

actual-parameter = 
      expression                    (* defaults to REF semantics *)
    | "VAR" expression              (* mutable alias + rebinding allowed *)
    | "REF" expression              (* explicit reference *)
    | "CONST" expression ;          (* read-only reference *)

Function Declaration Example:

procedure Example(
    a: integer,                    (* REF by default: members mutable, binding immutable *)
    VAR b: integer,                (* full mutation + rebinding allowed *)
    REF c: pchar,                  (* explicit REF, same as bare *)
    CONST d: ^Node,                (* fully read-only... OR *)
    d: const ^Node                 (* TODO: Same as above? *)

);

3. Complete Examples

Example 1: Basic Usage

procedure Swap(VAR a: integer, VAR b: integer)
begin
    var temp: integer := a;
    a := b;
    b := temp;
end

// Call site
var x := 10;
var y := 20;
Swap(VAR x, VAR y);   // mutation is clearly visible

Example 2: Pointer Management (destroy)

TODO: If n := nil on some types for auto-free, what happens in the below example of free(n)?

procedure destroy(VAR n: ^Node)
begin
    if n != nil then
        free(n);
        n := nil;           // This works because of VAR
    end
end

// Usage
var root: ^Node;
...
destroy(VAR root);      // root becomes nil after call

Example 3: REF for pointer semantics

procedure ProcessBuffer(REF buf: pchar, len: size_t)
begin
    buf[0] := 'X';          // OK - modify through pointer
    // buf := something;    // Error: cannot rebind REF parameter
end

// Call site
var buffer: array[256] of char;
ProcessBuffer(REF buffer, 256);

Example 4: Mixing All Modes

procedure Complex(
    value: integer,           // immutable
    VAR count: integer,       // mutable
    REF buffer: pchar,        // pointer (immutable binding)
    CONST data: ^Node         // read-only pointer
)
begin
    count := count + 1;
    buffer^ := 'A';
    // value := 99;     // Error
    // buffer := nil;   // Error
    // data := nil;     // Error
end

4. Why This Design Is Strong

Advantages:

Escape Hatches (still fully supported):


5. Compiler Behavior Summary


This model is now considered the official parameter passing design for Mod-c.

It balances Pascal heritage, modern safety, performance, and C compatibility exceptionally well.

Can store this summary directly in other documentation (e.g., grammar.md or a new parameters.md file).

(* Parameter Passing Semantics (Mod-c 0.9.13)

parameter-def = [ “VAR” | “REF” | “CONST” ] identifier “:” type-specifier ;

Rules:

Bare / REF p: T   - Reference semantics (default). Auto-deref on dot.
                    Can mutate members. Cannot rebind the parameter.
VAR p: T          - Full mutability. Can rebind (if ^T). Can mutate members.
CONST p: T        - Read-only reference. Members immutable. Auto-deref on dot.

REF and CONST (and bare) accept both values and pointers. The compiler takes the address when a value is passed to REF or CONST. *)