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, UTF-8 string
libraries, collections, the TopSpeed B-tree Toolkit, communications,
pyserial, ModBase, network-library design, terminal escape libraries,
C FFI and GTK4/SQLite/SDL bindings, an audit of the existing gm2
bindings, the GTK4 OOP facade, 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.V3 has good language-level Unicode support but only a thin library layer. The architecture is "UTF-8 at the edges, codepoints internally", not "UTF-8 strings everywhere".
UCHAR — 32-bit code point.UString — codepoint-based literals and ARRAY OF UCHAR values.U'...' / U"..." literals with strict RFC 3629 decoding at
compile time (error 234 on malformed input).UCHR, CHR8, UORD.UString whole-string assignment to a fixed ARRAY OF UCHAR
(count-clamped, zero-codepoint terminated).+ concatenation for UString literals and single-codepoint
operands, plus LEN/LENGTH/HIGH for literals.runtime/syslib/Utf8 — scalar codec:
Decode (one sequence from a byte array to UCHAR and next) and
Encode (one UCHAR to bytes). MaxUTF8Length = 4,
MaxCodePoint = 1114111.lib/iso/TextIO — UTF-8 output:
UWriteChar, UWriteString, UWriteLn.utf8_prog, utext_prog, t_ustrcat, t_ustrassign,
t_unicode, plus malformed-literal rejection tests.Utf8 lives in runtime/syslib and is always shared. TextIO is in
lib/iso, so its U* output helpers are ISO-dialect library API; other
dialects can import Utf8 manually.
| Area | Current state |
|---|---|
| UTF-8 codec | One codepoint at a time only |
| Whole-string validation | Manual Decode loop only |
| UTF-8 input | None |
| UTF-8 output | TextIO.U* only |
| UTF-8 length/index/slice | None |
| UString library | None |
| UString comparison/search | None |
| Dynamic UString | None |
| Unicode case conversion | None |
| Unicode character classes | CharClass is ASCII-only |
| Normalization / collation / grapheme clusters | None |
| Locale/bidi support | None |
lib/common/Strings is byte-oriented:
Length counts bytes, not codepoints;Slice, Pos, Insert, Replace and Delete use byte positions;Capitalize is explicitly ASCII;lib/common/CharClass is ASCII-only; there is no UCHAR version and
no Unicode category lookup.
Input is byte/line oriented. SysIO, STextIO, IOChan, Files and
StreamFile have no UTF-8 decoding or validation policy.
A U"..." literal is a descriptor { count, codepoints... }. A
declared ARRAY OF UCHAR is a fixed-length array. As a result:
LEN/HIGH on a fixed array refer to declared bounds, not the number
of codepoints actually assigned;TextIO.UWriteString writes 0 .. HIGH(u), not just through a zero
terminator;UString concatenation is bounded and transient. The shim uses a static
4096-codepoint buffer, silently truncates beyond that, and the result is
valid only until the next m2ustrcat call. Single-codepoint conversion
uses a small rotating pool (16 descriptors). This is adequate for
literal expressions, not for a general string runtime.
Utf8.Encode documents that it returns 0 for a non-scalar value, but
its implementation does not reject the surrogate range D800..DFFF.
Those values are currently encoded as three-byte UTF-8 sequences, which
is invalid UTF-8. Add the surrogate check and a regression test.
Keep byte strings and codepoint strings as separate layers. Do not make
Strings Unicode-aware in place; existing code expects bytes.
1. UTF-8 byte-string module
For interoperability with files, sockets, C APIs and existing V3
ARRAY OF CHAR strings:
ValidateLength (codepoint count)ByteLengthIndex (codepoint index to byte offset)Next / PrevSliceCompareFindToUStringFromUStringThis is the most practical "UTF-8 string" library for V3 because CHAR
arrays are already the byte-oriented representation.
2. UString codepoint module
For operations over ARRAY OF UCHAR:
First resolve the length model: either consistently use a zero terminator, or introduce an explicit descriptor with separate length and capacity. Do not mix both.
3. Input/output
Add UTF-8 input alongside output:
UReadChar / UReadString (or equivalent);UCHAR/UString values.4. Unicode properties and case
This is a later layer:
These need generated Unicode tables, not hand-written ranges.
Utf8.Encode surrogate rejection and add a test.Strings/CharClass byte/ASCII semantics and add separate
Unicode modules rather than changing existing behaviour.V3 has no collections library. What exists is only infrastructure:
lib/common/Storage — ALLOCATE, DEALLOCATE, REALLOCATElib/gm2/SysStorage — raw allocationlib/gm2/DynamicStrings — heap stringscompiler/tests/showcase23lib.def — DList(T)compiler/tests/showcase24stack.def — GStack(T)Those test modules are proof of concept, not production libraries: they
have no Done/Clear, no ownership policy, and use one NEW per node.
The MyWork/Strings/DataStructures FIFO/LIFO/dynamic-array files are
incomplete and are not part of V3.
The real toolkit is installed with the TS-V3 tree. Useful references:
$HOME/bin/Dos/M2/TS-V3/INCLUDE/BTREE.DEF
$HOME/bin/Dos/M2/TS-V3/SRC/BTREE.MOD
$HOME/bin/Dos/M2/TS-V3/DOC/BTREE.DOC
$HOME/bin/Dos/M2/TS-V3/Manuals/B-Tree toolkit.odt
It is a disk-based B-tree/ISAM toolkit, not an in-memory collections library. Its architecture is:
FHandle)IHandle) in that physical fileAdd/DeleteCompareFunction and KeyFunctionFind/Search (exact and greater-or-equal), Next/Prev/Reset
cursorsAdd, Delete, Change, Open, OpenData, OpenIndex, Close,
Flush, LastError, LastFErrorPack/APACK2The current definition also includes AccessMode values such as
FixSize, Size16, Size32, NoDealoc and Compress.
The source is 16-bit DOS/OS/2 code: inline x86 assembly, segment
arithmetic, TopSpeed pragmas, FIOx, CoreMem, and raw
ADDRESS/ARRAY OF BYTE interfaces. It is a design reference, not a
portable V3 implementation.
Find, Search, Next, Prev, Reset) rather
than exposing pages.These map naturally to V3 generics:
Compare : PROCEDURE(a, b : K) : INTEGER;
Hash : PROCEDURE(k : K) : CARDINAL;
Equal : PROCEDURE(a, b : K) : BOOLEAN;
ADDRESS/ARRAY OF BYTE public interfaces: provide typed
wrappers.SHARE/FIOx locking: use V3 file APIs and POSIX locking later.Keep in-memory collections and the persistent B-tree separate.
In-memory containers
Vector(T) dynamic array
Deque(T) FIFO / double-ended queue
Stack(T) LIFO
HashMap(K,V) hash table with Hash/Equal callbacks
BTreeMap(K,V) optional ordered map/set with Compare
Vector is the foundation. Stack and Deque should reuse it or a
ring-buffer implementation. Avoid one allocation per element unless
stable references are required.
HashMap should use open addressing with tombstones or separate
chaining, a load factor around 0.7, and explicit resize/rehash. Add a
StringMap(V) convenience wrapper with owned string keys.
BTreeMap is the in-memory ordered structure: comparator callback,
first/last/next/prev, range scan, optional duplicate keys, no pager.
Persistent B-tree
BTreeFile — disk-based index, toolkit-inspired
This is a separate library, not part of Vector/HashMap. It needs:
The TopSpeed toolkit is the checklist for this stage.
Init/Done/Clear.Storage.REALLOCATE exists, but arbitrary pointer arithmetic does
not. TopSpeed uses AddAddr/IncAddr/DecAddr assembly helpers;
V3 cannot rely on that.Vector(T) needs one of:
Suggested error model:
TYPE Status = (Ok, Empty, NotFound, InvalidIndex,
OutOfMemory, DuplicateKey);
If enum handling is awkward in a given context, use integer status codes
or BOOLEAN plus LastError.
Vector(T)Deque(T) / Stack(T)HashMap(K,V)StringMap(V)BTreeMap(K,V)BTreeFile based on the TopSpeed architectureThe TopSpeed B-tree Toolkit should not delay steps 1–3.
These are three different things and should not be merged into one design:
All TopSpeed/ModBase source is commercial historical code; use it as a design reference, not copyable source. pyserial is BSD-3-Clause.
V3 has no serial, socket, terminal-emulation, scripting or database library. What exists:
lib/iso/IOChan, StreamFile, Files for byte/line channels;lib/common/SysIO and runtime/syslib/SysShim for stdio/file I/O;EXTERNAL and the C shim for libc access;Storage/SysStorage.References:
$HOME/bin/Dos/M2/TS-V3/SRC/RS232.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/XMODEM.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/YMODEM.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/WXMODEM.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/KERMIT.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/TERM.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/SCRIPTCO.MOD
$HOME/bin/Dos/M2/TS-V3/SRC/SCRIPTPR.MOD
$HOME/bin/Dos/M2/TS-V3/INCLUDE/RS232.DEF
$HOME/bin/Dos/M2/TS-V3/INCLUDE/FTSupport.DEF
$HOME/bin/Dos/M2/TS-V3/Manuals/Modula2-Communication-toolkit.odt
The low-level RS232 driver:
SerialRead timeout in tenths of a second;SerialWrite copies into the TX ring;Limitations:
SelectPort disables the previous handler;InitPort/Install have no error result;Install/UnInstall);The protocol layer:
FTSupport global state and FBuff;The protocol implementations are useful as protocol state-machine
references, but they call RS232 and FIO directly and drive status
windows through CommUtil/Window. They are not transport-agnostic.
Only one transfer can be active at a time.
The toolkit also includes:
SingleStep execution;WAITFOR, WHEN, macros, logging, file-transfer commands.The script VM is a useful design for an application-level automation engine, not a general V3 library requirement.
pyserial 3.5 is installed at:
/usr/lib/python3/dist-packages/serial
Its architecture:
SerialBase plus platform backends;termios, fcntl and select/poll;socket://, rfc2217://, loop://, spy://;list_ports and sysfs metadata;Useful API semantics:
in_waiting / out_waiting;cancel_read / cancel_write;flush waits for transmission to drain;send_break;flock;BOTHER, low-latency mode, RS485 on Linux;Comparison:
| Area | TopSpeed | pyserial |
|---|---|---|
| Target | DOS/OS-2 PC UART | POSIX/Windows OS tty |
| Serial layer | direct UART + IRQ | termios + OS driver + poll |
| Buffering | application ring buffers | kernel buffers + waiting counts |
| Timeouts | tenths, busy wait | read/write/inter-byte, monotonic |
| Cancellation | none | read/write cancellation |
| Multiple ports | one active singleton | multiple instances |
| Discovery | COM1–COM4 | device paths, URLs, sysfs |
| Advanced | hooks | RS485, custom baud, low latency, exclusive |
| Protocols | X/Y/WX/Kermit | none in core |
Recommendation: model V3’s serial layer on pyserial’s feature set, not on TopSpeed’s hardware driver. Keep TopSpeed’s protocol descriptions and state machines as references for optional file-transfer modules.
Core:
Serial POSIX tty transport
Transport read/write/flush/poll/cancel/close interface
Socket optional TCP transport
Loopback deterministic test transport
Protocols and application layers:
XModem / YModem / WXModem optional
Kermit only if required
Terminal optional VT100/ANSI emulation
Script optional application-level VM
The protocol modules must depend only on Transport, never directly on
Serial.
A C shim should implement the POSIX details:
m2serial_open
m2serial_configure
m2serial_read
m2serial_write
m2serial_poll
m2serial_flush
m2serial_set_modem
m2serial_send_break
m2serial_close
Use status codes, not fatal errors. Support:
in_waiting/out_waiting;A blocking read with timeout is sufficient initially. A reader coroutine or event loop can be added later; avoid binding the API to threads.
References:
$HOME/bin/Dos/M2/TS-V3/MODBASE/MODBASE3.DEF
$HOME/bin/Dos/M2/TS-V3/MODBASE/DBINDXES.DEF
$HOME/bin/Dos/M2/TS-V3/MODBASE/DBFIELDS.DEF
$HOME/bin/Dos/M2/TS-V3/MODBASE/LOCKS.DEF
$HOME/bin/Dos/M2/TS-V3/MODBASE/HASH.DEF
$HOME/bin/Dos/M2/TS-V3/Manuals/Modbase 3.odt
ModBase is a dBase III-compatible database engine:
.DBF data files, .NDX indexes, .DBT memo files;Core modules:
ModBase3 data file, record buffer, fields, memo, locks
DBIndxes B-tree index, cursors, build/rebuild, auto update
DBFields typed field access (date, logical, numeric, string)
DBRelate relational lookup through an index
Locks file/range locking and retry
DBIndxes uses 512-byte B-tree nodes, a configurable in-memory node
buffer, a 128-byte key limit, and a maximum depth of 24. Hash is an
opaque byte-key table from Repertoire, not part of the core dBase
engine.
Good ideas to borrow:
DBFile/DBIndex handles;GoTop, GoBottom, Next, Prev);Hash API shape for a generic hash table.Why not port it:
HandleIO, StringIO, LowLevel,
VStorage, ErrorManager, SmartScreen, FAPI) and DOS/OS-2
services;Recommendation:
BTreeFile;Serial POSIX transport with pyserial-inspired semantics.Transport abstraction plus Loopback tests.Socket transport.V3 currently has no socket/network support. lib/iso/IOChan is a
file/stdio-style channel API with no addresses, timeouts, non-blocking
mode or datagrams, so it can be a convenience bridge but not the primary
network API.
The network library should be layered:
Application
HTTP / WebSocket / custom protocols
|
TLS (optional)
|
Transport (connected byte stream or datagram)
|
Net (addresses, DNS, TCP/UDP sockets, listeners)
|
C shim / OS sockets
Transport is shared with the communications work:
Protocol modules should depend only on Transport, never directly on
Socket.
Use opaque handles; do not expose file descriptors or sockaddr.
DEFINITION MODULE Net;
TYPE
Family = (IPv4, IPv6);
Kind = (Stream, Datagram);
ShutdownMode = (StopReading, StopWriting, StopBoth);
Status = (Ok, WouldBlock, Timeout, Closed, Refused,
Unreachable, BadAddress, NotFound,
PermissionDenied, OutOfResources,
NetworkError, UnknownError);
Socket;
Address;
PROCEDURE Resolve(host : ARRAY OF CHAR; port : CARDINAL;
family : Family; kind : Kind;
VAR addr : Address) : Status;
PROCEDURE Listen(addr : Address; backlog : CARDINAL;
VAR listener : Socket) : Status;
PROCEDURE Accept(listener : Socket;
VAR conn : Socket;
VAR peer : Address) : Status;
PROCEDURE Connect(addr : Address; timeoutMS : CARDINAL;
VAR conn : Socket) : Status;
PROCEDURE Read(conn : Socket; VAR buf : ARRAY OF BYTE;
max, timeoutMS : CARDINAL;
VAR n : CARDINAL) : Status;
PROCEDURE Write(conn : Socket; VAR buf : ARRAY OF BYTE;
len, timeoutMS : CARDINAL;
VAR n : CARDINAL) : Status;
PROCEDURE WriteAll(conn : Socket; VAR buf : ARRAY OF BYTE;
len, timeoutMS : CARDINAL;
VAR written : CARDINAL) : Status;
PROCEDURE Shutdown(conn : Socket; mode : ShutdownMode);
PROCEDURE Close(VAR s : Socket);
PROCEDURE LocalAddress(s : Socket; VAR a : Address) : Status;
PROCEDURE PeerAddress(s : Socket; VAR a : Address) : Status;
PROCEDURE AddressToString(a : Address; VAR s : ARRAY OF CHAR);
PROCEDURE LastError(s : Socket) : CARDINAL;
Rules:
Socket and Address are opaque.ARRAY OF BYTE is the binary buffer type; text helpers wrap it.Read and Write may return short counts.WriteAll loops over partial writes with a timeout.WouldBlock is a normal result in non-blocking mode.Closed means orderly shutdown or peer close.LastError returns the OS error code for diagnostics.Do not model addresses as a single CARDINAL IPv4 value.
Use an opaque Address supporting:
Use getaddrinfo, not gethostbyname. A resolver may return several
addresses; Connect should be able to try them in order.
For listening sockets:
AI_PASSIVE for wildcard binds;IPV6_V6ONLY;SO_REUSEADDR for restart;A useful security default: servers bind to loopback unless the caller explicitly asks for a public interface.
Initial limitation: getaddrinfo is blocking and has no timeout. An
asynchronous resolver can be added later.
TCP:
Listen, Accept, Connect;Shutdown;SO_KEEPALIVE and TCP_NODELAY;UDP:
SendTo / RecvFrom;Keep TCP and UDP wrappers separate publicly even if they share a core socket module.
Do not require threads.
Phase 1:
poll in the C shim;Phase 2:
PollSet abstraction:TYPE
Event = SET OF (Readable, Writable, Error);
PROCEDURE Init(VAR p : PollSet);
PROCEDURE Add(VAR p : PollSet; s : Socket; events : Event);
PROCEDURE Remove(VAR p : PollSet; s : Socket);
PROCEDURE Wait(VAR p : PollSet; timeoutMS : CARDINAL;
VAR ready : CARDINAL) : Status;
PROCEDURE Events(p : PollSet; s : Socket) : Event;
Phase 3:
PollSet with PIM coroutines or an event loop;Use poll first; epoll is an optimization, not a public API
requirement.
Use status codes as the default; fatal errors and uncaught exceptions are not the normal path.
The shim must:
EINTR;EINPROGRESS for non-blocking connect;EAGAIN/EWOULDBLOCK to WouldBlock;SIGPIPE with MSG_NOSIGNAL or an ignored signal;Do not implement TLS. Use OpenSSL or mbedTLS through a C shim.
TLS module:
Tls.Connect(transport, hostname, ...)
Tls.Read
Tls.Write
Tls.Close
Defaults:
HTTP is later:
Content-Length;WebSocket is later still.
Start with POSIX/Linux:
socket, bind, listen, accept, connect,
send, recv, poll, getaddrinfo, setsockopt, shutdown,
close;sockaddr_storage, addrinfo, fd_set and pollfd behind the
shim.Later add a Windows Winsock2 backend behind the same API.
Do not expose sockaddr, fd_set, pollfd or raw file descriptors in
Modula-2.
runtime/syslib/net.c POSIX C shim
lib/common/Net.def/.mod addresses, DNS, sockets, status
lib/common/Transport.def/.mod connected byte transport
lib/common/Poll.def/.mod readiness/event API
lib/common/Loopback.def/.mod deterministic tests
lib/common/Tls.def/.mod optional, OpenSSL/mbedTLS
lib/common/Http.def/.mod optional, later
Possible names:
Net — socket/address layerTcp / Udp — protocol-specific wrappersPoll — event readinessTransport — shared with serialTls — optional security layerHttp — optional application protocolUse loopback and ephemeral ports only:
127.0.0.1 and ::1;SIGPIPE / write-after-close;SO_REUSEADDR;No external network is required for the suite.
Net status/address API.Poll and non-blocking sockets.Transport integration with serial/loopback.Do not start with HTTP or TLS. Do not expose raw sockets or
sockaddr. Do not require threads. Reuse the Transport abstraction
so serial, TCP, loopback and TLS share one protocol-facing interface.
The existing MyWork/VT100 repository contains four useful terminal
output libraries:
VT100 strict DEC VT100 escape sequences
VT52 strict DEC VT52 escape sequences
Ansi modern ANSI/xterm superset with colours and modes
Regis ReGIS vector graphics commands
There are 27 tests covering the output sequences, plus a SplitV1
helper used by the CPR (cursor-position report) test.
These modules are escape-sequence emitters. They generate output
such as ESC[2J, ESC[3;7H, and ReGIS commands. They do not parse
incoming terminal data or maintain a screen buffer.
That is the right scope for an output library. A terminal emulator — needed to render incoming VT100/ANSI data in the communications stack — is a separate parser/screen model and should not be confused with these modules.
The modules were written for gm2 -fiso and currently depend on:
STextIO
NumberIO
ASCII
IO
termios
FIO
V3 has STextIO and NumberIO, but the other dependencies are either
missing or not desirable in a portable library:
IO and termios are used only by Unbuffered/Buffered (raw
input for CPR replies);FIO.StdIn is used only for the same raw-input support;ASCII.esc can be replaced locally;NumberIO is PIM-oriented and should not be pulled into a common
library just to emit decimal CSI parameters.The current emitters also write directly to stdout through STextIO,
which prevents using them with a serial or TCP transport. That is the
main architectural change needed.
Add a small output sink:
DEFINITION MODULE TermOut;
TYPE Output = PROCEDURE(s : ARRAY OF CHAR);
PROCEDURE SetOutput(out : Output);
PROCEDURE WriteString(s : ARRAY OF CHAR);
PROCEDURE WriteChar(c : CHAR);
PROCEDURE WriteCard(n : CARDINAL); (* decimal, no padding *)
END TermOut.
Default output can remain STextIO, but it can be redirected to:
Transport;Transport;Then the terminal modules call TermOut.WriteString, TermOut.WriteChar
and TermOut.WriteCard, not STextIO/NumberIO directly.
Move raw-mode input into a separate optional module:
Tty — termios save/restore, raw mode, non-blocking reads
VT100.Unbuffered/Buffered and the equivalents in VT52, Ansi and
Regis should be removed from the emitters and replaced by Tty
operations. This keeps the emitters pure and portable.
lib/common/TermOut.def/.mod output sink + decimal/ESC helpers
lib/common/VT100.def/.mod strict VT100 emitter
lib/common/VT52.def/.mod strict VT52 emitter
lib/common/Ansi.def/.mod ANSI/xterm emitter
lib/common/Regis.def/.mod ReGIS emitter (optional)
lib/common/Tty.def/.mod optional POSIX raw-mode input
lib/common is the simplest location because it is included by all
dialects. A dedicated lib/term folder is possible but would require
build-selection changes.
Regis is specialist vector graphics; include it but mark it optional.
SplitV1 is test-only and should not become a core library module.
Convert the existing tests to headless byte tests:
VAR buf : ARRAY [0..1023] OF CHAR;
TermOut.SetOutput(Capture);
VT100.CursorPosition(3, 7);
Check(buf, "ESC[3;7H");
Benefits:
od byte checks become assertions;Cover at least:
The CPR/live-terminal test needs a pty or a real terminal. Keep it as an optional integration test, not part of the default headless suite.
TermOut makes the emitters usable with the communications design:
Transport;Transport;TermOut points at the transport writer;This gives a clean split:
terminal emitters -> TermOut -> Transport
terminal emulator <- Transport <- incoming bytes
The MyWork/VT100 repository currently has no explicit license file.
Before vendoring the files into V3:
The reference documents in docs/ are independent of the code.
TermOut.VT100, VT52 and Ansi to TermOut.Unbuffered/Buffered from the emitters.Tty separately.Regis.No compiler changes are required.
The planned GTK4, SQLite and SDL bindings are not just three wrapper
libraries. They need a C ABI/FFI layer, a binding generator and
pkg-config-aware build/link integration.
V3 already has:
DEFINITION/IMPLEMENTATION MODULE FOR "C";EXPORT UNQUALIFIED;EXTERNAL procedures;FROM libc IMPORT ... path with known prototypes and
permissive varargs for unknown names;ADDRESS, POINTER TO and pointer-sized procedure values;libc intrinsic path.What is missing for large C APIs:
float;size_t/ssize_t widths;bool vs V3 BOOLEAN;The scanner already accepts underscores:
ident = ( letter | "_" ) { letter | digit | "_" } .
This was added for __FILE__ / __LINE__ compatibility (commit
a33fc7c). Names such as these already parse:
gtk_application_new
SDL_CreateWindow
sqlite3_open_v2
The docs in docs/features.md and docs/language-report.md still say
“no underscores” and should be updated.
This reduces binding friction:
GTK_ALIGN_FILL;SDL_Event.type;It does not solve:
char * and string ownership;float;size_t/ssize_t width;bool;Policy recommendation:
_ as a documented V3 extension;EXTERNAL "c_name" for long/decorated names
and C wrappers.SymTab.Name is 64 bytes, so names longer than 63 characters can
truncate; link names need an independent, larger mechanism.
The binding layer needs a C ABI profile:
TYPE
CInt = INTEGER; (* C int *)
CUInt = CARDINAL; (* C unsigned int *)
CLong = LONGINT; (* C long on LP64 *)
CULong = LONGCARD;
CSize = LONGCARD; (* size_t, 64-bit on amd64 *)
CSSize = LONGINT; (* ssize_t *)
CFloat = SHORTREAL; (* needs a true 4-byte float *)
CDouble = REAL;
CBool = INTEGER; (* C int / gboolean / SDL_bool *)
CEnum = INTEGER; (* C enum, 32-bit *)
Required changes:
SHORTREAL/REAL32 4-byte float;CSIZE_T/CSSIZE_T widths on amd64;bool (1 byte) separate from BOOLEAN (4 bytes);long/unsigned long target-dependent aliases;void * as ADDRESS;V3 records are tightly packed, with no alignment/padding. One- dimensional array fields use V3 descriptors; C arrays are inline. Variant records are not C unions.
Two strategies:
C accessor/wrapper strategy (recommended first)
ADDRESS handles and gets/sets fields through C
functions.SDL_Event, SDL_AudioSpec, SQLite structs, GTK
GdkRGBA/GValue.C-compatible record layout (long term)
ALIGN, PACKED, UNION, inline arrays and bitfields.For the first bindings, use wrappers. Do not start by making every V3 record C-compatible.
Add a small C support module, for example lib/c/C:
char * <-> ARRAY OF CHAR conversion;GetLastError mapping;The exact API can evolve, but ownership must be explicit:
free/g_free;Recommended source by target:
| Target | Best source | Strategy |
|---|---|---|
| GTK4 | GObject Introspection .gir files |
Generate types, enums, functions, signals, ownership |
| GLib/GObject/GIO | .gir files |
Base bindings |
| SQLite | sqlite3.h |
Small hand-written thin binding |
| SDL3 | SDL3/SDL.h |
Header parsing plus hand-written struct/callback wrappers |
The generator should emit:
.def declarations with safe Modula-2 names;EXTERNAL "c_name" link names;GError**, GValue
and string ownership;For GTK4, use the installed .gir files as the source of truth rather
than raw headers. They include type names, transfer/ownership rules,
nullable annotations, signals and properties.
Do not attempt full GTK4 generation first. Start with application, window, widget basics, buttons, labels, boxes, lists, files and signals.
M2make currently supports one extra C file and links -lm only. It
needs:
--cflag ...
--ldflag ...
--include ...
--c-source ...
--library ...
--pkg-config gtk4
--pkg-config sqlite3
--pkg-config sdl3
Requirements:
-pthread where needed;pkg-config --cflags --libs;GTK4 needs glib-2.0, gobject-2.0, gio-2.0, gtk4 and usually
-pthread. SQLite and SDL3 are much simpler.
C callbacks need special handling:
ADDRESS and decoded by a dispatcher;GTK4 also brings in GLib/GObject:
g_object_ref/g_object_unref);g_signal_connect_data);GError** out-parameters;GValue and properties;GMainLoop/GtkApplication integration.A V3 GTK binding must define its main-loop and ownership policy explicitly.
SQLite first
SQLite is the best first binding:
sqlite3 and sqlite3_stmt;sqlite3_int64;A safe wrapper should hide statement finalization, sqlite3_free,
SQLITE_TRANSIENT ownership, error strings and column lifetimes.
SQLite is an excellent FFI conformance test for ints, int64, doubles,
blobs, strings, pointers, callbacks and error codes.
SDL3 second
SDL3 needs:
SDL_Event, SDL_Keysym, SDL_Rect, SDL_Color, SDL_AudioSpec;SDL_bool, Uint32, Sint32, Uint64;float or conversion wrappers;SDL_Log varargs;Use C helpers to translate SDL_Event and SDL_AudioSpec to small V3
records. Prefer SDL3 unless SDL2 compatibility is required. Test with
SDL_VIDEODRIVER=dummy and SDL_AUDIODRIVER=dummy.
GTK4 last
GTK4 is the largest:
GError;GValue and properties;GList/GSList;GMainLoop/GtkApplication;GTK4 should come after the C FFI foundation, callbacks, build/link integration and GIR generator. A first binding should be a curated subset, not the full API.
EXTERNAL "c_name" / FFIDENT;size_t/ssize_t;pkg-config-aware build integration and ABI stamping.Bindings should live outside the core library:
lib/c/ C FFI helpers
bindings/sqlite3/ SQLite binding and wrapper
bindings/sdl3/ SDL3 binding and wrapper
bindings/gtk4/ GLib/GObject/GTK4 bindings
The existing GNU Modula-2 binding projects are a strong foundation, but they are gm2-targeted and are not drop-in for V3.
m2SQLITEContents:
src/SQLite.def — thin FOR "C" binding of the core SQLite APIlib/SQLiteUtils — pure-M2 helpersversion, hello_dbshowcase_allQuality:
ADDRESS);sqlite3_int64 as LONGINT;double as REAL;Main V3 blocker: the C string boundary.
The binding declares:
PROCEDURE sqlite3_open (filename: ARRAY OF CHAR;
VAR db: DbHandle) : INTEGER;
gm2's FOR "C" passes ARRAY OF CHAR as char *. V3 currently passes
the descriptor address in a normal external call. A generated V3
call was checked:
call $check_str(l $CStr_str0)
$CStr_str0 is { length, bytes... }, not the first character. A C
function expecting char * sees the length word instead.
This must be fixed before SQLite, SDL or GTK can bind to C string parameters.
SQLite is otherwise the best first binding.
m2SDL/SDL3There are two independent bindings:
m2SDL/SDL2 — older subsetm2SDL/SDL3 — current, more completeUse SDL3 for new work; keep SDL2 only for legacy compatibility.
SDL3 contents:
.def modulesSDL3UtilsSDL3Rect and SDL3KeyADDRESSGood design points:
SDL_Event as a V3 variant record;SDLXxx type aliases;SDL_VIDEODRIVER=dummy,
SDL_AUDIODRIVER=dummy).V3 blockers:
SHORTREAL is 4 bytes in gm2 but currently aliases 8-byte REAL
in V3. This breaks SDL_FRect, SDL_FPoint, float draw
coordinates and SetRenderDrawColorFloat.bool is 1 byte. V3 BOOLEAN is 4 bytes, so [ BOOLEAN ]
results and bool parameters need a CBool/BYTE mapping or C
wrappers.ARRAY OF CHAR descriptor/string ABI issue above.LE32; correct on tested Linux/amd64 but fragile. A C helper using
offsetof or a C event decoder is safer.SDL_Point/SDL_Rect records match C layout; float records
do not until true float32 exists.m2-GTK4This is the largest and most complete binding project.
Contents:
.def modules in src/GtkUtils and GtkClosures helpers in lib/lib/m2gtkshim.cm2 gtk-demo browsertools/gir2def.py generatortools/gir_verify.py verifierGenerator status:
src/ modules.The architecture is good:
FOR "C" module per GTK/GDK/Gsk/Gio/Adw class;ADDRESS;GtkClosures provides per-connection callback contexts and destroy
handlers;m2gtkshim.c provides C-side GObject subclasses for custom widgets,
paintables, list models and layout managers.GtkClosures + m2gtkshim.c is exactly the kind of trampoline/shim
layer V3 will need.
V3 blockers:
PROCEDURE gtk_widget_get_visible (...) : [ INTEGER ];
V3 currently rejects : [ T ] (invalid ProcHeading). Either add
this extension or strip it while porting.
PROCEDURE g_object_new (...; ...) : ADDRESS;
PROCEDURE g_object_set (...; ...);
PROCEDURE gtk_alert_dialog_new (format: ARRAY OF CHAR; ...) : ...;
V3 has no general ... declarations/calls. Use typed C wrappers or
avoid those APIs in the first subset.
C string boundary, as for SQLite/SDL.
True 4-byte float is needed for GdkRGBA, Graphene, Gsk path
APIs, GtkFrame alignment and many Adw widgets.
Value structs:
GdkRectangle (four int) likely matches V3 tight records;GdkRGBA (four gfloat) does not until float32 is fixed;GValue is 24 bytes and may match;GtkTreeIter, PangoRectangle, Graphene records need layout
audits.GError**, GValue and property APIs need C helpers or explicit
wrapper conventions.
gir2def.py is intentionally coarse: value structs become opaque
ADDRESS, GError** and varargs are dropped, and signals/properties
are not generated. It is a bootstrap tool, not a complete GTK API.
GTK needs pkg-config gtk4, -pthread, optional libadwaita /
GtkSourceView / libspelling / libpanel, and multiple C shims.
V3's M2make currently supports only one extra C file and links
-lm only.
Directly reusable:
.def and SQLiteUtils as the first port;GtkClosures, GtkUtils design and m2gtkshim.c;Not directly reusable:
[ T ] result syntax without a V3 parser addition;... varargs declarations without wrappers;SHORTREAL assumption;--extra C file build model.char * / buffer ABI;SHORTREAL/REAL32;bool;[ T ] or stripped definitions;EXTERNAL "c_name" and long names;pkg-config build integration.SQLite.def + SQLiteUtils;GLib, GObject, Gio, GtkApplication, GtkWindow,
GtkButton, GtkLabel, GtkBox;GtkUtils, GtkClosures, m2gtkshim.c; bindings/sqlite3/
bindings/sdl3/
bindings/gtk4/
Each package owns its .def/.mod, C shim, tests, examples,
license/provenance and V3 build manifest.
Do not vendor upstream GTK/SDL/SQLite source trees into V3; keep them as read-only references or system packages.
m2SQLITE: best first target, but string ABI is a prerequisite.m2SDL3: solid second target after float32, C bool and string ABI.m2-GTK4: excellent long-term asset; do not port all 432 modules
before the FFI/parser work.The efficient route is: fix the C FFI, port SQLite, port SDL3, then port a small GTK4 subset using the existing closure/shim architecture.
V3 classes can make GTK4 application code look much more like Modula-2, but they should be an optional high-level facade, not the implementation mechanism for the binding itself. Directly mapping V3 classes onto GObject classes is not viable: V3's Clarion-form classes and GObject's C object model have incompatible layouts and lifecycles.
The binding should distinguish three layers:
1. raw GTK4 C binding -- ADDRESS handles and free procedures
2. typed/opaque wrapper -- Modula-2 types and module procedures
3. optional OOP facade -- CLASS Window, CLASS Button, methods
The existing m2-GTK4 is layer 1. That is the right canonical
low-level layer: generated or reviewed FOR "C" modules with opaque
ADDRESS handles.
V3 classes belong at layer 3, for application code. A typed opaque layer can sit between them, especially where class fields would expose implementation details.
V3 classes:
THIS receiver in methods;GObject has:
GObject header, type system and class/instance structures;GObjectClass/GtkWidgetClass vfunc tables;A V3 class instance cannot be passed to C as a GtkWidget *. The V3
wrapper and the GTK object must remain separate:
CLASS Window;
handle : ADDRESS; (* the real GtkWindow *, not the V3 object *)
...
END Window;
C never sees the V3 object. It receives only the handle stored inside
it. Direct GObject subclassing still requires C shims such as the
existing m2gtkshim.c.
A V3 class copy copies its fields. If a wrapper contains a handle and
an owner flag, copying the wrapper can create two apparent owners of
the same GObject and lead to a double Unref.
Use pointer wrappers and explicit lifetime operations:
TYPE
CLASS Object;
handle : ADDRESS;
owned : BOOLEAN;
PROCEDURE Ref;
PROCEDURE Unref;
END Object;
CLASS Widget (Object);
PROCEDURE SetVisible (on : BOOLEAN);
END Widget;
CLASS Window (Widget);
PROCEDURE SetTitle (s : ARRAY OF CHAR);
PROCEDURE Present;
END Window;
WindowPtr = POINTER TO Window;
Constructors should return pointer wrappers, for example
NewWindow (app) : WindowPtr.
Rules:
DISPOSE frees only the V3 wrapper, never the GObject;Unref/Close explicitly before disposing the wrapper;BEGIN ... END as a per-object constructor: a
class initialisation body runs once at startup, not for each object;V3 has no destructors, so ownership must be documented and tested, not inferred from scope exit.
V3 classes have no private fields. A class declared in a definition module exposes its fields to clients, including the GTK handle and owner flag. This weakens the abstraction.
Options:
A true private-field facility is compiler work and should not block the
first GTK4 facade. For a small facade, exposing handle as an
implementation detail may be acceptable; for a public library API, an
opaque wrapper is cleaner.
V3 single inheritance can model GTK's primary class chain:
GObject -> GtkWidget -> GtkWindow -> GtkApplicationWindow
GObject -> GtkWidget -> GtkButton
GObject -> GtkWidget -> GtkBox
It cannot model GTK's interfaces. GTK and Gio use interfaces such as
GtkAccessible, GtkBuildable, GtkNative, GtkRoot,
GtkShortcutManager, GListModel and GtkTreeModel.
Do not encode interfaces as extra V3 parents: V3 rejects multiple parents. Provide interface capabilities as separate modules or free-function groups:
Accessible.SetLabel
Buildable.AddFromString
Native.GetSurface
Only the main widget chain should use class inheritance. Services and
non-widget objects such as GtkApplication, GtkBuilder, models and
file dialogs can use their own wrapper roots or opaque handles.
GTK construction is C-style:
gtk_application_window_new (app)
g_object_new (type, "property", value, ...)
V3 has no named-property arguments or general varargs. Constructors in the facade should be ordinary module procedures:
PROCEDURE NewWindow (app : Application) : WindowPtr;
PROCEDURE NewButton (label : ARRAY OF CHAR) : ButtonPtr;
Property-heavy construction should use C wrappers or
g_object_new_with_properties, not an invented V3 constructor syntax.
Keep gtk_*_new and g_object_new wrappers in the raw binding; the
OOP facade should call them rather than duplicate them.
Do not pass a V3 instance method directly to g_signal_connect_data.
A V3 method has a hidden receiver and is not a C function pointer with
GObject's callback ABI.
Use the existing GtkClosures pattern:
gpointer user data;For example, a clicked handler can receive the wrapper pointer as user
data, cast it back to ButtonPtr, and invoke a V3 method or virtual
method. The trampoline must still be a top-level procedure with the C
callback signature.
The same rule applies to GtkApplication activate/startup handlers,
GtkDrawingArea draw functions and model callbacks.
Custom widgets, custom GdkPaintable implementations, custom
GtkLayoutManagers and custom GListModels still require C:
class_init/instance_init;V3 classes cannot do this. Keep m2gtkshim.c for subclassing. The
OOP facade can wrap the resulting C object afterward, but it cannot
replace the shim.
bindings/gtk4/raw/ FOR "C" modules, ADDRESS handles
bindings/gtk4/wrap/ optional typed wrappers / opaque types
bindings/gtk4/oop/ optional CLASS facade
bindings/gtk4/shim/ m2gtkshim.c, GtkClosures
The raw binding remains canonical. The OOP facade should cover only a small, useful application subset:
Object, Widget, Window, ApplicationWindow,
Button, Label, Box, Entry, ListBox, DrawingArea,
Application, Builder
Do not create a parallel class hierarchy for all 432 GTK/GDK/Gsk/Gio/ Adw modules. Keep uncommon APIs at the raw or typed-wrapper layer. The GIR generator can help produce raw modules; the OOP facade should be hand-curated because ownership and API shape need judgement.
Not necessarily. Classic Modula-2 style is primarily:
V3 CLASS is a Clarion-form extension, not ISO Modula-2 OO. An OOP
facade can be nicer for application code, but it is not automatically
more standard. The most idiomatic design is probably:
opaque handle types + module procedures
with classes as an optional convenience layer for users who prefer:
win^.SetTitle("Demo");
btn^.OnClicked(OnQuit);
ADDRESS-based FOR "C" modules as the canonical layer.POINTER TO CLASS wrappers holding a C handle and ownership flag.Ref/Unref/Close; do not rely on DISPOSE.GtkClosures/top-level trampolines for signals; never pass V3
methods directly to C.m2gtkshim.c for GObject subclassing.This gives a more Modula-2 application API without pretending that V3 classes are GObject classes.
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.Utf8.Encode, add byte-string
and UString modules, then UTF-8 input.Vector, Deque/Stack, HashMap, then optional
BTreeMap; persistent BTreeFile last.TermOut, VT100, VT52, Ansi, optional
Regis; Tty and headless tests.bindings/ packages.Object/Widget hierarchy, pointer wrappers, explicit
Ref/Unref/Close, GtkClosures trampolines; no direct GObject
subclassing.Serial/Transport first; protocols and terminal
emulation optional.Net/Transport, TCP/UDP, Poll, then optional TLS/HTTP.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.