Skip to content

Commit 9743e8a

Browse files
committed
feat(lang): generic interfaces via monomorphization
A user interface may now take type parameters — `interface Box<T> { get() -> T }` — and a class implements a concrete instantiation — `class IntBox implements Box<Integer>`. Deps take instantiated generics too: `deps { b: Box<Integer> }`. How: a post-parse pass (impl/codegen/generics.xi) treats a generic interface as a template. For each class that implements `Base<A1, A2>` it synthesizes a concrete non-generic interface with the type params substituted (ctype-suffix into ctypes, type-name into default-body tokens), names it `Base_A1_A2`, rewrites the class's implements to it, and drops the generic template. Everything downstream (vtable, caster, resolver, default-method emission) works unchanged on the concrete interface. Supports: multiple type params, `extends` between generic interfaces (the base's substituted methods are flattened into the concrete vtable and the base instantiation is added to the implementor so casts to it resolve), and interface default methods that use the type params — including `=> expr` inline defaults (now parsed for interfaces) and defaults that decode via `as` (codec detection now scans interface default bodies). Monomorphization is a faithful no-op for non-generic programs; the self-hosting fixpoint holds. examples/generics/ covers instantiation+dispatch and extends+default-method inheritance (the entity<->model convert pattern).
1 parent 0c5b0b3 commit 9743e8a

9 files changed

Lines changed: 477 additions & 10 deletions

File tree

compiler/impl/codegen/feature_detect.xi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ predicate Program.progUsesAsDecode() {
152152
while mi < mn { if methodSpecGet(cs.methList, mi).bodyTokens.hasKind(209) { return true } mi = mi + 1 }
153153
ci = ci + 1
154154
}
155+
// interface default bodies (incl. monomorphized generic ones, e.g. a
156+
// Repository's convertTo/convertFrom that decode via `as`)
157+
let fi = 0
158+
let fn2 = ifaceSpecLen(this.ifaces)
159+
while fi < fn2 {
160+
let is2 = ifaceSpecGet(this.ifaces, fi)
161+
let mi = 0
162+
let mn = methodSpecLen(is2.methList)
163+
while mi < mn { if methodSpecGet(is2.methList, mi).bodyTokens.hasKind(209) { return true } mi = mi + 1 }
164+
fi = fi + 1
165+
}
155166
return false
156167
}
157168

compiler/impl/codegen/generics.xi

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// xc codegen — generic interface monomorphization. (spliced via xc.xi)
2+
3+
// Rebuild a Program swapping only its interface + class lists (the monomorphized
4+
// sets); every other field is carried through unchanged.
5+
mapper Program.withMono(ifaces: IfaceSpec[], classes: ClassSpec[]) -> Program {
6+
return Program {
7+
types: this.types, ifaces: ifaces, classes: classes,
8+
modules: this.modules, functions: this.functions, externs: this.externs,
9+
entrySpec: this.entrySpec, interrupts: this.interrupts, atoms: this.atoms,
10+
machines: this.machines, eventTypes: this.eventTypes, tables: this.tables,
11+
tests: this.tests, scheduled: this.scheduled, libraries: this.libraries,
12+
infixFns: this.infixFns, cIncludes: this.cIncludes, cFlags: this.cFlags
13+
}
14+
}
15+
16+
//
17+
// A generic interface (typeParams non-empty) is a template. For each class that
18+
// implements a concrete instantiation `Base<A1, A2>`, we synthesize a concrete
19+
// non-generic interface with the type parameters textually substituted, so all
20+
// the existing vtable / caster / default-method codegen works unchanged. The
21+
// class's `implements` entry is rewritten to the mangled concrete name, and any
22+
// interfaces the template `extends` are flattened in (their substituted methods
23+
// copied down) and added to the class so casts to a base interface also work.
24+
//
25+
// This runs once, right after parsing, producing a Program with the generic
26+
// templates removed and the concrete instantiations added.
27+
28+
// "Integer,User" -> ["Integer","User"] ("" -> [])
29+
mapper splitCsv(s: String) -> String[] {
30+
let out: String[] = []
31+
if string_len(s) == 0 { return out }
32+
let start = 0
33+
let i = 0
34+
let n = string_len(s)
35+
while i <= n {
36+
let atSep = i == n
37+
if not atSep { if string_char_at(s, i) == 44 { atSep = true } } // ','
38+
if atSep {
39+
out = appendString(out, string_slice(s, start, i))
40+
start = i + 1
41+
}
42+
i = i + 1
43+
}
44+
return out
45+
}
46+
47+
// mangled concrete interface name: Base<Integer,User> -> Base_Integer_User
48+
mapper monoName(base: String, argsCsv: String) -> String {
49+
return base + "_" + argsCsv.replaceAll(",", "_")
50+
}
51+
52+
// The ctype-suffix form of an Xi type name (Integer -> integer, User -> User),
53+
// so substitution into a ctype string produces the right primitive casing.
54+
mapper ctypeSuffixOf(xname: String) -> String {
55+
let ct = xname.xnameToCtype()
56+
if ct.startsWith2("xc_") and ct.endsWith2("_t") { return string_slice(ct, 3, string_len(ct) - 2) }
57+
return xname
58+
}
59+
60+
// substitute each type param -> its arg inside a ctype / params string. Type
61+
// vars in a ctype are always underscore-bounded (`xc_T_t`, `xc_opt_T_t`,
62+
// `xc_arr_T_t`), so match `_<param>_` to avoid clobbering an unrelated name that
63+
// merely contains the param as a substring (e.g. T inside Timestamp). The arg is
64+
// substituted in its ctype-suffix form (Integer -> integer).
65+
mapper substStr(s: String, params: String[], args: String[]) -> String {
66+
let out = s
67+
let i = 0
68+
while i < stringArrLen(params) {
69+
if i < stringArrLen(args) {
70+
out = out.replaceAll("_" + stringArrGet(params, i) + "_", "_" + ctypeSuffixOf(stringArrGet(args, i)) + "_")
71+
}
72+
i = i + 1
73+
}
74+
return out
75+
}
76+
77+
// substitute type params in a method's default-body tokens (identifier text)
78+
mapper substTokens(toks: Token[], params: String[], args: String[]) -> Token[] {
79+
let out: Token[] = []
80+
let i = 0
81+
let n = tokenArrLen(toks)
82+
while i < n {
83+
let t = tokenArrGet(toks, i)
84+
let txt = t.text
85+
if t.kind == 1 {
86+
let p = 0
87+
while p < stringArrLen(params) {
88+
if txt == stringArrGet(params, p) and p < stringArrLen(args) { txt = stringArrGet(args, p) }
89+
p = p + 1
90+
}
91+
}
92+
out = appendToken(out, Token { kind: t.kind, text: txt, line: t.line, file: t.file })
93+
i = i + 1
94+
}
95+
return out
96+
}
97+
98+
mapper substMethod(ms: MethodSpec, params: String[], args: String[]) -> MethodSpec {
99+
return MethodSpec {
100+
isAsync: ms.isAsync, kind: ms.kind, name: ms.name,
101+
params: substStr(ms.params, params, args),
102+
retCtype: substStr(ms.retCtype, params, args),
103+
bodyTokens: substTokens(ms.bodyTokens, params, args),
104+
topic: ms.topic, hasWhere: ms.hasWhere,
105+
whereTokens: substTokens(ms.whereTokens, params, args),
106+
fnDeps: ms.fnDeps
107+
}
108+
}
109+
110+
// Substitute type params in an args list "TKey,TEntity" -> "Integer,User" — a
111+
// name-level (not ctype) substitution, used for an extended interface's args
112+
// which feed the mangled name.
113+
mapper substArgs(argsCsv: String, params: String[], args: String[]) -> String {
114+
let parts = splitCsv(argsCsv)
115+
let out = ""
116+
let i = 0
117+
while i < stringArrLen(parts) {
118+
let repl = stringArrGet(parts, i)
119+
let j = 0
120+
while j < stringArrLen(params) {
121+
if repl == stringArrGet(params, j) and j < stringArrLen(args) { repl = stringArrGet(args, j) }
122+
j = j + 1
123+
}
124+
if string_len(out) > 0 { out = out + "," }
125+
out = out + repl
126+
i = i + 1
127+
}
128+
return out
129+
}
130+
131+
mapper findGenericIface(prog: Program, name: String) -> IfaceSpec {
132+
let i = 0
133+
let n = ifaceSpecLen(prog.ifaces)
134+
while i < n {
135+
let is2 = ifaceSpecGet(prog.ifaces, i)
136+
if is2.name == name { return is2 }
137+
i = i + 1
138+
}
139+
return IfaceSpec { name: "", extendsNames: [], methList: [], typeParams: [], extendsArgs: [] }
140+
}
141+
142+
predicate ifaceAccHas(acc: IfaceSpec[], name: String) {
143+
let d = 0
144+
while d < ifaceSpecLen(acc) { if ifaceSpecGet(acc, d).name == name { return true } d = d + 1 }
145+
return false
146+
}
147+
148+
// Synthesize the concrete interface for Base<argsCsv> plus every generic
149+
// interface it (transitively) extends, appending them to `acc` (deduped). The
150+
// concrete methList flattens in the substituted methods of extended interfaces.
151+
mapper synthIface(prog: Program, base: String, argsCsv: String, acc: IfaceSpec[]) -> IfaceSpec[] {
152+
let mangled = monoName(base, argsCsv)
153+
if ifaceAccHas(acc, mangled) { return acc }
154+
155+
let tmpl = findGenericIface(prog, base)
156+
let params = tmpl.typeParams
157+
let args = splitCsv(argsCsv)
158+
159+
// this interface's own (substituted) methods
160+
let methList: MethodSpec[] = []
161+
let mi = 0
162+
while mi < methodSpecLen(tmpl.methList) {
163+
methList = appendMethodSpec(methList, substMethod(methodSpecGet(tmpl.methList, mi), params, args))
164+
mi = mi + 1
165+
}
166+
// flatten extended interfaces: substitute their args, synth them, copy methods down
167+
let exNames: String[] = []
168+
let ei = 0
169+
while ei < stringArrLen(tmpl.extendsNames) {
170+
let bn = stringArrGet(tmpl.extendsNames, ei)
171+
let ba = ""
172+
if ei < stringArrLen(tmpl.extendsArgs) { ba = substArgs(stringArrGet(tmpl.extendsArgs, ei), params, args) }
173+
if string_len(ba) > 0 {
174+
let baseM = monoName(bn, ba)
175+
exNames = appendString(exNames, baseM)
176+
acc = synthIface(prog, bn, ba, acc)
177+
// copy the base's (already substituted) methods into this vtable
178+
let bi = 0
179+
while bi < ifaceSpecLen(acc) {
180+
let cand = ifaceSpecGet(acc, bi)
181+
if cand.name == baseM {
182+
let cm = 0
183+
while cm < methodSpecLen(cand.methList) {
184+
methList = appendMethodSpec(methList, methodSpecGet(cand.methList, cm))
185+
cm = cm + 1
186+
}
187+
}
188+
bi = bi + 1
189+
}
190+
} else {
191+
exNames = appendString(exNames, bn)
192+
}
193+
ei = ei + 1
194+
}
195+
let concrete = IfaceSpec { name: mangled, extendsNames: exNames, methList: methList,
196+
typeParams: [], extendsArgs: [] }
197+
return appendIfaceSpec(acc, concrete)
198+
}
199+
200+
// All concrete interface names (self + transitive generic bases) for Base<args>.
201+
mapper monoChain(prog: Program, base: String, argsCsv: String, out: String[]) -> String[] {
202+
let mangled = monoName(base, argsCsv)
203+
if strArrContains(out, mangled) { return out }
204+
out = appendString(out, mangled)
205+
let tmpl = findGenericIface(prog, base)
206+
let ei = 0
207+
while ei < stringArrLen(tmpl.extendsNames) {
208+
let ba = ""
209+
if ei < stringArrLen(tmpl.extendsArgs) {
210+
ba = substArgs(stringArrGet(tmpl.extendsArgs, ei), tmpl.typeParams, splitCsv(argsCsv))
211+
}
212+
if string_len(ba) > 0 { out = monoChain(prog, stringArrGet(tmpl.extendsNames, ei), ba, out) }
213+
ei = ei + 1
214+
}
215+
return out
216+
}
217+
218+
predicate Program.isGenericIfaceName(name: String) {
219+
let i = 0
220+
let n = ifaceSpecLen(this.ifaces)
221+
while i < n {
222+
let is2 = ifaceSpecGet(this.ifaces, i)
223+
if is2.name == name and stringArrLen(is2.typeParams) > 0 { return true }
224+
i = i + 1
225+
}
226+
return false
227+
}
228+
229+
// The whole pass: rewrite classes' generic implements to concrete names and
230+
// return a Program whose ifaces = (non-generic originals) + (synthesized).
231+
mapper monomorphize(prog: Program) -> Program {
232+
let synth: IfaceSpec[] = []
233+
let newClasses: ClassSpec[] = []
234+
235+
let ci = 0
236+
while ci < classSpecLen(prog.classes) {
237+
let cs = classSpecGet(prog.classes, ci)
238+
let newImpl: String[] = []
239+
let ii = 0
240+
while ii < stringArrLen(cs.implNames) {
241+
let ifn = stringArrGet(cs.implNames, ii)
242+
let arg = ""
243+
if ii < stringArrLen(cs.implArgs) { arg = stringArrGet(cs.implArgs, ii) }
244+
if string_len(arg) > 0 and prog.isGenericIfaceName(ifn) {
245+
// this + every generic base become concrete implemented interfaces
246+
let chain = monoChain(prog, ifn, arg, emptyStrings())
247+
let k = 0
248+
while k < stringArrLen(chain) {
249+
if not strArrContains(newImpl, stringArrGet(chain, k)) {
250+
newImpl = appendString(newImpl, stringArrGet(chain, k))
251+
}
252+
k = k + 1
253+
}
254+
synth = synthIface(prog, ifn, arg, synth)
255+
} else {
256+
newImpl = appendString(newImpl, ifn)
257+
}
258+
ii = ii + 1
259+
}
260+
newClasses = appendClassSpec(newClasses, ClassSpec {
261+
name: cs.name, implNames: newImpl, depList: cs.depList, methList: cs.methList,
262+
stateFields: cs.stateFields, stateInit: cs.stateInit, implArgs: cs.implArgs
263+
})
264+
ci = ci + 1
265+
}
266+
267+
// final ifaces: keep non-generic originals, drop templates, add synthesized
268+
let newIfaces: IfaceSpec[] = []
269+
let fi = 0
270+
while fi < ifaceSpecLen(prog.ifaces) {
271+
let is2 = ifaceSpecGet(prog.ifaces, fi)
272+
if stringArrLen(is2.typeParams) == 0 { newIfaces = appendIfaceSpec(newIfaces, is2) }
273+
fi = fi + 1
274+
}
275+
let si = 0
276+
while si < ifaceSpecLen(synth) {
277+
newIfaces = appendIfaceSpec(newIfaces, ifaceSpecGet(synth, si))
278+
si = si + 1
279+
}
280+
281+
return prog.withMono(newIfaces, newClasses)
282+
}

compiler/impl/driver/xc_compiler.xi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class XcCompiler implements Compiler {
3232
prog = parser.parse(tokens2)
3333
}
3434

35+
prog = monomorphize(prog) // expand generic interface instantiations
3536
checkMachines(prog) // static machine-graph validation
3637
checkPurity(prog) // pure-kind functions stay side-effect-free
3738

0 commit comments

Comments
 (0)