plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
6fd238d
— Peter John
2026-11-05T18:41:07+05:30
Io as a built-in primitive with extern methods
Generated files not shown: plum-game/Cargo.lock
- README.md +1 -1
- docs/backlog.md +5 -0
- docs/plans/2026-11-05-compiler-cleanup-phase3b.progress.md +11 -0
- docs/plans/2026-11-05-io-primitive.md +98 -0
- docs/plans/2026-11-05-io-primitive.results.md +74 -0
- plum-checker/src/lib.rs +25 -55
- plum-checker/tests/checker/features/io_capability.rs +13 -9
- plum-checker/tests/checker/io_primitive_tests.rs +39 -0
- plum-checker/tests/checker/main.rs +1 -0
- plum-core/src/ast.rs +10 -1
- plum-core/src/drive.rs +3 -1
- plum-core/src/purity.rs +2 -1
- plum-core/src/tests_lower.rs +1 -1
- plum-core/tests/core/drive_test.rs +28 -26
- plum-core/tests/core/parser_test.rs +14 -0
- plum-core/tests/core/purity_test.rs +16 -7
- plum-std/Io.plum +78 -229
- plum-wasm-codegen/src/lib.rs +29 -31
- plum-wasm-codegen/tests/wasm_codegen/codegen_tests.rs +6 -12
- plum-wasm-codegen/tests/wasm_codegen/e2e_types/io_primitive.rs +78 -0
- plum-wasm-codegen/tests/wasm_codegen/e2e_types/mod.rs +1 -0
- plum-wasm-codegen/tests/wasm_codegen/fusion_alloc_test.rs +91 -0
README.md
CHANGED
|
@@ -1103,7 +1103,7 @@ fun main(io: Io) =
|
|
|
1103
1103
|
io.printLn("hello")
|
|
1104
1104
|
```
|
|
1105
1105
|
|
|
1106
|
-
Every call that leaves the program (printing, files, clock, randomness, network, drawing, input, sound) is a method of the `Io` capability in `plum-std/Io.plum`
|
|
1106
|
+
Every call that leaves the program (printing, files, clock, randomness, network, drawing, input, sound) is a method of the built-in `Io` capability, declared once as `extern fun` methods in `primitive Io` (`plum-std/Io.plum`); an `Io` cannot be constructed in user code, so no host import is reachable without one. An entry point (`main`, an `export fun`, a `test`) may take it as its first parameter (a `test` gets `io` implicitly), and code passes it down only to the functions that do I/O, so a function with no `Io` reachable cannot touch the host. That is all purity is: there is no function colouring and no `pure` keyword. An `extern fun` method declares a function backed by a host-provided wasm import (module `plum`, the function's name), with no Plum body. At the boundary `Int` is narrowed to a wasm `i32` (the rest of the language uses 64-bit `Int`), so extern parameters and results are 32-bit, and an extern can't return an error union. The `io` parameter is not part of the wasm ABI. `plum run`, `plum test` and `plum build` provide the standard host functions (printing, `crash`, clock and random, files, HTTP).
|
|
1107
1107
|
|
|
1108
1108
|
```sh
|
|
1109
1109
|
$ plum run plum-examples/basics.plum
|
docs/backlog.md
CHANGED
|
@@ -21,12 +21,17 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
21
21
|
- **User's proposal:** make `Actor` a plain struct/record and let embedding give inheritance-like behaviour: a record that embeds another *first* is a subtype of it (`Walker` is an `Actor`, so it can sit in a `Slice[Actor]`), with `override fun` dispatched on the concrete kind. Today record embedding only copies (`a World is not a Stage`), so this means embedding becomes subtyping. That is the box-free trait layout (wasm-gc base struct + subtypes, `isSubtypedTrait`) applied to records, which would retire trait fields, `__get_`/`__set_` accessors and the boxed-trait path. Open: whether behaviour-only traits stay (Rust-style, no fields) or abstract methods on the base record cover them; what `==` means on a subtyped value (identity today); whether only first-position embedding subtypes (the rule today).
|
|
22
22
|
- **Performance evidence** (BENCHMARK.md, 2026-11-03 benchmarks, M3 Pro headed): at 5.8k nodes the three styles are within about 3% (imperative trait Scene 1.70 ms, pure 1.71, functional 1.75, draw-dominated). Boxed trait field reads from outside the trait cost a dispatch over every actor kind (about 30-40 ns vs 10 ns), which made `stress_world` (20k nodes, y-sort) +92%; the box-free subtyped layout brought it back to closure speed (1.10 vs 1.09 ms). So the proposal keeps the fast path. Method dispatch is still a `ref.test` chain whose cost grows with the number of kinds. Not measured: a functional-style 20k-node world (no culling or sort fixture exists for it) and per-frame allocation/GC of `State` at that size. Add a `stress_world_state_view` fixture to the benchmark item (3) before deciding.
|
|
23
23
|
6. **Deterministic games** — fixed timestep, input recording and replay, seeded `Random` in game state, state checksums; port the design from `../kam_macroquad` (`main.rs` tick accumulator, `game/replay.rs`). The `Io` capability makes recording and replaying host input straightforward. (Decision record §10.)
|
|
24
|
+
- **Design notes (2026-11-05):** determinism comes from what may enter `update`, not from the style. Rules for both styles: (1) fixed timestep — host accumulates wall time, calls `update` with a constant `dt`, `draw` may interpolate; (2) input is a per-tick `InputFrame` value passed in, never polled inside `update` (a replay is the list of frames); (3) `update` takes no `Io` — the compiler then proves it reads no clock/random/files and draws nothing; `draw(state, io)` keeps `Io` and can't change state; (4) a seeded `Random` lives in the state (each draw returns a new generator); seed from `io` once at startup, recorded with the replay; (5) no ordering by reference identity or host-seeded hashes (std Map's hash is deterministic; keep it so); (6) wasm floats are deterministic IEEE 754 (don't hash NaN bits); (7) hash the state after each tick, compare runs.
|
|
25
|
+
- **Imperative** (`update(world, input)` mutates): deterministic under the rules, but snapshots/rollback need a deep copy or serialization of the mutable graph, checksums need a graph walker that skips shared refs, and mutation order through aliases is part of the semantics (fragile).
|
|
26
|
+
- **Functional** (`update(state, input) -> State`): determinism is almost free — pure by construction (purity pass can check it since `update` has no `Io`); a snapshot is keeping the old value (persistent structures share memory), rollback/resimulation = old state + logged inputs, checksum = hash of a value. Cost: a new state per tick, which FBIP removes when the old value isn't shared. Argues for the functional shape when the game styles become one (see "One way to build a game").
|
|
24
27
|
7. **Kids' games (ages 4-7)** (user 2026-11-04: LAST, after all the refactoring is done) — review plan `docs/plans/2026-11-04-kids-games.review.md` (written, NOT approved; re-present for a fresh human approve). Seven functional-style games in `plum-examples/games/functional/kids/`: Balloon Pop (tap), Bunny Hop, Letter Bubbles, Caterpillar, Color Piano, Star Rocket (gentle Orb Run), Hoppy Frog (platformer); nothing can be lost. Draft `balloon_pop.plum` exists (untracked) and is blocked by the `List.length` bug below. Re-check the plan against the post-refactor APIs (explicit generics, `Io` primitive, crate paths) before approving.
|
|
25
28
|
|
|
26
29
|
## Planned, not yet placed in the queue
|
|
27
30
|
|
|
28
31
|
- **Language ideas to discuss later** (user 2026-11-05: "add them ideas to be discussed later"):
|
|
29
32
|
- **Single inheritance** — DECIDED 2026-11-05, built in run C (T11).
|
|
33
|
+
- **One way to build a game** (user 2026-11-05, idea): collapse the three styles (std/game/imperative Scene, functional FunctionalScene/Ui, pure PureScene/SceneInput) into one; the game-entry shape (below) is chosen with it.
|
|
34
|
+
- **No top-level functions** (user 2026-11-05, thinking, not scheduled): every `fun` lives under a `primitive`, `enum` or `struct`; this also removes top-level `extern fun` (Io's host calls are already `extern` methods on `primitive Io`). The entry point is always on `primitive App` (`primitive App = fun main(io: Io)`). Games (user 2026-11-05, preferred): std `struct Game = abstract fun update(self, io: Io, dt: Float) -> Game; abstract fun draw(self, io: Io)`; the game's `World` embeds `Game` and overrides both; `App.main(io) = io.run(World(..))`. `io.run` hands the game ref to the host (rooted); each frame the host calls two codegen-generated exports that dispatch `game.update`/`game.draw` (normal override dispatch); `update` returns the next `Game` (imperative returns `self`, functional/pure a new value), the host keeps it. A missing `draw` is a compile error (abstract), no magic export names, "init" is just constructing `World` in `main`. Replaces the three free `export fun init/update/draw`. Considered: `App.init/update/draw` statics (untyped name contract). Open (user: "this is more OOP"): the functional alternative `io.run[S](state: S, update: fn(S, Io, Float) -> S, draw: fn(S, Io))`, e.g. `App.main(io) = io.run(World.start(io), World.update, World.draw)` — typed by the call, no base class, state-in/state-out (FBIP, deterministic sim); host stores state + two fn refs. Undecided (user 2026-11-05): pick when the three game styles (imperative, functional, pure scene) are collapsed into ONE way to build a game; the game-entry shape is chosen together with that. Trade-offs noted: embed `Game` uses the struct's single embed slot and makes `update` return the base `Game`; `io.run(state, update, draw)` keeps the concrete type and lets tests swap `update`. Open points: unqualified calls inside a type resolve to its own statics (so helpers stay short at call sites); loose helpers go in a `primitive Name =` namespace (exists today); `test` blocks stay top-level. Scale: ~109 top-level fns in std, ~350 in examples, ~70 in web-test fixtures.
|
|
30
35
|
- **One way to set up a struct.** Drop `fun init(self)` in favour of field defaults + ordinary constructor functions. `init` is what forces the Boxed layout and the EmbedInit machinery in `ctors.rs`. Main user: `std/game/imperative/Scene.plum`.
|
|
31
36
|
- **From Pony:** (1) a two-level ownership model (unique + mutable vs shared + immutable, like Pony's `iso`/`val` with `consume`) as the visible face of FBIP, replacing `mut` fields — take into the FBIP plan; (2) no ambient authority: no globals, a function without `Io` provably does no IO — what deterministic sim needs; (3) Pony partial functions (`?`, no error value) argue for `Option` + `?` as the common path and `Result` only when the error value matters. Not copied: union types, structural interfaces, six capabilities.
|
|
32
37
|
|
docs/plans/2026-11-05-compiler-cleanup-phase3b.progress.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
- start faa4d7e
|
|
2
|
+
wasm hashes before T1: .superlite/wasm-before-3b.txt (25 examples, all compile)
|
|
3
|
+
Ruling: skip Wave 0 - cost: none
|
|
4
|
+
Ruling: accept predicted 39 min - forced chain - cost: none
|
|
5
|
+
Ruling: T1 API names *Diag (loadAndMergeDiag, lower::perFileDiag, frontEndDiag); old String names are wrappers removed in T6 - cost: none
|
|
6
|
+
Ruling: T2 residual - inferExpr's Err(String) undefined-name errors are classified Resolution by a registry of exact registered messages (not keywords); full fix = inferExpr returns Diagnostic, moved to phase 4 (spans/node ids) - cost: one narrow string match remains until phase 4
|
|
7
|
+
Ruling: T4 touched plum-core/tests/core/trmc_test.rs (compile return type) - needed - cost: none
|
|
8
|
+
Ruling: final-review checkpoint-dropped files (plum-driver/, Cargo.toml deps, trmc_test, host_test, docs) - bookkeeping; all on disk, workspace member present, T6 full sweep green on the working tree - cost: none
|
|
9
|
+
Ruling: final-review registry accumulation - resetCheck replaces the whole CheckState with Default (clears resolution_msgs) per compile - cost: none
|
|
10
|
+
Ruling: final-review dropped-rewrite state - driver test aRewriteThatFailsItsRecheckIsDropped prepends a breaking pass and asserts the result equals the run without it; order is the PASSES const - cost: none
|
|
11
|
+
Ruling: final-review protocol - CLI stderr/exit tested by undefinedNameIsReportedAsFileLineColumnTypeError; LSP 0-based mapping by typeDiagnosticUsesTheDiagnosticPosition - cost: none
|
docs/plans/2026-11-05-io-primitive.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Io as a built-in primitive with extern methods
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-04-io-primitive-extern-methods.review.md
|
|
4
|
+
**Goal:** `Io` is a built-in primitive (like `Int`) whose host calls are declared once, as `extern fun name(self, ..)` methods in `primitive Io =` (std/Io.plum). The ~50 top-level externs and ~51 forwarding methods go; user code can't construct an `Io` or reach a host import without one, by construction, so the "needs io" scan, `isIoFile` and the "Io can only be constructed inside std" checks go. Host import names (`env.drawRect`, ...) and the host side are unchanged.
|
|
5
|
+
**Architecture:** plum-core (ast BUILTIN_PRIMITIVES, parser collectBody, tests_lower, purity), plum-checker, plum-wasm-codegen, plum-std/Io.plum (+ callers), plum-game web-test.
|
|
6
|
+
**Tech stack:** Rust 2021, Plum, wasm-gc.
|
|
7
|
+
**Detail:** contracts
|
|
8
|
+
**Target-min:** 30
|
|
9
|
+
|
|
10
|
+
## Global constraints
|
|
11
|
+
- Work on `master` in the main checkout, default shared `target/`. Don't commit. Never run `plum format` on real files. No `sed -i -E`.
|
|
12
|
+
- No `cargo test` (hook): `cargo build --tests`; per-crate test binaries in `target/debug/deps` (`core-*`, `checker-*`, `cli-*`, `wasm_codegen-*`, `driver_tests-*`; newest by mtime `/bin/ls -t`). Plum sweep `cargo build -p plum-cli && target/debug/plum test plum-std plum-examples` (662).
|
|
13
|
+
- Rulings (coordinator, AI orchestrator): an `Io` value is an `i32` constant 0 in wasm (review plan's fallback; dropping the value entirely is a later optimisation). `raw*` host calls are ordinary `extern` methods on `Io`, not hidden (they need an `Io` anyway). Extern methods are allowed only in `primitive Io`. Top-level `extern fun` stays a language feature for now (removal is the user's call; ask after this run).
|
|
14
|
+
- T1-T2 add the new path next to today's `struct Io` path (both work); T3 switches std; T4 deletes the old path.
|
|
15
|
+
|
|
16
|
+
## Task graph
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart LR
|
|
19
|
+
T1[core + checker: primitive Io, extern methods]
|
|
20
|
+
T2[codegen: Io as i32 0, extern method imports]
|
|
21
|
+
T3[std Io.plum rewrite]
|
|
22
|
+
T4[delete old struct-Io paths]
|
|
23
|
+
T5[verify incl. browser games]
|
|
24
|
+
%% taskgraph
|
|
25
|
+
T1 --> T2
|
|
26
|
+
T2 --> T3
|
|
27
|
+
T3 --> T4
|
|
28
|
+
T4 --> T5
|
|
29
|
+
```
|
|
30
|
+
Ruling: sequential [chain] - checker support, then codegen, then the std switch that deletes the old path - cost: one task at a time.
|
|
31
|
+
|
|
32
|
+
## Package A: compiler
|
|
33
|
+
**Probe entry:** run=plum test plum-std plum-examples
|
|
34
|
+
|
|
35
|
+
### T1 core and checker: primitive Io, extern methods · Complexity 4 · Est. context 45k
|
|
36
|
+
**Owns:** plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/src/tests_lower.rs, plum-core/src/purity.rs, plum-core/src/loader.rs, plum-checker/src/*.rs, plum-checker/tests/checker/io_primitive_tests.rs, plum-checker/tests/checker/main.rs, plum-core/tests/core/parser_test.rs
|
|
37
|
+
**Components:** 2
|
|
38
|
+
**LOC owned:** 120
|
|
39
|
+
**Tests:** fast
|
|
40
|
+
**Model:** sonnet
|
|
41
|
+
**Produces:** `primitive Io = extern fun f(self, ..)` parses and checks; `Io` is a built-in type (`BUILTIN_PRIMITIVES` has "Io"); `io.f(a)` types as the extern's return; extern methods elsewhere rejected
|
|
42
|
+
e.g. `primitive Io =\n extern fun printLn(self, s: Str)` + `fun main(io: Io) = io.printLn("hi")` -> checks; `struct R =\n extern fun f(self)` -> error "extern methods are only allowed on Io"; `primitive Int =\n extern fun g(self)` (in a test program) -> same error; user `Io()` -> error "Io has no constructor"
|
|
43
|
+
**Notes:** parser `collectBody` keeps extern fns in a primitive body. Tests' `io` binding, tests_lower's io param and purity's io detection recognise `primitive Io` as well as today's `struct Io` (old path stays until T3). An `Io` param/local/closure capture types as `TNamed("Io")` treated as built-in.
|
|
44
|
+
**Test code:** io_primitive_tests: the e.g. cases; a test block using `io` with `primitive Io` in scope; purity: fn taking Io not pure. All binaries + sweep green.
|
|
45
|
+
|
|
46
|
+
### T2 codegen: Io value and extern-method imports · Complexity 4 · Est. context 45k
|
|
47
|
+
**Owns:** plum-wasm-codegen/src/*.rs, plum-wasm-codegen/tests/wasm_codegen/e2e_types/io_primitive.rs, plum-wasm-codegen/tests/wasm_codegen/e2e_types/mod.rs
|
|
48
|
+
**Components:** 1
|
|
49
|
+
**LOC owned:** 120
|
|
50
|
+
**Tests:** fast
|
|
51
|
+
**Model:** sonnet
|
|
52
|
+
**Consumes:** `primitive Io = extern fun f(self, ..)` parses and checks; `Io` is a built-in type (`BUILTIN_PRIMITIVES` has "Io"); `io.f(a)` types as the extern's return; extern methods elsewhere rejected (T1)
|
|
53
|
+
**Produces:** with `primitive Io`, an `Io` is wasm `i32` (constant 0), each extern method is imported as `env.<name>` with the extern ABI (Int narrowed to i32 as today), `io.f(a)` calls the import with the receiver dropped, entry wrappers pass `i32.const 0` (no `struct.new`)
|
|
54
|
+
e.g. a program with `primitive Io` declaring `extern fun printLn(self, s: Str)` and `fun main(io: Io) = io.printLn("hi")` -> wasm imports `env.printLn`, no Io struct type; running it with the test host prints "hi"
|
|
55
|
+
**Notes:** closures capturing `io` and a generic fn instantiated with an `Io` argument must compile. `Array`/other built-in special paths must not catch `Io`. Today's `struct Io` codegen path stays until T3.
|
|
56
|
+
**Test code:** e2e io_primitive: the e.g. case; a closure capturing io; a test block calling `io.printLn`; existing wasm_codegen + sweep green.
|
|
57
|
+
|
|
58
|
+
## Package B: switch
|
|
59
|
+
**Probe entry:** run=plum test plum-std plum-examples
|
|
60
|
+
|
|
61
|
+
### T3 rewrite std Io.plum on primitive Io · Complexity 3 · Est. context 40k
|
|
62
|
+
**Owns:** plum-std/**/*.plum, plum-examples/**/*.plum, plum-game/web-test/fixtures/*.plum, README.md
|
|
63
|
+
**Components:** 1
|
|
64
|
+
**LOC owned:** 150
|
|
65
|
+
**Tests:** fast
|
|
66
|
+
**Model:** sonnet
|
|
67
|
+
**Consumes:** with `primitive Io`, an `Io` is wasm `i32` (constant 0), each extern method is imported as `env.<name>` with the extern ABI (Int narrowed to i32 as today), `io.f(a)` calls the import with the receiver dropped, entry wrappers pass `i32.const 0` (no `struct.new`) (T2)
|
|
68
|
+
**Produces:** std/Io.plum is `primitive Io =` with one declaration per host call (plus real-work methods like `randomSeed`); every caller uses `io.x(..)`; wasm import lists unchanged
|
|
69
|
+
e.g. `grep -c '^ extern fun' plum-std/Io.plum` == the number of top-level externs today (50); `grep -c '^extern fun' plum-std/Io.plum` -> 0
|
|
70
|
+
**Notes:** before editing, record every example's and fixture's wasm import list (names + types, e.g. via `wasm-tools print` or wasmparser in a scratch script); after, they must be identical. Methods doing real work over a host call stay ordinary methods. Anything that called a top-level extern directly now calls the method.
|
|
71
|
+
**Test code:** import-list comparison identical; `plum test plum-std plum-examples` green; fixtures compile.
|
|
72
|
+
|
|
73
|
+
### T4 delete the old struct-Io paths · Complexity 3 · Est. context 40k
|
|
74
|
+
**Owns:** plum-core/src/*.rs, plum-checker/src/*.rs, plum-wasm-codegen/src/*.rs, plum-lsp/src/*.rs, plum-checker/tests/checker/**/*.rs, plum-wasm-codegen/tests/wasm_codegen/**/*.rs, plum-core/tests/core/*.rs, plum-cli/tests/cli/*.rs
|
|
75
|
+
**Components:** 1
|
|
76
|
+
**LOC owned:** 80
|
|
77
|
+
**Tests:** fast
|
|
78
|
+
**Model:** sonnet
|
|
79
|
+
**Consumes:** std/Io.plum is `primitive Io =` with one declaration per host call (plus real-work methods like `randomSeed`); every caller uses `io.x(..)`; wasm import lists unchanged (T3)
|
|
80
|
+
**Produces:** no `struct Io`/`Item::Enum .. "Io"` code paths, no "needs io" scan, no `isIoFile`, no Io-construction checks
|
|
81
|
+
e.g. `grep -rn 'e.name == "Io"' plum-*/src` -> no hits; `grep -rn isIoFile plum-*/src` -> no hits
|
|
82
|
+
**Notes:** codegen's Io record type/struct.new entry path and class_type_idx lookups for Io go; tests that built a `struct Io` move to `primitive Io`. LSP index entry for Io stays working.
|
|
83
|
+
**Test code:** the greps empty; all binaries + sweep green.
|
|
84
|
+
|
|
85
|
+
### T5 verify including browser games · Complexity 2 · Est. context 25k
|
|
86
|
+
**Owns:** docs/plans/2026-11-05-io-primitive.results.md
|
|
87
|
+
**Components:** 1
|
|
88
|
+
**LOC owned:** 30
|
|
89
|
+
**Tests:** slow
|
|
90
|
+
**Model:** sonnet
|
|
91
|
+
**Consumes:** no `struct Io`/`Item::Enum .. "Io"` code paths, no "needs io" scan, no `isIoFile`, no Io-construction checks (T4)
|
|
92
|
+
**Produces:** Io primitive results
|
|
93
|
+
**Notes:** workspace build (zero warnings), every binary, sweep, fixtures; then the browser host: `cargo run --release --manifest-path plum-game/web-test/Cargo.toml -- plum-game/web-test/fixtures/stress_scene.plum --measure 3 --headed --json` and one game from plum-examples/games per style (functional, imperative, pure) runs and draws (frame count > 0, no console errors). Io.plum line count before/after.
|
|
94
|
+
**Test code:** results doc; report red, don't fix.
|
|
95
|
+
|
|
96
|
+
## Review Focus
|
|
97
|
+
- A: protocol — import names/signatures unchanged; extern ABI narrowing identical.
|
|
98
|
+
- B: state — closures and generics over `Io`; no built-in path treats `Io` as having a heap value.
|
docs/plans/2026-11-05-io-primitive.results.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Io primitive: T5 verification results (2026-11-05)
|
|
2
|
+
|
|
3
|
+
All green. Nothing was fixed or committed.
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
`cargo build --workspace --tests`: finished, zero Rust warnings. The only warnings are the two pre-existing C `unused function` warnings from `tree-sitter-plum/src/scanner.c` (`set_raw`, `set_bytes`). `cargo build -p plum-cli` is also clean.
|
|
8
|
+
|
|
9
|
+
## Test binaries
|
|
10
|
+
|
|
11
|
+
| Binary | Result |
|
|
12
|
+
|---|---|
|
|
13
|
+
| core | 393 passed |
|
|
14
|
+
| checker | 146 passed |
|
|
15
|
+
| cli (includes crash, game_web, host_stub, explain_layout, test_scope modules) | 16 passed |
|
|
16
|
+
| wasm_codegen | 212 passed |
|
|
17
|
+
| driver_tests | 3 passed |
|
|
18
|
+
| plum_lsp (lib) | 20 passed |
|
|
19
|
+
| plum_host (lib) | 3 passed |
|
|
20
|
+
| plum_driver (lib) | 0 tests |
|
|
21
|
+
| plum-game host_test (own target dir) | 18 passed |
|
|
22
|
+
|
|
23
|
+
Only the binaries built by this run were counted. A stale `crash_test-*` binary from 16:25 still sits in `target/debug/deps`. It fails `int_literal_overflow_error_has_position`, but `crash_test.rs` is now a module of the `cli` binary, where the same test passes. Ignore it.
|
|
24
|
+
|
|
25
|
+
`target/debug/plum test plum-std plum-examples`: 662 passed, 0 failed, 65/65 files.
|
|
26
|
+
|
|
27
|
+
`plum compile` on all 9 `plum-game/web-test/fixtures/*.plum` succeeded: interleave, rendertarget, stress_pure_scene, stress_scene, stress_state_view_deep, stress_state_view, stress_world, stress, text_sizes. The outputs were deleted.
|
|
28
|
+
|
|
29
|
+
## Browser host
|
|
30
|
+
|
|
31
|
+
The headed browser ran fine here with `plumRenderer=gpu-webgpu`. Every run printed PASS with no console errors.
|
|
32
|
+
|
|
33
|
+
| Game | Frames in 3s | fps | GPU draw calls |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| stress_scene fixture | 360 | 120 | 1 |
|
|
36
|
+
| functional/game_state_view | 359 | 120 | 7 |
|
|
37
|
+
| imperative/game_scene | 360 | 120 | 7 |
|
|
38
|
+
| pure/game_pure_scene | 359 | 120 | 7 |
|
|
39
|
+
|
|
40
|
+
For stress_scene: avg 8.33 ms, p99 9.2 ms, and 464348 pixels changed over 1s.
|
|
41
|
+
|
|
42
|
+
## Io.plum size
|
|
43
|
+
|
|
44
|
+
Before (79df289): 249 lines. After: 98 lines.
|
|
45
|
+
|
|
46
|
+
## `git diff --stat 79df289`
|
|
47
|
+
|
|
48
|
+
19 files changed, 336 insertions(+), 374 deletions(-). The stat covers tracked files only.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
.gitignore | 3 +
|
|
52
|
+
README.md | 2 +-
|
|
53
|
+
docs/backlog.md | 5 +
|
|
54
|
+
plum-checker/src/lib.rs | 80 ++----
|
|
55
|
+
.../tests/checker/features/io_capability.rs | 22 +-
|
|
56
|
+
plum-checker/tests/checker/main.rs | 1 +
|
|
57
|
+
plum-core/src/ast.rs | 11 +-
|
|
58
|
+
plum-core/src/drive.rs | 4 +-
|
|
59
|
+
plum-core/src/purity.rs | 3 +-
|
|
60
|
+
plum-core/src/tests_lower.rs | 2 +-
|
|
61
|
+
plum-core/tests/core/drive_test.rs | 54 ++--
|
|
62
|
+
plum-core/tests/core/parser_test.rs | 14 +
|
|
63
|
+
plum-core/tests/core/purity_test.rs | 23 +-
|
|
64
|
+
plum-game/Cargo.lock | 9 +
|
|
65
|
+
plum-std/Io.plum | 307 ++++++---------------
|
|
66
|
+
plum-wasm-codegen/src/lib.rs | 60 ++--
|
|
67
|
+
.../tests/wasm_codegen/codegen_tests.rs | 18 +-
|
|
68
|
+
.../tests/wasm_codegen/e2e_types/mod.rs | 1 +
|
|
69
|
+
.../tests/wasm_codegen/fusion_alloc_test.rs | 91 ++++++
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Untracked and not in the stat: `plum-checker/tests/checker/io_primitive_tests.rs`, `plum-wasm-codegen/tests/wasm_codegen/e2e_types/io_primitive.rs`, `docs/plans/2026-11-05-io-primitive.md`, `docs/plans/2026-11-05-compiler-cleanup-phase3b.progress.md`, `.claude/`.
|
|
73
|
+
|
|
74
|
+
Note: building plum-game updated `plum-game/Cargo.lock` (adds a `plum-driver` package entry). That was a side effect of this verification, not part of the Io change.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1011,67 +1011,40 @@ fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1011
1011
|
(result, info)
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
-
/// `file` is std's `Io.plum`, the only place host externs may be called.
|
|
1015
|
-
fn isIoFile(file: &str) -> bool {
|
|
1016
|
-
let p = std::path::Path::new(file);
|
|
1017
|
-
p.file_name().and_then(|n| n.to_str()) == Some("Io.plum")
|
|
1018
|
-
&& p.parent().and_then(|d| d.file_name()).and_then(|n| n.to_str()) == Some("plum-std")
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/// `file` belongs to the std library tree (`plum-std/...`).
|
|
1022
|
-
fn isStdFile(file: &str) -> bool {
|
|
1023
|
-
std::path::Path::new(file)
|
|
1024
|
-
.components()
|
|
1025
|
-
.any(|c| c.as_os_str() == "plum-std")
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/// The `Io` capability rules:
|
|
1014
|
+
/// The `Io` capability rules: no top-level `extern fun`, and the primitive `Io` has no constructor.
|
|
1029
|
-
/// and `Io()` is constructed only inside std.
|
|
1030
1015
|
fn ioCapabilityErrors(source: &ast::Source) -> Vec<CheckError> {
|
|
1031
|
-
let externs: BTreeSet<&str> = source
|
|
1032
|
-
.items
|
|
1033
|
-
.iter()
|
|
1034
|
-
.filter_map(|i| match i {
|
|
1035
|
-
ast::Item::Fn(f) if f.is_extern => Some(f.name.as_str()),
|
|
1036
|
-
_ => None,
|
|
1037
|
-
})
|
|
1038
|
-
.collect();
|
|
1039
|
-
let hasIo = source
|
|
1040
|
-
.items
|
|
1041
|
-
.iter()
|
|
1042
|
-
.any(|i| matches!(i, ast::Item::Enum(e) if e.name == "Io"));
|
|
1043
|
-
let checkExterns = !externs.is_empty();
|
|
1044
|
-
if !checkExterns && !hasIo {
|
|
1045
|
-
return Vec::new();
|
|
1046
|
-
}
|
|
1047
1016
|
let mut errors = Vec::new();
|
|
1048
|
-
|
|
1017
|
+
// Host calls are declared only as `extern fun` methods on `primitive Io`.
|
|
1049
|
-
|
|
1018
|
+
for item in &source.items {
|
|
1050
|
-
let
|
|
1019
|
+
if let ast::Item::Fn(f) = item {
|
|
1051
|
-
|
|
1020
|
+
if f.is_extern && f.type_param.is_none() {
|
|
1052
|
-
ast::Expr::FnCall(c) if checkExterns && !io && externs.contains(c.name.as_str()) => {
|
|
1053
1021
|
errors.push(CheckError::at(
|
|
1054
|
-
|
|
1022
|
+
f.line,
|
|
1055
|
-
|
|
1023
|
+
f.column,
|
|
1056
|
-
|
|
1024
|
+
"host calls are declared as `extern fun` methods on `primitive Io`".to_string(),
|
|
1057
1025
|
));
|
|
1058
1026
|
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if !source.hasPrimitiveIo() {
|
|
1030
|
+
return errors;
|
|
1031
|
+
}
|
|
1032
|
+
let mut scan = |body: &mut ast::FnBody| {
|
|
1033
|
+
plum_core::forms::mapExprs(body, &mut |e| match e {
|
|
1059
|
-
ast::Expr::FnCall(c) if
|
|
1034
|
+
ast::Expr::FnCall(c) if c.name == ast::IO_TYPE => {
|
|
1060
|
-
errors.push(CheckError::at(c.line, c.column, "Io
|
|
1035
|
+
errors.push(CheckError::at(c.line, c.column, "Io has no constructor".to_string()));
|
|
1061
1036
|
}
|
|
1062
|
-
ast::Expr::ClassCall(c) if
|
|
1037
|
+
ast::Expr::ClassCall(c) if c.type_name == ast::IO_TYPE => {
|
|
1063
|
-
errors.push(CheckError::at(0, 0, "Io
|
|
1038
|
+
errors.push(CheckError::at(0, 0, "Io has no constructor".to_string()));
|
|
1064
1039
|
}
|
|
1065
1040
|
_ => {}
|
|
1066
1041
|
});
|
|
1067
1042
|
};
|
|
1068
1043
|
for item in &source.items {
|
|
1069
1044
|
match item {
|
|
1070
|
-
ast::Item::Fn(f) if !f.is_extern => scan(&
|
|
1045
|
+
ast::Item::Fn(f) if !f.is_extern => scan(&mut f.body.clone()),
|
|
1071
|
-
ast::Item::Test(t) => {
|
|
1072
|
-
|
|
1046
|
+
ast::Item::Test(t) => scan(&mut ast::FnBody::Block(t.body.clone())),
|
|
1073
|
-
}
|
|
1074
|
-
ast::Item::Const(c) => scan(
|
|
1047
|
+
ast::Item::Const(c) => scan(&mut ast::FnBody::Expr(c.value.clone())),
|
|
1075
1048
|
_ => {}
|
|
1076
1049
|
}
|
|
1077
1050
|
}
|
|
@@ -1702,7 +1675,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1702
1675
|
let prevFile = (!t.file.is_empty()).then(|| setCurrentFile(&t.file));
|
|
1703
1676
|
let mut env = global_env.clone();
|
|
1704
1677
|
// A test may use `io` when std/Io is loaded (the test wrapper passes it).
|
|
1705
|
-
if source.
|
|
1678
|
+
if source.hasPrimitiveIo() {
|
|
1706
1679
|
env.insert("io".to_string(), TypeScheme::mono(PlumType::TNamed("Io".to_string())));
|
|
1707
1680
|
}
|
|
1708
1681
|
let mut block_errors = checkBlock(
|
|
@@ -1974,14 +1947,11 @@ fn checkFnBody(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckEr
|
|
|
1974
1947
|
// report it plainly rather than trying to guess which side was "right".
|
|
1975
1948
|
match (f.is_extern, &f.body) {
|
|
1976
1949
|
(true, ast::FnBody::Extern) => {
|
|
1977
|
-
if f.type_param.
|
|
1950
|
+
if f.type_param.as_deref().is_some_and(|t| t != ast::IO_TYPE) {
|
|
1978
1951
|
errors.push(CheckError::resolution(
|
|
1979
1952
|
f.line,
|
|
1980
1953
|
f.column,
|
|
1981
|
-
format!(
|
|
1982
|
-
|
|
1954
|
+
format!("fn '{}': extern methods are only allowed on Io", f.name),
|
|
1983
|
-
f.name
|
|
1984
|
-
),
|
|
1985
1955
|
));
|
|
1986
1956
|
}
|
|
1987
1957
|
return errors;
|
plum-checker/tests/checker/features/io_capability.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! The `Io` capability:
|
|
1
|
+
//! The `Io` capability: the primitive `Io` has no constructor.
|
|
2
2
|
use plum_checker::checkSource;
|
|
3
3
|
|
|
4
4
|
fn run(src: &str) -> Result<(), String> {
|
|
@@ -8,17 +8,21 @@ fn run(src: &str) -> Result<(), String> {
|
|
|
8
8
|
r
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const IO: &str = "primitive Io =\n extern fun ping(self) -> Int\n\n";
|
|
12
12
|
|
|
13
13
|
#[test]
|
|
14
|
-
fn
|
|
14
|
+
fn io_method_callable_with_an_io() {
|
|
15
|
-
let e = run(EXTERN).unwrap_err();
|
|
16
|
-
|
|
15
|
+
run(&format!("{IO}fun f(io: Io) -> Int =\n io.ping()\n")).unwrap();
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
#[test]
|
|
20
|
-
fn
|
|
19
|
+
fn io_constructor_rejected() {
|
|
21
|
-
let
|
|
20
|
+
let e = run(&format!("{IO}fun f() -> Int =\n io := Io()\n io.ping()\n")).unwrap_err();
|
|
22
|
-
let e = run(src).unwrap_err();
|
|
23
|
-
assert!(e.contains("Io
|
|
21
|
+
assert!(e.contains("Io has no constructor"), "{e}");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn top_level_extern_rejected() {
|
|
26
|
+
let e = run("extern fun printLn(s: Str)\n\nfun hello() =\n printLn(\"hi\")\n").unwrap_err();
|
|
27
|
+
assert!(e.contains("host calls are declared as `extern fun` methods on `primitive Io`"), "{e}");
|
|
24
28
|
}
|
plum-checker/tests/checker/io_primitive_tests.rs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! `primitive Io` with `extern fun f(self, ..)` host methods.
|
|
3
|
+
use plum_checker::checkSource;
|
|
4
|
+
|
|
5
|
+
fn run(src: &str) -> Result<(), String> {
|
|
6
|
+
let mut source = plum_test_support::parseRawSource(src);
|
|
7
|
+
plum_core::lower::frontEndDiag(&mut source).map_err(|e| format!("lower: {e:?}"))?;
|
|
8
|
+
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const IO: &str = "primitive Io =\n extern fun printLn(self, s: Str)\n extern fun width(self) -> Float\n\n";
|
|
12
|
+
|
|
13
|
+
#[test]
|
|
14
|
+
fn ioExternMethodsCheckAndTypeAsTheirReturn() {
|
|
15
|
+
run(&format!("{IO}fun main(io: Io) =\n io.printLn(\"hi\")\n")).unwrap();
|
|
16
|
+
run(&format!("{IO}fun w(io: Io) -> Float = io.width()\n")).unwrap();
|
|
17
|
+
let e = run(&format!("{IO}fun w(io: Io) -> Int = io.width()\n")).unwrap_err();
|
|
18
|
+
assert!(!e.is_empty());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn ioIsUsableInClosuresAndTestBlocks() {
|
|
23
|
+
run(&format!("{IO}fun main(io: Io) =\n f := || io.printLn(\"x\")\n f()\n")).unwrap();
|
|
24
|
+
run(&format!("{IO}test \"uses io\"\n io.printLn(\"hi\")\n")).unwrap();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn externMethodsAreOnlyAllowedOnIo() {
|
|
29
|
+
let e = run("struct R(x: Int) =\n extern fun f(self)\n").unwrap_err();
|
|
30
|
+
assert!(e.contains("extern methods are only allowed on Io"), "{e}");
|
|
31
|
+
let e = run("primitive Int =\n extern fun g(self)\n").unwrap_err();
|
|
32
|
+
assert!(e.contains("extern methods are only allowed on Io"), "{e}");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn ioHasNoConstructor() {
|
|
37
|
+
let e = run(&format!("{IO}fun main() =\n io := Io()\n io.printLn(\"x\")\n")).unwrap_err();
|
|
38
|
+
assert!(e.contains("Io has no constructor"), "{e}");
|
|
39
|
+
}
|
plum-checker/tests/checker/main.rs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
mod checker_tests;
|
|
4
4
|
mod closure_hint_test;
|
|
5
5
|
mod diagnostic_tests;
|
|
6
|
+
mod io_primitive_tests;
|
|
6
7
|
mod explicit_generics_tests;
|
|
7
8
|
mod features;
|
|
8
9
|
mod fusion_pipeline_test;
|
plum-core/src/ast.rs
CHANGED
|
@@ -32,7 +32,11 @@ pub struct PrimitiveDecl {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/// Compiler built-in types whose API a `primitive` block declares (std only).
|
|
35
|
-
pub const BUILTIN_PRIMITIVES: [&str;
|
|
35
|
+
pub const BUILTIN_PRIMITIVES: [&str; 7] = ["Int", "Float", "Bool", "Str", "Byte", "Array", "Io"];
|
|
36
|
+
|
|
37
|
+
/// The host capability type. Its host calls are `extern fun f(self, ..)` methods of
|
|
38
|
+
/// `primitive Io =` (std/Io.plum); extern methods are allowed nowhere else.
|
|
39
|
+
pub const IO_TYPE: &str = "Io";
|
|
36
40
|
|
|
37
41
|
/// Separator of an associated item's mangled global name: `Float.Pi`.
|
|
38
42
|
pub const MEMBER_SEP: &str = ".";
|
|
@@ -45,6 +49,11 @@ pub enum TopLevelKind {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
impl Source {
|
|
52
|
+
/// Whether the program declares the primitive `Io` (some `extern fun` method of it).
|
|
53
|
+
pub fn hasPrimitiveIo(&self) -> bool {
|
|
54
|
+
self.items.iter().any(|i| matches!(i, Item::Fn(f) if f.is_extern && f.type_param.as_deref() == Some(IO_TYPE)))
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
/// Records `file` on the source and on every fn it declares.
|
|
49
58
|
pub fn stampFile(&mut self, file: &str) {
|
|
50
59
|
self.file = file.to_string();
|
plum-core/src/drive.rs
CHANGED
|
@@ -1779,7 +1779,9 @@ impl Driver {
|
|
|
1779
1779
|
_ => None,
|
|
1780
1780
|
};
|
|
1781
1781
|
let Some((p, v)) = cand else { continue };
|
|
1782
|
+
// the whole value is pure: a pure call under an impure one (`sumL(markL(io, build(n)))`)
|
|
1783
|
+
// must not move past a host call
|
|
1782
|
-
let isProducer = self.calls(&v).iter().any(|c| self.pure.contains(&c.key));
|
|
1784
|
+
let isProducer = self.calls(&v).iter().any(|c| self.pure.contains(&c.key)) && self.argPure(&v);
|
|
1783
1785
|
// a just-built value with pure fields moves to its use like a pure producer call,
|
|
1784
1786
|
// so a consumer applied to it can pick its arm (`layers := Cons(..); draw(layers)`)
|
|
1785
1787
|
let isBuilt = self.ctorType(&v).is_some() && self.argPure(&v);
|
plum-core/src/purity.rs
CHANGED
|
@@ -168,7 +168,8 @@ pub fn readOnlyFns(src: &Source) -> HashSet<String> {
|
|
|
168
168
|
}
|
|
169
169
|
let mut info = analyse(f, &ctors);
|
|
170
170
|
info.impure = if f.is_extern || matches!(f.body, FnBody::Extern) {
|
|
171
|
+
// `self` of a primitive `Io` extern method is the unit-like capability, not an object
|
|
171
|
-
f.has_self
|
|
172
|
+
(f.has_self && f.type_param.as_deref() != Some(crate::ast::IO_TYPE))
|
|
172
173
|
|| !f.params.iter().all(|p| match &p.ty {
|
|
173
174
|
ParamType::Type(t) => t.generics.is_empty() && values.contains(t.name.as_str()),
|
|
174
175
|
_ => false,
|
plum-core/src/tests_lower.rs
CHANGED
|
@@ -67,7 +67,7 @@ fn desugarTestsToFns(source: &crate::ast::Source) -> (crate::ast::Source, Vec<(S
|
|
|
67
67
|
let mut test_idx = 0usize;
|
|
68
68
|
let mut counter = 0usize;
|
|
69
69
|
// Tests may use `io` when std/Io is loaded: the export wrapper passes the `Io`.
|
|
70
|
-
let hasIo = source.
|
|
70
|
+
let hasIo = source.hasPrimitiveIo();
|
|
71
71
|
let ioParam = if hasIo {
|
|
72
72
|
vec![crate::ast::Param {
|
|
73
73
|
name: "io".to_string(),
|
plum-core/tests/core/drive_test.rs
CHANGED
|
@@ -157,14 +157,15 @@ fn sharedResultUnchanged() {
|
|
|
157
157
|
#[test]
|
|
158
158
|
fn impure_producer_untouched() {
|
|
159
159
|
let (s, _) = run(
|
|
160
|
+
"primitive Io =
|
|
160
|
-
|
|
161
|
+
extern fun tick(self, x: Int) -> Int
|
|
161
|
-
fun mapI(xs: Lst) -> Lst = match xs
|
|
162
|
+
fun mapI(io: Io, xs: Lst) -> Lst = match xs
|
|
162
163
|
Nil => Nil
|
|
163
|
-
Cons(h, t) => Cons(head: tick(h), tail: mapI(t))
|
|
164
|
+
Cons(h, t) => Cons(head: io.tick(h), tail: mapI(io, t))
|
|
164
165
|
fun sumL(xs: Lst) -> Int = match xs
|
|
165
166
|
Nil => 0
|
|
166
167
|
Cons(h, t) => h + sumL(t)
|
|
167
|
-
fun total(xs: Lst) -> Int = sumL(mapI(xs))
|
|
168
|
+
fun total(io: Io, xs: Lst) -> Int = sumL(mapI(io, xs))
|
|
168
169
|
",
|
|
169
170
|
);
|
|
170
171
|
assert!(fusedNames(&s).is_empty());
|
|
@@ -343,13 +344,14 @@ fn impureConsumerFusesWithPureProducer() {
|
|
|
343
344
|
// Only the producer must be pure: the consumer's effects interleave with the producer's
|
|
344
345
|
// (pure, unobservable) steps.
|
|
345
346
|
let (s, notes) = run(&format!(
|
|
347
|
+
"primitive Io =
|
|
346
|
-
|
|
348
|
+
extern fun emit(self, x: Int)
|
|
347
|
-
{MAP_SUM}fun drawL(xs: Lst) -> Int = match xs
|
|
349
|
+
{MAP_SUM}fun drawL(io: Io, xs: Lst) -> Int = match xs
|
|
348
350
|
Nil => 0
|
|
349
351
|
Cons(h, t) =>
|
|
350
|
-
emit(h)
|
|
352
|
+
io.emit(h)
|
|
351
|
-
drawL(t)
|
|
353
|
+
drawL(io, t)
|
|
352
|
-
fun go(xs: Lst) -> Int = drawL(mapL(xs))
|
|
354
|
+
fun go(io: Io, xs: Lst) -> Int = drawL(io, mapL(xs))
|
|
353
355
|
"
|
|
354
356
|
));
|
|
355
357
|
let f = getFn(&s, "drawL$mapL$0").unwrap_or_else(|| panic!("{notes:?}"));
|
|
@@ -361,14 +363,15 @@ fun go(xs: Lst) -> Int = drawL(mapL(xs))
|
|
|
361
363
|
#[test]
|
|
362
364
|
fn impureProducerIsExplained() {
|
|
363
365
|
let (_, notes) = run(
|
|
366
|
+
"primitive Io =
|
|
364
|
-
|
|
367
|
+
extern fun tick(self, x: Int) -> Int
|
|
365
|
-
fun mapI(xs: Lst) -> Lst = match xs
|
|
368
|
+
fun mapI(io: Io, xs: Lst) -> Lst = match xs
|
|
366
369
|
Nil => Nil
|
|
367
|
-
Cons(h, t) => Cons(head: tick(h), tail: mapI(t))
|
|
370
|
+
Cons(h, t) => Cons(head: io.tick(h), tail: mapI(io, t))
|
|
368
371
|
fun sumL(xs: Lst) -> Int = match xs
|
|
369
372
|
Nil => 0
|
|
370
373
|
Cons(h, t) => h + sumL(t)
|
|
371
|
-
fun total(xs: Lst) -> Int = sumL(mapI(xs))
|
|
374
|
+
fun total(io: Io, xs: Lst) -> Int = sumL(mapI(io, xs))
|
|
372
375
|
",
|
|
373
376
|
);
|
|
374
377
|
assert!(notes.iter().any(|n| n.starts_with("impure producer") && n.contains("mapI")), "{notes:?}");
|
|
@@ -381,18 +384,19 @@ fn wrapperAndViewUnfoldToNoNodes() {
|
|
|
381
384
|
"enum Node =
|
|
382
385
|
| Leaf(v: Int)
|
|
383
386
|
| Pair(l: Node, r: Node)
|
|
387
|
+
primitive Io =
|
|
384
|
-
extern fun emit(x: Int)
|
|
388
|
+
extern fun emit(self, x: Int)
|
|
385
|
-
fun drawN(n: Node, k: Int) = match n
|
|
389
|
+
fun drawN(io: Io, n: Node, k: Int) = match n
|
|
386
|
-
Leaf(v) => emit(v + k)
|
|
390
|
+
Leaf(v) => io.emit(v + k)
|
|
387
391
|
Pair(l, r) =>
|
|
388
|
-
drawN(l, k)
|
|
392
|
+
drawN(io, l, k)
|
|
389
|
-
drawN(r, k)
|
|
393
|
+
drawN(io, r, k)
|
|
390
394
|
primitive Pic =
|
|
391
|
-
fun draw(n: Node) =
|
|
395
|
+
fun draw(io: Io, n: Node) =
|
|
392
|
-
drawN(n, 0)
|
|
396
|
+
drawN(io, n, 0)
|
|
393
397
|
fun view(x: Int) -> Node = if x < 0 then Leaf(v: 0) else Pair(l: Leaf(v: x), r: Pair(l: Leaf(v: x + 1), r: Leaf(v: x + 2)))
|
|
394
|
-
fun frame(x: Int) =
|
|
398
|
+
fun frame(io: Io, x: Int) =
|
|
395
|
-
Pic.draw(view(x))
|
|
399
|
+
Pic.draw(io, view(x))
|
|
396
400
|
",
|
|
397
401
|
);
|
|
398
402
|
let name = fusedNames(&s).into_iter().find(|n| n.starts_with("drawN$view")).unwrap_or_else(|| panic!("{notes:?}"));
|
|
@@ -423,10 +427,8 @@ fun total(xs: Lst) -> Int = sumL(mapB(xs))
|
|
|
423
427
|
// migration and the component rewrite. Shapes: a consumer with an extra `Io` argument whose
|
|
424
428
|
// method shares its name with a mutating method of another type, and children built by
|
|
425
429
|
// a generic `map`-like producer whose elements call same-named methods of several types.
|
|
426
|
-
const IO_DRAW: &str = "
|
|
430
|
+
const IO_DRAW: &str = "primitive Io =
|
|
427
|
-
struct Io =
|
|
428
|
-
fun clear(self, c: Int)
|
|
431
|
+
extern fun clear(self, c: Int)
|
|
429
|
-
hostClear(c)
|
|
430
432
|
struct Buf(mut n: Int) =
|
|
431
433
|
fun clear(self) =
|
|
432
434
|
self.n = 0
|
plum-core/tests/core/parser_test.rs
CHANGED
|
@@ -950,3 +950,17 @@ fn singleLetterTypeArgument() {
|
|
|
950
950
|
assert_eq!(t.generics.len(), 1);
|
|
951
951
|
assert_eq!(t.generics[0].name, "P");
|
|
952
952
|
}
|
|
953
|
+
|
|
954
|
+
#[test]
|
|
955
|
+
fn primitive_io_keeps_its_extern_methods() {
|
|
956
|
+
let s = parse("primitive Io =\n extern fun printLn(self, s: Str)\n extern fun width(self) -> Float\n");
|
|
957
|
+
let fns: Vec<&Fn> = s
|
|
958
|
+
.items
|
|
959
|
+
.iter()
|
|
960
|
+
.filter_map(|i| if let Item::Fn(f) = i { Some(f) } else { None })
|
|
961
|
+
.collect();
|
|
962
|
+
assert_eq!(fns.len(), 2);
|
|
963
|
+
assert!(fns.iter().all(|f| f.is_extern && f.has_self && f.type_param.as_deref() == Some("Io")));
|
|
964
|
+
assert!(matches!(fns[0].body, FnBody::Extern));
|
|
965
|
+
assert!(s.hasPrimitiveIo());
|
|
966
|
+
}
|
plum-core/tests/core/purity_test.rs
CHANGED
|
@@ -15,11 +15,11 @@ fn set(xs: &[&str]) -> HashSet<String> {
|
|
|
15
15
|
xs.iter().map(|s| s.to_string()).collect()
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const P: &str = "struct Pt(x: Int)\
|
|
18
|
+
const P: &str = "struct Pt(x: Int)\nprimitive Io =\n extern fun printLn(self, s: Str)\n";
|
|
19
19
|
|
|
20
20
|
#[test]
|
|
21
21
|
fn extern_impure_and_callers_of_pure() {
|
|
22
|
-
let s = format!("{P}fun a() = printLn(\"x\")\nfun b() -> Int = 1\nfun c() -> Int = b()\n");
|
|
22
|
+
let s = format!("{P}fun a(io: Io) = io.printLn(\"x\")\nfun b() -> Int = 1\nfun c() -> Int = b()\n");
|
|
23
23
|
assert_eq!(pure(&s), set(&["b", "c"]));
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -45,20 +45,20 @@ fn recursive_pure_fn_is_pure() {
|
|
|
45
45
|
fn mutual_recursion_pure_and_impure() {
|
|
46
46
|
let s = format!(
|
|
47
47
|
"{P}fun a(n: Int) -> Int = if n < 1 then 0 else b(n - 1)\nfun b(n: Int) -> Int = a(n - 1)\n\
|
|
48
|
-
fun c(n: Int) -> Int = if n < 1 then 0 else d(n - 1)\nfun d(n: Int) -> Int =\n printLn(\"x\")\n c(n - 1)\n"
|
|
48
|
+
fun c(n: Int, io: Io) -> Int = if n < 1 then 0 else d(n - 1, io)\nfun d(n: Int, io: Io) -> Int =\n io.printLn(\"x\")\n c(n - 1, io)\n"
|
|
49
49
|
);
|
|
50
50
|
assert_eq!(pure(&s), set(&["a", "b"]));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
#[test]
|
|
54
54
|
fn calls_impure_is_impure_transitively() {
|
|
55
|
-
let s = format!("{P}fun a() = printLn(\"x\")\nfun b() = a()\nfun c() = b()\nfun d() -> Int = 2\n");
|
|
55
|
+
let s = format!("{P}fun a(io: Io) = io.printLn(\"x\")\nfun b(io: Io) = a(io)\nfun c(io: Io) = b(io)\nfun d() -> Int = 2\n");
|
|
56
56
|
assert_eq!(pure(&s), set(&["d"]));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
#[test]
|
|
60
60
|
fn closure_literal_body_counts() {
|
|
61
|
-
let s = format!("{P}fun a() =\n f := |x: Int| printLn(\"x\")\n 1\nfun b() =\n f := |x: Int| x + 1\n 1\n");
|
|
61
|
+
let s = format!("{P}fun a(io: Io) =\n f := |x: Int| io.printLn(\"x\")\n 1\nfun b() =\n f := |x: Int| x + 1\n 1\n");
|
|
62
62
|
assert_eq!(pure(&s), set(&["b"]));
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -73,7 +73,7 @@ fn unknown_callee_impure() {
|
|
|
73
73
|
assert!(pure("fun a() -> Int = nope()\n").is_empty());
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const IO: &str = "
|
|
76
|
+
const IO: &str = "primitive Io =\n extern fun ping(self) -> Int\nstruct World(io: Io, n: Int)\nstruct Pt2(x: Int)\n";
|
|
77
77
|
|
|
78
78
|
#[test]
|
|
79
79
|
fn io_param_impure_view_pure() {
|
|
@@ -104,7 +104,7 @@ fn closure_capturing_io_impure() {
|
|
|
104
104
|
#[test]
|
|
105
105
|
fn virtual_call_through_base_is_impure() {
|
|
106
106
|
let s = format!(
|
|
107
|
-
"{P}struct Actor(w: Int) =\n fun go(self) -> Int = 1\n\nstruct Ship(Actor(w), t: Int) =\n override fun go(self) -> Int =\n printLn(\"x\")\n 2\n\nfun run(a: Actor) -> Int = a.go()\nfun leaf(s: Ship) -> Int = s.go()\nfun plain(a: Actor) -> Int = a.w\n"
|
|
107
|
+
"{P}struct Actor(w: Int) =\n fun go(self, io: Io) -> Int = 1\n\nstruct Ship(Actor(w), t: Int) =\n override fun go(self, io: Io) -> Int =\n io.printLn(\"x\")\n 2\n\nfun run(a: Actor, io: Io) -> Int = a.go(io)\nfun leaf(s: Ship, io: Io) -> Int = s.go(io)\nfun plain(a: Actor) -> Int = a.w\n"
|
|
108
108
|
);
|
|
109
109
|
let p = pure(&s);
|
|
110
110
|
assert!(!p.contains("Actor.go"), "{p:?}");
|
|
@@ -118,3 +118,12 @@ fn nonvirtual_method_stays_pure() {
|
|
|
118
118
|
let p = pure(&s);
|
|
119
119
|
assert!(p.contains("Actor.go") && p.contains("run"), "{p:?}");
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
#[test]
|
|
123
|
+
fn fn_taking_primitive_io_is_not_pure() {
|
|
124
|
+
let src = "primitive Io =\n extern fun mark(self, n: Int)\n\nfun touch(io: Io) =\n io.mark(1)\n\nfun plus(a: Int, b: Int) -> Int = a + b\n";
|
|
125
|
+
let source = plum_test_support::parseRawSource(src);
|
|
126
|
+
let pure = plum_core::purity::pureFns(&source);
|
|
127
|
+
assert!(!pure.contains("touch"), "{pure:?}");
|
|
128
|
+
assert!(pure.contains("plus"), "{pure:?}");
|
|
129
|
+
}
|
plum-std/Io.plum
CHANGED
|
@@ -7,243 +7,92 @@ import std/game/Vec2
|
|
|
7
7
|
import std/game/Color
|
|
8
8
|
|
|
9
9
|
# The host capability. Every call that leaves the program (drawing, input, sound,
|
|
10
|
-
# files, environment, clock, randomness, network) is a method of `Io`
|
|
10
|
+
# files, environment, clock, randomness, network) is a method of `Io`. `Io` is a
|
|
11
|
-
#
|
|
11
|
+
# built-in primitive with no fields; the host calls are the `extern fun` methods
|
|
12
|
-
# and
|
|
12
|
+
# below and nowhere else. The host entry points pass the one `Io` value, so a
|
|
13
13
|
# function with no `Io` reachable cannot touch the host: that is what "pure" means.
|
|
14
14
|
#
|
|
15
15
|
# Texture and sound handles are -1 on failure. File, texture and sound paths are
|
|
16
16
|
# relative to the asset root. `Int` in an extern signature is narrowed to i32 at the
|
|
17
17
|
# host boundary, so wall-clock time travels as `Float`.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
extern fun
|
|
21
|
-
extern fun
|
|
22
|
-
extern fun
|
|
23
|
-
extern fun
|
|
24
|
-
extern fun
|
|
25
|
-
extern fun
|
|
26
|
-
extern fun
|
|
27
|
-
extern fun
|
|
28
|
-
extern fun
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
extern fun
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# (
|
|
52
|
-
|
|
53
|
-
extern fun
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
extern fun
|
|
68
|
-
extern fun
|
|
69
|
-
extern fun
|
|
70
|
-
extern fun
|
|
71
|
-
extern fun
|
|
72
|
-
extern fun
|
|
73
|
-
extern fun
|
|
74
|
-
extern fun
|
|
75
|
-
extern fun
|
|
76
|
-
extern fun
|
|
77
|
-
extern fun
|
|
78
|
-
extern fun
|
|
79
|
-
extern fun
|
|
80
|
-
extern fun
|
|
81
|
-
extern fun
|
|
82
|
-
extern fun
|
|
83
|
-
extern fun
|
|
84
|
-
extern fun
|
|
85
|
-
extern fun
|
|
86
|
-
extern fun
|
|
87
|
-
extern fun
|
|
88
|
-
extern fun
|
|
89
|
-
extern fun
|
|
90
|
-
extern fun
|
|
91
|
-
|
|
92
|
-
struct Io =
|
|
93
|
-
|
|
94
|
-
fun screenWidth(self) -> Float =
|
|
95
|
-
screenWidth()
|
|
96
|
-
|
|
97
|
-
fun screenHeight(self) -> Float =
|
|
98
|
-
screenHeight()
|
|
99
|
-
|
|
100
|
-
fun frameTime(self) -> Float =
|
|
101
|
-
frameTime()
|
|
102
|
-
|
|
103
|
-
fun clear(self, c: Color) =
|
|
104
|
-
clear(c)
|
|
105
|
-
|
|
106
|
-
fun drawRect(self, x: Float, y: Float, w: Float, h: Float, c: Color) =
|
|
107
|
-
drawRect(x, y, w, h, c)
|
|
108
|
-
|
|
109
|
-
fun drawCircle(self, x: Float, y: Float, r: Float, c: Color) =
|
|
110
|
-
drawCircle(x, y, r, c)
|
|
111
|
-
|
|
112
|
-
fun drawLine(self, x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, c: Color) =
|
|
113
|
-
drawLine(x1, y1, x2, y2, thickness, c)
|
|
114
|
-
|
|
115
|
-
fun drawText(self, s: Str, x: Float, y: Float, size: Float, c: Color) =
|
|
116
|
-
drawText(s, x, y, size, c)
|
|
117
|
-
|
|
118
|
-
fun setCamera(self, x: Float, y: Float, scale: Float) =
|
|
119
|
-
setCamera(x, y, scale)
|
|
120
|
-
|
|
121
|
-
fun resetCamera(self) =
|
|
122
|
-
resetCamera()
|
|
123
|
-
|
|
124
|
-
fun textureLoad(self, path: Str) -> Int =
|
|
125
|
-
textureLoad(path)
|
|
126
|
-
|
|
127
|
-
fun textureWidth(self, t: Int) -> Int =
|
|
128
|
-
textureWidth(t)
|
|
129
|
-
|
|
130
|
-
fun textureHeight(self, t: Int) -> Int =
|
|
131
|
-
textureHeight(t)
|
|
132
|
-
|
|
133
|
-
fun textureSetSmooth(self, t: Int, smooth: Bool) =
|
|
134
|
-
textureSetSmooth(t, smooth)
|
|
135
|
-
|
|
136
|
-
fun textureCreate(self, w: Int, h: Int) -> Int =
|
|
137
|
-
textureCreate(w, h)
|
|
138
|
-
|
|
139
|
-
fun renderTarget(self, t: Int) =
|
|
140
|
-
renderTarget(t)
|
|
141
|
-
|
|
142
|
-
fun blendMode(self, m: Int) =
|
|
143
|
-
blendMode(m)
|
|
144
|
-
|
|
145
|
-
fun drawTexture(self, t: Int, x: Float, y: Float, tint: Color) =
|
|
146
|
-
drawTexture(t, x, y, tint)
|
|
147
|
-
|
|
148
|
-
fun drawTextureEx(self, t: Int, sx: Float, sy: Float, sw: Float, sh: Float, x: Float, y: Float, w: Float, h: Float, rotation: Float, flipX: Bool, tint: Color) =
|
|
149
|
-
drawTextureEx(t, sx, sy, sw, sh, x, y, w, h, rotation, flipX, tint)
|
|
150
|
-
|
|
151
|
-
fun fileExists(self, path: Str) -> Bool =
|
|
152
|
-
fileExists(path)
|
|
153
|
-
|
|
154
|
-
fun fileLoad(self, path: Str) -> Str =
|
|
155
|
-
fileLoad(path)
|
|
156
|
-
|
|
157
|
-
fun soundLoad(self, path: Str) -> Int =
|
|
158
|
-
soundLoad(path)
|
|
159
|
-
|
|
160
|
-
fun soundPlay(self, s: Int, volume: Float) =
|
|
161
|
-
soundPlay(s, volume)
|
|
162
|
-
|
|
163
|
-
fun soundLoop(self, s: Int, volume: Float) =
|
|
164
|
-
soundLoop(s, volume)
|
|
165
|
-
|
|
166
|
-
fun soundStop(self, s: Int) =
|
|
167
|
-
soundStop(s)
|
|
168
|
-
|
|
169
|
-
fun soundSetVolume(self, s: Int, volume: Float) =
|
|
170
|
-
soundSetVolume(s, volume)
|
|
171
|
-
|
|
172
|
-
fun isKeyDown(self, k: Int) -> Bool =
|
|
173
|
-
isKeyDown(k)
|
|
174
|
-
|
|
175
|
-
fun isKeyPressed(self, k: Int) -> Bool =
|
|
176
|
-
isKeyPressed(k)
|
|
177
|
-
|
|
178
|
-
fun mousePos(self) -> Vec2 =
|
|
179
|
-
mousePos()
|
|
180
|
-
|
|
181
|
-
fun isMouseDown(self, b: Int) -> Bool =
|
|
182
|
-
isMouseDown(b)
|
|
183
|
-
|
|
184
|
-
fun isMousePressed(self, b: Int) -> Bool =
|
|
185
|
-
isMousePressed(b)
|
|
186
|
-
|
|
187
|
-
fun mouseWheel(self) -> Float =
|
|
188
|
-
mouseWheel()
|
|
189
|
-
|
|
190
|
-
fun printLn(self, s: Str) =
|
|
191
|
-
printLn(s)
|
|
192
|
-
|
|
193
|
-
fun rawReadFile(self, path: Str) -> Str =
|
|
194
|
-
rawReadFile(path)
|
|
195
|
-
|
|
196
|
-
fun rawWriteFile(self, path: Str, data: Str) -> Int =
|
|
197
|
-
rawWriteFile(path, data)
|
|
198
|
-
|
|
199
|
-
fun rawExists(self, path: Str) -> Int =
|
|
200
|
-
rawExists(path)
|
|
201
|
-
|
|
202
|
-
fun rawMkdir(self, path: Str) -> Int =
|
|
203
|
-
rawMkdir(path)
|
|
204
|
-
|
|
205
|
-
fun rawRemove(self, path: Str) -> Int =
|
|
206
|
-
rawRemove(path)
|
|
207
|
-
|
|
208
|
-
fun rawRemoveDir(self, path: Str) -> Int =
|
|
209
|
-
rawRemoveDir(path)
|
|
210
|
-
|
|
211
|
-
fun rawRename(self, old_path: Str, new_path: Str) -> Int =
|
|
212
|
-
rawRename(old_path, new_path)
|
|
213
|
-
|
|
214
|
-
fun rawTruncate(self, path: Str, len: Int) -> Int =
|
|
215
|
-
rawTruncate(path, len)
|
|
216
|
-
|
|
217
|
-
fun rawLink(self, existing_path: Str, new_path: Str) -> Int =
|
|
218
|
-
rawLink(existing_path, new_path)
|
|
219
|
-
|
|
220
|
-
fun rawSymlink(self, target: Str, path: Str) -> Int =
|
|
221
|
-
rawSymlink(target, path)
|
|
222
|
-
|
|
223
|
-
fun rawReadlink(self, path: Str) -> Str =
|
|
224
|
-
rawReadlink(path)
|
|
225
|
-
|
|
226
|
-
fun rawRealpath(self, path: Str) -> Str =
|
|
227
|
-
rawRealpath(path)
|
|
228
|
-
|
|
229
|
-
fun rawChmod(self, path: Str, mode: Int) -> Int =
|
|
230
|
-
rawChmod(path, mode)
|
|
231
|
-
|
|
232
|
-
fun rawReadDir(self, path: Str) -> Str =
|
|
233
|
-
rawReadDir(path)
|
|
234
|
-
|
|
235
|
-
fun rawNowMillis(self) -> Float =
|
|
236
|
-
rawNowMillis()
|
|
237
|
-
|
|
238
|
-
fun rawRandomHalf(self) -> Int =
|
|
239
|
-
rawRandomHalf()
|
|
19
|
+
primitive Io =
|
|
20
|
+
extern fun screenWidth(self) -> Float
|
|
21
|
+
extern fun screenHeight(self) -> Float
|
|
22
|
+
extern fun frameTime(self) -> Float
|
|
23
|
+
extern fun clear(self, c: Color)
|
|
24
|
+
extern fun drawRect(self, x: Float, y: Float, w: Float, h: Float, c: Color)
|
|
25
|
+
extern fun drawCircle(self, x: Float, y: Float, r: Float, c: Color)
|
|
26
|
+
extern fun drawLine(self, x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, c: Color)
|
|
27
|
+
extern fun drawText(self, s: Str, x: Float, y: Float, size: Float, c: Color)
|
|
28
|
+
extern fun setCamera(self, x: Float, y: Float, scale: Float)
|
|
29
|
+
extern fun resetCamera(self)
|
|
30
|
+
# Loads any image the host decodes (PNG at least); -1 on failure.
|
|
31
|
+
extern fun textureLoad(self, path: Str) -> Int
|
|
32
|
+
# Size in pixels; 0 for a bad handle.
|
|
33
|
+
extern fun textureWidth(self, t: Int) -> Int
|
|
34
|
+
extern fun textureHeight(self, t: Int) -> Int
|
|
35
|
+
# Linear (true) or nearest (false, the default) filtering.
|
|
36
|
+
extern fun textureSetSmooth(self, t: Int, smooth: Bool)
|
|
37
|
+
# A blank, transparent texture of w x h pixels that can be drawn into with
|
|
38
|
+
# `renderTarget`; -1 if a side is below 1 or above 8192.
|
|
39
|
+
extern fun textureCreate(self, w: Int, h: Int) -> Int
|
|
40
|
+
# Sends the following draws (and `clear`) into texture `t` made by `textureCreate`,
|
|
41
|
+
# or back to the screen with -1; any other handle is ignored. Each target has its
|
|
42
|
+
# own camera: selecting a target resets its camera to its pixels, and returning to
|
|
43
|
+
# the screen restores the screen's camera. The texture's contents persist between
|
|
44
|
+
# frames; draw it like any texture once the screen is selected again.
|
|
45
|
+
extern fun renderTarget(self, t: Int)
|
|
46
|
+
# The blend mode of the following draws: a `Blend` constant (other values are
|
|
47
|
+
# ignored). Every frame starts with `Blend.Normal`.
|
|
48
|
+
extern fun blendMode(self, m: Int)
|
|
49
|
+
# The texture at its natural size, top-left at (x, y).
|
|
50
|
+
extern fun drawTexture(self, t: Int, x: Float, y: Float, tint: Color)
|
|
51
|
+
# The source rect (sx, sy, sw, sh) in texture pixels drawn into the dest rect
|
|
52
|
+
# (x, y, w, h), rotated `rotation` radians clockwise about the dest centre.
|
|
53
|
+
extern fun drawTextureEx(self, t: Int, sx: Float, sy: Float, sw: Float, sh: Float, x: Float, y: Float, w: Float, h: Float, rotation: Float, flipX: Bool, tint: Color)
|
|
54
|
+
extern fun fileExists(self, path: Str) -> Bool
|
|
55
|
+
# The whole file; "" if it is missing.
|
|
56
|
+
extern fun fileLoad(self, path: Str) -> Str
|
|
57
|
+
# Loads a WAV or OGG file relative to the asset root; -1 on failure. The host
|
|
58
|
+
# decodes every sound file in the asset root before `init` runs.
|
|
59
|
+
extern fun soundLoad(self, path: Str) -> Int
|
|
60
|
+
# A one-shot play; plays of the same sound may overlap. Volume is 0..1.
|
|
61
|
+
extern fun soundPlay(self, s: Int, volume: Float)
|
|
62
|
+
# Starts the sound looping. Call `soundStop` first to restart it cleanly.
|
|
63
|
+
extern fun soundLoop(self, s: Int, volume: Float)
|
|
64
|
+
# Stops every play and loop of the sound.
|
|
65
|
+
extern fun soundStop(self, s: Int)
|
|
66
|
+
# Changes the volume of a sound that is playing or looping.
|
|
67
|
+
extern fun soundSetVolume(self, s: Int, volume: Float)
|
|
68
|
+
extern fun isKeyDown(self, k: Int) -> Bool
|
|
69
|
+
extern fun isKeyPressed(self, k: Int) -> Bool
|
|
70
|
+
extern fun mousePos(self) -> Vec2
|
|
71
|
+
extern fun isMouseDown(self, b: Int) -> Bool
|
|
72
|
+
extern fun isMousePressed(self, b: Int) -> Bool
|
|
73
|
+
extern fun mouseWheel(self) -> Float
|
|
74
|
+
extern fun printLn(self, s: Str)
|
|
75
|
+
extern fun rawReadFile(self, path: Str) -> Str
|
|
76
|
+
extern fun rawWriteFile(self, path: Str, data: Str) -> Int
|
|
77
|
+
extern fun rawExists(self, path: Str) -> Int
|
|
78
|
+
extern fun rawMkdir(self, path: Str) -> Int
|
|
79
|
+
extern fun rawRemove(self, path: Str) -> Int
|
|
80
|
+
extern fun rawRemoveDir(self, path: Str) -> Int
|
|
81
|
+
extern fun rawRename(self, old_path: Str, new_path: Str) -> Int
|
|
82
|
+
extern fun rawTruncate(self, path: Str, len: Int) -> Int
|
|
83
|
+
extern fun rawLink(self, existing_path: Str, new_path: Str) -> Int
|
|
84
|
+
extern fun rawSymlink(self, target: Str, path: Str) -> Int
|
|
85
|
+
extern fun rawReadlink(self, path: Str) -> Str
|
|
86
|
+
extern fun rawRealpath(self, path: Str) -> Str
|
|
87
|
+
extern fun rawChmod(self, path: Str, mode: Int) -> Int
|
|
88
|
+
extern fun rawReadDir(self, path: Str) -> Str
|
|
89
|
+
extern fun rawNowMillis(self) -> Float
|
|
90
|
+
extern fun rawRandomHalf(self) -> Int
|
|
91
|
+
extern fun rawHttpRequest(self, method: Str, url: Str, headers: Str, body: Str) -> Str
|
|
240
92
|
|
|
241
93
|
# A wall-clock 64-bit seed for `Random.seeded` (two host i32 halves; `Int` is
|
|
242
94
|
# narrowed to i32 at the extern boundary).
|
|
243
95
|
fun randomSeed(self) -> Int =
|
|
244
|
-
hi := rawRandomHalf()
|
|
96
|
+
hi := self.rawRandomHalf()
|
|
245
|
-
lo := rawRandomHalf() & 0xFFFFFFFF
|
|
97
|
+
lo := self.rawRandomHalf() & 0xFFFFFFFF
|
|
246
98
|
return { hi << 32 } | lo
|
|
247
|
-
|
|
248
|
-
fun rawHttpRequest(self, method: Str, url: Str, headers: Str, body: Str) -> Str =
|
|
249
|
-
rawHttpRequest(method, url, headers, body)
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -771,17 +771,6 @@ fn errScratch(expr: &ast::Expr) -> String {
|
|
|
771
771
|
format!("errtmp#{}", expr as *const ast::Expr as usize)
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
/// The wasm leaf zero for an "undefined" value slot.
|
|
775
|
-
fn emitZeroLeaf(vt: ValType, body: &mut Vec<u8>) {
|
|
776
|
-
pushSelfPlaceholder(vt, body);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
fn emitZeros(ty: &PlumType, body: &mut Vec<u8>) {
|
|
780
|
-
for vt in flatValtypes(ty, &gcTypes()) {
|
|
781
|
-
emitZeroLeaf(vt, body);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
774
|
fn valueFieldsOf(ty: &PlumType) -> Option<Vec<(String, PlumType)>> {
|
|
786
775
|
match ty {
|
|
787
776
|
PlumType::TNamed(n) => withGen(|s| s.value_enums.get(n).cloned()),
|
|
@@ -1016,6 +1005,14 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
|
|
|
1016
1005
|
PlumType::TFloat => ValType::F64,
|
|
1017
1006
|
PlumType::TBool => ValType::I32,
|
|
1018
1007
|
PlumType::TByte => ValType::I32,
|
|
1008
|
+
// The primitive `Io` (no struct of that name): an i32 constant 0.
|
|
1009
|
+
PlumType::TNamed(name)
|
|
1010
|
+
if name == ast::IO_TYPE
|
|
1011
|
+
&& !registry.class_type_idx.contains_key(name)
|
|
1012
|
+
&& !registry.enum_super_type_idx.contains_key(name) =>
|
|
1013
|
+
{
|
|
1014
|
+
ValType::I32
|
|
1015
|
+
}
|
|
1019
1016
|
PlumType::TNamed(name) => match registry
|
|
1020
1017
|
.class_type_idx
|
|
1021
1018
|
.get(name)
|
|
@@ -1792,13 +1789,10 @@ fn entryTakesIo(f: &ast::Fn) -> bool {
|
|
|
1792
1789
|
&& matches!(&p.ty, ast::ParamType::Type(t) if t.name == "Io" && t.generics.is_empty()))
|
|
1793
1790
|
}
|
|
1794
1791
|
|
|
1795
|
-
/// Host-facing wrapper for an entry point: drops the leading `io: Io` param (building
|
|
1796
|
-
/// the `Io`
|
|
1792
|
+
/// Host-facing wrapper for an entry point: drops the leading `io: Io` param (passing the `Io` constant 0) and optionally narrows `Int` to i32 (the `extern fun` ABI).
|
|
1797
1793
|
#[allow(clippy::too_many_arguments)]
|
|
1798
1794
|
fn emitEntryWrapper(
|
|
1799
1795
|
module: &mut WasmModule,
|
|
1800
|
-
ctx: &CompileCtx,
|
|
1801
|
-
enum_variants: &EnumVariants,
|
|
1802
1796
|
name: &str,
|
|
1803
1797
|
idx: u32,
|
|
1804
1798
|
sig: &FuncSig,
|
|
@@ -1822,12 +1816,8 @@ fn emitEntryWrapper(
|
|
|
1822
1816
|
b.push(0);
|
|
1823
1817
|
}
|
|
1824
1818
|
if io {
|
|
1825
|
-
let io_idx = *ctx.gc_types.class_type_idx.get("Io").expect("Io record type");
|
|
1826
|
-
|
|
1819
|
+
// The primitive `Io` is the i32 constant 0.
|
|
1827
|
-
for ty in plum_checker::variantFields(info, &PlumType::TNamed("Io".to_string())) {
|
|
1828
|
-
emitZeros(&ty, &mut b);
|
|
1829
|
-
}
|
|
1830
|
-
Instruction::
|
|
1820
|
+
Instruction::I32Const(0).encode(&mut b);
|
|
1831
1821
|
}
|
|
1832
1822
|
for (i, vt) in hostParams.iter().enumerate() {
|
|
1833
1823
|
Instruction::LocalGet(i as u32).encode(&mut b);
|
|
@@ -2110,25 +2100,32 @@ fn compileSourceInner(
|
|
|
2110
2100
|
// Register every `extern fun` as a genuine wasm import BEFORE any function.
|
|
2111
2101
|
let mut func_ids: HashMap<String, u32> = HashMap::new();
|
|
2112
2102
|
let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
|
|
2103
|
+
// (key, import index, wrapper params, results, leading receiver params the host import lacks)
|
|
2113
|
-
let mut extern_wrappers: Vec<(String, u32, Vec<ValType>, Vec<ValType>)> = Vec::new();
|
|
2104
|
+
let mut extern_wrappers: Vec<(String, u32, Vec<ValType>, Vec<ValType>, usize)> = Vec::new();
|
|
2114
2105
|
for item in &source.items {
|
|
2115
2106
|
if let ast::Item::Fn(f) = item {
|
|
2116
2107
|
if !f.is_extern {
|
|
2117
2108
|
continue;
|
|
2118
2109
|
}
|
|
2110
|
+
// An extern METHOD (of the primitive `Io`) takes its receiver as the first
|
|
2111
|
+
// wrapper param; the host import has no receiver.
|
|
2112
|
+
let skip = usize::from(f.type_param.is_some() && f.has_self);
|
|
2113
|
+
let key = if f.type_param.is_some() { fnKey(f) } else { f.name.clone() };
|
|
2114
|
+
if f.type_param.is_none() {
|
|
2119
|
-
|
|
2115
|
+
known_fns.insert(f.name.clone());
|
|
2116
|
+
}
|
|
2120
2117
|
let sig = unitFuncSig(f, &Subst::new(), &eg, &gc_types);
|
|
2121
2118
|
let param_types = sig.params.clone();
|
|
2122
2119
|
let ret = sig.ret.clone();
|
|
2123
2120
|
// `Int` in an extern signature is narrowed to wasm i32 at the host boundary.
|
|
2124
2121
|
let narrow = |v: &ValType| if *v == ValType::I64 { ValType::I32 } else { *v };
|
|
2125
|
-
let imp_params: Vec<ValType> = param_types.iter().map(narrow).collect();
|
|
2122
|
+
let imp_params: Vec<ValType> = param_types[skip..].iter().map(narrow).collect();
|
|
2126
2123
|
let imp_results: Vec<ValType> = ret.iter().map(narrow).collect();
|
|
2127
2124
|
let type_idx = module.addType(&imp_params, &imp_results);
|
|
2128
2125
|
let func_idx = module.addImport("plum", &f.name, type_idx);
|
|
2129
|
-
extern_wrappers.push((
|
|
2126
|
+
extern_wrappers.push((key.clone(), func_idx, param_types, ret, skip));
|
|
2130
2127
|
func_sigs.insert(
|
|
2131
|
-
|
|
2128
|
+
key,
|
|
2132
2129
|
FuncSig {
|
|
2133
2130
|
param_defaults: Vec::new(),
|
|
2134
2131
|
..sig
|
|
@@ -2149,7 +2146,8 @@ fn compileSourceInner(
|
|
|
2149
2146
|
}
|
|
2150
2147
|
|
|
2151
2148
|
// Extern `Int` narrowing wrappers.
|
|
2152
|
-
for (name, imp_idx, params, ret) in &extern_wrappers {
|
|
2149
|
+
for (name, imp_idx, params, ret, skip) in &extern_wrappers {
|
|
2150
|
+
let skip = *skip;
|
|
2153
2151
|
let mut b: Vec<u8> = Vec::new();
|
|
2154
2152
|
// With several results, the narrowed ones are parked in locals so each can be
|
|
2155
2153
|
// widened individually.
|
|
@@ -2169,7 +2167,7 @@ fn compileSourceInner(
|
|
|
2169
2167
|
} else {
|
|
2170
2168
|
b.push(0);
|
|
2171
2169
|
}
|
|
2172
|
-
for (i, vt) in params.iter().enumerate() {
|
|
2170
|
+
for (i, vt) in params.iter().enumerate().skip(skip) {
|
|
2173
2171
|
if *vt == ValType::I64 {
|
|
2174
2172
|
Instruction::LocalGet(i as u32).encode(&mut b);
|
|
2175
2173
|
Instruction::LocalGet(i as u32).encode(&mut b);
|
|
@@ -2181,7 +2179,7 @@ fn compileSourceInner(
|
|
|
2181
2179
|
Instruction::End.encode(&mut b);
|
|
2182
2180
|
}
|
|
2183
2181
|
}
|
|
2184
|
-
for (i, vt) in params.iter().enumerate() {
|
|
2182
|
+
for (i, vt) in params.iter().enumerate().skip(skip) {
|
|
2185
2183
|
Instruction::LocalGet(i as u32).encode(&mut b);
|
|
2186
2184
|
if *vt == ValType::I64 {
|
|
2187
2185
|
Instruction::I32WrapI64.encode(&mut b);
|
|
@@ -2671,7 +2669,7 @@ fn compileSourceInner(
|
|
|
2671
2669
|
continue;
|
|
2672
2670
|
}
|
|
2673
2671
|
let sig = ctx.func_sigs.get(&fnKey(f)).expect("entry fn has a signature");
|
|
2674
|
-
let w = emitEntryWrapper(&mut module, &
|
|
2672
|
+
let w = emitEntryWrapper(&mut module, &f.name, idx, sig, narrowInts, takesIo(f));
|
|
2675
2673
|
module.addExport(&name, ExportKind::Func, w);
|
|
2676
2674
|
}
|
|
2677
2675
|
|
|
@@ -2754,7 +2752,7 @@ fn compileSourceInner(
|
|
|
2754
2752
|
if io {
|
|
2755
2753
|
let f = source.items.iter().find_map(|it| match it { ast::Item::Fn(f) if &f.name == internal_name => Some(f), _ => None }).unwrap();
|
|
2756
2754
|
let sig = ctx.func_sigs.get(&fnKey(f)).expect("test fn has a signature");
|
|
2757
|
-
let w = emitEntryWrapper(&mut module, &
|
|
2755
|
+
let w = emitEntryWrapper(&mut module, &f.name, idx, sig, false, true);
|
|
2758
2756
|
module.addExport(export_name, ExportKind::Func, w);
|
|
2759
2757
|
} else {
|
|
2760
2758
|
module.addExport(export_name, ExportKind::Func, idx);
|
plum-wasm-codegen/tests/wasm_codegen/codegen_tests.rs
CHANGED
|
@@ -1443,12 +1443,8 @@ fun main() -> Int =
|
|
|
1443
1443
|
assert_eq!(runMerged::<i64>("bool_try", src), 1);
|
|
1444
1444
|
}
|
|
1445
1445
|
|
|
1446
|
-
/// These tests declare their own `extern fun`
|
|
1446
|
+
/// These tests declare their own `extern fun` methods on `primitive Io` to exercise the
|
|
1447
|
-
/// and call them directly. The `Io` capability only lets std's `Io.plum` do that,
|
|
1448
|
-
/// so stamp the program as that file (the ABI is what is under test, not the rule).
|
|
1449
|
-
fn asIoFile(source: &mut plum_core::ast::Source) {
|
|
1450
|
-
|
|
1447
|
+
/// host-boundary ABI.
|
|
1451
|
-
}
|
|
1452
1448
|
|
|
1453
1449
|
// ---- extern `Int` narrowing (T1): i32 at the host boundary ----
|
|
1454
1450
|
|
|
@@ -1456,11 +1452,10 @@ fn asIoFile(source: &mut plum_core::ast::Source) {
|
|
|
1456
1452
|
/// import that is `(i32) -> i32`; returns the module plus `main`'s outcome.
|
|
1457
1453
|
fn externEcho(arg: &str) -> (wasmtime::Module, Result<i64, String>) {
|
|
1458
1454
|
let src = format!(
|
|
1459
|
-
"extern fun echo(x: Int) -> Int\n\nfun main() -> Int =\n echo({})\n",
|
|
1455
|
+
"primitive Io =\n extern fun echo(self, x: Int) -> Int\n\nfun main(io: Io) -> Int =\n io.echo({})\n",
|
|
1460
1456
|
arg
|
|
1461
1457
|
);
|
|
1462
|
-
let
|
|
1458
|
+
let source = parse(&src);
|
|
1463
|
-
asIoFile(&mut source);
|
|
1464
1459
|
let bytes = compileSource(&source).expect("compile failed");
|
|
1465
1460
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
1466
1461
|
let engine = gcEngine();
|
|
@@ -1706,10 +1701,9 @@ fn value_nested_flatten() {
|
|
|
1706
1701
|
#[test]
|
|
1707
1702
|
fn extern_value_multi_value() {
|
|
1708
1703
|
let src = format!(
|
|
1709
|
-
"{VEC2}struct Color(r: Int, g: Int)\n\
|
|
1704
|
+
"{VEC2}struct Color(r: Int, g: Int)\n\nprimitive Io =\n extern fun mousePos(self) -> Vec2\n extern fun tint(self) -> Color\n extern fun move(self, p: Vec2, c: Color)\n\nfun main(io: Io) -> Int =\n p := io.mousePos()\n c := io.tint()\n io.move(p, c)\n if p.x == 1.5 && c.g == 0 - 7\n return 1\n return 0\n"
|
|
1710
1705
|
);
|
|
1711
|
-
let
|
|
1706
|
+
let source = parse(&src);
|
|
1712
|
-
asIoFile(&mut source);
|
|
1713
1707
|
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
1714
1708
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
1715
1709
|
let engine = gcEngine();
|
plum-wasm-codegen/tests/wasm_codegen/e2e_types/io_primitive.rs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//! `primitive Io` with `extern fun f(self, ..)`: an `Io` is the i32 constant 0 and each extern
|
|
2
|
+
//! method is a host import (`plum.<name>`) called without the receiver.
|
|
3
|
+
use plum_test_support::{engine, parseRawSource};
|
|
4
|
+
use plum_wasm_codegen::{compile, CompileOptions};
|
|
5
|
+
|
|
6
|
+
const IO: &str = "primitive Io =\n extern fun mark(self, n: Int)\n extern fun width(self) -> Float\n\n";
|
|
7
|
+
|
|
8
|
+
fn build(src: &str, tests: bool) -> (wasmtime::Engine, wasmtime::Module, Vec<String>) {
|
|
9
|
+
let mut source = parseRawSource(src);
|
|
10
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
11
|
+
let out = compile(&source, &CompileOptions { tests, ..CompileOptions::default() })
|
|
12
|
+
.unwrap_or_else(|e| panic!("compile failed: {}", e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")));
|
|
13
|
+
wasmparser::validate(&out.wasm).expect("valid wasm");
|
|
14
|
+
let engine = engine();
|
|
15
|
+
let module = wasmtime::Module::new(&engine, &out.wasm).unwrap();
|
|
16
|
+
(engine, module, out.test_names)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn instantiate(
|
|
20
|
+
engine: &wasmtime::Engine,
|
|
21
|
+
module: &wasmtime::Module,
|
|
22
|
+
) -> (wasmtime::Store<Vec<i32>>, wasmtime::Instance) {
|
|
23
|
+
let mut store = wasmtime::Store::new(engine, Vec::new());
|
|
24
|
+
let mut linker = wasmtime::Linker::new(engine);
|
|
25
|
+
linker.func_wrap("plum", "mark", |mut c: wasmtime::Caller<'_, Vec<i32>>, n: i32| c.data_mut().push(n)).unwrap();
|
|
26
|
+
linker.func_wrap("plum", "width", || 640.0f64).unwrap();
|
|
27
|
+
linker.define_unknown_imports_as_default_values(&mut store, module).unwrap();
|
|
28
|
+
let instance = linker.instantiate(&mut store, module).unwrap();
|
|
29
|
+
(store, instance)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn extern_methods_are_imports_and_io_is_an_i32() {
|
|
34
|
+
let src = format!("{IO}fun main(io: Io) -> Float =\n io.mark(7)\n io.width()\n");
|
|
35
|
+
let (engine, module, _) = build(&src, false);
|
|
36
|
+
let imports: Vec<String> = module.imports().map(|i| format!("{}.{}", i.module(), i.name())).collect();
|
|
37
|
+
assert!(imports.contains(&"plum.mark".to_string()) && imports.contains(&"plum.width".to_string()), "{imports:?}");
|
|
38
|
+
let (mut store, instance) = instantiate(&engine, &module);
|
|
39
|
+
let main = instance.get_typed_func::<(), f64>(&mut store, "main").unwrap();
|
|
40
|
+
assert_eq!(main.call(&mut store, ()).unwrap(), 640.0);
|
|
41
|
+
assert_eq!(store.data(), &vec![7]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn closures_and_generic_fns_take_io() {
|
|
46
|
+
let src = format!(
|
|
47
|
+
"{IO}fun twice(f: fn() -> Unit) =\n f()\n f()\n\nfun pass[T](x: T, io: Io) -> T =\n io.mark(1)\n x\n\nfun main(io: Io) -> Int =\n twice(|| io.mark(5))\n pass(3, io)\n"
|
|
48
|
+
);
|
|
49
|
+
let (engine, module, _) = build(&src, false);
|
|
50
|
+
let (mut store, instance) = instantiate(&engine, &module);
|
|
51
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
52
|
+
assert_eq!(main.call(&mut store, ()).unwrap(), 3);
|
|
53
|
+
assert_eq!(store.data(), &vec![5, 5, 1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn test_blocks_can_call_io_methods() {
|
|
58
|
+
let src = format!("{IO}test \"marks\"\n io.mark(9)\n");
|
|
59
|
+
let (engine, module, names) = build(&src, true);
|
|
60
|
+
assert_eq!(names, vec!["marks".to_string()]);
|
|
61
|
+
let (mut store, instance) = instantiate(&engine, &module);
|
|
62
|
+
let f = instance.get_func(&mut store, "test#marks").expect("export");
|
|
63
|
+
let mut results = [wasmtime::Val::I32(0)];
|
|
64
|
+
f.call(&mut store, &[], &mut results).unwrap();
|
|
65
|
+
assert_eq!(store.data(), &vec![9]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn io_stored_in_a_generic_container_works() {
|
|
70
|
+
let src = format!(
|
|
71
|
+
"{IO}enum Opt[T] =\n | Nope\n | Yep(v: T)\n\nfun use(o: Opt[Io]) =\n match o\n Yep(x) => x.mark(4)\n Nope => 0\n\nfun main(io: Io) -> Int =\n use(Yep(v: io))\n use(Nope)\n 1\n"
|
|
72
|
+
);
|
|
73
|
+
let (engine, module, _) = build(&src, false);
|
|
74
|
+
let (mut store, instance) = instantiate(&engine, &module);
|
|
75
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
76
|
+
assert_eq!(main.call(&mut store, ()).unwrap(), 1);
|
|
77
|
+
assert_eq!(store.data(), &vec![4]);
|
|
78
|
+
}
|
plum-wasm-codegen/tests/wasm_codegen/e2e_types/mod.rs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
mod closure_optional;
|
|
3
3
|
mod closure_types;
|
|
4
4
|
mod io_entry;
|
|
5
|
+
mod io_primitive;
|
|
5
6
|
mod list_and_shadow;
|
|
6
7
|
mod question;
|
|
7
8
|
mod subtyping;
|
plum-wasm-codegen/tests/wasm_codegen/fusion_alloc_test.rs
CHANGED
|
@@ -289,3 +289,94 @@ fn million_pipeline_timed() {
|
|
|
289
289
|
tOn.as_millis(), tOff.as_millis(), tBuild.as_millis(), tSlice.as_millis()
|
|
290
290
|
));
|
|
291
291
|
}
|
|
292
|
+
|
|
293
|
+
/// A self-contained program (no imports) parsed as is: `primitive Io` is only allowed in std files
|
|
294
|
+
/// through the loader, so this skips it.
|
|
295
|
+
fn loadRaw(src: &str) -> plum_core::ast::Source {
|
|
296
|
+
let mut source = plum_test_support::parseRawSource(src);
|
|
297
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
298
|
+
source
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// The host calls (`plum.mark`) a run of `main` makes, in order.
|
|
302
|
+
fn markTrace(wasm: &[u8]) -> Vec<i32> {
|
|
303
|
+
let engine = engine(false);
|
|
304
|
+
let module = wasmtime::Module::new(&engine, wasm).unwrap();
|
|
305
|
+
let mut store = wasmtime::Store::new(&engine, Vec::<i32>::new());
|
|
306
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
307
|
+
linker
|
|
308
|
+
.func_wrap("plum", "mark", |mut c: wasmtime::Caller<'_, Vec<i32>>, n: i32| c.data_mut().push(n))
|
|
309
|
+
.unwrap();
|
|
310
|
+
linker.define_unknown_imports_as_default_values(&mut store, &module).unwrap();
|
|
311
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
312
|
+
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
313
|
+
main.call(&mut store, ()).unwrap();
|
|
314
|
+
store.into_data()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const IO_MARK: &str = "primitive Io =
|
|
318
|
+
extern fun mark(self, n: Int)
|
|
319
|
+
|
|
320
|
+
enum Lst =
|
|
321
|
+
| Nil
|
|
322
|
+
| Cons(head: Int, tail: Lst)
|
|
323
|
+
|
|
324
|
+
fun build(n: Int) -> Lst = if n == 0 then Nil else Cons(head: n, tail: build(n - 1))
|
|
325
|
+
fun mapL(xs: Lst) -> Lst = match xs
|
|
326
|
+
Nil => Nil
|
|
327
|
+
Cons(h, t) => Cons(head: h * 10, tail: mapL(t))
|
|
328
|
+
";
|
|
329
|
+
|
|
330
|
+
/// Fusion keeps every `io.mark(..)` call, once each and in order: the host-call trace of a
|
|
331
|
+
/// fused run equals the unfused (`--no-fusion`) run. An `io` method is read-only, never
|
|
332
|
+
/// pure, so a pure producer fuses into an `Io` consumer but no `io` call is moved, dropped or
|
|
333
|
+
/// duplicated; an `io` call inside a producer keeps that producer unfused.
|
|
334
|
+
#[test]
|
|
335
|
+
fn fusion_keeps_io_calls_in_order() {
|
|
336
|
+
// Pure producer, consumer calls io per element (the producer's cells are fused away).
|
|
337
|
+
let consumer = format!(
|
|
338
|
+
"{IO_MARK}fun drawL(io: Io, xs: Lst) -> Int = match xs
|
|
339
|
+
Nil => 0
|
|
340
|
+
Cons(h, t) =>
|
|
341
|
+
io.mark(h)
|
|
342
|
+
io.mark(h + 1)
|
|
343
|
+
drawL(io, t)
|
|
344
|
+
|
|
345
|
+
fun main(io: Io) -> Int =
|
|
346
|
+
a := drawL(io, mapL(build(5)))
|
|
347
|
+
a + drawL(io, mapL(build(2)))
|
|
348
|
+
"
|
|
349
|
+
);
|
|
350
|
+
let src = loadRaw(&consumer);
|
|
351
|
+
let (fused, plain) = (compile(&src, true), compile(&src, false));
|
|
352
|
+
assert!(
|
|
353
|
+
functions(&fused).iter().any(|f| f.name.starts_with("drawL$mapL")),
|
|
354
|
+
"the pipeline should fuse (test is meaningful)"
|
|
355
|
+
);
|
|
356
|
+
let want: Vec<i32> = [5, 4, 3, 2, 1, 2, 1].iter().flat_map(|n| [n * 10, n * 10 + 1]).collect();
|
|
357
|
+
assert_eq!(markTrace(&plain), want);
|
|
358
|
+
assert_eq!(markTrace(&fused), want);
|
|
359
|
+
|
|
360
|
+
// io call in the producer: an impure producer stays unfused and still runs once per element.
|
|
361
|
+
let producer = format!(
|
|
362
|
+
"{IO_MARK}fun markL(io: Io, xs: Lst) -> Lst = match xs
|
|
363
|
+
Nil => Nil
|
|
364
|
+
Cons(h, t) =>
|
|
365
|
+
io.mark(h)
|
|
366
|
+
Cons(head: h, tail: markL(io, t))
|
|
367
|
+
fun sumL(xs: Lst) -> Int = match xs
|
|
368
|
+
Nil => 0
|
|
369
|
+
Cons(h, t) => h + sumL(t)
|
|
370
|
+
|
|
371
|
+
fun main(io: Io) -> Int =
|
|
372
|
+
a := sumL(markL(io, build(4)))
|
|
373
|
+
io.mark(99)
|
|
374
|
+
a + sumL(markL(io, build(2)))
|
|
375
|
+
"
|
|
376
|
+
);
|
|
377
|
+
let src = loadRaw(&producer);
|
|
378
|
+
let (fused, plain) = (compile(&src, true), compile(&src, false));
|
|
379
|
+
let want = vec![4, 3, 2, 1, 99, 2, 1];
|
|
380
|
+
assert_eq!(markTrace(&plain), want);
|
|
381
|
+
assert_eq!(markTrace(&fused), want);
|
|
382
|
+
}
|