Update 2026-10-08 (branch
generics-inc1). Two things changed since this analysis: (1) V3 now has an AST andLoweremits from it, so the "V3 has no AST" premise in §3 is stale — though the substitution-scope re-parse is still the pragmatic route, because the AST stores resolvedTypeIndex, not type names; (2) the chosen syntax is the ISO 10514-2 refinement form (GENERIC … MODULE+MODULE X = G(args)), per../Generics/m2generic-syntaxe-analysis.md, notIMPORT X<T>.Done (Increment 1, commit
e1d0472): grammar recognition of the four module categories —GENERIC DEFINITION/IMPLEMENTATION MODULEwith TYPE formals, and refiningDEFINITION/IMPLEMENTATION MODULE X = G(actuals). Formals are entered as unresolved alias types (SymTab.EnterTypeParam); refinements are recorded. A refinement still produces an empty module.Next (Increment 2): instantiate by re-parsing the generic source with a substitution scope, driven by a driver session pre-pass — record each generic's def/impl file path; on a refinement, re-open the generic, bind the formals to the actuals, name the module
X, dedup, and lower it before the importer. Requires moving the normal body'sBeginDefinto the;branch (so the refinement does not pre-registerX), aSymTab.EnterTypeParamBound, a grammar instantiation context + accessors, and driver re-parse logic.Done (Increment 3 — 2026-10-16): value (constant) parameters and actual-kind constraints. A generic formal may now be a value rather than a type:
GENERIC DEFINITION MODULE Buf (size : CARDINAL); TYPE T = ARRAY [0..size] OF CHAR; …instantiated byMODULE X = Buf(10);. Grammar:GenericFormalcollects its names then accepts: TYPE(type formal) or: TypeIdent(value formal);Actualaccepts an integer,charConstor an identifier (type name = type actual,CONSTname = value actual). Refinement actuals now carry a kind (type/value) and a value text (refKind/refVal,instKind/instVal+InstActualKind/InstActualVal/RefineActualKind/RefineActualVal). On instantiation a value formal becomes aCONSTbound to the actual value; in the abstract template it is aCONSTwith a placeholder value ("0"), so template parsing succeeds — templates are never lowered, so only instances matter. Constraints: the actual's kind must match the formal (a type formal needs a type actual, a value formal a value one) — otherwise233; tested byg_badbuf. Suite 210/210, fixpoint OK, corpus unchanged.Still open for inc3+: a constraint model beyond kind-matching
Done (Increment 3 — 2026-10-16, constraint model): a type formal may carry an explicit constraint:
T : TYPE (ORDINAL)(alsoINTEGER,REAL,POINTER,SCALAR). At instantiation the actual type must satisfy it (ConsOk: ORDINAL →IsOrdinal, INTEGER →ClInt, REAL →ClReal, POINTER →ClPtr, SCALAR → ordinal or real), else233. Verified:OrdBox(INTEGER)compiles and runs;OrdBox(REAL)is rejected. SuiteOrdProg/g_realbox.Done (2026-10-16, nested generics as actuals). A qualified actual is classified as a procedure (
Mod.Proc) or a type (Mod.Type), so an instance's exported type can feed another generic:WrapB = Wrap(IntBox.Box). Verified end-to-end (suiteGWrapProg).Still open:
GENERIC CLASS(classes + generics); instance dedup.Done (Increment 3 — 2026-10-16, continued): procedure parameters. A formal may be a procedure:
> GENERIC DEFINITION MODULE Sort > (Element : TYPE; Compare : PROCEDURE(a, b : Element) : INTEGER); > PROCEDURE Smaller(x, y : Element) : Element; > END Sort. > DEFINITION/IMPLEMENTATION MODULE IntSort = Sort(INTEGER, Cmp.IntCompare); > ``` > The formal's type is an inline `ProcType`. The **actual is a > *qualified* procedure name** (`Cmp.IntCompare`) — the refinement form > parses its actuals before any import, so a qualified name is the only > way to reach a procedure without one. In the **abstract template** the > formal is entered as a proc-typed callable (KindVar + proc type), so > the body type-checks (calls are indirect there); at **instantiation** > it is aliased to the actual procedure: > - `SymTab.AliasProc(formal, mod, actual)` clones the actual's > `SymNode` under the formal's name (keeps `uid`/`mod`/`sym`), so > name resolution and argument checks see the actual; > - `Lower.AliasProc(formal, uid, res, depth, ext)` adds a **proc-table > entry** (without disturbing the scope stack) so `Lower`'s call > emission targets the actual; > - `SymTab.ProcBaseByUid` + a uid→base map let `QbeGen.Mangled` recover > the actual's base name after the instance scope has been popped. > > Verified end-to-end: `Smaller(7, 3)` through `IntSort` (with > `IntCompare` in a separate module) returns 3. Suite **211/211** > (`SortProg`), fixpoint OK, corpus unchanged. Study reviewed: `../Generics/` (the three `m2-generics-coco-r-*.md` analyses, the `m2generic/` prototype: `M2Generic.atg`, `gm2-gcc-0.2.atg` → `gm2-gcc-generics-0.3.atg`, `src/GenericGen.mod`, `templates/{Stack,List,Map}.g*`). Verdict in one line: **the study's recommendation (integrate into the compiler, not a preprocessor) is right — but its plan is written for an AST-based compiler, and V3 has no AST, so the practical design is re-entrant template parsing with a type-substitution scope, not AST specialization.** ## 1. What the study proposes Two options, with a clear lean to the second: 1. **Integrated preprocessor** — the grammar accepts `IMPORT Stack<INTEGER>`; a tool instantiates templates, **rewrites the user's source** (`IMPORT Stack<INTEGER>` → `IMPORT StackInteger`), then `gm2` compiles the result. 2. **Fully in the compiler** — the compiler understands `X<T>` natively, specializes internally, type-checks, and generates code; no `gm2`, no source rewriting. `m2generic` is a competent **option-1** prototype: - `M2Generic.atg` (48 lines) parses a `.gin` file of `INSTANTIATE Name<Type> FROM "x.gdef", "x.gmod";`. - `GenericGen` (`Generate`/`Generate2`) does **lexical** substitution of the identifier `T` (or `K`,`V`) outside strings/chars/comments, plus module renaming via `MakeName`/`MakeName2` (`Stack<INTEGER>` → `StackInteger`), writing real `.def`/`.mod`. - `gm2-gcc-generics-0.3.atg` adds the generic syntax to the full gm2 grammar's `Import`:GenericTypeActual = Qualident. GenericTypeActualList = GenericTypeActual { "," GenericTypeActual }. GenericModuleDesignator = Ident "<" GenericTypeActualList ">". ImportModule = Ident | GenericModuleDesignator. ImportModuleList = ImportModule { "," ImportModule }. Import = "FROM" ImportModule "IMPORT" IdentList ";" |
"IMPORT" ImportModuleList ";".Its README already flags the gap: **parsing ≠ generation** — the import-rewriting step (source→source) is still missing. The supporting docs (`analysis-0`, `analysis-1`) survey Modula-2 generic techniques (ADDRESS + `SYSTEM`, code generation, generic modules, procedure parameters, opaque types) and conclude that for a *custom compiler with OOP* the in-compiler route is preferable, with a 3-level plan: **type generics → multiple parameters → generics + OOP**. ## 2. Assessment of the study **Strengths.** Right conclusion for a custom compiler; good survey of the design space; a working prototype that pins the naming convention and the `X<T>` syntax; the 3-level staging is sensible. **Weaknesses (for the in-compiler path).** - **Lexical `T` replacement is fragile** — it can't tell the type parameter from a coincidentally-named user identifier, and it must special-case strings/comments/chars by hand. No nesting, no constraints, no shadowing discipline. - **Errors point at generated code**, not the source. - **`gm2` stays the type-checker** — the whole point of having V3 is not to need it. - **Hard-wired arity** — `MakeName`/`MakeName2` bake in 1–2 parameters; `Map<K,V>` is a special case, not a general map. - **No instance dedup** and no caching across instantiations. - **No AST to specialize** — see below. ## 3. The critical mismatch: V3 has no AST The study's recommended architecture is:Generic AST → instantiation → specialized AST → type check → codegen
V3's frontend is a **Coco/R grammar with inline semantic actions** (`M2.atg`) that call `SymTab`/`QbeGen` *while parsing* — single-pass, declaration-before-use, **no intermediate tree**. So "specialize the AST" is not directly implementable. Two ways to read "integrate into the compiler": - **(A) Internal monomorphization via a substitution scope** — when an instance is needed, **re-parse the generic template with a scope that binds the type parameter(s) to the actual type(s)**, producing ordinary symbols + code under a mangled module name. No lexical replacement, no tree: the existing `TypeIdent`/`Lookup` path simply finds `T` as a type. This is V3's native analogue of the study's "specialized AST", and strictly better than `GenericGen`. - **(B) True type-parameterized symbols** — carry `T` abstractly and specialize at instantiation. Much harder without an AST; a redesign. **(A) is the target.** ## 4. Recommended design for V3 (option A) Syntax (minimal, grammatical subset first): ```modula-2 GENERIC DEFINITION MODULE Stack<T>; TYPE Stack; PROCEDURE Push(VAR s : Stack; x : T); ... END Stack. GENERIC IMPLEMENTATION MODULE Stack<T>; ... END Stack.
Use:
IMPORT Stack<INTEGER>;
FROM Stack<INTEGER> IMPORT Stack, Push;
Mechanism, as a session-expansion pre-pass over the driver's unit list (reusing the fact that V3 already compiles several units — def + impl + program — per session):
IMPORT Name<Args> / FROM Name<Args> IMPORT … in the
inputs. (Cheap token scan, or fold it into the parser's import
action.)Name<Args…> once
(dedup cache), topologically ordered before its importer..gdef then .gmod) with a substitution
scope live: push a scope binding T (or K,V) to the resolved
actual type indices; the normal TypeIdent/Lookup path resolves
T. Module/type names are mangled per instance
(Stack<INTEGER> → StackInteger).StackInteger as an ordinary module:
full type checking and real codegen, no source rewriting, no
external tool, no gm2.Naming: generalise the mangle to N parameters
(Map<INTEGER,REAL> → MapIntegerReal) and to nested actuals
(Stack<Stack<INTEGER>> → e.g. StackStackInteger, hashed if long).
IMPORT X<T> / FROM X<T> IMPORT …, dedup, substitution-scope
parse. (Stack<INTEGER>, Stack<REAL>.)Map<K,V>) — a map, not
special cases.T must be ordinal), then
generics + OOP (GENERIC CLASS).Parse callable from an action if the
pre-pass proves insufficient..gdef/.gmod files or is
recognised inside ordinary .def/.mod by the GENERIC keyword.T replacement for the in-compiler path — use a
substitution scope; keep GenericGen only as a test oracle.gm2 from the final pipeline (V3 type-checks).GENERIC … MODULE X<T>; + IMPORT X<T> first;
FROM X<T> IMPORT … next.The ../Generics study is a solid survey and its recommendation
(in-compiler) is right for V3. Its concrete plan assumes an AST, which
V3 doesn't have, so the realizable design is re-entrant template
parsing with a type-substitution scope plus a session-expansion/dedup
pre-pass — no AST rewriting, no textual substitution, no gm2. That
is a genuine language feature of roughly CLASS-lowering scale, and it
fits V3's architecture and self-hosting guard.