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:
.mc (sole
author-edited source type).c, .h (for
the C compiler).mh (for
modc only — machine-readable symbol exports; not
user-authored)Status: evolving design — simplified, safety-oriented, heavily influenced by Pascal/Modula-2/Oberon-07/C + modern touches
DIV. Update type
definitions to include compound types._ digit
seperator. Solidify parameter passing rules.codegen_c_switch_stmt +
verification. Comma labels emit as cascaded case labels;
ELSE (required) becomes default; explicit
break after every arm; tests/test_switch.mc
runs correctly end-to-end.:= and compounds). Assignments
are statements only (matching Pascal/Modula-2/Oberon and safety
goals).(
and ) around expressions forrequire,
ensure, invariant, and assert.
Emphasize that these are language level keywords, not macros or
functions, similar to return. -18 June 2026: Updates for
extern on procedures and variables. -19 June 2026: Add
POINTER TO syntax in addtion to the existing ^
syntax. Cast syntax of type(expr) removed from grammar.
Stick with the two current methods. User can simulate function style
casts if wanted.UNION as a keyword.string, closed builtin operations;
StringBuffer explicitly stdlib-only. Revise: keep
== / <> only (no + concat,
no ordering); LENGTH() as generic builtin (like
SIZEOF); string::cstring as type-qualified
builtin (see 4 July 2026 for call syntax).TName,
StringBuffer_ prefixes).Type::name(...)
definitions, Type::name(...) calls, :: →
__ C mangling. Rename builtin LENGTH →
LEN; add arrays/length metadata (no array→pointer
decay; ^T length unknown; future bounds checking). Dynamic
array release: VAR assign NIL
finalizes heap storage (planned). Open arrays
(interim): array of T lowers to T*;
manual malloc/free. (Instance call syntax
revised 4 July 2026: drop obj:name, plan
obj.name.).mc source type; generated
.mh export tables; IMPORT … FROM symtab rules
(.mh vs .h);
EXTERN TYPE|VAR|PROCEDURE|FUNCTION; minimal builtin
type set; remove DEFINITION as a
reserved keyword. See IMPORT section for
qualident FROM (0.24.5) and deferred IMPORT *
/ AS.enum-typeSET —
document deferred Pascal-style sets; prefer cardinal
bitmasks.FOR semantics
(bound evaluated once; control var read-only). (WHILE
… BYFORWARD keyword.obj:method() sugar (clashes with
switch/case/? :); instance calls
use . (deferred).
Type::method(...) only in phase 1; lowers
to Type__method(...). Struct layout
independent of Type:: declarations; methods may be declared
in any module for an existing type. Ambiguity error
when callable field and Type::method share a name. Struct
fields: unique names (no C++ member/function homonyms).Type::name declarations and Type::name(...)
calls in parser, semantics (semantic_mangle_method, mangled
symtab registration), and codegen; removed postfix
obj:method() from parser; string::cstring(s)
call syntax documented throughout; tests/test_method.mc
end-to-end. VAR output parameters
workaround: TProcSymName struct return until
VAR formal-parameter codegen is complete.CONST vs LET vs
parameter CONST — document compile-time
CONST (Pascal/Oberon), runtime LET, and
parameter qualifiers (VAR / bare / REF /
CONST); clarify that CONST declarations are
not C read-only objects; note intended C lowering (enum /
#define / literal fold) vs current
static const codegen gap.; for multiple
on one line; indentation not significant). Expand EOS exceptions:
bracketed expressions, trailing continuators, anchor-terminated forms
(IMPORT … FROM), explicit \. Comma-separated
lists may span lines without \.integer /
cardinal lowering (0.24.3) — everyday
signed/unsigned types lower to C int /
unsigned int (Oberon-like 32-bit on typical LP64 targets),
not intptr_t / uintptr_t. Pointer-sized or
size_t work: width-qualified integer N /
cardinal N (planned) or IMPORT from C.
Document generated width typedefs
(int8…uint64) in prelude.parse_unary and consume layout skips;
RETURN newline exception; unreachable code after early
exits is fatal. See End-of-statement rules prose.IMPORT FROM without
items — include-only form; optional import-item
list in EBNF; distinct from deferred IMPORT *.src/ until subdirectory build lands.FROM (0.24.5)
— import-source = string-literal | qualident implemented;
unquoted qualident names a Mod-c module (.mh symtab +
.h #include); quoted string unchanged for
foreign/C and explicit paths. Resolution relative to compiling unit
directory. Project-root src/ search paths,
IMPORT *, and IMPORT … AS … remain
deferred.T prefix). Compiler binary and system module:
tmodc; shared C ABI header:
tmodc.h. Mod-c /
modc remain during transition (repo paths,
binary alias, modc-mh/1 format id unchanged for now).
Domains: tmodc.com, tmodc.org, tmodc.net..mh export enum name complete; enum members
also as export const name complete;
MH_KIND_ENUM in mh_exportkind.h.WHILE … BY not
planned — iteration step via defer at the start of
WHILE / LOOP body (canonical idiom; see
defer-stmt and loop notes below).program hello()
import printf from "stdio.h"
begin
printf("Hello from Mod-c!\n")
end
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::append →
TStringBuffer__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 (int8…uint64),
nil, and other shared C ABI typedefs are moving into
tmodc.mc → tmodc.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.
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.
.mh symtab +
.h ABI). The spelling tells the reader which world you are
in.IMPORT FROM with an empty
item list.IMPORT … FROM "*.h" registers names and emits
#include "…"; optional EXTERN refines kind and
signature..mh: full
exported symtab when available; generated C always
#includes the paired .h,
never .mh..mh and
qualident module paths resolve relative to the directory of the
compiling .mc file (same directory as the current
source unit). Project-root-relative paths from
src/ (Java-style packages) remain planned
until subdirectory compilation and module search paths exist in the
Makefiles.IMPORT * FROM … (bulk symtab import from .mh),
IMPORT … AS …, IMPORT module without
FROM.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.
SETMod-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 scopeimport-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:
import-item (when present) registers one
spelling in the current scope (case-sensitive
identifier rules). An empty list registers nothing.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 |
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.
Multiple imports of the same FROM
path should merge includes (one #include per distinct
path).
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 "".
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):
.mc file
(absolute paths starting with / are used as-is). Same rule
for "symtab.mh", "fixtures/minimod.mh", and
"symbols/simkind.h".. maps to one path
component; the last segment is the file stem. Dots become slashes; modc
appends .mh for semantic analysis and .h for
codegen:
symtab → symtab.mh / symtab.h
(beside the importer, or under a relative subdirectory)fixtures.minimod → fixtures/minimod.mh /
fixtures/minimod.hcodegen.codegen_header →
codegen/codegen_header.mh /
codegen/codegen_header.hImport 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.
src/ resolution —
qualidents and quoted project paths resolving from a fixed module root
(not only the compiling file’s directory). Requires Makefile
subdirectory compilation and module/include search paths.IMPORT * FROM … — bulk symtab import
from .mh.IMPORT … AS … — rename imported
spellings.IMPORT module without
FROM.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 symbolsEXTERN 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:
IMPORT x FROM "foo.h" — name exists, kind minimal,
include foo.h. Qualident FROM always targets a
Mod-c .mh module, not a bare foreign header.EXTERN TYPE|VAR|PROCEDURE|FUNCTION —
refines the same spelling in the current scope.EXTERN or .mh signature wins for
checking when both apply.EXPORT —
generate .mh for other Mod-c unitsEXPORT 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:
module_name.c / module_name.h — for Cmodule_name.mh — export symbol table (generated, not
hand-edited).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.
bool, byte, char,
integer, cardinal, real,
string only.IMPORT … FROM — symtab +
#include "path"; remove hard-coded stdio/stdlib mapping in
codegen.EXTERN TYPE | VAR | PROCEDURE | FUNCTION
— parse, symtab refine, codegen..mh writer on EXPORT
compile..mh reader on
IMPORT … FROM Mod-c module sources (qualident or
"*.mh").import … from "….h"
blocks.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.
| 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.
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:
type-identifier is usually a struct
type (TStringBuffer, TType). Builtin types
(string) may have compiler-provided qualified methods
(e.g. cstring).self parameter — not a keyword; by
convention the first parameter of an instance method is
the receiver. Semantics require its type to match
type-identifier on the left of
::.self parameter;
called as Type::name(...) with no hidden receiver.VAR / REF / CONST /
bare on self follow normal parameter rules.MODULE (or PROGRAM where applicable), often
EXPORTed.Removed: Oberon-style
procedure (sb: TStringBuffer) append(...) and Lua-style
obj:method(...) are not in the
grammar.
:: 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.
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.
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.
Builtin types may define compiler-provided qualified methods:
string::cstring(self: string) — call as
string::cstring(msg); lowers to
string__cstring(msg).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.
| 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) |
(*
* 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 } ... "*)"
(***************************************************************************)
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 / matchEOS —
match 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).
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_t … uint64_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.
stringstring 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.
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):
length is the byte length of the
payload (UTF-8 code units), not a Unicode code-point count.length >= 0. A negative length is a compiler/runtime
logic error (signed length catches overflow on size arithmetic).data[length] is always NUL
(#0). Every string is therefore NUL-terminated
for C library interop.string values are immutable: no
statement may modify the bytes of an existing value; only
rebinding a VAR to a new
string is allowed.Users do not declare or access length /
data in source. The type is opaque at the language
level.
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 *)
string may appear in type-specifier
(parameters, returns, VAR / LET /
CONST, fields).LET bindings are runtime-immutable; CONST
declarations are compile-time constants (see § CONST, LET, and
immutability).VAR s: string may be rebound
(s := other) but the content of a bound value is never
mutated in place.StringBuffer), which produce a new builtin
string via to_string().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):
+ concatenation — always implies
allocation and ownership policy; deferred to stdlib
(e.g. StringBuffer + to_string(), or
concat(a, b)). No simple implementation rule that stays
honest in generated C without a full allocation story.<, >,
<=, >=) — locale, normalization, and
collation are unresolved; may be added later or live only in stdlib with
explicit locale parameters.+= on string content.FOR ident IN string iteration in the grammar (stdlib
/ future).LEN on
stringByte 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.).
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.
| From → To | Rule |
|---|---|
string-literal → string |
Implicit (literal typing). |
string → ^char |
Only via string::cstring(s) (explicit
borrow). |
^char → string |
Not builtin — stdlib (string_from_z,
etc.). |
char → string |
Not builtin in phase 1 — stdlib or future builtin. |
string → integer / real |
Not builtin — stdlib parsers. |
| unrelated types | No implicit conversion (see Overload Resolution). |
NILWhether 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.
The following are explicitly outside the language grammar:
StringBuffer (or equivalent mutable builder) and
concat /
+-equivalent helpersformat, join, split,
trim, replace, line I/O helpersstring_to_integer, etc.)Stdlib modules may IMPORT normally and return builtin
string from functions such as to_string().
procedure greet(msg: string)
begin
require LEN(msg) > 0
printf("%s\n", string::cstring(msg))
end
LENLEN 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.
| 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 | — |
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.
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:
N);
checks may be constant-folded.len field wrapping the data buffer).
LEN(a) reads that field. Length is not
stored in a[0] (contrast Pascal short strings).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):
n := NIL — free (or
equivalent) on the old descriptor’s data buffer; clear length / mark
unallocated.n := New(...) or
n := other — finalize the
old value before storing the new one (avoid leaks;
prefer move from other where
possible).VAR
dynamic array leaving scope is finalized the same way (implicit release
at end of block / procedure), unless arena-owned with explicit lifetime
rules.LET — not used for owning heap dynamic
arrays (no later := NIL); use
VAR or non-owning views.Dispose(n) (stdlib) may be provided as
readability sugar; behavior is n := NIL
(no separate FREE keyword).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.
let msg: string = "hello"
let n: integer = LEN(msg) (* 5 *)
var buf: array[10] of char
let count: integer = LEN(buf) (* 10 — element count *)
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.
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.
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.
Among all candidates with the matching name, the compiler selects the best match according to the following priority order:
byte → char →
integer → longintfloat → doubleinteger ↔︎ 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.
Conversions can be performed by calling a function whose name is the target type (resolved after semantic analysis).
This follows the exact same overload resolution rules as other calls.
Built-in scalar conversions (integer,
real, char, string,
bool, etc.) are provided by the compiler.
User-defined conversions must be explicitly declared as:
function TargetType(source: SourceType): TargetTypeType::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.
Type::name(args)
— matches Type::name with the given arity;
no implicit receiver
(TStringBuffer::new()).Type::name(self, args...) — same callee; self
is an ordinary first argument
(TStringBuffer::append(sb, "hi")).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.
Implicit narrowing on assignment (smaller := larger) is not allowed.
The compiler shall issue a warning (or error, depending on strictness mode) when a larger type is assigned to a smaller type.
Narrowing requires an explicit conversion function, e.g.:
i := integer(floor(r)) -- or a user-provided narrowing conversionThe 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 and DEC are first-class expressions in
Mod-c (a deliberate extension over classic Pascal/Modula-2).
They may be used as statements:
INC(x)
DEC(y, 5)They may also be used inside expressions (returning the new value):
z := INC(x)
a[i] := DEC(a[i], 2)
INC(INC(x)) (* TODO: INC will need to know that (INC(...)) **isn't** a REFerence value. *)AS
or by calling a function named after the target type.Type::name) and module free
procedures are resolved with the same overload rules where
applicable.Type::name →
Type__name in generated C (stable library ABI). Module free
procedures use module_verb mangling.ASexpression AS TargetType[] () .)expression to
TargetType.
AS and may warn.AS can be used with built-in scalar types
(integer, real, char, etc.) and
user types.ASSERTC11 only has assert(
DIVDIV always performs integer division. Both operands are
cast to currently (long) before / is applied.
_ Digit SeperatorThe digit seperator _ is used for convience to group
digits into thousands or other grouping. It is ignored in
processing.
| 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:
VAR lets you do x := something
(rebind or treat as mutable alias to caller).REF, and VAR all allow
x.field := val (member mutation).CONST protects both the binding and the
members.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? *)
);
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
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
REF
for pointer semanticsprocedure 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);
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
Advantages:
VAR allows rebinding), while
members are naturally mutable unless CONST (matches C/C++
expectations).VAR / REF /
CONST.VAR for full
alias/rebinding, REF/bare for reference use,
CONST for read-only.VAR/REF → T* in generated
C).Escape Hatches (still fully supported):
@expr — address-of operator^expr — dereference operatorREF parameters → passed as pointer (or address
taken), auto-deref on dot, members mutable.VAR parameters → passed as pointer, full mutation +
rebinding of the parameter allowed.CONST parameters → passed as const T*, no
mutation of members.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. *)