plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a2a00dd
— Peter John
2026-11-04T17:33:40+05:30
docs: plan for Io as a built-in primitive with extern methods
docs/backlog.md
CHANGED
|
@@ -6,6 +6,7 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
6
6
|
|
|
7
7
|
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.
|
|
8
8
|
- **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.
|
|
9
|
+
- **Io as a built-in primitive with extern methods** (user 2026-11-04: LAST, once the whole compiler refactor and explicit generics are done) — `docs/plans/2026-11-04-io-primitive-extern-methods.review.md`. `primitive Io` with `extern fun f(self, ..)` methods replaces the ~55 top-level externs + forwarding `record Io` methods. A fake/recording `Io` comes later via a trait or language mocks.
|
|
9
10
|
2. **Native inline variants, then decide union enums** — compile inline variants directly as struct subtypes of their enum and delete the hidden-record lowering (`hoistInlineVariants`, the `List__Cons(Cons(..))` wrappers, union forwarders). Then decide whether user-facing union enums (`enum X = A | B` of records) stay as a thin feature or fold into traits; if they go, `JsonParse` in `plum-std/Json.plum` becomes inline variants and the union examples (`unions.plum`, `struct_union_members.plum`) are rewritten. `unboxed_variants.plum` is really a compiler probe: move it to a codegen test. (Decision record §8b.)
|
|
10
11
|
3. **Benchmark** before FBIP (user: FBIP waits for it).
|
|
11
12
|
4. **FBIP: no heap mutation** — persistent std collections updated in place when unique, Koka-style `fip` functions as a checked in-place guarantee, a reference count used only as a uniqueness check if static proof falls short. Worklist = the `mut` fields left today (see below). (Decision record §9.)
|
docs/plans/2026-11-04-io-primitive-extern-methods.review.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Io as a built-in primitive with extern methods: review plan
|
|
2
|
+
|
|
3
|
+
## When this runs
|
|
4
|
+
Last, after the whole compiler cleanup (`docs/plans/2026-11-04-compiler-cleanup.review.md`, phases 2-6) and the explicit function generics change are merged. Phases 3-5 move the code named here (checker/codegen state into structs, positioned errors, node ids, typed codegen, file splits). Before planning tasks, find every place again by symbol name; the line numbers below are from master `f198426` and will be out of date.
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
`plum-std/Io.plum` declares every host call twice: about 55 top-level `extern fun`s, and then a `record Io =` with one wrapper method for each, which only forwards to the extern. `Io` is an empty record that the entry wrappers build with `struct.new` and pass in.
|
|
8
|
+
This plan makes `Io` a built-in primitive, like `Int` and `Float`, and lets its methods be declared `extern` directly:
|
|
9
|
+
|
|
10
|
+
```plum
|
|
11
|
+
primitive Io =
|
|
12
|
+
extern fun drawRect(self, x: Float, y: Float, w: Float, h: Float, c: Color)
|
|
13
|
+
extern fun textureLoad(self, path: Str) -> Int
|
|
14
|
+
...
|
|
15
|
+
# A wall-clock 64-bit seed for `Random.seeded`.
|
|
16
|
+
fun randomSeed(self) -> Int =
|
|
17
|
+
hi := self.rawRandomHalf()
|
|
18
|
+
lo := self.rawRandomHalf() & 0xFFFFFFFF
|
|
19
|
+
return { hi << 32 } | lo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The host import names (`env.drawRect`, ...) and the host side stay as they are.
|
|
23
|
+
|
|
24
|
+
Methods that do real work over a host call stay ordinary methods. Examples are `randomSeed`, and the file/OS methods that turn a `raw*` status code into an `IoError` union (being moved from `Os.plum` into `Io.plum` by another change in progress on 2026-11-04). Their `raw*` host calls become `extern` methods on `Io` as well. Decide at planning time whether those raw methods need to be hidden from user code (a std-only or private marker), or whether exposing them is fine.
|
|
25
|
+
|
|
26
|
+
## What you will get
|
|
27
|
+
- `Io.plum` about half its size: one declaration per host call.
|
|
28
|
+
- No user code can construct an `Io`, by construction: a primitive has no constructor. The "Io can only be constructed inside std" checks go.
|
|
29
|
+
- No user code can reach a host import without an `Io`: the imports exist only as `Io` methods. The "X needs io: call io.X(..)" check and `isIoFile` go.
|
|
30
|
+
- No empty `Io` struct type in the wasm module, and no `struct.new` in each entry wrapper.
|
|
31
|
+
|
|
32
|
+
## What will be built
|
|
33
|
+
| Part | What it does | Size |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Built-in `Io` | Add `"Io"` to `BUILTIN_PRIMITIVES` (`plum-core/src/ast.rs`) and give it a checker type (`PlumType` variant or a fixed `TNamed("Io")` treated as built-in, matching how the other built-ins are named in `primitiveName`/`fromPrimitiveName`). Wasm representation: zero-size. Preferred: an `Io` value has no wasm value at all (params/locals of type `Io` are dropped). Fallback if that is too invasive: `i32` constant 0. `primitive Io` then takes `self` like `Int` does (it is no longer a "stateless namespace" in `checkPrimitives`). | M |
|
|
36
|
+
| Extern methods | The checker allows `extern fun f(self, ..)` on a built-in primitive whose value is zero-size (today only `Io`); it still rejects extern methods anywhere else (`checkFn`, "extern functions can't be methods"). Codegen imports each one under its bare name (`drawRect`) with the extern ABI (Int narrowed to i32), and lowers `io.drawRect(a, b)` to a call of that import with the receiver dropped. Parser: `collectBody` must keep extern fns in a primitive body (grammar already accepts `extern` on any `fun`; confirm). | M |
|
|
37
|
+
| Io special cases | Every check that finds `Io` as `Item::Enum(e) if e.name == "Io"` switches to the primitive: checker `io` binding for tests, extern/construction scan, codegen `desugarTestsToFns` io param, `entryTakesIo`/`emitEntryWrapper` (no `struct.new`; pass nothing or `i32.const 0`), purity `ioTypes` (already seeds `"Io"` by name; confirm a primitive-typed param still counts). If test desugaring moved into the front end in cleanup phase 2, the change is there. | S |
|
|
38
|
+
| Delete the duplicates | Rewrite `plum-std/Io.plum` as above. Remove the top-level externs, the wrapper methods, the "needs io" scan and `isIoFile`, the `FnCall`/`ClassCall` "Io" construction checks. Top-level `extern fun` stays a language feature only if something else still uses it (today only `Io.plum` and `plum-core/tests/drive_test.rs`); if not, decide with the user whether to remove it. | S |
|
|
39
|
+
| Tests | Checker: extern method allowed on `Io`, rejected on a record and on `Int`; `Io(..)` rejected in user code with a clear error; `io` unusable without a param. Codegen e2e: an entry `main(io: Io)` drawing and printing through `io.*`; a test block using `io.printLn`. Purity: a fn taking `Io` is not pure, a fn with no `Io` reachable is. Every game in `games/` and every example still compiles and runs in the browser host. | M |
|
|
40
|
+
|
|
41
|
+
## Decisions you can change
|
|
42
|
+
- **Zero-size representation.** Plan prefers "no wasm value". `i32 0` is simpler but leaves a useless param in every function that takes `io`.
|
|
43
|
+
- **Extern methods only on `Io`.** Kept narrow on purpose. Allowing them on any built-in primitive would be easy later.
|
|
44
|
+
- **Top-level `extern fun`.** Likely unused after this; removal is a separate yes/no.
|
|
45
|
+
|
|
46
|
+
## Out of scope
|
|
47
|
+
- A fake or recording `Io` for tests and replays (decision record §2). A built-in primitive with extern methods can't be replaced from Plum code; that comes later, either by making `Io` a trait or by adding mocks to the language (user 2026-11-04). Until then, faking is only possible on the host side (different wasm imports).
|
|
48
|
+
- Deterministic-sim input recording; it happens at the host boundary until the item above exists.
|
|
49
|
+
|
|
50
|
+
## Risks
|
|
51
|
+
- Dropping a zero-size value touches param/local layout in codegen; closures capturing `io` and generic code instantiated at `Io` need tests.
|
|
52
|
+
- `Seq`/`Array` are the other built-ins with special lowering; `Io` must not be caught by their code paths (e.g. anything that treats every built-in primitive as having a value type).
|
|
53
|
+
|
|
54
|
+
## How we will know it works
|
|
55
|
+
- `cargo test --workspace` and `plum test` green.
|
|
56
|
+
- `plum-std/Io.plum` has one declaration per host call.
|
|
57
|
+
- Every game and example builds and runs in the browser host with no host-side change.
|
|
58
|
+
- `grep -rn 'Item::Enum.*"Io"'` finds nothing.
|