plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
950794d
— Peter John
2026-11-06T11:06:24+05:30
plans: top-level functions with receivers, module namespaces
docs/backlog.md
CHANGED
|
@@ -4,7 +4,7 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
4
4
|
|
|
5
5
|
## Planned (in order)
|
|
6
6
|
|
|
7
|
-
**Current order (user 2026-11-05):** explicit fn generics (done faa4d7e) → cleanup phase 3b (pass list, positioned errors; running) → Io primitive + extern methods → move Rust crates under `compiler/` → tooling + scripts moved under compiler/ (done) → cleanup phase 4 (done: node ids, typed codegen, `inferExpr` errors at spans; `docs/plans/2026-11-05-compiler-cleanup-phase4.results.md`) → phases 5 and 6 (done: file splits, type kinds, tests in Plum; `docs/plans/2026-11-05-compiler-cleanup-phase5-6.results.md`) → benchmark (incl. iterator unboxing / niche Option, run C game A/B) → FBIP (stop for user approval) → deterministic sim → kids' games. The items below keep their detail; this line wins on order.
|
|
7
|
+
**Current order (user 2026-11-05):** explicit fn generics (done faa4d7e) → cleanup phase 3b (pass list, positioned errors; running) → Io primitive + extern methods → move Rust crates under `compiler/` → tooling + scripts moved under compiler/ (done) → cleanup phase 4 (done: node ids, typed codegen, `inferExpr` errors at spans; `docs/plans/2026-11-05-compiler-cleanup-phase4.results.md`) → phases 5 and 6 (done: file splits, type kinds, tests in Plum; `docs/plans/2026-11-05-compiler-cleanup-phase5-6.results.md`) → game engine refactor (done) → FBIP (user approved 2026-11-06; running, stage 1 first) → top-level functions with receivers and module namespaces (`docs/plans/2026-11-06-receiver-functions.review.md`, needs the user's decisions; replaces the "no top-level functions" note) → benchmark (incl. iterator unboxing / niche Option, run C game A/B) → FBIP (stop for user approval) → deterministic sim → kids' games. The items below keep their detail; this line wins on order.
|
|
8
8
|
|
|
9
9
|
1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Approved 2026-11-04; running on branch `superlite/compiler-cleanup`, phase 1 first.
|
|
10
10
|
- **Explicit function generics** (user 2026-11-04: right after cleanup phase 2) — functions declare their generics (`fun map[B](...)`) instead of every single capital letter being a generic; allows longer generic names and one-letter concrete types/tags. Grammar change + about 140 signatures in std/examples. Phase 2's "generics by scope" makes this mostly a grammar + signature rewrite.
|
docs/plans/2026-11-06-receiver-functions.review.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Top-level functions with receivers: review plan
|
|
2
|
+
|
|
3
|
+
**Status:** planned, not approved. Runs after FBIP, because both rewrite the same files. Nothing is built until you say so.
|
|
4
|
+
**Your idea (2026-11-06):** all functions are top level, written `fun Color.toStr(self)`. `primitive` is restricted to the standard library. Functions that need grouping go in their own module.
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
Today a method is written inside its type's body, and a group of related functions goes in a `primitive`. This plan gives one way to define a function: always at the top level, with the type in its name when it belongs to one.
|
|
9
|
+
|
|
10
|
+
```plum
|
|
11
|
+
enum Color =
|
|
12
|
+
| Red
|
|
13
|
+
| Green
|
|
14
|
+
| Blue
|
|
15
|
+
|
|
16
|
+
fun Color.toStr(self) -> Str = match self
|
|
17
|
+
Red => "Red"
|
|
18
|
+
Green => "Green"
|
|
19
|
+
Blue => "Blue"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Calling code doesn't change: `c.toStr()` still works, and so does everything built on it, such as generics, operators, `for` and overrides. The compiler already lifts nested methods to top-level functions with a receiver type internally, so this is mostly a change to the grammar and the source files.
|
|
23
|
+
|
|
24
|
+
## What you will get
|
|
25
|
+
|
|
26
|
+
- One way to write a function, and one less level of indentation everywhere.
|
|
27
|
+
- A type's declaration shows only its fields or variants, so it's easy to read at a glance.
|
|
28
|
+
- The definition looks like the call: `fun Vec2.add` and `Vec2.add` read the same way.
|
|
29
|
+
- User code has no `primitive` to learn.
|
|
30
|
+
- Modules do the grouping, like Gleam, which also gives Plum real namespaces.
|
|
31
|
+
|
|
32
|
+
## How it looks
|
|
33
|
+
|
|
34
|
+
```plum
|
|
35
|
+
struct Vec2(x: Float, y: Float)
|
|
36
|
+
|
|
37
|
+
fun Vec2.add(self, o: Vec2) -> Vec2 = # self: a method, called v.add(o)
|
|
38
|
+
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
39
|
+
|
|
40
|
+
fun Vec2.unit() -> Vec2 = # no self: a static, called Vec2.unit()
|
|
41
|
+
Vec2(x: 1.0, y: 1.0)
|
|
42
|
+
|
|
43
|
+
fun Box[T].map[B](self, f: fn(T) -> B) -> Box[B] = # generics
|
|
44
|
+
Box(value: f(self.value))
|
|
45
|
+
|
|
46
|
+
override fun Orbs.update(self, dt: Float) = ... # overrides
|
|
47
|
+
|
|
48
|
+
abstract fun Game.draw(self) # abstract methods
|
|
49
|
+
|
|
50
|
+
fun steer(ship: Ship, dt: Float) -> Ship = ... # a plain function
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The real work: modules need to become namespaces
|
|
54
|
+
|
|
55
|
+
**This is the main finding, and the biggest part of the plan.** Grouping functions in a module only works if a module has its own names. Today it doesn't: the loader merges every imported file's items into one program, so all names share one global namespace. Two files can't both define `clamp`, as that's a "duplicate declaration" error, and there's no `math.clamp(x)` form. `Math.sqrt` only works today because `Math` is a `primitive`.
|
|
56
|
+
|
|
57
|
+
So user code needs something like this:
|
|
58
|
+
|
|
59
|
+
```plum
|
|
60
|
+
import app/rules
|
|
61
|
+
|
|
62
|
+
fun main(io: Io) =
|
|
63
|
+
n := rules.clamp(5, 0, 3)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## What will be built
|
|
67
|
+
|
|
68
|
+
| Part | What it does | Size |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Module namespaces | `import a/b` gives the name `b`, with `b.f()`, `b.T` and `b.Const`. Two modules can define the same name. Type methods come with their type | L |
|
|
71
|
+
| Receiver syntax | `fun T.m(self)`, statics, generics, `override`, `abstract`, `extern` on `Io`, in the grammar, parser, formatter and LSP | M |
|
|
72
|
+
| Top-level constants | `Vec2.Zero = ...` for a type, and `MaxRetries = 3` for a module (193 constants today, all inside bodies) | S |
|
|
73
|
+
| Migration tool | A script rewrites nested methods to top level. About 1,050 functions: 741 in `std`, 252 in `examples`, 64 in `tests` | M |
|
|
74
|
+
| User primitives | `Config`, `Grid`, `Id`, `Layout`, `Rules`, `Shelf` in the examples become types or modules, and `primitive` outside `std` becomes an error | S |
|
|
75
|
+
| Remove the old form | A method inside a type body becomes a parse error with a pointer to the new form | S |
|
|
76
|
+
| Docs | Language guide and examples | S |
|
|
77
|
+
|
|
78
|
+
## Decisions for you
|
|
79
|
+
|
|
80
|
+
1. **What about std's namespaces?** `Math`, `Os`, `Http`, `Testing`, `Base64`, `Ui`, `Key`, `Mouse` and `Blend` are `primitive` namespaces in `std`. You said `primitive` stays allowed in `std`, so `Math.sqrt` can stay as is. **I'd convert the plain namespaces to modules too,** keeping `primitive` for what it's truly for, declaring built-ins (`Int`, `Str`, `Io`, `Array`). Then `primitive` means one thing, and users see one model. That changes the case: `import std/math`, then `math.sqrt(x)`. It's more churn, though, and could be a later step.
|
|
81
|
+
|
|
82
|
+
2. **Must a method live in the same file as its type?** I'd say yes, like Go: there are no surprise methods from imports, and one file shows all of a type's behaviour. `std/Json` adds `toJson` to `Int`, `Str` and others, so std stays exempt, as it is today.
|
|
83
|
+
|
|
84
|
+
3. **How are names imported?** I'd start with `import a/b` then `b.f()`, like Gleam, plus types usable unqualified where the import names them. Does `import std/game/Game` still give plain `Game`, `Key` and `Canvas`? **I'd keep types and enum variants unqualified by import,** since `Game` and `Some` everywhere would otherwise get noisy, and require the module name only for plain functions and constants. Worth a look at real code before settling.
|
|
85
|
+
|
|
86
|
+
4. **Free functions in `main`'s file?** You earlier noted the opposite idea, that every function should belong to something. This plan replaces that note: plain functions stay, in modules. I'd also keep `steer(ship, dt)` style rules as plain functions.
|
|
87
|
+
|
|
88
|
+
## Risks
|
|
89
|
+
|
|
90
|
+
- **Namespaces touch the loader, checker, codegen and LSP.** They're the risky part, since names are assumed global throughout. Internally, the simplest route is to mangle names per module when merging, so later passes stay unchanged.
|
|
91
|
+
- **A large diff.** About 1,050 functions move, so review has to rely on tooling. The check is that example wasm stays byte-identical across the syntax migration, since the syntax changes and the program doesn't.
|
|
92
|
+
- **Self-hosting and FBIP.** The FBIP `fip` keyword has to fit the new form, as `fip fun T.m(self)`, and stage 2's call-site rewrites should land first.
|
|
93
|
+
- **Scattering.** Methods can drift away from their type within a file. `plum format` could keep them together if this becomes annoying.
|
|
94
|
+
|
|
95
|
+
## Out of scope
|
|
96
|
+
|
|
97
|
+
Package management, visibility (`pub`/private), and multi-file types.
|
|
98
|
+
|
|
99
|
+
## How we will know it works
|
|
100
|
+
|
|
101
|
+
- Wasm is byte-identical before and after the syntax migration, on every example.
|
|
102
|
+
- Two modules can define the same function name and both work.
|
|
103
|
+
- All tests pass, and the docs show one form only.
|
|
104
|
+
- `primitive` outside `std` and a method inside a type body each give a clear error.
|
|
105
|
+
|
|
106
|
+
## Rough time and cost
|
|
107
|
+
|
|
108
|
+
Two builds. Namespaces first, about 6 to 8 tasks, then receiver syntax and migration, about 5 to 6 tasks. Could land in either order, but namespaces first lets the migration move grouped functions straight into modules.
|