Skip to content

Commit 4ea0a56

Browse files
committed
feat(lang): extension functions on types — mapper Type.method(...)
Add a method to any type (primitive or user-defined) by qualifying the name: mapper Integer.double() -> Integer => this * 2 mapper Person.fullName() -> String { return this.name + " " + this.family } - parser: `Type.method` -> function `<TypeName>__method` with the receiver prepended as a `this` parameter (typed via primKindToCtype/identToCtype), so `this` / `this.field` work through ordinary codegen; emission unchanged. - codegen: postfix dispatch `recv.method(args)` resolves to the extension when `<recvXtype>__method` exists, passing the receiver as the first arg. Keyed on the xtype (the type name, e.g. "Integer"/"Person"), so it matches receivers and chains (`n.double().plus(1)`). Any function kind works. Verified on primitives, String, user types, predicates, args, chaining; self-hosting fixpoint byte-identical; adds examples/extensions_test.xi.
1 parent fcbf73f commit 4ea0a56

5 files changed

Lines changed: 88 additions & 2 deletions

File tree

compiler/impl/codegen/postfix.xi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ mapper genPostfix(toks: Token[], pos: Integer, ctx: GCtx) -> ExprRes {
1010
let cont = true
1111
while cont {
1212
let k = gkind(toks, p)
13+
// extension function call: recv.method(args) -> xc_<typ>__<method>(recv, args)
14+
if (k == 107 or k == 129) and gkind(toks, p + 2) == 100 and isFuncNameC(ctx.prog, typ + "__" + gtext(toks, p + 1)) {
15+
let ext = typ + "__" + gtext(toks, p + 1)
16+
let al = genArgs(toks, p + 2, ctx)
17+
let callArgs = code
18+
if string_len(al.code) > 0 { callArgs = code + ", " + al.code }
19+
code = "xc_" + ext + "(" + callArgs + ")"
20+
typ = funcRetXType(ctx.prog, ext)
21+
p = al.pos
22+
continue
23+
}
1324
if k == 107 or k == 129 {
1425
let fld = gtext(toks, p + 1)
1526
if isListXType(typ) and fld == "asSequence" {

compiler/impl/parser/decl_parser.xi

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,21 @@ mapper parseFunc(ps: PState, isAsync: Bool, isCreator: Bool) -> FuncResult {
4141
}
4242
}
4343

44-
// name
44+
// name — may be an extension function `Type.method`, e.g. `Integer.double`
45+
// or `Person.fullName`. The receiver is passed as a `this` parameter, and the
46+
// function is named `<recvSuffix>__<method>` (e.g. integer__double).
4547
let nameTok = peek(ps2)
4648
ps2 = advance(ps2)
49+
let fname = nameTok.text
50+
let extThis = ""
51+
if peek(ps2).kind == 107 { // '.' -> extension on a type
52+
let recvCtype = identToCtype(nameTok.text)
53+
if nameTok.kind >= 260 and nameTok.kind <= 269 { recvCtype = primKindToCtype(nameTok.kind) }
54+
ps2 = advance(ps2) // '.'
55+
fname = nameTok.text + "__" + peek(ps2).text // keyed by xtype (the type name)
56+
ps2 = advance(ps2) // method name
57+
extThis = recvCtype + " this"
58+
}
4759

4860
// (params) — table-form decisions have none (columns are declared in the body)
4961
let pr = ParamResult { params: "", ps: ps2 }
@@ -53,6 +65,11 @@ mapper parseFunc(ps: PState, isAsync: Bool, isCreator: Bool) -> FuncResult {
5365
ps2 = pr.ps
5466
if peek(ps2).kind == 101 { ps2 = advance(ps2) } // )
5567
}
68+
// extension: prepend the receiver as `this` (so `this`/`this.field` just work)
69+
if string_len(extThis) > 0 {
70+
if string_len(pr.params) > 0 { pr = ParamResult { params: extThis + ", " + pr.params, ps: pr.ps } }
71+
else { pr = ParamResult { params: extThis, ps: pr.ps } }
72+
}
5673

5774
// -> rettype
5875
let rr = parseRetType(ps2)
@@ -145,7 +162,7 @@ mapper parseFunc(ps: PState, isAsync: Bool, isCreator: Bool) -> FuncResult {
145162
isCreator: isCreator,
146163
isAsync: isAsync,
147164
kind: kindStr,
148-
name: nameTok.text,
165+
name: fname,
149166
params: pr.params,
150167
retCtype: retCtype,
151168
bodyTokens: br.bodyTokens,

docs/language-guide.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,28 @@ Infix functions bind at low precedence (looser than arithmetic, like `to`), so
291291
`predicate`, `creator`, …); the `infix` modifier goes first, before `async` and
292292
the kind. See `examples/infix_demo.xi`.
293293

294+
## Extension functions — `mapper Type.method(...)`
295+
296+
Add a method to **any type** — a primitive or your own — by qualifying the name
297+
with the receiver type. Inside the body, `this` is the receiver:
298+
299+
```x
300+
mapper Integer.double() -> Integer => this * 2
301+
302+
type Person = { name: String, family: String }
303+
mapper Person.fullName() -> String { return this.name + " " + this.family }
304+
305+
let n = 21
306+
n.double() // 42
307+
Person { name: "Ada", family: "Lovelace" }.fullName() // "Ada Lovelace"
308+
```
309+
310+
Call them with the usual `receiver.method(args)` syntax. Extensions take regular
311+
parameters after the receiver, return like any function, and **chain**
312+
(`n.double().plus(1)`). Any function kind works (`mapper`, `predicate`, …). They
313+
desugar to a plain function taking the receiver as a `this` parameter, so there's
314+
no runtime cost. See `examples/extensions_test.xi`.
315+
294316
## `capture` — name a sub-expression's value
295317

296318
`EXPR capture name: Type` evaluates `EXPR`, binds its value to `name` (of the

docs/skill.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ infix mapper plus(a: Integer, b: Integer) -> Integer { return a + b }
198198
5 plus 3 // 8
199199
```
200200

201+
**Extension functions** — qualify the name with a receiver type (primitive or
202+
user-defined) to add a method; `this` is the receiver. Chains like a method:
203+
204+
```x
205+
mapper Integer.double() -> Integer => this * 2 // 21.double() ... use (21).double()
206+
type Person = { name: String, family: String }
207+
mapper Person.fullName() -> String { return this.name + " " + this.family }
208+
```
209+
201210
**`capture`**`EXPR capture name: Type` binds a sub-expression's value (the
202211
type annotation is required) and still yields it; the name is usable for the rest
203212
of the function (works in functions, methods, entry — not inside a `where` guard):

examples/extensions_test.xi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Feature: extension functions on types — `mapper Type.method(...)` with `this`.
2+
mapper Integer.double() -> Integer => this * 2
3+
mapper Integer.plus(n: Integer) -> Integer { return this + n }
4+
mapper String.shout() -> String { return this + "!" }
5+
type Pt = { x: Integer, y: Integer }
6+
mapper Pt.sum() -> Integer { return this.x + this.y }
7+
predicate Pt.isOrigin() { return this.x == 0 and this.y == 0 }
8+
9+
test "extension on a primitive (Integer)" {
10+
let n: Integer = 21
11+
assertEq(n.double(), 42)
12+
assertEq(n.plus(9), 30)
13+
}
14+
test "extension on String" {
15+
let s = "hi"
16+
assertEq(s.shout(), "hi!")
17+
}
18+
test "extension on a user type" {
19+
let p = Pt { x: 3, y: 4 }
20+
assertEq(p.sum(), 7)
21+
assert not p.isOrigin()
22+
assert Pt { x: 0, y: 0 }.isOrigin()
23+
}
24+
test "chained extension calls" {
25+
let n: Integer = 5
26+
assertEq(n.double().plus(1), 11)
27+
}

0 commit comments

Comments
 (0)