This note records the design discussions from 2026-10-25 so the
questions do not have to be re-derived later. It covers TBITSIZE,
address arithmetic, set ordering, complex types, pragmas, PIM versus ISO
coroutines, external assembly, and the dynamic-set design. It is not a
commitment to implement everything.
State at the time of writing: suite 310/310, one-shot fixpoint OK
(3,845,439 bytes), per-object fixpoint OK (11 objects), corpus
521/612 compile-OK, 0 crashes, 64 LANG and 27 LIB.
TBITSIZETBITSIZE is a GNU Modula-2 SYSTEM procedure function:
PROCEDURE TBITSIZE (<type>) : CARDINAL;
It returns the minimum number of bits needed to represent a type,
not its normal storage size in bytes. It is mainly useful for packed
records/bitfields. A BOOLEAN may be one bit logically but occupy a
word in an unpacked record.
M2WIDESET).SIZE/TSIZE report storage size in bytes.TBITSIZE(BYTE) = 8 works in CONST;It is a small-to-medium language gap, not a runtime feature.
ADDRESS arithmeticISO SYSTEM defines:
PROCEDURE ADDADR (addr: ADDRESS; offset: CARDINAL): ADDRESS;
PROCEDURE SUBADR (addr: ADDRESS; offset: CARDINAL): ADDRESS;
PROCEDURE DIFADR (addr1, addr2: ADDRESS): INTEGER;
PROCEDURE MAKEADR (high: <type>; ...): ADDRESS;
V3 currently supports:
ADRADDRESS/pointer assignmentADDRESS/pointer equalityVAL/CAST for compatible scalar representationsV3 does not support:
ADDADR/SUBADR/DIFADR/MAKEADRADDRESS + offset, ADDRESS - offset, ADDRESS - ADDRESSPOINTER TO valuesADDRESS is an opaque pointer-like value in V3's type system. Raw
pointer arithmetic is not part of the PIM/ISO base language and would
weaken V3's separation between typed pointers and addresses.
Add the ISO SYSTEM functions first, not general pointer arithmetic:
ADDADR/SUBADR;DIFADR returning the byte difference (range-checked for INTEGER);MAKEADR;For external C/assembly interaction, a C helper remains the pragmatic escape hatch.
PIM/ISO set operations are:
=, inequality #, membership IN;+, difference -, intersection *, symmetric difference /.There is no standard <, <=, >, >= ordering for sets. GNU
Modula-2 adds inclusion ordering:
a <= b (* a is a subset of b *)
a < b (* a is a proper subset of b *)
a >= b (* a is a superset of b *)
a > b (* a is a proper superset of b *)
V3 currently rejects these; corpus case largeset5 remains a LANG
failure.
Fixed/inline sets:
a <= b iff (a AND NOT b) == 0 for every word
a < b iff a <= b AND a # b
Dynamic sorted-range sets:
a <= b iff every range of a is covered by a range of b
a < b iff a <= b AND a # b
Compiler work:
SymTab.RelCheck/OrdCheck: accept set ordering;Lower: branch fixed/dynamic set ordering;QbeGen: subset/superset comparisons;largeset5 would likely move from LANG to compile-OK.This is a safe gm2 compatibility extension, not standard PIM/ISO.
Low-level SYSTEM primitives:
TYPE PROCESS;
PROCEDURE NEWPROCESS (body: PROC; workspace: ADDRESS;
size: CARDINAL; VAR p: PROCESS);
PROCEDURE TRANSFER (VAR from: PROCESS; to: PROCESS);
PROCEDURE IOTRANSFER (VAR from, to: PROCESS;
interruptNo: CARDINAL);
IOTRANSFER uses an explicit interrupt number.V3 implements this with ucontext; IOTRANSFER currently degrades to a
normal transfer because there is no interrupt subsystem.
COROUTINESDEFINITION MODULE COROUTINES;
TYPE
COROUTINE;
INTERRUPTSOURCE = CARDINAL;
PROTECTION = [UnassignedPriority .. 7];
PROCEDURE NEWCOROUTINE (procBody: PROC; workspace: ADDRESS;
size: CARDINAL; VAR cr: COROUTINE;
[initProtection: PROTECTION]);
PROCEDURE TRANSFER (VAR from: COROUTINE; to: COROUTINE);
PROCEDURE IOTRANSFER (VAR from: COROUTINE; to: COROUTINE);
PROCEDURE ATTACH (source: INTERRUPTSOURCE);
PROCEDURE DETACH (source: INTERRUPTSOURCE);
PROCEDURE IsATTACHED (source: INTERRUPTSOURCE): BOOLEAN;
PROCEDURE HANDLER (source: INTERRUPTSOURCE): COROUTINE;
PROCEDURE CURRENT (): COROUTINE;
PROCEDURE LISTEN (p: PROTECTION);
PROCEDURE PROT (): PROTECTION;
PROCEDURE TurnInterrupts (to: PROTECTION): PROTECTION;
PROCEDURE ListenLoop;
END COROUTINES.
Main differences:
| Area | PIM | ISO |
|---|---|---|
| Context type | PROCESS |
COROUTINE |
| Creation | NEWPROCESS |
NEWCOROUTINE + optional protection |
| Switch | TRANSFER |
TRANSFER |
| Interrupt transfer | IOTRANSFER(..., interruptNo) |
IOTRANSFER(from,to) with attached source |
| Current context | none | CURRENT |
| Interrupt registration | none | ATTACH/DETACH/IsATTACHED/HANDLER |
| Protection/priority | none | PROTECTION, LISTEN, PROT, TurnInterrupts |
| Event loop | user-written | ListenLoop |
V3 has PIM coroutines but no ISO COROUTINES module. A pure
coroutine subset (NEWCOROUTINE, TRANSFER, CURRENT, workspace
management) could map onto the existing ucontext layer. The real
interrupt/protection parts need an OS signal/event layer.
ASMExternal assembly works today through EXTERNAL and the normal linker;
no compiler support for inline ASM is required.
DEFINITION MODULE AsmOps;
PROCEDURE Add (a, b : CARDINAL) : CARDINAL;
END AsmOps.
IMPLEMENTATION MODULE AsmOps;
PROCEDURE Add (a, b : CARDINAL) : CARDINAL;
EXTERNAL;
END AsmOps.
.text
.globl Add
.type Add,@function
Add:
movl %edi, %eax
addl %esi, %eax
ret
cc -c asm_add.s -o asm_add.o
M2 -c AsmOps.def AsmOps.mod
qbe -o AsmOps.s AsmOps.ssa
cc -c AsmOps.s -o AsmOps.o
cc Prog.o AsmOps.o asm_add.o runtime/syslib/shim.c -o Prog -lm
For M2make --separate, add the assembly object with --extra
asm_add.o or link manually.
amd64_sysv.INTEGER/CARDINAL are 32-bit; LONGINT is 64-bit.l values.VAR parameters receive addresses.ARRAY OF CHAR, are passed as descriptor
addresses, not raw C arrays.For anything beyond scalar arguments, use a small C wrapper around the assembly routine so the V3-facing signature is C-compatible.
This is the recommended route for low-level code; inline ASM is
unnecessary.
SET OF CARDINAL / INTEGER — chosen designV3 now uses a sorted linked list of disjoint inclusive ranges:
set = pointer to
{ lo : CARDINAL; hi : CARDINAL; next : pointer }
This was chosen over a full 2^32-bit bit-vector (512 MiB per set value) and over a linked list of individual bits (too much per-value overhead). The range-list representation is:
INTEGER values map with VAL(CARDINAL, x) XOR 0x80000000.Implemented operations include empty/range literals, INCL/EXCL/IN,
assignment, equality, union/intersection/difference and symmetric
difference. Bounded sets such as SET OF [0..100000] keep the inline
bit-vector form.
SET CONST is rejected (loud 230), because no static
range-list layout exists yet;malloc; assignment frees old lists but there is no GC or
program-exit cleanup;PACKEDSET remains capped at 32 bits.Future work: runtime-initialised dynamic constants, deep composite copy, ownership/lifetime rules, and optionally a hash/paged bitset if range lists become too fragmented.
V3 currently has no complex arithmetic. COMPLEX32, COMPLEX64 and
COMPLEX128 are declaration-compatible aliases of REAL (currently 8
bytes each), so they are placeholders, not real complex types. Their
names imply two 4-, 8- and 16-byte components (8, 16 and 32 bytes
respectively), so the current aliases are also the wrong size. The
comparison table correctly marks COMPLEX/LONGCOMPLEX as unsupported.
ISO Modula-2 treats COMPLEX/LONGCOMPLEX as predefined types.
The language is expected to provide:
+, -, *, /);CMPLX constructor;RE/IM accessors;Transcendental functions live in the separate ComplexMath and
LongComplexMath library modules. The ISO architecture is therefore:
compiler-known COMPLEX/LONGCOMPLEX types
+ operator lowering / builtins
+ ComplexMath / LongComplexMath libraries
A separate library can provide complex-number functions, but it cannot
by itself provide natural Modula-2 syntax such as a + b or CMPLX.
Operator overloading does not exist in Modula-2, so a library-only design
is necessarily a nonstandard utility API.
Classes are a poor fit for complex numbers:
a.Add(b) instead of a + b;CLASS is Clarion-form);RE/IM syntax.Use a value record or a compiler-known type, not a class.
V3 has ISO 10514-2 generics, so a generic implementation could share code between real and long-real variants:
GENERIC MODULE Complex(T);
TYPE Value = RECORD re, im : T END;
...
END Complex.
That is useful for implementation sharing, but generics still do not
provide operators, literals or ISO source compatibility. Since V3's
REAL and LONGREAL are both 8 bytes today, two small concrete modules
are also acceptable and simpler for separate compilation.
For practical complex math without an ISO-conformance claim:
DEFINITION MODULE Complex;
TYPE
T = RECORD
re, im : REAL
END;
PROCEDURE Add (a, b : T) : T;
PROCEDURE Sub (a, b : T) : T;
PROCEDURE Mul (a, b : T) : T;
PROCEDURE Div (a, b : T) : T;
PROCEDURE Conj (z : T) : T;
PROCEDURE Abs (z : T) : REAL;
PROCEDURE Arg (z : T) : REAL;
END Complex.
Advantages: value semantics, no allocation, simple ABI, easy to test.
Limitations: call syntax only; no operator or literal support; does not
satisfy ISO COMPLEX.
Do not call this type COMPLEX or reuse the existing COMPLEX*
aliases, because that would falsely suggest ISO support.
Implement COMPLEX/LONGCOMPLEX as compiler-known value types, with
arithmetic lowered to runtime helpers:
COMPLEX = two REAL values
a + b -> __m2_complex_add(a, b)
a * b -> __m2_complex_mul(a, b)
CMPLX -> compiler builtin
RE/IM -> compiler builtins
Then implement ComplexMath and LongComplexMath as libraries on top.
This avoids requiring QBE to understand a native complex type while still
providing ISO syntax and semantics.
Work items:
COMPLEX is 16 bytes with the current 8-byte
REAL; COMPLEX32 cannot be 8 bytes until a real 4-byte SHORTREAL
exists);CMPLX, RE, IM and constant expressions;abs, arg, conj, sqrt, exp, ln,
trigonometric functions and polarToComplex;For C interop, do not assume the C99 _Complex ABI without checking;
prefer small C wrappers where necessary.
Complex library first, with no classes and no fake COMPLEX names.COMPLEX32/64/128 REAL aliases so programs cannot silently get
wrong-size, non-complex values.V3 has no general pragma facility. It currently parses and ignores two
gm2 declaration pragmas (__BUILTIN__ and __ATTRIBUTE__
__BUILTIN__ (( name ))) after PROCEDURE, and its TopSpeed sidecar
lexes (*# ... *) as an ordinary comment. The local EXCEPTIONS.def
therefore omits <* noreturn *>.
There are two separate design questions:
The recommendation is to separate them: use the portable, scoped model as the native design, and add a narrow TopSpeed compatibility mode.
TopSpeed Modula-2 pragmas are a special form of comment:
(*# save *)
(*# check(range => on, index => off, overflow => off) *)
PROCEDURE P (...);
(*# restore *)
They are stateful compiler directives:
save / restore maintain a stack of compiler settings;They are grouped into pragma classes:
check(...) — range, index, overflow, NIL, stack checks;call(...) — calling convention, register parameters, interrupt
handlers, open-array passing;data(...) — segments, packing, volatile, enum size, stack/heap size;optimize(...) — CSE, register allocation, peephole, CPU choice;module(...) — initialization code, implementation-object rules;name(...) — external symbol naming;debug(...) — debug information and procedure tracing;option(...) — language extensions such as bitwise operations on
cardinals;save / restore — compiler-option stack;link, project, define — project-system integration.Older $ compiler directives are also accepted by TopSpeed for
compatibility.
Strengths
(*# ... *).Problems for V3
reg_param, iopl, memory models, overlays, CPU and
coprocessor selection. These do not map to V3's amd64/QBE target.(*# ... *) is a comment to other compilers, ignoring it
silently changes checks, layout or ABI without any diagnostic.TopSpeed pragmas are therefore useful as a compatibility adapter, not as V3's native pragma model.
The portable proposal (used by Modula-2 R10/BSK) uses <* ... *>:
PROCEDURE Reboot <*NORETURN*>;
TYPE Header = RECORD <*PACKED*>
...
END;
VAR Port <*VOLATILE*> : BYTE;
It defines:
<*v3.SomeOption=...*>;PRESETS / UNSET for external pragma settings instead of a
TopSpeed-style global save / restore;DEPRECATED, FFI, FFIDENT, INLINE and
PRIVATETO, plus ENCODING where Unicode is supported.The full proposal is much larger: ABI, ADDR, ALIGN, PACKED,
PADBITS, LEADBITS, VOLATILE, NORETURN, PURE, WEAK, IN,
OUT, SINGLEASSIGN, ARC, GC, OWNER, BORROWER, RETAIN,
RELEASE, MSG, TELL, TICKET and others. R10 itself omits some of
these (RANGE, RELEASE, RETAIN).
Strengths
<* ... *> aligns with ISO-style/gm2 pragma syntax.Problems
PURE/WEAK/DETM need effect checking, IN/OUT need
parameter-write analysis, NORETURN needs control-flow checks and
SINGLEASSIGN needs data-flow checks.ARC, GC, OWNER, BORROWER,
RETAIN, RELEASE; RESIDENT, CONTIGUOUS and LOWLATENCY are
mostly OS/runtime hints; ENCODING needs scanner/Unicode work.ADDR, ALIGN, PACKED, PADBITS and LEADBITS require real
layout support.| Aspect | TopSpeed V3 | Benjamin / portable |
|---|---|---|
| Status | Vendor dialect | Proposed portable specification, used by R10/BSK |
| Delimiters | (*# ... *) |
<* ... *> |
| Form | class(name => value, ...), save, restore |
PRAGMA, PRAGMA=expr, clause sequences |
| Scope | Stateful from point forward | Attached to explicit syntactic scopes |
| ABI/layout | Many 16-bit-specific directives | Portable ABI/layout annotations |
| Unknown pragmas | Often ignored or comment-like | Standard versus implementation-defined, diagnosable |
| Separate compilation | Needs external project-state discipline | Naturally attachable to symbols and object stamps |
| V3 fit | Compatibility adapter | Native pragma design |
TopSpeed is not a standard; the <* ... *> syntax is the better
long-term direction because it is already used by ISO-style/gm2
compilers and by the portable proposal.
Use <* ... *> with explicit scope attachment as the native pragma
syntax. Do not make (*# ... *) the default; handle it only in the
TopSpeed sidecar or behind an explicit compatibility flag.
Phase 1 — infrastructure and low-risk semantics
<* ... *> and adjacent blocks;DEPRECATED;NORETURN;NOINLINE;INLINE as advisory, with a warning if not honoured;MSG / TICKET diagnostics if desired;PRIVATETO.NORETURN is especially valuable immediately: it lets V3 consume
standard ISO/gm2 definition files without stripping pragmas.
Phase 2 — layout and ABI
PACKED;ALIGN / MEMALIGN;FFI / FFIDENT;ADDR only once absolute variables and address arithmetic are
designed;PADBITS / LEADBITS only after packed-record layout exists.Every ABI/layout pragma must participate in:
M2make --separate.Otherwise two objects can disagree about record layout or symbol names and still link.
Phase 3 — contracts
PURE, WEAK, DETM;IN / OUT;SINGLEASSIGN.These should not be accepted as no-ops. If V3 cannot verify them, it should reject them or emit a clear "accepted but not enforced" warning. Silently pretending to enforce a promise is worse than not supporting it.
Do not implement initially
ARC, GC, OWNER, BORROWER, RETAIN, RELEASE;RESIDENT, CONTIGUOUS, LOWLATENCY;PRESETS / pragma-file machinery;ENCODING.These need runtime support V3 does not have, or are platform hints with little value on the current target.
Keep TopSpeed pragmas out of the default V3 grammar. Handle them in the
existing TopSpeed-V3-M2.atg sidecar, or add a
--topspeed-pragmas mode to the main compiler.
Map only the meaningful subset:
check(range/index/overflow/nil/stack);data(packed/volatile);module(init_code);call(c_conv);save / restore.Reject or warn about:
near_call, far, segments, seg_name, ds_*;reg_param, reg_saved, reg_return;interrupt, iopl;cpu, copro;TopSpeed stateful pragmas can be translated by keeping an option stack during parsing and snapshotting the current state onto each declaration. That is an adapter, not the native model.
v3.* pragma should be a warning or informational message.Otherwise V3 will compile code with different checks or ABI and call it success.
trijezdci/M2-Pragmas wiki, especially Pragma Specification,
Pragma Placement and Pragma Scopes.TBITSIZE (small, standard-facing).ADDADR/SUBADR/DIFADR (small, ISO SYSTEM).largeset5).COMPLEX* aliases.<* ... *> parser/IR plus a low-risk semantic
subset; later add the TopSpeed adapter and ABI/layout pragmas.COROUTINES subset, then interrupt/protection layer.SET CONST and deep composite copies.External assembly needs no compiler work; document it as the supported low-level escape hatch.