plum

#treesitter#compiler#wasm

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.