plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


803be1b — Peter John 2026-11-05T19:00:27+05:30
Move Rust crates under compiler/, rename std and examples

Generated files not shown: compiler/game/Cargo.lock, compiler/game/web-test/Cargo.lock, compiler/runtime/Cargo.lock

Files changed (279) hide show
  1. .gitignore +3 -0
  2. Cargo.toml +1 -1
  3. Makefile.toml +5 -5
  4. README.md +38 -38
  5. compiler/checker/Cargo.toml +13 -0
  6. {plum-checker → compiler/checker}/src/lib.rs +1 -1
  7. {plum-checker → compiler/checker}/src/state.rs +0 -0
  8. {plum-checker → compiler/checker}/src/subtyping.rs +0 -0
  9. {plum-checker → compiler/checker}/src/types.rs +0 -0
  10. {plum-checker → compiler/checker}/tests/checker/checker_tests.rs +2 -2
  11. {plum-checker → compiler/checker}/tests/checker/closure_hint_test.rs +0 -0
  12. {plum-checker → compiler/checker}/tests/checker/diagnostic_tests.rs +0 -0
  13. {plum-checker → compiler/checker}/tests/checker/explicit_generics_tests.rs +0 -0
  14. {plum-checker → compiler/checker}/tests/checker/features/closure_types.rs +0 -0
  15. {plum-checker → compiler/checker}/tests/checker/features/io_capability.rs +0 -0
  16. {plum-checker → compiler/checker}/tests/checker/features/mod.rs +0 -0
  17. {plum-checker → compiler/checker}/tests/checker/features/mutability.rs +0 -0
  18. {plum-checker → compiler/checker}/tests/checker/features/optional_unpinned.rs +0 -0
  19. {plum-checker → compiler/checker}/tests/checker/features/pattern_casing.rs +0 -0
  20. {plum-checker → compiler/checker}/tests/checker/features/primitive_generics.rs +0 -0
  21. {plum-checker → compiler/checker}/tests/checker/features/types.rs +0 -0
  22. {plum-checker → compiler/checker}/tests/checker/features/variant_pattern.rs +0 -0
  23. {plum-checker → compiler/checker}/tests/checker/fusion_pipeline_test.rs +3 -3
  24. {plum-checker → compiler/checker}/tests/checker/inline_variants_tests.rs +0 -0
  25. {plum-checker → compiler/checker}/tests/checker/io_primitive_tests.rs +0 -0
  26. {plum-checker → compiler/checker}/tests/checker/main.rs +0 -0
  27. {plum-checker → compiler/checker}/tests/checker/question_tests.rs +0 -0
  28. {plum-checker → compiler/checker}/tests/checker/subtyping_tests.rs +0 -0
  29. {plum-checker → compiler/checker}/tests/fixtures/custom_actor_probe2.plum +0 -0
  30. compiler/cli/Cargo.toml +25 -0
  31. {plum-cli → compiler/cli}/build.rs +4 -4
  32. {plum-cli → compiler/cli}/src/editor.rs +9 -9
  33. {plum-cli → compiler/cli}/src/game.rs +3 -3
  34. {plum-cli → compiler/cli}/src/main.rs +2 -2
  35. {plum-cli → compiler/cli}/tests/cli/assert_once_test.rs +1 -1
  36. {plum-cli → compiler/cli}/tests/cli/compile_tests.rs +1 -1
  37. {plum-cli → compiler/cli}/tests/cli/crash_test.rs +2 -2
  38. {plum-cli → compiler/cli}/tests/cli/explain_layout_test.rs +1 -1
  39. {plum-cli → compiler/cli}/tests/cli/game_web_test.rs +2 -2
  40. {plum-cli → compiler/cli}/tests/cli/host_stub_test.rs +1 -1
  41. {plum-cli → compiler/cli}/tests/cli/main.rs +0 -0
  42. {plum-cli → compiler/cli}/tests/cli/test_scope_test.rs +2 -2
  43. compiler/core/Cargo.toml +19 -0
  44. {plum-core → compiler/core}/src/ast.rs +0 -0
  45. {plum-core → compiler/core}/src/builtin_usage.rs +0 -0
  46. {plum-core → compiler/core}/src/closure_escape.rs +0 -0
  47. {plum-core → compiler/core}/src/closure_spec.rs +0 -0
  48. {plum-core → compiler/core}/src/ctors.rs +0 -0
  49. {plum-core → compiler/core}/src/diag.rs +0 -0
  50. {plum-core → compiler/core}/src/drive.rs +0 -0
  51. {plum-core → compiler/core}/src/embeds.rs +0 -0
  52. {plum-core → compiler/core}/src/formatter.rs +1 -1
  53. {plum-core → compiler/core}/src/forms.rs +0 -0
  54. {plum-core → compiler/core}/src/generics.rs +0 -0
  55. {plum-core → compiler/core}/src/layout.rs +0 -0
  56. {plum-core → compiler/core}/src/lib.rs +0 -0
  57. {plum-core → compiler/core}/src/loader.rs +25 -17
  58. {plum-core → compiler/core}/src/lower.rs +0 -0
  59. {plum-core → compiler/core}/src/parser.rs +0 -0
  60. {plum-core → compiler/core}/src/purity.rs +0 -0
  61. {plum-core → compiler/core}/src/rename.rs +0 -0
  62. {plum-core → compiler/core}/src/reuse.rs +0 -0
  63. {plum-core → compiler/core}/src/session.rs +0 -0
  64. {plum-core → compiler/core}/src/simplify.rs +0 -0
  65. {plum-core → compiler/core}/src/subtyping.rs +0 -0
  66. {plum-core → compiler/core}/src/tail.rs +0 -0
  67. {plum-core → compiler/core}/src/tests_lower.rs +1 -1
  68. {plum-core → compiler/core}/src/trmc.rs +0 -0
  69. {plum-core → compiler/core}/src/unions.rs +0 -0
  70. {plum-core → compiler/core}/src/uniqueness.rs +0 -0
  71. {plum-core → compiler/core}/src/visit.rs +0 -0
  72. {plum-core → compiler/core}/src/whistle.rs +0 -0
  73. {plum-core → compiler/core}/tests/core/closure_escape_test.rs +1 -1
  74. {plum-core → compiler/core}/tests/core/closure_spec_test.rs +0 -0
  75. {plum-core → compiler/core}/tests/core/closure_types_parse_test.rs +0 -0
  76. {plum-core → compiler/core}/tests/core/diag_test.rs +0 -0
  77. {plum-core → compiler/core}/tests/core/drive_test.rs +0 -0
  78. {plum-core → compiler/core}/tests/core/for_next_test.rs +0 -0
  79. {plum-core → compiler/core}/tests/core/formatter_test.rs +1 -1
  80. {plum-core → compiler/core}/tests/core/forms_list_range_test.rs +0 -0
  81. {plum-core → compiler/core}/tests/core/fused_closure_generic_test.rs +0 -0
  82. {plum-core → compiler/core}/tests/core/generics_test.rs +0 -0
  83. {plum-core → compiler/core}/tests/core/inline_variants_test.rs +0 -0
  84. {plum-core → compiler/core}/tests/core/layout_test.rs +4 -4
  85. {plum-core → compiler/core}/tests/core/list_literal_test.rs +0 -0
  86. {plum-core → compiler/core}/tests/core/loader_test.rs +23 -5
  87. {plum-core → compiler/core}/tests/core/lower_test.rs +0 -0
  88. {plum-core → compiler/core}/tests/core/main.rs +0 -0
  89. {plum-core → compiler/core}/tests/core/match_body_parse_test.rs +0 -0
  90. {plum-core → compiler/core}/tests/core/override_parse_test.rs +0 -0
  91. {plum-core → compiler/core}/tests/core/parser_test.rs +0 -0
  92. {plum-core → compiler/core}/tests/core/purity_test.rs +0 -0
  93. {plum-core → compiler/core}/tests/core/question_parse_test.rs +0 -0
  94. {plum-core → compiler/core}/tests/core/record_embed_test.rs +0 -0
  95. {plum-core → compiler/core}/tests/core/record_init_parse_test.rs +0 -0
  96. {plum-core → compiler/core}/tests/core/record_init_test.rs +0 -0
  97. {plum-core → compiler/core}/tests/core/rename_test.rs +0 -0
  98. {plum-core → compiler/core}/tests/core/session_test.rs +0 -0
  99. {plum-core → compiler/core}/tests/core/simplify_test.rs +0 -0
  100. {plum-core → compiler/core}/tests/core/subtyping_test.rs +0 -0
  101. {plum-core → compiler/core}/tests/core/tail_test.rs +0 -0
  102. {plum-core → compiler/core}/tests/core/tests_lower_test.rs +0 -0
  103. {plum-core → compiler/core}/tests/core/trmc_test.rs +0 -0
  104. {plum-core → compiler/core}/tests/core/unions_test.rs +0 -0
  105. {plum-core → compiler/core}/tests/core/uniqueness_test.rs +0 -0
  106. {plum-core → compiler/core}/tests/core/visit_test.rs +0 -0
  107. {plum-core → compiler/core}/tests/core/whistle_test.rs +0 -0
  108. compiler/driver/Cargo.toml +11 -0
  109. {plum-driver → compiler/driver}/src/lib.rs +0 -0
  110. {plum-driver → compiler/driver}/tests/driver_tests.rs +0 -0
  111. {plum-game → compiler/game}/.gitignore +0 -0
  112. {plum-game → compiler/game}/Cargo.toml +3 -3
  113. {plum-game → compiler/game}/src/api.rs +1 -1
  114. {plum-game → compiler/game}/src/host.rs +0 -0
  115. {plum-game → compiler/game}/src/lib.rs +0 -0
  116. {plum-game → compiler/game}/src/main.rs +0 -0
  117. {plum-game → compiler/game}/src/recorder.rs +0 -0
  118. {plum-game → compiler/game}/tests/fixtures/assets_fixture.plum +0 -0
  119. {plum-game → compiler/game}/tests/fixtures/host_fixture.plum +0 -0
  120. {plum-game → compiler/game}/tests/fixtures/target_fixture.plum +0 -0
  121. {plum-game → compiler/game}/tests/host_test.rs +7 -7
  122. {plum-game → compiler/game}/web-test/BENCHMARK.md +4 -4
  123. {plum-game → compiler/game}/web-test/Cargo.toml +0 -0
  124. compiler/game/web-test/README.md +28 -0
  125. {plum-game → compiler/game}/web-test/examples/install-browsers.rs +0 -0
  126. {plum-game → compiler/game}/web-test/fixtures/assets/sprites.png +0 -0
  127. {plum-game → compiler/game}/web-test/fixtures/assets/tex0.png +0 -0
  128. {plum-game → compiler/game}/web-test/fixtures/assets/tex1.png +0 -0
  129. {plum-game → compiler/game}/web-test/fixtures/assets/tex2.png +0 -0
  130. {plum-game → compiler/game}/web-test/fixtures/assets/tex3.png +0 -0
  131. {plum-game → compiler/game}/web-test/fixtures/assets/tex4.png +0 -0
  132. {plum-game → compiler/game}/web-test/fixtures/assets/tex5.png +0 -0
  133. {plum-game → compiler/game}/web-test/fixtures/assets/tex6.png +0 -0
  134. {plum-game → compiler/game}/web-test/fixtures/assets/tex7.png +0 -0
  135. {plum-game → compiler/game}/web-test/fixtures/interleave.plum +0 -0
  136. {plum-game → compiler/game}/web-test/fixtures/rendertarget.plum +0 -0
  137. {plum-game → compiler/game}/web-test/fixtures/stress.plum +1 -1
  138. {plum-game → compiler/game}/web-test/fixtures/stress_pure_scene.plum +0 -0
  139. {plum-game → compiler/game}/web-test/fixtures/stress_scene.plum +0 -0
  140. {plum-game → compiler/game}/web-test/fixtures/stress_state_view.plum +0 -0
  141. {plum-game → compiler/game}/web-test/fixtures/stress_state_view_deep.plum +0 -0
  142. {plum-game → compiler/game}/web-test/fixtures/stress_world.plum +0 -0
  143. {plum-game → compiler/game}/web-test/fixtures/text_sizes.plum +0 -0
  144. {plum-game → compiler/game}/web-test/fixtures/tools/make_textures.py +0 -0
  145. {plum-game → compiler/game}/web-test/src/lib.rs +1 -1
  146. {plum-game → compiler/game}/web-test/src/main.rs +1 -1
  147. {plum-game → compiler/game}/web-test/tests/web_check.rs +15 -15
  148. {plum-game → compiler/game}/web/index.html +0 -0
  149. {plum-game → compiler/game}/web/plum_bridge.js +0 -0
  150. {plum-host → compiler/host}/Cargo.toml +0 -0
  151. {plum-host → compiler/host}/src/lib.rs +2 -2
  152. {plum-lsp → compiler/lsp}/Cargo.toml +4 -4
  153. {plum-lsp → compiler/lsp}/src/completion.rs +2 -2
  154. {plum-lsp → compiler/lsp}/src/main.rs +7 -7
  155. {plum-runtime → compiler/runtime}/.gitignore +0 -0
  156. {plum-runtime → compiler/runtime}/Cargo.toml +1 -1
  157. {plum-runtime → compiler/runtime}/src/main.rs +0 -0
  158. compiler/test-support/Cargo.toml +13 -0
  159. {plum-test-support → compiler/test-support}/src/lib.rs +1 -1
  160. compiler/wasm-codegen/Cargo.toml +17 -0
  161. {plum-wasm-codegen → compiler/wasm-codegen}/src/lib.rs +3 -3
  162. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/codegen_tests.rs +3 -3
  163. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/mod.rs +0 -0
  164. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/reuse.rs +0 -0
  165. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_inplace.rs +0 -0
  166. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_spread.rs +0 -0
  167. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/empty_record_embed.rs +0 -0
  168. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/enum_init.rs +0 -0
  169. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/inline_variants.rs +0 -0
  170. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/mod.rs +0 -0
  171. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_embed.rs +0 -0
  172. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_init.rs +0 -0
  173. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/unboxed_variants.rs +0 -0
  174. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/closure_tail.rs +0 -0
  175. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/error_tail.rs +0 -0
  176. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/loop_break.rs +0 -0
  177. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/mod.rs +0 -0
  178. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/tail.rs +0 -0
  179. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_optional.rs +0 -0
  180. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_types.rs +0 -0
  181. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_entry.rs +0 -0
  182. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_primitive.rs +0 -0
  183. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/list_and_shadow.rs +0 -0
  184. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/mod.rs +0 -0
  185. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/question.rs +0 -0
  186. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/subtyping.rs +0 -0
  187. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/type_param_static.rs +0 -0
  188. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/examples_test.rs +1 -1
  189. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/fusion_alloc_test.rs +1 -1
  190. {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/main.rs +0 -0
  191. docs/backlog.md +4 -4
  192. docs/game-scene-styles.md +1 -1
  193. docs/imperative-vs-functional.md +239 -0
  194. docs/plans/2026-11-05-compiler-folder-move.md +66 -0
  195. docs/plans/2026-11-05-compiler-folder-move.results.md +42 -0
  196. docs/plans/2026-11-05-io-primitive.progress.md +9 -0
  197. docs/self-hosting.md +4 -4
  198. {plum-examples → examples}/arrays.plum +0 -0
  199. {plum-examples → examples}/basics.plum +0 -0
  200. {plum-examples → examples}/closures.plum +0 -0
  201. {plum-examples → examples}/control_flow.plum +0 -0
  202. {plum-examples → examples}/errors.plum +0 -0
  203. {plum-examples → examples}/functional.plum +0 -0
  204. {plum-examples → examples}/games/assets/gameover.wav +0 -0
  205. {plum-examples → examples}/games/assets/hit.wav +0 -0
  206. {plum-examples → examples}/games/assets/music.wav +0 -0
  207. {plum-examples → examples}/games/assets/pickup.wav +0 -0
  208. {plum-examples → examples}/games/assets/sprites.png +0 -0
  209. {plum-examples → examples}/games/functional/game_state_view.plum +1 -1
  210. {plum-examples → examples}/games/functional/kids/balloon_pop.plum +1 -1
  211. {plum-examples → examples}/games/imperative/game.plum +1 -1
  212. {plum-examples → examples}/games/imperative/game_loop.plum +0 -0
  213. {plum-examples → examples}/games/imperative/game_scene.plum +1 -1
  214. {plum-examples → examples}/games/imperative/scene.plum +1 -1
  215. {plum-examples → examples}/games/pure/game_pure_scene.plum +1 -1
  216. {plum-examples → examples}/games/tools/make_sounds.py +1 -1
  217. {plum-examples → examples}/games/tools/make_sprites.py +0 -0
  218. {plum-examples → examples}/generics.plum +0 -0
  219. {plum-examples → examples}/json.plum +0 -0
  220. {plum-examples → examples}/list_literal_probe.plum +0 -0
  221. {plum-examples → examples}/maps.plum +0 -0
  222. {plum-examples → examples}/match.plum +0 -0
  223. {plum-examples → examples}/numbers.plum +2 -2
  224. {plum-examples → examples}/recursion.plum +0 -0
  225. {plum-examples → examples}/sequences.plum +0 -0
  226. {plum-examples → examples}/strings.plum +0 -0
  227. {plum-examples → examples}/structs_records.plum +0 -0
  228. {plum-examples → examples}/subtyping.plum +0 -0
  229. {plum-examples → examples}/unions.plum +0 -0
  230. plum-checker/Cargo.toml +0 -13
  231. plum-cli/Cargo.toml +0 -25
  232. plum-core/Cargo.toml +0 -19
  233. plum-driver/Cargo.toml +0 -11
  234. plum-game/web-test/README.md +0 -28
  235. plum-test-support/Cargo.toml +0 -13
  236. plum-tooling/vscode-plum/package.json +1 -1
  237. plum-tooling/vscode-plum/src/extension.ts +1 -1
  238. plum-wasm-codegen/Cargo.toml +0 -17
  239. scripts/wasm-hashes.sh +2 -2
  240. {plum-std → std}/Array.plum +0 -0
  241. {plum-std → std}/Base64.plum +0 -0
  242. {plum-std → std}/Bool.plum +0 -0
  243. {plum-std → std}/Buffer.plum +0 -0
  244. {plum-std → std}/Byte.plum +0 -0
  245. {plum-std → std}/Deque.plum +0 -0
  246. {plum-std → std}/Float.plum +0 -0
  247. {plum-std → std}/Grid.plum +0 -0
  248. {plum-std → std}/Heap.plum +0 -0
  249. {plum-std → std}/Http.plum +0 -0
  250. {plum-std → std}/Int.plum +0 -0
  251. {plum-std → std}/Io.plum +0 -0
  252. {plum-std → std}/Json.plum +0 -0
  253. {plum-std → std}/List.plum +0 -0
  254. {plum-std → std}/Map.plum +0 -0
  255. {plum-std → std}/Math.plum +0 -0
  256. {plum-std → std}/Mem.plum +0 -0
  257. {plum-std → std}/Option.plum +0 -0
  258. {plum-std → std}/Os.plum +0 -0
  259. {plum-std → std}/Pair.plum +0 -0
  260. {plum-std → std}/Random.plum +0 -0
  261. {plum-std → std}/Result.plum +0 -0
  262. {plum-std → std}/Set.plum +0 -0
  263. {plum-std → std}/Slice.plum +0 -0
  264. {plum-std → std}/Str.plum +0 -0
  265. {plum-std → std}/Testing.plum +1 -1
  266. {plum-std → std}/Time.plum +0 -0
  267. {plum-std → std}/Uuid.plum +0 -0
  268. {plum-std → std}/game/Affine.plum +0 -0
  269. {plum-std → std}/game/Color.plum +0 -0
  270. {plum-std → std}/game/Game.plum +0 -0
  271. {plum-std → std}/game/Input.plum +0 -0
  272. {plum-std → std}/game/Vec2.plum +0 -0
  273. {plum-std → std}/game/functional/Anim.plum +0 -0
  274. {plum-std → std}/game/functional/FunctionalScene.plum +0 -0
  275. {plum-std → std}/game/functional/Ui.plum +0 -0
  276. {plum-std → std}/game/imperative/Scene.plum +0 -0
  277. {plum-std → std}/game/pure/Actions.plum +1 -1
  278. {plum-std → std}/game/pure/PureScene.plum +0 -0
  279. {plum-std → std}/game/pure/SceneInput.plum +0 -0
.gitignore CHANGED
@@ -26,3 +26,6 @@ dist/
26
26
  Thumbs.db
27
27
  .idea/
28
28
  *.swp
29
+
30
+ # superlite: dependency, build and cache folders
31
+ coverage/
Cargo.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [workspace]
2
- members = ["plum-core", "plum-driver", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support", "plum-host"]
2
+ members = ["compiler/core", "compiler/driver", "compiler/cli", "compiler/checker", "compiler/wasm-codegen", "compiler/lsp", "compiler/test-support", "compiler/host"]
3
3
  resolver = "2"
4
4
 
5
5
  [profile.dev]
Makefile.toml CHANGED
@@ -60,7 +60,7 @@ else
60
60
  echo "Helix not installed (run \`plum editor helix\` first) — skipping"
61
61
  fi
62
62
 
63
- # Extension id/version must match plum-cli/src/editor.rs's VSCODE_PUBLISHER /
63
+ # Extension id/version must match compiler/cli/src/editor.rs's VSCODE_PUBLISHER /
64
64
  # VSCODE_EXTENSION_NAME / VSCODE_EXTENSION_VERSION consts.
65
65
  found_vscode=0
66
66
  for dir_name in .vscode .vscode-insiders; do
@@ -85,7 +85,7 @@ fi
85
85
  #
86
86
  # Only the `plum` binary itself is distributed — `plum-lsp`, the tree-sitter
87
87
  # grammar `.so`, and the VSCode extension bundle are all embedded INTO it by
88
- # plum-cli/build.rs already, and `plum editor helix`/`vscode` extract them at
88
+ # compiler/cli/build.rs already, and `plum editor helix`/`vscode` extract them at
89
89
  # install time. There's nothing separate to ship.
90
90
  #
91
91
  # Linux/Windows are cross-compiled via https://github.com/cross-rs/cross
@@ -98,12 +98,12 @@ fi
98
98
  #
99
99
  # `cargo make release` is the one-shot version: builds all 3 platforms, tags
100
100
  # v<version> locally, regenerates README.md's download links, and uploads
101
- # everything to R2. Version comes from plum-cli/Cargo.toml — bump that first.
101
+ # everything to R2. Version comes from compiler/cli/Cargo.toml — bump that first.
102
102
 
103
103
  [env]
104
104
  # Distributed binaries embed the editor assets (dev builds skip them).
105
105
  PLUM_EMBED_EDITOR = "1"
106
- PLUM_VERSION = { script = ["grep -m1 '^version' plum-cli/Cargo.toml | sed -E 's/version = \"(.*)\"/\\1/'"] }
106
+ PLUM_VERSION = { script = ["grep -m1 '^version' compiler/cli/Cargo.toml | sed -E 's/version = \"(.*)\"/\\1/'"] }
107
107
 
108
108
  [tasks.dist-macos]
109
109
  description = "Build a release plum-cli binary for this Mac into dist/plum-cli/"
@@ -152,7 +152,7 @@ description = "Build release plum-cli binaries for macOS, Linux, and Windows int
152
152
  dependencies = ["dist-macos", "dist-linux", "dist-windows"]
153
153
 
154
154
  [tasks.tag-release]
155
- description = "Create a local git tag v<version> (from plum-cli/Cargo.toml) for this release, if it doesn't already exist"
155
+ description = "Create a local git tag v<version> (from compiler/cli/Cargo.toml) for this release, if it doesn't already exist"
156
156
  script_runner = "@shell"
157
157
  script = '''
158
158
  set -e
README.md CHANGED
@@ -113,7 +113,7 @@ A call that returns `Unit` can be a bare statement, and so can the last expressi
113
113
  Prebuilt binaries need nothing else. To build from source you need a recent stable Rust toolchain and a C compiler (for the tree-sitter parser). `node` and `npm` are only needed to rebuild the VSCode extension bundle (`npm install && npm run build` in `plum-tooling/vscode-plum`); without them `plum editor vscode` still installs syntax highlighting.
114
114
 
115
115
  ```sh
116
- cargo install --path plum-cli
116
+ cargo install --path compiler/cli
117
117
  ```
118
118
 
119
119
  Installs a release build as `plum` in `~/.cargo/bin`, which `cargo` puts on your `PATH` for you — no `sudo`/`/usr/local/bin` needed. Re-run this after pulling changes to the compiler; it doesn't auto-update. Use `cargo run -p plum-cli --` instead while actively developing the compiler itself.
@@ -152,12 +152,12 @@ plum game file.plum # serve a game and open it in the browser (see Gam
152
152
  plum editor helix|vscode # install highlighting and the language server
153
153
  ```
154
154
 
155
- `import std/Name` resolves to `<lib-path>/plum-std/Name.plum`, and `--lib-path` defaults to the current directory, so run the commands from a checkout of this repository (or pass `--lib-path`).
155
+ `import std/Name` resolves to `<lib-path>/std/Name.plum`, and `--lib-path` defaults to the current directory, so run the commands from a checkout of this repository (or pass `--lib-path`).
156
156
 
157
157
  ### Run the example game
158
158
 
159
159
  ```sh
160
- cargo run -q -p plum-cli -- game plum-examples/games/imperative/game.plum # or, after `cargo install --path plum-cli`: plum game plum-examples/games/imperative/game.plum
160
+ cargo run -q -p plum-cli -- game examples/games/imperative/game.plum # or, after `cargo install --path compiler/cli`: plum game examples/games/imperative/game.plum
161
161
  ```
162
162
 
163
163
  This compiles the game, serves it on `http://127.0.0.1:8080/` (any free port if that one is taken) and opens it in your default browser. It needs a browser with WebGPU: Chrome or Edge, Safari 26+ or Firefox. Arrow keys or WASD steer the ship, collect the orbs, dodge the mines; Enter or R restarts after the game is over. Press a key first: browsers start audio only after a user gesture.
@@ -278,7 +278,7 @@ struct Vec2(x: Float, y: Float) =
278
278
  Vec2(x: self.x * s, y: self.y * s)
279
279
  ```
280
280
 
281
- See [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
281
+ See [`examples/numbers.plum`](examples/numbers.plum).
282
282
 
283
283
  ## Variables and assignment
284
284
 
@@ -302,7 +302,7 @@ o: Option[Int] := None
302
302
  s = Rect(w: 1.0, h: 2.0) # checked, and converted, against Shape
303
303
  ```
304
304
 
305
- See [`plum-examples/structs_records.plum`](plum-examples/structs_records.plum).
305
+ See [`examples/structs_records.plum`](examples/structs_records.plum).
306
306
 
307
307
  ## Control flow
308
308
 
@@ -391,7 +391,7 @@ Details:
391
391
  - `else if` chains can mix plain conditions, `Some(x) := opt` bindings (see [Option and Result](#option-and-result)) and patterns.
392
392
  - It works on results too: `if Err(TooBig) := r` tests one error variant.
393
393
 
394
- See [`plum-examples/match.plum`](plum-examples/match.plum).
394
+ See [`examples/match.plum`](examples/match.plum).
395
395
 
396
396
  ## Functions
397
397
 
@@ -455,7 +455,7 @@ u2 := u
455
455
  u2.age = 4 # u.age is now 4: a boxed struct is shared on assignment
456
456
  ```
457
457
 
458
- A flat struct is copied on assignment and when passed, compares field by field with `==`, and is boxed inside `Option[T]` and other generic slots. Its fields can be assigned, since it is copied. Arrays of flat structs (`Array[Vec2]`, and so `Slice[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. A boxed struct's fields are immutable unless declared `mut` (`u.name = "x"` is an error), and its `==` is identity. To change an immutable field, build an updated value with a spread (`User(..u, name: "bob")`, see [In-place updates](#in-place-updates)). Methods and `match` work as for any enum. See [`plum-examples/structs_records.plum`](plum-examples/structs_records.plum).
458
+ A flat struct is copied on assignment and when passed, compares field by field with `==`, and is boxed inside `Option[T]` and other generic slots. Its fields can be assigned, since it is copied. Arrays of flat structs (`Array[Vec2]`, and so `Slice[Vec2]` and `Grid[Tile]`) are stored field by field, one native array per field, with no per-element allocation. A boxed struct's fields are immutable unless declared `mut` (`u.name = "x"` is an error), and its `==` is identity. To change an immutable field, build an updated value with a spread (`User(..u, name: "bob")`, see [In-place updates](#in-place-updates)). Methods and `match` work as for any enum. See [`examples/structs_records.plum`](examples/structs_records.plum).
459
459
 
460
460
  ### Enums
461
461
 
@@ -523,7 +523,7 @@ fun areaOf(s: Shape) -> Float =
523
523
  s.area()
524
524
  ```
525
525
 
526
- See [`plum-examples/unions.plum`](plum-examples/unions.plum). Open-ended dispatch (new kinds added elsewhere) is done with [struct subtyping](#embedding-structs-and-override) instead.
526
+ See [`examples/unions.plum`](examples/unions.plum). Open-ended dispatch (new kinds added elsewhere) is done with [struct subtyping](#embedding-structs-and-override) instead.
527
527
 
528
528
  Variants can hold the enum recursively, which is how recursive data is written:
529
529
 
@@ -554,7 +554,7 @@ Using a variant as a type is an error, as are unnamed fields and reading a field
554
554
 
555
555
  ### Embedding structs and override
556
556
 
557
- A struct whose header lists exactly one other struct, first, embeds it as its base and is its subtype. It gains the base's fields, `init` and instance methods, converts to the base wherever the base is expected, and `override fun` picks the subtype's method at runtime (dispatch on the concrete type). The header names the base fields a caller passes (`Stage(camScale)`); the rest are set by the base `init`, which runs before the struct's own, or by the struct's `init`. Statics are not copied. A base method written without a body, `abstract fun m(self) -> T`, must be overridden by every subtype, and such a base cannot be constructed. See [`plum-examples/subtyping.plum`](plum-examples/subtyping.plum).
557
+ A struct whose header lists exactly one other struct, first, embeds it as its base and is its subtype. It gains the base's fields, `init` and instance methods, converts to the base wherever the base is expected, and `override fun` picks the subtype's method at runtime (dispatch on the concrete type). The header names the base fields a caller passes (`Stage(camScale)`); the rest are set by the base `init`, which runs before the struct's own, or by the struct's `init`. Statics are not copied. A base method written without a body, `abstract fun m(self) -> T`, must be overridden by every subtype, and such a base cannot be constructed. See [`examples/subtyping.plum`](examples/subtyping.plum).
558
558
 
559
559
  ```plum
560
560
  struct Stage(camScale: Float) =
@@ -668,7 +668,7 @@ match book
668
668
  FantasyBook(title, _, _) => "Fantasy book \"{title}\" has no mythical creatures"
669
669
  ```
670
670
 
671
- Pattern names follow a casing rule: a lowercase name always binds, a Capitalised name must be a known member or tag (`unknown tag Foo` otherwise), and a lowercase name equal to a tag is an error (`did you mean Foo?`). Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum; compared), a constructor pattern (`Circle(r)`, binding its fields, possibly nested), a bare member name (`Float`, which narrows the subject to that type, see [Enums](#enums)), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard. A `match` over an enum must be exhaustive. See [`plum-examples/match.plum`](plum-examples/match.plum).
671
+ Pattern names follow a casing rule: a lowercase name always binds, a Capitalised name must be a known member or tag (`unknown tag Foo` otherwise), and a lowercase name equal to a tag is an error (`did you mean Foo?`). Patterns can be literals, a bare identifier (binds), a bare capitalized name (a member or tag of an enum; compared), a constructor pattern (`Circle(r)`, binding its fields, possibly nested), a bare member name (`Float`, which narrows the subject to that type, see [Enums](#enums)), or `_` (wildcard). `match a, b, ...` supports multiple subjects, and a case can carry a `when <expr>` guard. A `match` over an enum must be exhaustive. See [`examples/match.plum`](examples/match.plum).
672
672
 
673
673
  ### Enum `init`
674
674
 
@@ -734,7 +734,7 @@ test "slice"
734
734
  assert s.at(3) == 4
735
735
  ```
736
736
 
737
- **`Array[T]`** is the fixed-length array, unchanged: arrays of structs are stored field by field. See [`plum-examples/arrays.plum`](plum-examples/arrays.plum).
737
+ **`Array[T]`** is the fixed-length array, unchanged: arrays of structs are stored field by field. See [`examples/arrays.plum`](examples/arrays.plum).
738
738
 
739
739
  ## Generics
740
740
 
@@ -789,7 +789,7 @@ sumWith([1, 2, 3].toSlice(), intNum) # 6
789
789
  sumWith([1.5, 2.5].toSlice(), floatNum) # 4.0
790
790
  ```
791
791
 
792
- `num.add(a, b)` calls the closure stored in the fn-typed field `add`; if the type also declares a method named `add`, the method wins. See `num_record_passed_explicitly` in [`plum-examples/generics.plum`](plum-examples/generics.plum).
792
+ `num.add(a, b)` calls the closure stored in the fn-typed field `add`; if the type also declares a method named `add`, the method wins. See `num_record_passed_explicitly` in [`examples/generics.plum`](examples/generics.plum).
793
793
 
794
794
  ## How fusion works
795
795
 
@@ -839,7 +839,7 @@ The built-in types carry theirs: `Int.Min`, `Int.Max`, `Float.Pi`, `Float.Tau`,
839
839
  `primitive Name =` has two meanings, by name:
840
840
 
841
841
  - **a namespace**: any name that is not a built-in type declares a stateless namespace of constants and static functions, usable anywhere. It has no `self`, no values and no fields, so `Name` is not a type and can't be constructed or used as a value; it only appears as `Name.Const` or `Name.f()`. Use an `enum` when you need markers or a set of cases.
842
- - **a built-in's API**: `primitive Int =`, `primitive Float =`, `primitive Bool =`, `primitive Str =`, `primitive Byte =`, and `primitive Array[T] =` declare the methods, statics and constants of a compiler built-in, without declaring a dummy `enum X = | X` just to hang them on. This is only allowed in `plum-std`; anywhere else it is an error (`primitive Int declares a built-in type and is only allowed in std`).
842
+ - **a built-in's API**: `primitive Int =`, `primitive Float =`, `primitive Bool =`, `primitive Str =`, `primitive Byte =`, and `primitive Array[T] =` declare the methods, statics and constants of a compiler built-in, without declaring a dummy `enum X = | X` just to hang them on. This is only allowed in `std`; anywhere else it is an error (`primitive Int declares a built-in type and is only allowed in std`).
843
843
 
844
844
  ```plum
845
845
  primitive Config =
@@ -854,12 +854,12 @@ fun main() =
854
854
  ```
855
855
 
856
856
  ```plum
857
- primitive Byte = # in plum-std/Byte.plum
857
+ primitive Byte = # in std/Byte.plum
858
858
  fun toInt(self) -> Int =
859
859
  Int(self)
860
860
  ```
861
861
 
862
- The std library has no free functions: each function is a static of its type or of a `primitive` namespace, so `Math.sqrt(x)`, `Os.readFile(io, path)`, `Base64.encode(s)`, `Http.get(url)` and `Testing.expectEq(a, b)` (see [Standard library highlights](#standard-library-highlights)). `Time.now(io)`, `Uuid.v4(rng)`, `Uuid.fromStr(s)`, `Random.seeded(seed)` are statics of their types. `Random` is a plain value: `r.nextInt()`, `r.nextIntBelow(n)` and `r.nextFloat()` return a `Rolled` (the `value` and the next `rng`), so the same seed gives the same sequence; `io.randomSeed()` supplies a wall-clock seed. See [`plum-examples/constants.plum`](plum-examples/constants.plum) and [`plum-examples/numbers.plum`](plum-examples/numbers.plum).
862
+ The std library has no free functions: each function is a static of its type or of a `primitive` namespace, so `Math.sqrt(x)`, `Os.readFile(io, path)`, `Base64.encode(s)`, `Http.get(url)` and `Testing.expectEq(a, b)` (see [Standard library highlights](#standard-library-highlights)). `Time.now(io)`, `Uuid.v4(rng)`, `Uuid.fromStr(s)`, `Random.seeded(seed)` are statics of their types. `Random` is a plain value: `r.nextInt()`, `r.nextIntBelow(n)` and `r.nextFloat()` return a `Rolled` (the `value` and the next `rng`), so the same seed gives the same sequence; `io.randomSeed()` supplies a wall-clock seed. See [`examples/constants.plum`](examples/constants.plum) and [`examples/numbers.plum`](examples/numbers.plum).
863
863
 
864
864
  ## Closures
865
865
 
@@ -877,7 +877,7 @@ fun main() -> Int =
877
877
  each(|v| v * 3) + useCapturingClosure()
878
878
  ```
879
879
 
880
- A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field, and a named function can be passed wherever a `fn` value is expected. `Slice` and `Map` have no closure-taking iteration methods (use `for`, below); closures are for passing behavior around as values (see `Num` under [Generics](#how-generics-compile)). See [`plum-examples/closures.plum`](plum-examples/closures.plum).
880
+ A closure literal is `|params| body`. Capture is snapshot-by-value: a closure copies the outer variables it references at creation time, not re-read live later. A `fn(...) -> T` type annotates a closure-typed parameter or field, and a named function can be passed wherever a `fn` value is expected. `Slice` and `Map` have no closure-taking iteration methods (use `for`, below); closures are for passing behavior around as values (see `Num` under [Generics](#how-generics-compile)). See [`examples/closures.plum`](examples/closures.plum).
881
881
 
882
882
  A closure parameter may carry a type, `|b: Box| b.get()`, and then it has that type (a mismatch with an expected `fn(..)` is a type error). An untyped closure stored in a local takes its parameter types from the first call of that local; if there is none, the error is `cannot infer the type of b; write |b: Box|`.
883
883
 
@@ -964,7 +964,7 @@ for u := range units
964
964
  alive.push(u)
965
965
  ```
966
966
 
967
- See [`plum-examples/sequences.plum`](plum-examples/sequences.plum) and [`plum-examples/control_flow.plum`](plum-examples/control_flow.plum).
967
+ See [`examples/sequences.plum`](examples/sequences.plum) and [`examples/control_flow.plum`](examples/control_flow.plum).
968
968
 
969
969
  ### Iteration cheat sheet
970
970
 
@@ -1071,7 +1071,7 @@ The patterns are positional, so a `match` over a `Result[T, ParseError]` is exha
1071
1071
 
1072
1072
  `x!` unwraps an `Option` or `Result`, or crashes with a message that shows the source position (`file:line`). `!=` wins by maximal munch, so write `{f()!} == x`. `crash "msg"` aborts the same way. Use both for bugs (a broken invariant), not for expected failures; `unwrap()` is the method form.
1073
1073
 
1074
- See [`plum-examples/errors.plum`](plum-examples/errors.plum) and [`plum-examples/error_propagation.plum`](plum-examples/error_propagation.plum).
1074
+ See [`examples/errors.plum`](examples/errors.plum) and [`examples/error_propagation.plum`](examples/error_propagation.plum).
1075
1075
 
1076
1076
  ## Testing
1077
1077
 
@@ -1086,13 +1086,13 @@ test "add works"
1086
1086
  ```
1087
1087
 
1088
1088
  ```sh
1089
- $ plum test plum-examples/basics.plum
1089
+ $ plum test examples/basics.plum
1090
1090
  └─ add works ✔
1091
1091
 
1092
1092
  1 passed, 0 failed
1093
1093
  ```
1094
1094
 
1095
- A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`Testing.expectEq`, `Testing.expectTrue`, `Testing.expectSuccess`, ...). The `plum-std` library and every file in `plum-examples` carry their own tests: `plum test plum-std plum-examples`.
1095
+ A failing `assert` prints the actual and expected values for `==` comparisons. `import std/Testing` adds small assertion helpers (`Testing.expectEq`, `Testing.expectTrue`, `Testing.expectSuccess`, ...). The `std` library and every file in `examples` carry their own tests: `plum test std examples`.
1096
1096
 
1097
1097
  ## `extern` functions and hosts
1098
1098
 
@@ -1103,10 +1103,10 @@ 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 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).
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` (`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
- $ plum run plum-examples/basics.plum
1109
+ $ plum run examples/basics.plum
1110
1110
  hello from plum
1111
1111
  hello, world!
1112
1112
  ```
@@ -1124,13 +1124,13 @@ Every module that imports `std/Str` also exports four helpers for hosts that can
1124
1124
 
1125
1125
  ## Games
1126
1126
 
1127
- Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_bridge.js`): the Plum module calls the bridge directly, one extern call per draw, and the bridge batches everything into one buffer upload per frame. A draw call binds up to 16 textures (each vertex carries its texture slot), so mixing atlases in y-sorted scenes costs a handful of draw calls; the canvas is `CSS size x devicePixelRatio` while every game coordinate (`screenWidth`, `mousePos`, the camera) stays in CSS pixels; text is drawn from a signed-distance-field glyph atlas, so it stays crisp at any size or zoom; and if the GPU device is lost the renderer requests a new one, re-uploads the textures and logs `GPU device lost (<reason>); restored` (render-target contents start transparent again). WebGPU is required (Chrome or Edge, Safari 26+, Firefox); there is no fallback. The bridge reads nothing from the wasm module's import or export lists (`WebAssembly.Module.imports` throws in Safari for typed GC references), so it loads the same way in Safari; `plum-web-test --browser webkit` checks that against Playwright's WebKit.
1127
+ Games run in the browser on Plum's own WebGPU renderer (`compiler/game/web/plum_bridge.js`): the Plum module calls the bridge directly, one extern call per draw, and the bridge batches everything into one buffer upload per frame. A draw call binds up to 16 textures (each vertex carries its texture slot), so mixing atlases in y-sorted scenes costs a handful of draw calls; the canvas is `CSS size x devicePixelRatio` while every game coordinate (`screenWidth`, `mousePos`, the camera) stays in CSS pixels; text is drawn from a signed-distance-field glyph atlas, so it stays crisp at any size or zoom; and if the GPU device is lost the renderer requests a new one, re-uploads the textures and logs `GPU device lost (<reason>); restored` (render-target contents start transparent again). WebGPU is required (Chrome or Edge, Safari 26+, Firefox); there is no fallback. The bridge reads nothing from the wasm module's import or export lists (`WebAssembly.Module.imports` throws in Safari for typed GC references), so it loads the same way in Safari; `plum-web-test --browser webkit` checks that against Playwright's WebKit.
1128
1128
 
1129
1129
  `plum game file.plum [--assets DIR] [--port N] [--no-open]` compiles the game, serves it on 127.0.0.1 (port 8080, else any free port; `--port` picks one), prints the URL and opens the default browser unless `--no-open` is given. Ctrl-C stops it. `plum game file.plum -o dist [--assets DIR]` writes a static build instead and exits: `index.html`, `plum_bridge.js`, `plum.wasm`, `preload.txt` and an `assets/` copy of the whole asset directory (every file in it is listed in `preload.txt` and fetched before the first frame). Host that directory on any static file server; the browser needs wasm GC support. The asset root defaults to `assets` beside the source file.
1130
1130
 
1131
- `plum-examples/games/imperative/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`io.blendMode(Blend.Add)`) and an animated sprite sheet (`plum-examples/games/assets/sprites.png`, generated by `plum-examples/games/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/games/assets/*.wav`, generated by `plum-examples/games/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
1131
+ `examples/games/imperative/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`io.blendMode(Blend.Add)`) and an animated sprite sheet (`examples/games/assets/sprites.png`, generated by `examples/games/tools/make_sprites.py`) and plays music and sound effects (`examples/games/assets/*.wav`, generated by `examples/games/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test examples` covers them without a browser.
1132
1132
 
1133
- **`std/game/imperative/Scene`** is a scene-graph library written in Plum (see [`plum-examples/games/imperative/scene.plum`](plum-examples/games/imperative/scene.plum)): a tree of actors under a `Stage`, with parent-relative position, rotation, scale and colour, per-actor `act`, `draw` and `hit` behaviour, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. It is libGDX Scene2D-style subclassing on a base struct (a game's own world can embed the `Stage` struct to get its root, camera and `act`/`draw`, see [Embedding structs and override](#embedding-structs-and-override)): `Actor` is a base struct holding the position, size, origin, scale, rotation, colour, visibility, children and culling fields, and `Group`, `Sprite`, `Rect`, `Circle` and `Text` are structs embedding it. `Actor.init` sets every field (position and size 0.0, scale 1.0, white, visible, a fresh `children` list), so a struct exposes only what a caller passes (`struct Rect(Actor(width, height, color))`) and its own `init` sets the rest. A game adds its own kinds the same way and overrides what it needs:
1133
+ **`std/game/imperative/Scene`** is a scene-graph library written in Plum (see [`examples/games/imperative/scene.plum`](examples/games/imperative/scene.plum)): a tree of actors under a `Stage`, with parent-relative position, rotation, scale and colour, per-actor `act`, `draw` and `hit` behaviour, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. It is libGDX Scene2D-style subclassing on a base struct (a game's own world can embed the `Stage` struct to get its root, camera and `act`/`draw`, see [Embedding structs and override](#embedding-structs-and-override)): `Actor` is a base struct holding the position, size, origin, scale, rotation, colour, visibility, children and culling fields, and `Group`, `Sprite`, `Rect`, `Circle` and `Text` are structs embedding it. `Actor.init` sets every field (position and size 0.0, scale 1.0, white, visible, a fresh `children` list), so a struct exposes only what a caller passes (`struct Rect(Actor(width, height, color))`) and its own `init` sets the rest. A game adds its own kinds the same way and overrides what it needs:
1134
1134
 
1135
1135
  ```plum
1136
1136
  struct Walker(Actor(x, y), speed: Float) =
@@ -1163,7 +1163,7 @@ world.setCullingArea(cam.x, cam.y, viewW, viewH) # in world's own coordinates
1163
1163
  world.clearCullingArea()
1164
1164
  ```
1165
1165
 
1166
- `sortChildren(before)` is a natural merge sort with a scratch list kept on the actor: a list that is already sorted costs n - 1 comparisons and a nearly sorted one (a y-sorted layer re-sorted every frame) about 2n, with O(n log n) as the worst case; equal keys keep their order, so ties do not flicker. `setCullingArea` makes a group skip each direct child whose box (position and size, scaled about its origin; rotation ignored) lies outside the rectangle, in both `draw` and `hit`, with the child's whole subtree; children with no size (plain groups) are never culled. In [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) a 20,000-sprite y-sorted layer costs 1.15 ms per frame with culling and 4.20 ms without on the earlier closure-based `Node` (2.09 ms with culling on the `Actor` base struct).
1166
+ `sortChildren(before)` is a natural merge sort with a scratch list kept on the actor: a list that is already sorted costs n - 1 comparisons and a nearly sorted one (a y-sorted layer re-sorted every frame) about 2n, with O(n log n) as the worst case; equal keys keep their order, so ties do not flicker. `setCullingArea` makes a group skip each direct child whose box (position and size, scaled about its origin; rotation ignored) lies outside the rectangle, in both `draw` and `hit`, with the child's whole subtree; children with no size (plain groups) are never culled. In [`compiler/game/web-test/BENCHMARK.md`](compiler/game/web-test/BENCHMARK.md) a 20,000-sprite y-sorted layer costs 1.15 ms per frame with culling and 4.20 ms without on the earlier closure-based `Node` (2.09 ms with culling on the `Actor` base struct).
1167
1167
 
1168
1168
  A layered world (a layer recipe for a large map) is a few groups under the stage root, drawn in order: `terrain` (static, culled), `world` (y-sorted every frame, culled), `effects` (culled) and `ui` (not culled, in screen space with a second `Stage` or camera). Each frame: `world.sortChildren(|a, b| a.y < b.y)`, then `setCullingArea` on `terrain`, `world` and `effects` from the camera view.
1169
1169
 
@@ -1178,9 +1178,9 @@ export fun draw(io: Io, g: G)
1178
1178
 
1179
1179
  The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Io`, `std/game/Game`, `std/game/Vec2` and `std/game/Color`; pure logic can import `std/game/Vec2` and `std/game/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1180
1180
 
1181
- **Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path plum-game/Cargo.toml` checks games' draw-call sequences (`plum-game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
1181
+ **Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path compiler/game/Cargo.toml` checks games' draw-call sequences (`compiler/game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
1182
1182
 
1183
- **Immediate mode or Scene?** [`plum-examples/games/imperative/game.plum`](plum-examples/games/imperative/game.plum) is immediate mode: one state struct, a pure `update`, and `draw` issues the calls each frame; it suits flat arcade games with a few kinds of thing. [`plum-examples/games/imperative/scene.plum`](plum-examples/games/imperative/scene.plum) and [`plum-examples/games/imperative/game_scene.plum`](plum-examples/games/imperative/game_scene.plum) are retained mode on the `Actor` base struct: a tree with transforms, z-order, culling, hit-testing and per-kind `act`/`draw`, which suits UI, layered worlds and unit crowds. `game_scene.plum` is the same Orb Run as `game.plum` rebuilt on the scene graph, so you can compare the two styles on identical rules.
1183
+ **Immediate mode or Scene?** [`examples/games/imperative/game.plum`](examples/games/imperative/game.plum) is immediate mode: one state struct, a pure `update`, and `draw` issues the calls each frame; it suits flat arcade games with a few kinds of thing. [`examples/games/imperative/scene.plum`](examples/games/imperative/scene.plum) and [`examples/games/imperative/game_scene.plum`](examples/games/imperative/game_scene.plum) are retained mode on the `Actor` base struct: a tree with transforms, z-order, culling, hit-testing and per-kind `act`/`draw`, which suits UI, layered worlds and unit crowds. `game_scene.plum` is the same Orb Run as `game.plum` rebuilt on the scene graph, so you can compare the two styles on identical rules.
1184
1184
 
1185
1185
  ### Pure scene graph
1186
1186
 
@@ -1212,21 +1212,21 @@ export fun draw(io: Io, w: World) =
1212
1212
  w.stage.draw(io)
1213
1213
  ```
1214
1214
 
1215
- - **Immediate mode** ([`game.plum`](plum-examples/games/imperative/game.plum)): one state struct and a `draw` that issues the calls every frame. Use it for flat arcade games with a few kinds of thing.
1216
- - **Scene** ([`game_scene.plum`](plum-examples/games/imperative/game_scene.plum)): a retained tree of actors with `mut` fields. Use it when you want objects that change in place and a familiar `act`/`draw` object model.
1217
- - **Pure scene** ([`game_pure_scene.plum`](plum-examples/games/pure/game_pure_scene.plum)): every update takes an actor and hands back an updated one (written in place when uniqueness proves it), pointer clicks arrive as `UiEvent`s, and the rules can be tested without a host. Use it when you want pure updates and tests that run without a game host.
1218
- - **State/view** ([`game_state_view.plum`](plum-examples/games/functional/game_state_view.plum)): the game state is plain data and a `view` turns it into a `SceneNode` (`std/game/functional/FunctionalScene`), a recursive data type whose methods draw, measure and hit-test it. Use it when you want to draw from state instead of keeping a tree of actors.
1215
+ - **Immediate mode** ([`game.plum`](examples/games/imperative/game.plum)): one state struct and a `draw` that issues the calls every frame. Use it for flat arcade games with a few kinds of thing.
1216
+ - **Scene** ([`game_scene.plum`](examples/games/imperative/game_scene.plum)): a retained tree of actors with `mut` fields. Use it when you want objects that change in place and a familiar `act`/`draw` object model.
1217
+ - **Pure scene** ([`game_pure_scene.plum`](examples/games/pure/game_pure_scene.plum)): every update takes an actor and hands back an updated one (written in place when uniqueness proves it), pointer clicks arrive as `UiEvent`s, and the rules can be tested without a host. Use it when you want pure updates and tests that run without a game host.
1218
+ - **State/view** ([`game_state_view.plum`](examples/games/functional/game_state_view.plum)): the game state is plain data and a `view` turns it into a `SceneNode` (`std/game/functional/FunctionalScene`), a recursive data type whose methods draw, measure and hit-test it. Use it when you want to draw from state instead of keeping a tree of actors.
1219
1219
 
1220
1220
  ### Functional scenes (state/view)
1221
1221
 
1222
- Three scene styles ship, each in its own folder: `std/game/imperative/Scene` (actors with `mut` fields, in `plum-examples/games/imperative`), `std/game/pure/*` (pure-update actors, in `plum-examples/games/pure`) and `std/game/functional/*` (plain state plus a `view`, in `plum-examples/games/functional`); the shared assets and generators are in `plum-examples/games/assets` and `plum-examples/games/tools`. The functional style is a loop of `State -> view -> draw`: the game state is plain data, `view(state)` builds a `SceneNode` (`std/game/functional/FunctionalScene`) and `draw` walks it. Nothing is retained between frames.
1222
+ Three scene styles ship, each in its own folder: `std/game/imperative/Scene` (actors with `mut` fields, in `examples/games/imperative`), `std/game/pure/*` (pure-update actors, in `examples/games/pure`) and `std/game/functional/*` (plain state plus a `view`, in `examples/games/functional`); the shared assets and generators are in `examples/games/assets` and `examples/games/tools`. The functional style is a loop of `State -> view -> draw`: the game state is plain data, `view(state)` builds a `SceneNode` (`std/game/functional/FunctionalScene`) and `draw` walks it. Nothing is retained between frames.
1223
1223
 
1224
1224
  The functional game is written in a component style: each kind of thing is a struct with an `update(self, ..)` that returns the next value and a `render(self, ..)` that returns its `SceneNode` (`Ship.update`/`Ship.render`, `Orb`, `Mine`, `Star`), and `State.render` composes them. `update` never changes a value it was given.
1225
1225
 
1226
1226
  - **`Anim`** describes motion as a function of time: `MoveBy`, `RotateBy`, `ScaleTo`, `Delay`, `Seq`, `Par` and `Forever`. `Anim.at(anim, t)` returns an `AnimPose` (dx, dy, rotation, scaleX, scaleY) for any `t`, so a view just applies the pose for `state.clock`.
1227
1227
  - **`Ui`** is pure pointer state. `view.hitId(x, y)` finds the `Clickable(id, msg, child)` node under the pointer; `Ui.step(ui, input, hit)` returns a `UiStep` with the new `UiState` and the `UiEvent`s raised (`Press`, `Release`, `Click`, `Enter`, `Exit`, with pointer capture), and `Ui.messages(view, events)` turns the clicks into the game's own messages. `Ui.button(id, msg, label, x, y, w, h, ui)` builds a button that shows hover and pressed looks and sends `msg` when clicked. The picture is a `SceneNode[Msg]`, generic over the game's message type.
1228
1228
 
1229
- From [`game_state_view.plum`](plum-examples/games/functional/game_state_view.plum) (a method of `State`): `feed` runs `Ui.step` against the picture the player sees, then hands each event to `update`.
1229
+ From [`game_state_view.plum`](examples/games/functional/game_state_view.plum) (a method of `State`): `feed` runs `Ui.step` against the picture the player sees, then hands each event to `update`.
1230
1230
 
1231
1231
  ```
1232
1232
  fun feed(self, input: Input) -> State =
@@ -1238,7 +1238,7 @@ fun feed(self, input: Input) -> State =
1238
1238
 
1239
1239
  The button's id only identifies it for hover and press state (a boxed struct's `==` is identity today, structural `==` is a deferred item); what a click means is its message.
1240
1240
 
1241
- **Browser check:** `cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/games/imperative/game.plum --screenshot /tmp/orb-run.png` builds the static web version, serves it and drives headless Chromium (software WebGPU; `--browser webkit` drives Playwright's WebKit, the Safari engine, instead) through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. It also takes `--assets DIR`, `--require-audio`, `--measure SECS` (with `--json`, `draws` is the GPU draw calls per frame), `--headed`, `--dpr N` (a device pixel ratio of N), `--lose-device` (destroys the GPU device after a few frames and requires rendering to resume) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y)), `--expect-unsupported-webgpu` (hides `navigator.gpu` and requires the page's "WebGPU is not supported" message while plum.wasm still loads) and `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw, as Safari does). `plum-game/web-test/fixtures/` has `interleave.plum`, `text_sizes.plum` and `rendertarget.plum` for these. One-time setup: `cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers`. See [`plum-game/web-test/README.md`](plum-game/web-test/README.md); `docs/game.png` above was made with `--screenshot`. [`plum-game/web-test/BENCHMARK.md`](plum-game/web-test/BENCHMARK.md) records the renderer benchmark that led to this design.
1241
+ **Browser check:** `cargo run --manifest-path compiler/game/web-test/Cargo.toml -- examples/games/imperative/game.plum --screenshot /tmp/orb-run.png` builds the static web version, serves it and drives headless Chromium (software WebGPU; `--browser webkit` drives Playwright's WebKit, the Safari engine, instead) through [playwright-rs](https://crates.io/crates/playwright-rs): it fails if the canvas stays blank or stops changing, or the page logs an error, and reports whether audio started. It also takes `--assets DIR`, `--require-audio`, `--measure SECS` (with `--json`, `draws` is the GPU draw calls per frame), `--headed`, `--dpr N` (a device pixel ratio of N), `--lose-device` (destroys the GPU device after a few frames and requires rendering to resume) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y)), `--expect-unsupported-webgpu` (hides `navigator.gpu` and requires the page's "WebGPU is not supported" message while plum.wasm still loads) and `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw, as Safari does). `compiler/game/web-test/fixtures/` has `interleave.plum`, `text_sizes.plum` and `rendertarget.plum` for these. One-time setup: `cargo run --manifest-path compiler/game/web-test/Cargo.toml --example install-browsers`. See [`compiler/game/web-test/README.md`](compiler/game/web-test/README.md); `docs/game.png` above was made with `--screenshot`. [`compiler/game/web-test/BENCHMARK.md`](compiler/game/web-test/BENCHMARK.md) records the renderer benchmark that led to this design.
1242
1242
 
1243
1243
  **Asset root:** textures and files are read relative to the asset root. A path that escapes the root (`..` or absolute) fails like a missing file: `-1`, `""` or `false`. The headless runner's root is `--assets` or `assets` beside the wasm; it reads image headers for `textureWidth`/`textureHeight` and structs sounds without decoding them.
1244
1244
 
@@ -1279,13 +1279,13 @@ The button's id only identifies it for hover and press state (a boxed struct's `
1279
1279
  - **`Buffer`** (`import std/Buffer`) — a growable, mutable byte sequence for building up a `Str`, amortized O(n) writes
1280
1280
  - **`Array[T]`** (`import std/Array`) — the compiler-intrinsic fixed-length array that `Slice`, `Map` and `Str` are written on, with `Array.copy` and `Array.equal` for scalar ranges; `Grid[T]` is a flat 2D array
1281
1281
  - **`Slice[T]`** (`import std/Slice`) — a growable list built on `Array`, with `get`/`add`/`set`/`removeAt`/`remove`/`sort`/`join`/`chunk` (iteration and transformation via `for`). `xs.slice(i, j)` returns a view: a `Slice` over the same storage, with no copy. `set` on a view changes the original and a `push` on a view moves it to its own storage first; reordering the original in place (`removeAt`, `reverse`) is visible through its views, so use `sublist(i, j)` for a copy
1282
- - **`Map[K, V]`** (`import std/Map`) — `["a" => 1, "b" => 2]` builds a `Map[Str, Int]` (it is `Map.empty().with("a", 1).with("b", 2)`); `m: Map[K, V] := Map.empty()` is empty. A key can be any expression (`[n + 1 => x]`). A later duplicate key wins, as with `set`. See [`plum-examples/maps.plum`](plum-examples/maps.plum)
1282
+ - **`Map[K, V]`** (`import std/Map`) — `["a" => 1, "b" => 2]` builds a `Map[Str, Int]` (it is `Map.empty().with("a", 1).with("b", 2)`); `m: Map[K, V] := Map.empty()` is empty. A key can be any expression (`[n + 1 => x]`). A later duplicate key wins, as with `set`. See [`examples/maps.plum`](examples/maps.plum)
1283
1283
  - **`Set[T]`**, **`Deque[T]`**, **`Heap[T]`** — a set over `Map`'s bucketed hash table, a ring-buffer double-ended queue and a key-ordered min-heap
1284
1284
  - **`Json`**, **`Base64`**, **`Uuid`**, **`Time`**, **`Random`** — parsing, encoding, ids, wall-clock time and a seeded deterministic generator value (`Random.seeded(n)`; the wall-clock seed comes from `io.randomSeed()`)
1285
1285
  - **`Os`**, **`Http`** — printing and the file system (`Os.readFile`, `Os.writeFile`, ...); HTTP requests through the host (`Http.get`, `Http.post`, ...)
1286
1286
  - **`Testing`** (`import std/Testing`) — assertion helpers for `test` blocks
1287
1287
 
1288
- See [`plum-examples/`](plum-examples) for a runnable file per feature above, and [`plum-std/`](plum-std) for the standard library source.
1288
+ See [`examples/`](examples) for a runnable file per feature above, and [`std/`](std) for the standard library source.
1289
1289
 
1290
1290
  ### JSON
1291
1291
 
@@ -1316,7 +1316,7 @@ Also available: `Json.decodeWith(s, fn)`, `j.field(name)`, `j.optField(name)`, `
1316
1316
  - `Slice[Int].fromJson(j)` does not parse; use `j.into()` or `Json.decode`.
1317
1317
  - There is no derive: structs write `toJson`/`fromJson` by hand.
1318
1318
 
1319
- See [`plum-examples/json.plum`](plum-examples/json.plum).
1319
+ See [`examples/json.plum`](examples/json.plum).
1320
1320
 
1321
1321
  ## Planned performance work
1322
1322
 
compiler/checker/Cargo.toml ADDED
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "plum-checker"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ plum-core = { path = "../core" }
8
+
9
+ [dev-dependencies]
10
+ plum-test-support = { path = "../test-support" }
11
+ plum-driver = { path = "../driver" }
12
+ tree-sitter = "0.26"
13
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
{plum-checker → compiler/checker}/src/lib.rs RENAMED
@@ -3708,7 +3708,7 @@ fn inferMethodCall(
3708
3708
  // Inside an instantiated generic body the receiver is a concrete type:
3709
3709
  // a declared type lacking the method is a real (duck-typing) error.
3710
3710
  // Builtin/primitive receivers too: their methods are those declared in
3711
- // plum-std plus the compiler intrinsics handled before this lookup.
3711
+ // std plus the compiler intrinsics handled before this lookup.
3712
3712
  // A fully concrete generic receiver (`Map[Str, Int]`) is checked in the
3713
3713
  // erased pass too, so a typo'd method is a positioned checker error
3714
3714
  // rather than a late codegen failure.
{plum-checker → compiler/checker}/src/state.rs RENAMED
File without changes
{plum-checker → compiler/checker}/src/subtyping.rs RENAMED
File without changes
{plum-checker → compiler/checker}/src/types.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/checker_tests.rs RENAMED
@@ -292,7 +292,7 @@ fn typed_source_has_no_unresolved_infer_vars() {
292
292
  // need a host); add a path suffix here with a reason rather than dropping the sweep.
293
293
  let skip: &[&str] = &[];
294
294
  let mut files = Vec::new();
295
- for dir in ["plum-std", "plum-examples"] {
295
+ for dir in ["std", "examples"] {
296
296
  plumFiles(&root.join(dir), &mut files);
297
297
  }
298
298
  let mut bad = Vec::new();
@@ -565,7 +565,7 @@ fn const_errors_skip_memberless_types_and_type_names() {
565
565
  // ---------------------------------------------------------------------------
566
566
  // Rejected programs, table-driven by area. Each row is (source, needles): the
567
567
  // checker must reject the source and every needle must appear in its messages.
568
- // Valid programs are covered by the Plum `test` blocks in plum-std/plum-examples.
568
+ // Valid programs are covered by the Plum `test` blocks in std/examples.
569
569
  // ---------------------------------------------------------------------------
570
570
 
571
571
  type Row = (String, &'static [&'static str]);
{plum-checker → compiler/checker}/tests/checker/closure_hint_test.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/diagnostic_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/explicit_generics_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/closure_types.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/io_capability.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/mod.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/mutability.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/optional_unpinned.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/pattern_casing.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/primitive_generics.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/types.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/features/variant_pattern.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/fusion_pipeline_test.rs RENAMED
@@ -65,7 +65,7 @@ fn rewritten_program_still_checks() {
65
65
  }
66
66
 
67
67
  fn plumBin() -> Option<std::path::PathBuf> {
68
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.to_path_buf();
68
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2)?.to_path_buf();
69
69
  let bin = root.join("target/debug/plum");
70
70
  bin.exists().then_some(bin)
71
71
  }
@@ -88,7 +88,7 @@ fn fusion_off_matches_on() {
88
88
  eprintln!("skipped: target/debug/plum not built");
89
89
  return;
90
90
  }
91
- let f = "plum-std/List.plum";
91
+ let f = "std/List.plum";
92
92
  assert_eq!(runTest(true, f), runTest(false, f));
93
93
  }
94
94
 
@@ -98,7 +98,7 @@ fn explain_flag_prints_notes() {
98
98
  let root = bin.parent().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
99
99
  let out = std::process::Command::new(bin)
100
100
  .current_dir(&root)
101
- .args(["test", "--explain-fusion", "plum-std/List.plum"])
101
+ .args(["test", "--explain-fusion", "std/List.plum"])
102
102
  .output()
103
103
  .unwrap();
104
104
  let err = String::from_utf8_lossy(&out.stderr);
{plum-checker → compiler/checker}/tests/checker/inline_variants_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/io_primitive_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/main.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/question_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/checker/subtyping_tests.rs RENAMED
File without changes
{plum-checker → compiler/checker}/tests/fixtures/custom_actor_probe2.plum RENAMED
File without changes
compiler/cli/Cargo.toml ADDED
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "plum-cli"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ build = "build.rs"
6
+
7
+ [[bin]]
8
+ name = "plum"
9
+ path = "src/main.rs"
10
+
11
+ [dependencies]
12
+ plum-core = { path = "../core" }
13
+ plum-checker = { path = "../checker" }
14
+ plum-driver = { path = "../driver" }
15
+ plum-wasm-codegen = { path = "../wasm-codegen" }
16
+ clap = { version = "4", features = ["derive"] }
17
+ anyhow = "1"
18
+ tree-sitter = "0.26"
19
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
20
+ toml_edit = "0.22"
21
+ wasmtime = "47"
22
+ plum-host = { path = "../host" }
23
+
24
+ [dev-dependencies]
25
+ plum-test-support = { path = "../test-support" }
{plum-cli → compiler/cli}/build.rs RENAMED
@@ -55,7 +55,7 @@ fn main() {
55
55
  }
56
56
 
57
57
  fn buildGrammarSo(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
58
- let src_dir = manifest_dir.join("../plum-tooling/tree-sitter-plum/src");
58
+ let src_dir = manifest_dir.join("../../plum-tooling/tree-sitter-plum/src");
59
59
  let so_path = out_dir.join("plum-grammar.so");
60
60
 
61
61
  println!(
@@ -104,7 +104,7 @@ fn buildGrammarSo(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
104
104
  /// build script invoked BY a `cargo build` already holding that dir's lock
105
105
  /// deadlocks the outer build.
106
106
  fn buildPlumLsp(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
107
- let workspace_root = manifest_dir.join("..");
107
+ let workspace_root = manifest_dir.join("../..");
108
108
  let nested_target = out_dir.join("plum-lsp-target");
109
109
  let bin_name = if cfg!(windows) {
110
110
  "plum-lsp.exe"
@@ -115,7 +115,7 @@ fn buildPlumLsp(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
115
115
 
116
116
  // plum-lsp links plum-core and plum-checker, so a parser or checker change
117
117
  // must rebuild it too, or `plum editor` installs a stale server.
118
- for crate_src in ["../plum-lsp/src", "../plum-core/src", "../plum-checker/src"] {
118
+ for crate_src in ["../lsp/src", "../core/src", "../checker/src"] {
119
119
  println!(
120
120
  "cargo:rerun-if-changed={}",
121
121
  manifest_dir.join(crate_src).display()
@@ -167,7 +167,7 @@ fn buildPlumLsp(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
167
167
  /// whatever `out/extension.js` already exists on disk (however stale) if
168
168
  /// not, and to an empty placeholder if neither exists.
169
169
  fn buildVscodeExtensionClient(manifest_dir: &std::path::Path, out_dir: &std::path::Path) {
170
- let ext_dir = manifest_dir.join("../plum-tooling/vscode-plum");
170
+ let ext_dir = manifest_dir.join("../../plum-tooling/vscode-plum");
171
171
  let embed_path = out_dir.join("vscode-extension.js");
172
172
 
173
173
  println!(
{plum-cli → compiler/cli}/src/editor.rs RENAMED
@@ -23,11 +23,11 @@ pub enum EditorType {
23
23
 
24
24
  // ---- embedded VSCode extension assets -------------------------------------
25
25
 
26
- const VSCODE_PACKAGE_JSON: &str = include_str!("../../plum-tooling/vscode-plum/package.json");
26
+ const VSCODE_PACKAGE_JSON: &str = include_str!("../../../plum-tooling/vscode-plum/package.json");
27
27
  const VSCODE_LANGUAGE_CONFIG: &str =
28
- include_str!("../../plum-tooling/vscode-plum/language-configuration.json");
28
+ include_str!("../../../plum-tooling/vscode-plum/language-configuration.json");
29
29
  const VSCODE_TMLANGUAGE: &str =
30
- include_str!("../../plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json");
30
+ include_str!("../../../plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json");
31
31
 
32
32
  const VSCODE_PUBLISHER: &str = "plum-lang";
33
33
  const VSCODE_EXTENSION_NAME: &str = "plum";
@@ -36,14 +36,14 @@ const VSCODE_EXTENSION_VERSION: &str = "0.0.1";
36
36
  // ---- embedded Helix / tree-sitter query assets ----------------------------
37
37
 
38
38
  const HELIX_HIGHLIGHTS: &str =
39
- include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/highlights.scm");
39
+ include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/highlights.scm");
40
40
  const HELIX_INDENTS: &str =
41
- include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/indents.scm");
41
+ include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/indents.scm");
42
42
  const HELIX_INJECTIONS: &str =
43
- include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/injections.scm");
43
+ include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/injections.scm");
44
- const HELIX_TAGS: &str = include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/tags.scm");
44
+ const HELIX_TAGS: &str = include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/tags.scm");
45
45
  const HELIX_TEXTOBJECTS: &str =
46
- include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm");
46
+ include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm");
47
47
 
48
48
  /// Precompiled by `build.rs` from the same `parser.c`/`scanner.c` that back
49
49
  /// in-process parsing, on whatever machine builds this `plum` binary. Empty
@@ -73,7 +73,7 @@ const VSCODE_EXTENSION_JS: &[u8] = include_bytes!(env!("PLUM_VSCODE_EXTENSION_JS
73
73
  /// that goes stale the moment the repo moves.
74
74
  const TREE_SITTER_PLUM_DIR: &str = concat!(
75
75
  env!("CARGO_MANIFEST_DIR"),
76
- "/../plum-tooling/tree-sitter-plum"
76
+ "/../../plum-tooling/tree-sitter-plum"
77
77
  );
78
78
 
79
79
  /// [`TREE_SITTER_PLUM_DIR`] with the `..` component resolved away, for a tidier
{plum-cli → compiler/cli}/src/game.rs RENAMED
@@ -9,13 +9,13 @@ use std::sync::Arc;
9
9
 
10
10
  use anyhow::{bail, Context, Result};
11
11
 
12
- const INDEX_HTML: &str = include_str!("../../plum-game/web/index.html");
12
+ const INDEX_HTML: &str = include_str!("../../game/web/index.html");
13
- const BRIDGE_JS: &str = include_str!("../../plum-game/web/plum_bridge.js");
13
+ const BRIDGE_JS: &str = include_str!("../../game/web/plum_bridge.js");
14
14
 
15
15
  /// The repository root, fixed when plum-cli is compiled.
16
16
  fn repoRoot() -> PathBuf {
17
17
  Path::new(env!("CARGO_MANIFEST_DIR"))
18
- .parent()
18
+ .ancestors().nth(2)
19
19
  .expect("plum-cli lives inside the repository")
20
20
  .to_path_buf()
21
21
  }
{plum-cli → compiler/cli}/src/main.rs RENAMED
@@ -564,9 +564,9 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> Result
564
564
  /// working directory, so `plum build` works no matter where it's invoked from.
565
565
  fn runtimeCrateDir() -> std::path::PathBuf {
566
566
  std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
567
- .parent()
567
+ .ancestors().nth(2)
568
568
  .expect("plum-cli should live inside the workspace root")
569
- .join("plum-runtime")
569
+ .join("runtime")
570
570
  }
571
571
 
572
572
  fn cmdBuild(
{plum-cli → compiler/cli}/tests/cli/assert_once_test.rs RENAMED
@@ -40,7 +40,7 @@ test "operands once"
40
40
  )
41
41
  .unwrap();
42
42
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
43
- .parent()
43
+ .ancestors().nth(2)
44
44
  .unwrap();
45
45
  let out = Command::new(plumBin())
46
46
  .current_dir(root)
{plum-cli → compiler/cli}/tests/cli/compile_tests.rs RENAMED
@@ -88,7 +88,7 @@ fn returnBodySuggestedByTheParseErrorHintCompiles() {
88
88
  .unwrap();
89
89
  let out_path = dir.join("h.wasm");
90
90
  let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
91
- .parent()
91
+ .ancestors().nth(2)
92
92
  .unwrap();
93
93
  let output = Command::new(plumBin())
94
94
  .args([
{plum-cli → compiler/cli}/tests/cli/crash_test.rs RENAMED
@@ -18,7 +18,7 @@ fn crash_prints_message() {
18
18
  let src = dir.join("crash.plum");
19
19
  std::fs::write(&src, "import std/Str\n\nfun main() =\n crash \"boom\"\n").unwrap();
20
20
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
21
- .parent()
21
+ .ancestors().nth(2)
22
22
  .unwrap();
23
23
  let out = Command::new(plumBin())
24
24
  .current_dir(root)
@@ -45,7 +45,7 @@ fn int_literal_overflow_error_has_position() {
45
45
  )
46
46
  .unwrap();
47
47
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
48
- .parent()
48
+ .ancestors().nth(2)
49
49
  .unwrap();
50
50
  let out = Command::new(plumBin())
51
51
  .current_dir(root)
{plum-cli → compiler/cli}/tests/cli/explain_layout_test.rs RENAMED
@@ -22,7 +22,7 @@ fn explainPrintsStructLayouts() {
22
22
  "import std/Int\nimport std/Str\n\nstruct Vec2(x: Int, y: Int)\nstruct Named(label: Str, n: Int)\nstruct Cat(mut n: Int)\n\nfun main() -> Int =\n v := Vec2(x: 1, y: 2)\n v.x\n",
23
23
  )
24
24
  .unwrap();
25
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
25
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap();
26
26
  let out = Command::new(plumBin())
27
27
  .args(["run", "--explain", src.to_str().unwrap(), "--lib-path", root.to_str().unwrap()])
28
28
  .output()
{plum-cli → compiler/cli}/tests/cli/game_web_test.rs RENAMED
@@ -14,7 +14,7 @@ fn plumBin() -> std::path::PathBuf {
14
14
 
15
15
  fn root() -> std::path::PathBuf {
16
16
  std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
17
- .parent()
17
+ .ancestors().nth(2)
18
18
  .unwrap()
19
19
  .to_path_buf()
20
20
  }
@@ -29,7 +29,7 @@ fn makeAssets(name: &str) -> (std::path::PathBuf, std::path::PathBuf) {
29
29
  (scratch, assets)
30
30
  }
31
31
 
32
- const FIXTURE: &str = "plum-game/tests/fixtures/host_fixture.plum";
32
+ const FIXTURE: &str = "compiler/game/tests/fixtures/host_fixture.plum";
33
33
 
34
34
  #[test]
35
35
  fn staticBuildWritesTheDist() {
{plum-cli → compiler/cli}/tests/cli/host_stub_test.rs RENAMED
@@ -17,7 +17,7 @@ fn runPlumTest(name: &str, source: &str) -> (bool, String) {
17
17
  let src = dir.join("game.plum");
18
18
  std::fs::write(&src, source).unwrap();
19
19
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
20
- .parent()
20
+ .ancestors().nth(2)
21
21
  .unwrap();
22
22
  let out = Command::new(plumBin())
23
23
  .current_dir(root)
{plum-cli → compiler/cli}/tests/cli/main.rs RENAMED
File without changes
{plum-cli → compiler/cli}/tests/cli/test_scope_test.rs RENAMED
@@ -13,7 +13,7 @@ fn plumBin() -> std::path::PathBuf {
13
13
 
14
14
  fn runPlum(args: &[&str]) -> (String, bool) {
15
15
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
16
- .parent()
16
+ .ancestors().nth(2)
17
17
  .unwrap();
18
18
  let out = Command::new(plumBin())
19
19
  .current_dir(root)
@@ -53,7 +53,7 @@ fn directoryRunsSumOfOwnTests() {
53
53
  fn failingImportedTestDoesNotFailImporter() {
54
54
  // Imports resolve relative to the repo root (the lib path), so place the files under it.
55
55
  let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
56
- .parent()
56
+ .ancestors().nth(2)
57
57
  .unwrap();
58
58
  let dir = root
59
59
  .join("target")
compiler/core/Cargo.toml ADDED
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "plum-core"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ tree-sitter = "0.26"
8
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
9
+ topiary-core = "0.7.3"
10
+ topiary-tree-sitter-facade = "0.7.3"
11
+
12
+ [dev-dependencies]
13
+ plum-test-support = { path = "../test-support" }
14
+ tempfile = "3"
15
+ plum-checker = { path = "../checker" }
16
+ plum-driver = { path = "../driver" }
17
+ plum-wasm-codegen = { path = "../wasm-codegen" }
18
+ wasmparser = "0.220"
19
+ wasmtime = "47"
{plum-core → compiler/core}/src/ast.rs RENAMED
File without changes
{plum-core → compiler/core}/src/builtin_usage.rs RENAMED
File without changes
{plum-core → compiler/core}/src/closure_escape.rs RENAMED
File without changes
{plum-core → compiler/core}/src/closure_spec.rs RENAMED
File without changes
{plum-core → compiler/core}/src/ctors.rs RENAMED
File without changes
{plum-core → compiler/core}/src/diag.rs RENAMED
File without changes
{plum-core → compiler/core}/src/drive.rs RENAMED
File without changes
{plum-core → compiler/core}/src/embeds.rs RENAMED
File without changes
{plum-core → compiler/core}/src/formatter.rs RENAMED
@@ -4,7 +4,7 @@ use std::io::Cursor;
4
4
  use topiary_core::{formatter, Language, Operation, TopiaryQuery};
5
5
 
6
6
  const FORMAT_QUERY: &str =
7
- include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/format.scm");
7
+ include_str!("../../../plum-tooling/tree-sitter-plum/queries/plum/format.scm");
8
8
 
9
9
  #[derive(Debug)]
10
10
  pub enum FormatterError {
{plum-core → compiler/core}/src/forms.rs RENAMED
File without changes
{plum-core → compiler/core}/src/generics.rs RENAMED
File without changes
{plum-core → compiler/core}/src/layout.rs RENAMED
File without changes
{plum-core → compiler/core}/src/lib.rs RENAMED
File without changes
{plum-core → compiler/core}/src/loader.rs RENAMED
@@ -13,8 +13,8 @@ use crate::parser::AstParser;
13
13
  /// fields are not otherwise used once their `items` have been merged in) —
14
14
  /// only `items` accumulates across files.
15
15
  ///
16
- /// `import "std/option"` resolves to `<lib_path>/plum-std/option.plum` — the
16
+ /// `import "std/option"` resolves to `<lib_path>/std/option.plum` — the
17
- /// on-disk stdlib directory is `plum-std`, but source files keep writing
17
+ /// on-disk stdlib directory is `std`, but source files keep writing
18
18
  /// `std/...` imports, so the `std/` prefix is remapped here.
19
19
  pub fn loadAndMergeDiag(entry: &Path, lib_path: &Path) -> Result<Source, Vec<Diagnostic>> {
20
20
  loadAndMergeWithOverridesDiag(entry, lib_path, &HashMap::new())
@@ -51,7 +51,7 @@ pub fn loadAndMergeWithOverridesDiag(
51
51
  let entry_canon = canonicalizeMaybeMissing(entry)?;
52
52
  let entry_text = readText(&entry_canon, overrides)?;
53
53
  let entry_source = parseText(&entry_canon, &entry_text)?;
54
- checkBuiltinImports(&entry_canon, &entry_source)?;
54
+ checkBuiltinImports(&entry_canon, &entry_source, lib_path)?;
55
55
 
56
56
  let mut visited: HashSet<PathBuf> = HashSet::new();
57
57
  let mut items: Vec<Item> = Vec::new();
@@ -68,7 +68,7 @@ pub fn loadAndMergeWithOverridesDiag(
68
68
  }
69
69
  shadowStdTypes(&mut files, lib_path)?;
70
70
  for f in &files {
71
- mergeItems(&f.path, &f.source, &mut items, &mut names, &mut prims)?;
71
+ mergeItems(&f.path, &f.source, &mut items, &mut names, &mut prims, lib_path)?;
72
72
  }
73
73
  let entry_source = files.swap_remove(0).source;
74
74
 
@@ -140,14 +140,21 @@ struct Loaded {
140
140
  source: Source,
141
141
  }
142
142
 
143
+ /// The canonical `<lib_path>/std` directory: only files under it are std (a user
144
+ /// project's own `std/` folder is not).
143
- fn isStdFile(path: &Path) -> bool {
145
+ fn stdDirOf(lib_path: &Path) -> PathBuf {
144
- path.components().any(|c| c.as_os_str() == "plum-std")
146
+ let d = lib_path.join("std");
147
+ std::fs::canonicalize(&d).unwrap_or(d)
145
148
  }
146
149
 
150
+ fn isStdFile(path: &Path, lib_path: &Path) -> bool {
151
+ path.starts_with(stdDirOf(lib_path))
152
+ }
153
+
147
- /// `import std/x` -> `<lib_path>/plum-std/x.plum`, canonicalized.
154
+ /// `import std/x` -> `<lib_path>/std/x.plum`, canonicalized.
148
155
  fn resolveImport(import: &Import, lib_path: &Path) -> Loaded_<PathBuf> {
149
156
  let resolved_path = match import.path.strip_prefix("std/") {
150
- Some(rest) => format!("plum-std/{}", rest),
157
+ Some(rest) => format!("std/{}", rest),
151
158
  None => import.path.clone(),
152
159
  };
153
160
  let target = lib_path.join(format!("{}.plum", resolved_path));
@@ -176,7 +183,7 @@ fn collectImport(
176
183
  }
177
184
  let text = readText(&target_canon, overrides)?;
178
185
  let source = parseText(&target_canon, &text)?;
179
- checkBuiltinImports(&target_canon, &source)?;
186
+ checkBuiltinImports(&target_canon, &source, lib_path)?;
180
187
  let nested = source.imports.clone();
181
188
  files.push(Loaded { path: target_canon, text, source });
182
189
  for n in &nested {
@@ -207,7 +214,7 @@ fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) -> Loaded_<()> {
207
214
  let mut all: HashSet<String> = HashSet::new();
208
215
  for f in files.iter() {
209
216
  all.extend(declaredTypes(&f.source));
210
- if isStdFile(&f.path) {
217
+ if isStdFile(&f.path, lib_path) {
211
218
  continue;
212
219
  }
213
220
  user.extend(declaredTypes(&f.source));
@@ -219,7 +226,7 @@ fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) -> Loaded_<()> {
219
226
  }
220
227
  let mut rename: HashMap<String, String> = HashMap::new();
221
228
  for f in files.iter() {
222
- if !isStdFile(&f.path) || direct.contains(&f.path) {
229
+ if !isStdFile(&f.path, lib_path) || direct.contains(&f.path) {
223
230
  continue;
224
231
  }
225
232
  for n in declaredTypes(&f.source) {
@@ -237,7 +244,7 @@ fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) -> Loaded_<()> {
237
244
  if rename.is_empty() {
238
245
  return Ok(());
239
246
  }
240
- for f in files.iter_mut().filter(|f| isStdFile(&f.path)) {
247
+ for f in files.iter_mut().filter(|f| isStdFile(&f.path, lib_path)) {
241
248
  if let Some(text) = renameTypeIdents(&f.text, &rename)? {
242
249
  f.source = parseText(&f.path, &text)?;
243
250
  f.text = text;
@@ -285,8 +292,8 @@ fn renameTypeIdents(text: &str, rename: &HashMap<String, String>) -> Loaded_<Opt
285
292
  /// `import std/Int` / `import std/Float` / `import std/Str` — except in
286
293
  /// `Int.plum`/`Float.plum`/`Str.plum` themselves, which declare the type
287
294
  /// rather than import it.
288
- fn checkBuiltinImports(path: &Path, source: &Source) -> Loaded_<()> {
295
+ fn checkBuiltinImports(path: &Path, source: &Source, lib_path: &Path) -> Loaded_<()> {
289
- checkExtends(path, source)?;
296
+ checkExtends(path, source, lib_path)?;
290
297
  let used = usedBuiltinTypeNames(source);
291
298
  for name in ["Int", "Float", "Str"] {
292
299
  if !used.contains(name) {
@@ -654,6 +661,7 @@ fn mergeItems(
654
661
  items: &mut Vec<Item>,
655
662
  names: &mut HashMap<String, PathBuf>,
656
663
  prims: &mut Vec<PrimitiveDecl>,
664
+ lib_path: &Path,
657
665
  ) -> Loaded_<()> {
658
666
  // Removed forms, then inline variants as hidden records (per-file lowering).
659
667
  let mut hoisted = source.clone();
@@ -663,7 +671,7 @@ fn mergeItems(
663
671
  MERGED_LIT_ERRORS.with(|e| e.borrow_mut().extend(source.formErrors.iter().cloned()));
664
672
  // Namespaces (a `primitive` whose name is no built-in) travel with the
665
673
  // merged program so the checker can enforce their rules.
666
- if !file.components().any(|c| c.as_os_str() == "plum-std") {
674
+ if !isStdFile(file, lib_path) {
667
675
  USER_PRIMS.with(|u| {
668
676
  u.borrow_mut().extend(
669
677
  source
@@ -742,8 +750,8 @@ fn itemNameKey(item: &Item) -> String {
742
750
 
743
751
  /// A `primitive` named after a compiler built-in declares that type's API, so
744
752
  /// it is only for std; any other name is a stateless namespace, usable anywhere.
745
- fn checkExtends(path: &Path, source: &Source) -> Loaded_<()> {
753
+ fn checkExtends(path: &Path, source: &Source, lib_path: &Path) -> Loaded_<()> {
746
- let inStd = path.components().any(|c| c.as_os_str() == "plum-std");
754
+ let inStd = isStdFile(path, lib_path);
747
755
  for p in &source.primitives {
748
756
  if !inStd && BUILTIN_PRIMITIVES.contains(&p.name.as_str()) {
749
757
  return Err(vec![Diagnostic::new(
{plum-core → compiler/core}/src/lower.rs RENAMED
File without changes
{plum-core → compiler/core}/src/parser.rs RENAMED
File without changes
{plum-core → compiler/core}/src/purity.rs RENAMED
File without changes
{plum-core → compiler/core}/src/rename.rs RENAMED
File without changes
{plum-core → compiler/core}/src/reuse.rs RENAMED
File without changes
{plum-core → compiler/core}/src/session.rs RENAMED
File without changes
{plum-core → compiler/core}/src/simplify.rs RENAMED
File without changes
{plum-core → compiler/core}/src/subtyping.rs RENAMED
File without changes
{plum-core → compiler/core}/src/tail.rs RENAMED
File without changes
{plum-core → compiler/core}/src/tests_lower.rs RENAMED
@@ -35,7 +35,7 @@ fn strLit(s: String) -> crate::ast::Expr {
35
35
  /// test writes such a comparison in. Reuses `"{expr}"` string interpolation's
36
36
  /// own existing codegen (rather than any special stringification of its
37
37
  /// own) — including `Float`, which interpolation now supports by rendering
38
- /// through `Float.toStr` (`plum-std/Float.plum`), so no type needs
38
+ /// through `Float.toStr` (`std/Float.plum`), so no type needs
39
39
  /// special-casing out here any more. A non-comparison condition (`assert
40
40
  /// isEven(4)`) has no meaningful actual/expected split — a boolean condition
41
41
  /// is always false by the time this fires — so it's just the bare text.
{plum-core → compiler/core}/src/trmc.rs RENAMED
File without changes
{plum-core → compiler/core}/src/unions.rs RENAMED
File without changes
{plum-core → compiler/core}/src/uniqueness.rs RENAMED
File without changes
{plum-core → compiler/core}/src/visit.rs RENAMED
File without changes
{plum-core → compiler/core}/src/whistle.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/closure_escape_test.rs RENAMED
@@ -90,7 +90,7 @@ fn std_list_combinators_non_escaping() {
90
90
  std::fs::create_dir_all(&dir).unwrap();
91
91
  let path = dir.join("main.plum");
92
92
  std::fs::write(&path, "import std/Int\nimport std/List\n\nfun main() =\n [1]\n").unwrap();
93
- let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
93
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
94
94
  let r = plum_core::loadAndMergeDiag(&path, &root);
95
95
  let _ = std::fs::remove_dir_all(&dir);
96
96
  let m = nonEscapingFnParams(&r.unwrap_or_else(|e| panic!("load: {e:?}")));
{plum-core → compiler/core}/tests/core/closure_spec_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/closure_types_parse_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/diag_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/drive_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/for_next_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/formatter_test.rs RENAMED
@@ -350,7 +350,7 @@ fn format_inline_if_call_arg_no_space() {
350
350
  fn format_functional_example_unchanged() {
351
351
  let src = std::fs::read_to_string(concat!(
352
352
  env!("CARGO_MANIFEST_DIR"),
353
- "/../plum-examples/functional.plum"
353
+ "/../../examples/functional.plum"
354
354
  ))
355
355
  .unwrap();
356
356
  assert_eq!(formatSource(&src).unwrap(), src);
{plum-core → compiler/core}/tests/core/forms_list_range_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/fused_closure_generic_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/generics_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/inline_variants_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/layout_test.rs RENAMED
@@ -77,10 +77,10 @@ fn plumFiles(dir: &Path, out: &mut Vec<PathBuf>) {
77
77
  /// well-known std types land where expected.
78
78
  #[test]
79
79
  fn std_and_examples_layouts() {
80
- let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
80
+ let root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap();
81
81
  let mut files = Vec::new();
82
- plumFiles(&root.join("plum-std"), &mut files);
82
+ plumFiles(&root.join("std"), &mut files);
83
- plumFiles(&root.join("plum-examples"), &mut files);
83
+ plumFiles(&root.join("examples"), &mut files);
84
84
  let mut flat = 0;
85
85
  let mut boxed = 0;
86
86
  let mut known: Vec<(String, Layout)> = Vec::new();
@@ -94,7 +94,7 @@ fn std_and_examples_layouts() {
94
94
  let l = structLayout(e, &all);
95
95
  assert_eq!(l, plum_core::layout::itemLayout(e, &s.items), "{} in {}", e.name, f.display());
96
96
  if l == Layout::Flat { flat += 1 } else { boxed += 1 }
97
- if f.ends_with("plum-std/game/Vec2.plum") || f.ends_with("plum-std/Map.plum") {
97
+ if f.ends_with("std/game/Vec2.plum") || f.ends_with("std/Map.plum") {
98
98
  known.push((e.name.clone(), l));
99
99
  }
100
100
  }
{plum-core → compiler/core}/tests/core/list_literal_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/loader_test.rs RENAMED
@@ -405,7 +405,7 @@ fn primitive_builtin_std_only() {
405
405
  .unwrap();
406
406
  assert!(loadAndMergeDiag(&ok, dir.path()).is_ok());
407
407
  // Inside std, a built-in: fine.
408
- let std = dir.path().join("plum-std");
408
+ let std = dir.path().join("std");
409
409
  fs::create_dir_all(&std).unwrap();
410
410
  let byte = std.join("Byte.plum");
411
411
  fs::write(
@@ -422,7 +422,7 @@ fn source_and_fns_carry_their_file_path() {
422
422
  let lib_path = dir.path().join("libs");
423
423
  writeLibFile(
424
424
  &lib_path,
425
- "plum-std/Util",
425
+ "std/Util",
426
426
  "module std\n\nfun helperFn(b: Bool) -> Bool =\n b\n",
427
427
  );
428
428
  let entry = writeLibFile(
@@ -460,7 +460,7 @@ fn outOfRangeLiteralInEntryOrImportSurvivesTheMerge() {
460
460
  let lib_path = dir.path().join("libs");
461
461
  writeLibFile(
462
462
  &lib_path,
463
- "plum-std/Int",
463
+ "std/Int",
464
464
  "module fixtures\n\nprimitive Int =\n Zero = 0\n",
465
465
  );
466
466
  writeLibFile(&lib_path, "helper", "module fixtures\n\nimport std/Int\n\nfun big(u: Unit) -> Unit =\n x := 0x8000000000000000\n u\n");
@@ -518,7 +518,7 @@ fn nestedStdImportResolvesToASubdirectory() {
518
518
  let lib_path = dir.path().join("libs");
519
519
  writeLibFile(
520
520
  &lib_path,
521
- "plum-std/game/Util",
521
+ "std/game/Util",
522
522
  "module std\n\nfun helperFn(b: Bool) -> Bool =\n b\n",
523
523
  );
524
524
  let entry = writeLibFile(
@@ -538,7 +538,7 @@ fn nestedStdImportResolvesToASubdirectory() {
538
538
  assert!(loadAndMergeDiag(&missing, &lib_path).is_err());
539
539
  }
540
540
 
541
- /// Import-free programs (every plum-examples file imports std, so small inline programs are used):
541
+ /// Import-free programs (every examples file imports std, so small inline programs are used):
542
542
  /// the single-file `parseSource` front end must give the same AST as the loader.
543
543
  #[test]
544
544
  fn single_file_equals_loader() {
@@ -588,3 +588,21 @@ fn userTypeShadowsStdTypeOnlyWhenNotImportedDirectly() {
588
588
  let err = plum_test_support::load("direct_pair", &direct).unwrap_err();
589
589
  assert!(err.contains("duplicate declaration 'enum::Pair'"), "{err}");
590
590
  }
591
+
592
+ #[test]
593
+ fn std_means_under_the_lib_path_std_dir_only() {
594
+ let lib = tempfile::tempdir().unwrap();
595
+ let std = lib.path().join("std");
596
+ fs::create_dir_all(&std).unwrap();
597
+ let body = "primitive Int =\n fun f(self) -> Int =\n self\n";
598
+ let real = std.join("Int.plum");
599
+ fs::write(&real, body).unwrap();
600
+ assert!(loadAndMergeDiag(&real, lib.path()).is_ok());
601
+ // A project's own `std/` folder outside the lib path gets no std privileges.
602
+ let proj = tempfile::tempdir().unwrap();
603
+ let fake_dir = proj.path().join("proj").join("std");
604
+ fs::create_dir_all(&fake_dir).unwrap();
605
+ let fake = fake_dir.join("X.plum");
606
+ fs::write(&fake, body).unwrap();
607
+ assert!(loadAndMergeDiag(&fake, lib.path()).is_err());
608
+ }
{plum-core → compiler/core}/tests/core/lower_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/main.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/match_body_parse_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/override_parse_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/parser_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/purity_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/question_parse_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/record_embed_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/record_init_parse_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/record_init_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/rename_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/session_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/simplify_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/subtyping_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/tail_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/tests_lower_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/trmc_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/unions_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/uniqueness_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/visit_test.rs RENAMED
File without changes
{plum-core → compiler/core}/tests/core/whistle_test.rs RENAMED
File without changes
compiler/driver/Cargo.toml ADDED
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "plum-driver"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ plum-core = { path = "../core" }
8
+ plum-checker = { path = "../checker" }
9
+
10
+ [dev-dependencies]
11
+ plum-test-support = { path = "../test-support" }
{plum-driver → compiler/driver}/src/lib.rs RENAMED
File without changes
{plum-driver → compiler/driver}/tests/driver_tests.rs RENAMED
File without changes
{plum-game → compiler/game}/.gitignore RENAMED
File without changes
{plum-game → compiler/game}/Cargo.toml RENAMED
@@ -6,13 +6,13 @@ description = "Headless test runner for Plum game programs (the browser bridge l
6
6
 
7
7
  [dependencies]
8
8
  wasmtime = "47"
9
- plum-host = { path = "../plum-host" }
9
+ plum-host = { path = "../host" }
10
10
  # Image headers only: texture sizes for `textureWidth`/`textureHeight`.
11
11
  image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif", "bmp"] }
12
12
 
13
13
  [dev-dependencies]
14
- plum-core = { path = "../plum-core" }
14
+ plum-core = { path = "../core" }
15
- plum-wasm-codegen = { path = "../plum-wasm-codegen" }
15
+ plum-wasm-codegen = { path = "../wasm-codegen" }
16
16
 
17
17
  # Stand on its own rather than joining the parent directory's workspace: the
18
18
  # Plum compiler workspace must build without pulling in the test runner's
{plum-game → compiler/game}/src/api.rs RENAMED
@@ -1,5 +1,5 @@
1
1
  //! The host API a Plum game programs against: one `Backend` method per
2
- //! extern in `plum-std/game/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
2
+ //! extern in `std/game/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
3
3
  //! `i32`, `Str` is `&str`, a `Color` is `[f32; 4]`, a `Vec2` is `(f32, f32)`).
4
4
  //! `Int` handles are indexes into host vectors, `-1` on failure. The browser
5
5
  //! bridge (`web/plum_bridge.js`) implements the same API; this crate's
{plum-game → compiler/game}/src/host.rs RENAMED
File without changes
{plum-game → compiler/game}/src/lib.rs RENAMED
File without changes
{plum-game → compiler/game}/src/main.rs RENAMED
File without changes
{plum-game → compiler/game}/src/recorder.rs RENAMED
File without changes
{plum-game → compiler/game}/tests/fixtures/assets_fixture.plum RENAMED
File without changes
{plum-game → compiler/game}/tests/fixtures/host_fixture.plum RENAMED
File without changes
{plum-game → compiler/game}/tests/fixtures/target_fixture.plum RENAMED
File without changes
{plum-game → compiler/game}/tests/host_test.rs RENAMED
@@ -4,7 +4,7 @@ use plum_game::recorder::Recorder;
4
4
  use std::path::{Path, PathBuf};
5
5
 
6
6
  fn root() -> PathBuf {
7
- Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
7
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
8
8
  }
9
9
 
10
10
  fn compile(src_path: &Path) -> Vec<u8> {
@@ -111,7 +111,7 @@ fn manyFramesDoNotAccumulateRoots() {
111
111
  }
112
112
 
113
113
  fn examples_assets() -> PathBuf {
114
- root().join("plum-examples/games/assets")
114
+ root().join("examples/games/assets")
115
115
  }
116
116
 
117
117
  fn assets_fixture() -> Vec<u8> {
@@ -188,7 +188,7 @@ fn assetPathEscapeRejected() {
188
188
 
189
189
  #[test]
190
190
  fn exampleGameRunsFrames() {
191
- let wasm = compile(&root().join("plum-examples/games/imperative/game.plum"));
191
+ let wasm = compile(&root().join("examples/games/imperative/game.plum"));
192
192
  let mut g = PlumGame::load(&wasm, Recorder::with_assets(examples_assets())).expect("load game");
193
193
  g.init().expect("init");
194
194
  for _ in 0..60 {
@@ -252,7 +252,7 @@ fn soundPathEscapeRejected() {
252
252
 
253
253
  #[test]
254
254
  fn sceneExampleRunsFrames() {
255
- let wasm = compile(&root().join("plum-examples/games/imperative/scene.plum"));
255
+ let wasm = compile(&root().join("examples/games/imperative/scene.plum"));
256
256
  let mut g = PlumGame::load(&wasm, Recorder::with_assets(examples_assets())).expect("load game");
257
257
  g.init().expect("init");
258
258
  for _ in 0..10 {
@@ -264,7 +264,7 @@ fn sceneExampleRunsFrames() {
264
264
 
265
265
  #[test]
266
266
  fn pureSceneExampleRunsFrames() {
267
- let wasm = compile(&root().join("plum-examples/games/pure/game_pure_scene.plum"));
267
+ let wasm = compile(&root().join("examples/games/pure/game_pure_scene.plum"));
268
268
  let mut g = PlumGame::load(&wasm, Recorder::with_assets(examples_assets())).expect("load game");
269
269
  g.init().expect("init");
270
270
  for _ in 0..10 {
@@ -282,8 +282,8 @@ fn pureSceneMatchesSceneDrawCounts() {
282
282
  g.init().expect("init");
283
283
  g
284
284
  };
285
- let mut a = load("plum-examples/games/imperative/game_scene.plum");
285
+ let mut a = load("examples/games/imperative/game_scene.plum");
286
- let mut b = load("plum-examples/games/pure/game_pure_scene.plum");
286
+ let mut b = load("examples/games/pure/game_pure_scene.plum");
287
287
  let tex_ids = |calls: &[String]| {
288
288
  let mut v: Vec<String> = calls
289
289
  .iter()
{plum-game → compiler/game}/web-test/BENCHMARK.md RENAMED
@@ -81,10 +81,10 @@ Headless is software rendering: the GPU work runs on the CPU and competes with t
81
81
 
82
82
  ## Reproduce
83
83
 
84
- cargo build --manifest-path plum-game/web-test/Cargo.toml
84
+ cargo build --manifest-path compiler/game/web-test/Cargo.toml
85
- plum-game/web-test/target/debug/plum-web-test plum-game/web-test/fixtures/stress_cmd.plum --headed --uncapped --measure 10 --frames-wait 6 --json
85
+ compiler/game/web-test/target/debug/plum-web-test compiler/game/web-test/fixtures/stress_cmd.plum --headed --uncapped --measure 10 --frames-wait 6 --json
86
86
 
87
- Drop `--headed` for the software path. Native: `PLUM_GAME_MEASURE=10 target/release/plum game plum-game/web-test/fixtures/stress.plum`.
87
+ Drop `--headed` for the software path. Native: `PLUM_GAME_MEASURE=10 target/release/plum game compiler/game/web-test/fixtures/stress.plum`.
88
88
 
89
89
  ## Multi-texture batching and SDF text (renderer features)
90
90
 
@@ -141,6 +141,6 @@ The "A/B" rows are a paired run for the record-init change: master and this bran
141
141
  The "record embedding" rows are the same A/B for the record-embedding change (master at e465385 in a separate worktree, alternated A B A B, headed, uncapped, quiet machine): the fixtures only gained `override` markers, which lower to the same code, so stress_world (1.13 vs 1.12 mean) and stress_scene (1.71 vs 1.71) are within noise of master.
142
142
 
143
143
  - `stress_scene`: +5% (1.60 -> 1.69 ms), at the edge of noise. The per-node closure call became a trait dispatch, which is cheaper (`update` 0.055 -> 0.039 ms); `draw` grew by 0.08 ms because the draw walk reads child fields through the trait.
144
- - `stress_world`: +92% (1.09 -> 2.09 ms), above the 10% threshold. All of it is `update`, which here is the y-sort: `sortChildren(|a, b| a.y < b.y)` makes 2 x 19,999 `a.y` reads per frame and every read of a trait field is a dispatch (`__get_y`) over the program's actor kinds, about 30-40 ns each against about 10 ns for a direct field read. Without the sort `update` is 0.01 ms. The cost scales with the number of actor kinds in the program: dropping two unused test-only kinds took `update` from 1.65 to 1.29 ms, which is why the Scene tests that need their own `act`/`draw` records live in `plum-examples/games/imperative/scene.plum` and not in `std/game/imperative/Scene.plum`. Draw is +0.1 ms (culling and transforms run as trait default methods, so they read fields directly, one dispatch per child).
144
+ - `stress_world`: +92% (1.09 -> 2.09 ms), above the 10% threshold. All of it is `update`, which here is the y-sort: `sortChildren(|a, b| a.y < b.y)` makes 2 x 19,999 `a.y` reads per frame and every read of a trait field is a dispatch (`__get_y`) over the program's actor kinds, about 30-40 ns each against about 10 ns for a direct field read. Without the sort `update` is 0.01 ms. The cost scales with the number of actor kinds in the program: dropping two unused test-only kinds took `update` from 1.65 to 1.29 ms, which is why the Scene tests that need their own `act`/`draw` records live in `examples/games/imperative/scene.plum` and not in `std/game/imperative/Scene.plum`. Draw is +0.1 ms (culling and transforms run as trait default methods, so they read fields directly, one dispatch per child).
145
145
  - Box-free layout (T10): a trait whose every embedder lists it first is compiled as a wasm-gc base struct of the trait's fields with each embedding record's struct as a subtype. A record already is a trait value (no box, `==` is reference identity), a trait field read or write is one `struct.get`/`struct.set` on the base type, and only method dispatch and `match` narrowing use `ref.test`/`ref.cast`. `stress_world` goes 2.09 -> 1.10 ms (+1% against the closure version's 1.09, inside the 10% threshold; `update` 1.29 -> 0.37 ms) and `stress_scene` 1.69 -> 1.68 ms (+5% against 1.60, the same edge-of-noise gap as before). A trait that some record lists second or later keeps the boxed layout described above.
146
146
  - Reading the trait's fields from outside it (a game's sort key, hit tests on `.x`/`.y`) is the price of the trait. Default methods run on the concrete record and read its fields directly, which is why `local()`, `outside()`, `drawTree()` and `hitTree()` are trait methods rather than free functions.
{plum-game → compiler/game}/web-test/Cargo.toml RENAMED
File without changes
compiler/game/web-test/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # plum-web-test
2
+
3
+ Browser check for `plum game` builds, driven by [playwright-rs](https://crates.io/crates/playwright-rs) 0.20.
4
+ Standalone crate (own `[workspace]`, not part of the root workspace).
5
+
6
+ It builds `plum-cli`, exports the game with `plum game -o`, serves the output on
7
+ 127.0.0.1:0, opens it in headless Chromium (software WebGPU), clicks the canvas once, then asserts:
8
+ the page set `plumRenderer` (WebGPU started), the canvas is not one uniform colour, two
9
+ screenshots 1 s apart differ (the game animates), and there are no page or console errors.
10
+ AudioContext state is reported; a missing running context is only a warning unless
11
+ `--require-audio` is given.
12
+
13
+ ## One-time setup
14
+
15
+ Downloads the Chromium and WebKit builds matching the bundled Playwright driver (about 180 MB, to
16
+ `~/Library/Caches/ms-playwright` on macOS; the driver itself is fetched by the crate's build script):
17
+
18
+ cargo run --manifest-path compiler/game/web-test/Cargo.toml --example install-browsers
19
+
20
+ ## Usage
21
+
22
+ cargo run --manifest-path compiler/game/web-test/Cargo.toml -- examples/games/imperative/game.plum --screenshot /tmp/orb-run.png
23
+
24
+ Options: `--browser chromium|webkit` (default chromium; WebKit is Playwright's Safari-engine build and has WebGPU, so the same render assertions apply; a WebKit build without `navigator.gpu` is checked for the "WebGPU is not supported in this browser" message and a loadable plum.wasm instead), `--expect-unsupported-webgpu` (hides `navigator.gpu` in any browser and checks that path), `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw the TypeError Safari gives for typed GC references), `--frames-wait SECS` (default 8), `--screenshot OUT.png`, `--assets DIR` (default: the nearest `assets` folder beside the source or in a parent, e.g. `examples/games/assets`), `--require-audio`, `--json` (adds a one-line JSON object with the backend and frame stats), `--headed` (a real GPU window instead of headless software WebGPU), `--measure SECS` (records requestAnimationFrame intervals in the page and prints avg / p50 / p95 / p99 ms, fps and the mean per-frame update/draw/render split; `--json` adds `update_ms`, `draw_ms`, `render_ms`) and `--uncapped` (lifts vsync so the frame time is the work per frame), `--dpr N` (Playwright `deviceScaleFactor`; also checks the backing store is CSS size x ratio), `--lose-device` (destroys the GPU device from the page after a few frames and requires the `GPU device lost (...); restored` log and resumed rendering; the page exposes the device only when the harness asks) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y), default tolerance 2). `--json` also reports `draws`, the GPU draw calls per frame. `fixtures/interleave.plum`, `text_sizes.plum` and `rendertarget.plum` exercise multi-texture batching, SDF text and render targets/blend modes. `fixtures/stress.plum` and `fixtures/stress_scene.plum` are the scenes behind [BENCHMARK.md](BENCHMARK.md).
25
+ Exit code is non-zero with a message on failure.
26
+
27
+ `cargo test --manifest-path compiler/game/web-test/Cargo.toml` runs the same check on
28
+ `examples/games/imperative/game.plum`, `examples/games/imperative/game_scene.plum` and `examples/games/imperative/scene.plum`, plus WebKit runs; each skips with a note if its browser is not installed.
{plum-game → compiler/game}/web-test/examples/install-browsers.rs RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/sprites.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex0.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex1.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex2.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex3.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex4.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex5.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex6.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/assets/tex7.png RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/interleave.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/rendertarget.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/stress.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Stress scene for comparing web renderers (plum-game/web-test/BENCHMARK.md):
1
+ # Stress scene for comparing web renderers (compiler/game/web-test/BENCHMARK.md):
2
2
  # 5000 sprites, 500 rects, 200 circles and 50 texts with a changing number, drawn
3
3
  # through a camera. Everything moves as a function of the frame counter, so every
4
4
  # run draws the same frames.
{plum-game → compiler/game}/web-test/fixtures/stress_pure_scene.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/stress_scene.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/stress_state_view.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/stress_state_view_deep.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/stress_world.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/text_sizes.plum RENAMED
File without changes
{plum-game → compiler/game}/web-test/fixtures/tools/make_textures.py RENAMED
File without changes
{plum-game → compiler/game}/web-test/src/lib.rs RENAMED
@@ -230,7 +230,7 @@ pub fn webkit_installed() -> bool {
230
230
  }
231
231
 
232
232
  pub fn repo_root() -> PathBuf {
233
- Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().expect("repo root")
233
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").canonicalize().expect("repo root")
234
234
  }
235
235
 
236
236
  /// Builds plum-cli and exports `file` as a static web build into `out`.
{plum-game → compiler/game}/web-test/src/main.rs RENAMED
@@ -41,7 +41,7 @@ async fn main() {
41
41
  }
42
42
  let Some(file) = file else { fail(USAGE) };
43
43
  if !browser_installed(opts.browser) {
44
- fail(&format!("Playwright {} is not installed; see plum-game/web-test/README.md (one-time setup)", opts.browser.name()));
44
+ fail(&format!("Playwright {} is not installed; see compiler/game/web-test/README.md (one-time setup)", opts.browser.name()));
45
45
  }
46
46
  match run(&file, &opts).await {
47
47
  Ok(report) => println!("{report}\nPASS"),
{plum-game → compiler/game}/web-test/tests/web_check.rs RENAMED
@@ -2,27 +2,27 @@ use plum_web_test::{chromium_installed, repo_root, run, webkit_installed, Browse
2
2
 
3
3
  #[test]
4
4
  fn orb_run_renders_and_animates() {
5
- orb_run("plum-examples/games/imperative/game.plum");
5
+ orb_run("examples/games/imperative/game.plum");
6
6
  }
7
7
 
8
8
  #[test]
9
9
  fn orb_run_scene_renders_and_animates() {
10
- orb_run("plum-examples/games/imperative/game_scene.plum");
10
+ orb_run("examples/games/imperative/game_scene.plum");
11
11
  }
12
12
 
13
13
  #[test]
14
14
  fn pure_scene_renders_and_animates() {
15
- orb_run("plum-examples/games/pure/game_pure_scene.plum");
15
+ orb_run("examples/games/pure/game_pure_scene.plum");
16
16
  }
17
17
 
18
18
  #[test]
19
19
  fn state_view_renders_and_animates() {
20
- orb_run("plum-game/web-test/fixtures/stress_state_view.plum");
20
+ orb_run("compiler/game/web-test/fixtures/stress_state_view.plum");
21
21
  }
22
22
 
23
23
  #[test]
24
24
  fn scene_run_renders_and_animates() {
25
- orb_run("plum-examples/games/imperative/scene.plum");
25
+ orb_run("examples/games/imperative/scene.plum");
26
26
  }
27
27
 
28
28
  #[test]
@@ -41,7 +41,7 @@ fn stats_json_is_one_object() {
41
41
  /// use typed GC references; the bridge must instantiate without any module introspection.
42
42
  #[test]
43
43
  fn bridge_does_not_introspect_the_module() {
44
- let js = std::fs::read_to_string(repo_root().join("plum-game/web/plum_bridge.js")).unwrap();
44
+ let js = std::fs::read_to_string(repo_root().join("compiler/game/web/plum_bridge.js")).unwrap();
45
45
  for api in ["Module.imports(", "Module.exports(", "Module.customSections("] {
46
46
  assert!(!js.contains(api), "plum_bridge.js calls WebAssembly.{api}");
47
47
  }
@@ -50,24 +50,24 @@ fn bridge_does_not_introspect_the_module() {
50
50
  #[test]
51
51
  fn chromium_runs_without_module_introspection() {
52
52
  if !chromium_installed() {
53
- eprintln!("SKIP: Playwright Chromium not installed (see plum-game/web-test/README.md)");
53
+ eprintln!("SKIP: Playwright Chromium not installed (see compiler/game/web-test/README.md)");
54
54
  return;
55
55
  }
56
56
  let opts = Options { no_module_introspection: true, ..Options::default() };
57
57
  let rt = tokio::runtime::Runtime::new().unwrap();
58
- let report = rt.block_on(run(&repo_root().join("plum-examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| panic!("{e}"));
58
+ let report = rt.block_on(run(&repo_root().join("examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| panic!("{e}"));
59
59
  println!("{report}");
60
60
  }
61
61
 
62
62
  #[test]
63
63
  fn webkit_runs_without_module_introspection() {
64
64
  if !webkit_installed() {
65
- eprintln!("SKIP: Playwright WebKit not installed (see plum-game/web-test/README.md)");
65
+ eprintln!("SKIP: Playwright WebKit not installed (see compiler/game/web-test/README.md)");
66
66
  return;
67
67
  }
68
68
  let opts = Options { browser: Browser::Webkit, no_module_introspection: true, ..Options::default() };
69
69
  let rt = tokio::runtime::Runtime::new().unwrap();
70
- let report = rt.block_on(run(&repo_root().join("plum-examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| {
70
+ let report = rt.block_on(run(&repo_root().join("examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| {
71
71
  assert!(!e.contains("unable to produce import descriptors"), "{e}");
72
72
  panic!("{e}")
73
73
  });
@@ -77,18 +77,18 @@ fn webkit_runs_without_module_introspection() {
77
77
  #[test]
78
78
  fn webkit_without_webgpu_loads_the_wasm_and_says_so() {
79
79
  if !webkit_installed() {
80
- eprintln!("SKIP: Playwright WebKit not installed (see plum-game/web-test/README.md)");
80
+ eprintln!("SKIP: Playwright WebKit not installed (see compiler/game/web-test/README.md)");
81
81
  return;
82
82
  }
83
83
  let opts = Options { browser: Browser::Webkit, expect_unsupported_webgpu: true, no_module_introspection: true, ..Options::default() };
84
84
  let rt = tokio::runtime::Runtime::new().unwrap();
85
- let report = rt.block_on(run(&repo_root().join("plum-examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| panic!("{e}"));
85
+ let report = rt.block_on(run(&repo_root().join("examples/games/imperative/game.plum"), &opts)).unwrap_or_else(|e| panic!("{e}"));
86
86
  assert!(report.contains("plum.wasm load probe: ok"), "{report}");
87
87
  }
88
88
 
89
89
  fn orb_run(example: &str) {
90
90
  if !chromium_installed() {
91
- eprintln!("SKIP: Playwright Chromium not installed (see plum-game/web-test/README.md)");
91
+ eprintln!("SKIP: Playwright Chromium not installed (see compiler/game/web-test/README.md)");
92
92
  return;
93
93
  }
94
94
  let rt = tokio::runtime::Runtime::new().unwrap();
@@ -158,13 +158,13 @@ fn expect_pixel_parses_and_checks() {
158
158
  #[test]
159
159
  fn render_target_pixels() {
160
160
  if !chromium_installed() {
161
- eprintln!("SKIP: Playwright Chromium not installed (see plum-game/web-test/README.md)");
161
+ eprintln!("SKIP: Playwright Chromium not installed (see compiler/game/web-test/README.md)");
162
162
  return;
163
163
  }
164
164
  let pixels = ["130,130,255,0,0", "196,196,0,255,0", "130,196,51,51,51", "450,150,102,153,204", "600,150,26,51,26", "750,150,153,153,153", "900,150,153,26,26", "408,308,0,0,255", "408,408,153,26,26"];
165
165
  let opts = Options { expect_pixels: pixels.iter().map(|p| plum_web_test::ExpectPixel::parse(p).unwrap()).collect(), ..Options::default() };
166
166
  let rt = tokio::runtime::Runtime::new().unwrap();
167
- let file = repo_root().join("plum-game/web-test/fixtures/rendertarget.plum");
167
+ let file = repo_root().join("compiler/game/web-test/fixtures/rendertarget.plum");
168
168
  let report = rt.block_on(run(&file, &opts)).unwrap_or_else(|e| panic!("{e}"));
169
169
  println!("{report}");
170
170
  }
{plum-game → compiler/game}/web/index.html RENAMED
File without changes
{plum-game → compiler/game}/web/plum_bridge.js RENAMED
File without changes
{plum-host → compiler/host}/Cargo.toml RENAMED
File without changes
{plum-host → compiler/host}/src/lib.rs RENAMED
@@ -437,7 +437,7 @@ pub fn hostImports(
437
437
  }
438
438
 
439
439
  /// Performs one blocking HTTP round trip for `plum::rawHttpRequest` and packs
440
- /// the outcome into the wire format `plum-std/Http.plum`'s `request` expects
440
+ /// the outcome into the wire format `std/Http.plum`'s `request` expects
441
441
  /// (see that extern's doc comment): `"1\x01<status>\x01<headers>\x00<body>"`
442
442
  /// on success, `"0\x01\x01<message>\x00"` for a transport-level failure. A
443
443
  /// non-2xx HTTP status (404, 500, ...) is NOT a failure here — it's still a
@@ -505,7 +505,7 @@ fn randomI64() -> i64 {
505
505
  }
506
506
 
507
507
  /// The bytes of a `Str` value: `Str(data: Array[Byte], start: Int, len: Int)`
508
- /// (see `plum-std/Str.plum`), a view `[start, start + len)` of a packed byte array.
508
+ /// (see `std/Str.plum`), a view `[start, start + len)` of a packed byte array.
509
509
  pub fn strBytes(
510
510
  store: &mut impl wasmtime::AsContextMut,
511
511
  s: &wasmtime::Rooted<wasmtime::AnyRef>,
{plum-lsp → compiler/lsp}/Cargo.toml RENAMED
@@ -8,10 +8,10 @@ name = "plum-lsp"
8
8
  path = "src/main.rs"
9
9
 
10
10
  [dependencies]
11
- plum-core = { path = "../plum-core" }
11
+ plum-core = { path = "../core" }
12
- plum-checker = { path = "../plum-checker" }
12
+ plum-checker = { path = "../checker" }
13
- plum-driver = { path = "../plum-driver" }
13
+ plum-driver = { path = "../driver" }
14
14
  tree-sitter = "0.26"
15
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
15
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
16
16
  tower-lsp = "0.20"
17
17
  tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std"] }
{plum-lsp → compiler/lsp}/src/completion.rs RENAMED
@@ -514,14 +514,14 @@ pub fn memberItems(
514
514
  items
515
515
  }
516
516
 
517
- /// Maps every top-level name declared anywhere under `<lib_path>/plum-std/`
517
+ /// Maps every top-level name declared anywhere under `<lib_path>/std/`
518
518
  /// (subdirectories included) to that file's `import` path (`Str.plum` ->
519
519
  /// `"std/Str"`, `game/imperative/Scene.plum` -> `"std/game/imperative/Scene"`) — built by parsing
520
520
  /// each file standalone (no import resolution needed just to list names).
521
521
  /// Meant to be computed once and cached by the caller.
522
522
  pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
523
523
  let mut map = HashMap::new();
524
- let std_dir = lib_path.join("plum-std");
524
+ let std_dir = lib_path.join("std");
525
525
  let mut files = Vec::new();
526
526
  collectPlumFiles(&std_dir, &mut files);
527
527
  files.sort();
{plum-lsp → compiler/lsp}/src/main.rs RENAMED
@@ -690,9 +690,9 @@ async fn main() {
690
690
  // `--lib-path` mirrors `plum-cli`'s own required flag (see
691
691
  // plum-cli/src/main.rs's `Command::Run`/`Compile`) — the checker has no
692
692
  // built-in default location for the std lib. It's the directory
693
- // CONTAINING `plum-std/` (an `import std/Bool` resolves to
693
+ // CONTAINING `std/` (an `import std/Bool` resolves to
694
- // `<lib_path>/plum-std/Bool.plum` — see `plum_core::loader::loadImport`),
694
+ // `<lib_path>/std/Bool.plum` — see `plum_core::loader::loadImport`),
695
- // not `plum-std` itself; the repo root is `.` when run from a checkout.
695
+ // not `std` itself; the repo root is `.` when run from a checkout.
696
696
  let libPath = std::env::args()
697
697
  .collect::<Vec<_>>()
698
698
  .windows(2)
@@ -717,7 +717,7 @@ mod tests {
717
717
  use super::*;
718
718
 
719
719
  fn repoRoot() -> PathBuf {
720
- PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
720
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
721
721
  }
722
722
 
723
723
  // `Entity` is a base struct; `House` embeds it and is its subtype.
@@ -807,7 +807,7 @@ mod tests {
807
807
  /// two-pass checker) must find nothing wrong in the sequences example.
808
808
  #[test]
809
809
  fn seqExampleHasNoDiagnostics() {
810
- let path = repoRoot().join("plum-examples/sequences.plum");
810
+ let path = repoRoot().join("examples/sequences.plum");
811
811
  let text = std::fs::read_to_string(&path).unwrap();
812
812
  let tree = Backend::parseTree(&text);
813
813
  assert!(Backend::syntaxDiagnostics(&tree, &text).is_empty());
@@ -829,7 +829,7 @@ mod tests {
829
829
  "unions.plum",
830
830
  "games/imperative/game_loop.plum",
831
831
  ] {
832
- let path = repoRoot().join("plum-examples").join(f);
832
+ let path = repoRoot().join("examples").join(f);
833
833
  let text = std::fs::read_to_string(&path).unwrap();
834
834
  let tree = Backend::parseTree(&text);
835
835
  assert!(
@@ -1053,7 +1053,7 @@ mod tests {
1053
1053
  /// Every example file checks with zero syntax and type diagnostics.
1054
1054
  #[test]
1055
1055
  fn everyExampleHasNoDiagnostics() {
1056
- let dir = repoRoot().join("plum-examples");
1056
+ let dir = repoRoot().join("examples");
1057
1057
  let mut n = 0;
1058
1058
  for entry in std::fs::read_dir(&dir).unwrap() {
1059
1059
  let path = entry.unwrap().path();
{plum-runtime → compiler/runtime}/.gitignore RENAMED
File without changes
{plum-runtime → compiler/runtime}/Cargo.toml RENAMED
@@ -11,7 +11,7 @@ path = "src/main.rs"
11
11
 
12
12
  [dependencies]
13
13
  wasmtime = "47"
14
- plum-host = { path = "../plum-host" }
14
+ plum-host = { path = "../host" }
15
15
 
16
16
  # Stand on its own rather than joining the parent directory's workspace: `plum
17
17
  # build` compiles this crate on demand with PLUM_WASM_PATH set, and it would
{plum-runtime → compiler/runtime}/src/main.rs RENAMED
File without changes
compiler/test-support/Cargo.toml ADDED
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "plum-test-support"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ plum-core = { path = "../core" }
8
+ plum-driver = { path = "../driver" }
9
+ plum-wasm-codegen = { path = "../wasm-codegen" }
10
+ tree-sitter = "0.26"
11
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
12
+ wasmparser = "0.220"
13
+ wasmtime = "47"
{plum-test-support → compiler/test-support}/src/lib.rs RENAMED
@@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
8
8
  pub const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
9
9
 
10
10
  pub fn repoRoot() -> PathBuf {
11
- std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
11
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
12
12
  }
13
13
 
14
14
  fn parseTree(src: &str) -> (tree_sitter::Tree, Source) {
compiler/wasm-codegen/Cargo.toml ADDED
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "plum-wasm-codegen"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ wasm-encoder = "0.220"
8
+ plum-core = { path = "../core" }
9
+ plum-checker = { path = "../checker" }
10
+ plum-driver = { path = "../driver" }
11
+
12
+ [dev-dependencies]
13
+ plum-test-support = { path = "../test-support" }
14
+ wasmparser = "0.220"
15
+ tree-sitter = "0.26"
16
+ tree-sitter-plum = { path = "../../plum-tooling/tree-sitter-plum" }
17
+ wasmtime = "47"
{plum-wasm-codegen → compiler/wasm-codegen}/src/lib.rs RENAMED
@@ -924,7 +924,7 @@ pub struct GcTypeRegistry {
924
924
  /// Names of every declared enum.
925
925
  pub known_enums: HashSet<String>,
926
926
  /// The single shared `array<i8>` type index: packed `Array[Byte]`, the
927
- /// storage of `Str` (`Str(data, start, len)`, see `plum-std/Str.plum`) and
927
+ /// storage of `Str` (`Str(data, start, len)`, see `std/Str.plum`) and
928
928
  /// `Buffer`.
929
929
  pub byte_array_type_idx: u32,
930
930
  /// The single shared closure-value struct type index — `{table_idx: i32, env:
@@ -7399,7 +7399,7 @@ fn compileInterpolatedString(
7399
7399
  compileExpr(expr, body, ctx, state)?;
7400
7400
  }
7401
7401
  PlumType::TInt => {
7402
- // Ordinary `Int.toStr` (`plum-std/Int.plum`), like Float below.
7402
+ // Ordinary `Int.toStr` (`std/Int.plum`), like Float below.
7403
7403
  compileExpr(expr, body, ctx, state)?;
7404
7404
  let func_idx = *ctx.func_ids.get("Int::toStr")
7405
7405
  .ok_or_else(|| "codegen: interpolating an Int value requires `Int.toStr` (import std/Int)".to_string())?;
@@ -7415,7 +7415,7 @@ fn compileInterpolatedString(
7415
7415
  Instruction::End.encode(body);
7416
7416
  }
7417
7417
  PlumType::TFloat => {
7418
- // Reuses `Float.toStr` (`plum-std/Float.plum`) rather
7418
+ // Reuses `Float.toStr` (`std/Float.plum`) rather
7419
7419
  // than a separate hand-written float-formatting helper.
7420
7420
  // Any program using `Float` at all already must
7421
7421
  // `import std/Float` (see `plum-core::loader::
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/codegen_tests.rs RENAMED
@@ -1046,7 +1046,7 @@ fun main() -> Int =
1046
1046
  // ---- mixed types via enums (T4) ----
1047
1047
 
1048
1048
  /// Compiles `src` as a real file (so `import std/...` resolves against the repo's
1049
- /// `plum-std`) and returns the wasm instance's `main` export plus its store.
1049
+ /// `std`) and returns the wasm instance's `main` export plus its store.
1050
1050
  fn runMerged<R: wasmtime::WasmResults>(name: &str, src: &str) -> R {
1051
1051
  let dir = std::env::temp_dir().join(format!("plum_t3_{}_{}", name, std::process::id()));
1052
1052
  std::fs::create_dir_all(&dir).unwrap();
@@ -1923,7 +1923,7 @@ fn checkErrorsMerged(name: &str, src: &str) -> String {
1923
1923
  #[test]
1924
1924
  fn primitive_replaces_dummy_enums() {
1925
1925
  for f in ["Byte"] {
1926
- let text = std::fs::read_to_string(repoRoot().join(format!("plum-std/{f}.plum"))).unwrap();
1926
+ let text = std::fs::read_to_string(repoRoot().join(format!("std/{f}.plum"))).unwrap();
1927
1927
  assert!(
1928
1928
  text.contains(&format!("primitive {f} =")),
1929
1929
  "{f} should use primitive"
@@ -2372,7 +2372,7 @@ fun main() -> Float =
2372
2372
  // ---------------------------------------------------------------------------
2373
2373
 
2374
2374
  fn exportRepoRoot() -> std::path::PathBuf {
2375
- std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
2375
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
2376
2376
  }
2377
2377
 
2378
2378
  /// Compiles `src` through the real loader (so `import std/Str` resolves) and
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/mod.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/reuse.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_inplace.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_spread.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/empty_record_embed.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/enum_init.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/inline_variants.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/mod.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_embed.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_init.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/unboxed_variants.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/closure_tail.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/error_tail.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/loop_break.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/mod.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/tail.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_optional.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_types.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_entry.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_primitive.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/list_and_shadow.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/mod.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/question.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/subtyping.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/type_param_static.rs RENAMED
File without changes
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/examples_test.rs RENAMED
@@ -3,7 +3,7 @@ use plum_test_support::{engine, repoRoot};
3
3
  use plum_test_support::compileSource;
4
4
 
5
5
  fn examplesDir() -> std::path::PathBuf {
6
- std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../plum-examples")
6
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples")
7
7
  }
8
8
 
9
9
  /// Loads `name` via the REAL compilation path (`loadAndMerge`, resolving its
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/fusion_alloc_test.rs RENAMED
@@ -215,7 +215,7 @@ fn list_pipeline_loop_has_no_cons_alloc() {
215
215
  /// `List.map(|o| o.render(..))` children (each `render` resolved by its receiver type).
216
216
  #[test]
217
217
  fn state_view_draw_has_no_scenenode_alloc() {
218
- let g = loadFile(&repoRoot().join("plum-examples/games/functional/game_state_view.plum"));
218
+ let g = loadFile(&repoRoot().join("examples/games/functional/game_state_view.plum"));
219
219
  let ns = notes(&g);
220
220
  say(format!("state_view: fusion notes {ns:?}"));
221
221
  let w = compile(&g, true);
{plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/main.rs RENAMED
File without changes
docs/backlog.md CHANGED
@@ -9,7 +9,6 @@ Open work, in the order the user queued it, plus smaller items found during buil
9
9
  1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Approved 2026-11-04; running on branch `superlite/compiler-cleanup`, phase 1 first.
10
10
  - **Explicit function generics** (user 2026-11-04: right after cleanup phase 2) — functions declare their generics (`fun map[B](...)`) instead of every single capital letter being a generic; allows longer generic names and one-letter concrete types/tags. Grammar change + about 140 signatures in std/examples. Phase 2's "generics by scope" makes this mostly a grammar + signature rewrite.
11
11
  - **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.
12
- - **Move the Rust code into `compiler/`** (user 2026-11-04: once all the refactoring is done) — every Rust crate (plum-core, plum-checker, plum-wasm-codegen, plum-cli, plum-lsp, plum-host, plum-test-support, plum-runtime, plum-game) goes under `compiler/`; `plum-std` and `plum-examples` stay at the top level but are renamed to `std/` and `examples/` in the same change (user 2026-11-05); the loader's std lookup (`import std/X` → the `std/` folder), `isStdFile`/`isIoFile`, every test/CLI/web-test path, scripts, README and docs follow. Mechanical but touches hardcoded paths: workspace `Cargo.toml` members, every `CARGO_MANIFEST_DIR/..` repoRoot (test-support, tests, cli), the loader's std lookup, the checker's `isStdFile`/`isIoFile` path tests, `cmdBuild`'s runtime manifest path, plum-game web-test fixtures, scripts/, Makefile.toml, plum-tooling (vscode/tree-sitter paths), README and docs links. Open question for then: keep the `plum-` prefixes inside `compiler/` or drop them (`compiler/core`, …).
13
12
  2. **Simplify the language: no traits, no union enums, native inline variants** (decided by the user 2026-11-04; runs right after compiler-cleanup phase 3a, before explicit generics and 3b; decision record §13). Traits are removed as a language feature and replaced by records embedding records with subtyping + `override` dispatch (a `Ship` embedding `Node` is accepted wherever a `Node` is, and calls dispatch to Ship's overrides — the scene graph's `trait Actor` becomes a base record). Union enums (`enum X = A | B` of records) are removed: `JsonParse` and the union examples become inline-variant enums, union syntax becomes an error. Inline variants compile natively as struct subtypes of their enum, deleting `hoistInlineVariants`, the `List__Cons(Cons(..))` wrappers, union forwarders, `union_lower` and the checker's union plan. `unboxed_variants.plum` is a compiler probe: move it to a codegen test. Same plan also covers (user 2026-11-04): `=` reassign-only, no generators (`fun*`/`yield`; Seq deleted, one loop form `for x := range v` over Int or any `next(self) -> Option[Pair[T, Self]]`), no variadics (`...T`; `[..]` literal → `Cons`/`Nil`, `[k => v]` → `Map.empty().with(..)`), no value enums, `record` merged into `struct` now, and **run D: error sets / `T!E` / `T?` / `null` replaced by std `Option[T]` and `Result[T, E]` enums**, with postfix `?` as the ONLY sugar (revised 2026-11-05: `try`, `?:`, `?.`, `catch`, `T?`, `E!T`, `null` all removed; runs right after run C; decision record §13 item 1). Runs A (in progress), B (struct), C (traits/unions), D (Option/Result).
14
13
  3. **Benchmark** before FBIP (user: FBIP waits for it).
15
14
  - **Unbox iterators / niche Option layout** (user 2026-11-05: later, here, not before generics). Run D made `for` loops slower: for-over-List 1M 28 → 49 ms (+75%), for-over-Range 6 → 8 ms (+33%), Map.get +17% (docs/plans/2026-11-05-language-simplification-d.benchmark.md). Cause: `next() -> Option[Pair[T, Self]]` allocates a `Some` cell per step on top of the Pair (the old `Pair?` was a nullable ref). Options: `Option[R]` for a GC ref R as a nullable ref (niche), and/or fusion/drive unboxing the `Some(step) := it.next()` loop. Bisect 961bd21 vs d7658cc back-to-back first.
@@ -24,13 +23,14 @@ Open work, in the order the user queued it, plus smaller items found during buil
24
23
  - **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
24
  - **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
25
  - **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").
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.
26
+ 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 `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.
28
27
 
29
28
  ## Planned, not yet placed in the queue
30
29
 
31
30
  - **Language ideas to discuss later** (user 2026-11-05: "add them ideas to be discussed later"):
32
31
  - **Single inheritance** — DECIDED 2026-11-05, built in run C (T11).
33
32
  - **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.
33
+ - **Sprite animation + state machines** (2026-11-05 notes): playback is a function of time, so the functional sprite stores only `clip` + `startedAt` and computes the frame in `draw` (`(tick - startedAt) / ticksPerFrame % frames`) — no per-tick mutation, no drift, free under replay/rewind; std's `game/functional/Anim.plum` already works this way ("animations as pure functions of time"). The imperative sprite mutates `frame`/`timer` and must be advanced exactly once per tick per sprite. State machines (idle/run/jump/land) are an enum with per-state data (`Jump(since, vy)`) and a `step(pose, input, tick) -> Pose` match — exhaustive and type-checked; the clip follows from the pose, `since` doubles as the clip start. Imperative tends to `mut state: Int` + loose fields valid only in some states. Real functional costs: deep nested updates (rebuild the path; `T(..x, f: v)` spread helps, maybe lens sugar later; FBIP removes the allocation) and events returned as data (`step` returns pose + `[SpawnHitbox(..)]`, the world applies them) instead of mutating callbacks — more explicit, and what makes it replayable.
34
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.
35
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`.
36
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.
@@ -59,10 +59,10 @@ Open work, in the order the user queued it, plus smaller items found during buil
59
59
  - **`List.length()` on a locally built list fails codegen** — `fun count() -> Int =` / `xs := List(1, 2)` / `xs.length()` gives `codegen: unknown method 'List.lengthFrom' (in 'count')`; the same call on a `List` parameter, or inside a `test` block, works. Also hit with a static `Hue.palette().length()`. Found drafting the kids' games; the user wants it fixed (with a regression test) before those games are built. Check whether the cleanup refactor already fixed it.
60
60
  - **Spread from a field path** — `Stage(..w.stage, x: 1)` is rejected; only a plain variable can be a spread source. Worked around with small helpers in the pure game. (language-restrictions T14)
61
61
  - **Fields declared inside `init` can't be `mut`** (`self.camX: Float = 0.0`) — such fields were moved into the record header. (T8)
62
- - **Benchmark harness can't pass `--no-fusion`** — `plum-game/web-test` compiles without flags, so fusion-off numbers can't be taken since the env var was removed. (T11)
62
+ - **Benchmark harness can't pass `--no-fusion`** — `compiler/game/web-test` compiles without flags, so fusion-off numbers can't be taken since the env var was removed. (T11)
63
63
  - **`Orb`/`Mine` copy their shared `Node` per frame** in the pure game, because `Node`'s x/y/rotation stay immutable in std. Decide during FBIP. (T10)
64
64
  - **Uniqueness precision** — `adv(Cell(..))` inlined into `main` reports "s used after its spread" although `s.x` is only read inside the spread value (a false "not in place"). (T10)
65
- - **Pattern-casing hint for union members** — "did you mean Foo?" doesn't cover a lowercase name that spells a union member (lookup lives in `plum-core/src/unions.rs`). (T1)
65
+ - **Pattern-casing hint for union members** — "did you mean Foo?" doesn't cover a lowercase name that spells a union member (lookup lives in `compiler/core/src/unions.rs`). (T1)
66
66
  - **Constant `Anim` trees rebuilt every frame** in `games/functional/game_state_view.plum` (`renderTitle`, Ship render) — hoist them into `State` or constants. (F-T23-1)
67
67
  - **Static subtree reuse** in functional scenes (build unchanging parts of a picture once). Deferred by the user as a later optimisation. (state-view parity plan)
68
68
  - **Structural deep `==` for records** — user chose deep structural equality (like Rust `#[derive(PartialEq)]`), deferred before implementation; until then `==` on records is identity. (state-view parity T7)
docs/game-scene-styles.md CHANGED
@@ -120,7 +120,7 @@ Its ideas:
120
120
  - `Ui.button(id, msg, label, ..)` builds one. `Ui.messages(view, events)` turns the Clicks that `Ui.step` raised into the game's own messages, read from the view the pointer was on.
121
121
  - The game declares `RestartClicked` in its `Msg` and handles it in `update` like any other message. The `UiEv` wrapper and the `e.id == Id.Restart && e.kind == Click` check are gone. The id stays only to identify the button for hover and press state (`Ui` needs identity, and `==` on records is identity).
122
122
  - Cost: every scene signature says `SceneNode[Msg]`, and a local built from a bare constructor needs an annotation (`x: SceneNode[Msg] := Group(..)`); passing it straight to its use (`Filter(child: Group(..))`) avoids that, and also keeps fusion. Helpers that do not mention `M` (`hitLeaf`, `hitPrepend`, `nearLine`) are top-level functions, because a static method of a generic type must mention its parameter. Fusion of `render().draw(io)` still holds (`fusion_alloc_test`); allocations in the exempt panel/animation functions went from 15 to 12.
123
- - Found on the way: TRMC hoisted a list cell's head into an untyped temporary, so `Cons(head: Full(..), ..)` in a function returning `List[Box[Int]]` gave the temporary a `Box` with its own parameter left open, and produced invalid wasm. Fixed in `trmc.rs` (the temporary takes the field's concrete type); regression test `trmc_list_of_generic_enum` in `plum-examples/recursion.plum`.
123
+ - Found on the way: TRMC hoisted a list cell's head into an untyped temporary, so `Cons(head: Full(..), ..)` in a function returning `List[Box[Int]]` gave the temporary a `Box` with its own parameter left open, and produced invalid wasm. Fixed in `trmc.rs` (the temporary takes the field's concrete type); regression test `trmc_list_of_generic_enum` in `examples/recursion.plum`.
124
124
  - Pure and imperative styles keep integer ids (generic traits, `Actor[Msg]`, are not supported).
125
125
 
126
126
  ### Next
docs/imperative-vs-functional.md ADDED
@@ -0,0 +1,239 @@
1
+ # Imperative vs functional games in Plum
2
+
3
+ Plum ships three ways to build a game's scene: **imperative** (a mutable scene graph, Flame/Scene2D style), **functional** (an immutable `State`, a `render()` that builds a picture of it each frame, Elm style) and **pure** (a retained scene tree that every update returns anew). This page compares them one aspect at a time and collects what has been measured or discussed. It is a comparison, not a decision: the open choices are in the last two sections.
4
+
5
+ Related pages: [game-scene-styles.md](game-scene-styles.md) (API tables, engine analogies, input model), [decisions/2026-11-language-and-scene-decisions.md](decisions/2026-11-language-and-scene-decisions.md) (why the styles exist), [backlog.md](backlog.md) (deterministic games, FBIP, "One way to build a game").
6
+
7
+ Conventions: "measured" means a number from a file cited next to it; "proposal" and "undecided" mark things nobody has built or chosen. Timings are ms per frame on an Apple M3 Pro, headed Chromium (WebGPU), `--uncapped --measure 10`, unless a row says otherwise. Differences under about 5% are noise.
8
+
9
+ ## At a glance
10
+
11
+ | | Imperative | Functional | Pure |
12
+ |---|---|---|---|
13
+ | Std modules | `std/game/imperative/Scene` | `std/game/functional/{FunctionalScene, Ui, Anim}` | `std/game/pure/{PureScene, Actions, SceneInput}` |
14
+ | Example | `examples/games/imperative/game_scene.plum` | `examples/games/functional/game_state_view.plum` | `examples/games/pure/game_pure_scene.plum` |
15
+ | Game data lives in | actors (`struct Walker(Actor, speed: Float)`) | your own `State` and per-kind structs (`Ship`, `Orb`) | nodes (`Node` fields, `actions`, `Custom(actor)`) |
16
+ | The scene tree is | kept, changed in place | rebuilt from `State` every frame, then dropped (fusion removes it) | kept, each update returns the next tree |
17
+ | Per-frame entry | `stage.act(dt)`, `stage.draw(io)` | `state.update(msg)`, `state.render().draw(io)` | `stage.frame(dt, inputs)`, `stage.draw(io)` |
18
+ | Closest engine | Flame, libGDX Scene2D | Elm, Flutter's widget layer | an immutable Scene2D; Redux over a scene graph |
19
+ | Cost at 5.8k nodes | 1.70 ms | 1.75 ms | 1.71 ms |
20
+
21
+ ## State model
22
+
23
+ **Imperative.** State is a mutable object graph. Each actor owns its data and its behaviour (`act`, `draw`, `hit` overridden on the base `Actor` struct). `Actor` has 14 fields with defaults; kinds embed it first, which makes them subtypes, so a `Slice[Actor]` holds any of them. Nodes are found through object references (`same(a, b)` is `==`, which is identity on a struct today). Fields are immutable unless declared `mut`; the whole scene is `mut` by design.
24
+
25
+ **Functional.** State is one immutable `State` struct. Kinds are structs with `update(self, ..) -> Self` and `render(self, ..) -> SceneNode[Msg]` (the names are Elm's and React's). The tree (`SceneNode[M]`: `Group`, `Filter`, `Sprite`, `Text`, `RectShape`, `CircleShape`, `LineShape`, `Clickable`) is plain data and holds no game data. There is no local widget state, no keys, no layout, no diffing.
26
+
27
+ **Pure.** The tree is the state. A `Node` holds a `Transform`, an `id`, a colour and a `Kind` enum (`Group | Sprite | Label | Button | Custom(actor)`). Changing the game means finding a node (`findPath(id)`, `modify(id, f)`) and returning a new tree. Custom behaviour is a struct that embeds `Actor` inside `Custom`. Timed `Action`s (`moveBy`, `rotateBy`, `sequence`, `forever`, ..) live on the node.
28
+
29
+ Where the data lives is the main difference between functional and pure; both avoid mutation in game logic. The shells (`World`) are still mutable in all three examples: the functional one stores `w.state = t` after each tick.
30
+
31
+ ## Updating state
32
+
33
+ | | Imperative | Functional | Pure |
34
+ |---|---|---|---|
35
+ | Change | assign fields, call methods that mutate | return a new value: `State(..self, clock: self.clock + dt)` | return a new tree: `node.update(dt)`, `node.modify(id, f)` |
36
+ | Deep updates | `a.child.x = 1.0` | rebuild the path; spread `T(..x, f: v)` helps, lens sugar maybe later (proposal) | `modify` along the path; a spread source must be a plain variable (`Stage(..w.stage, x: 1)` is rejected, backlog) |
37
+ | Aliasing | any alias sees the change; mutation order is part of the semantics | none: nothing can change under you | none for the program, but the compiler must prove uniqueness to update in place |
38
+ | New allocation per tick | none | a new `State` (and the `Cons` cells of lists) | new nodes, avoided in place when unique |
39
+
40
+ Functional cost, stated in the backlog: deep nested updates, and events returned as data instead of mutating callbacks. Pure cost, measured: see "Performance" (Orb and Mine copy their shared `Node` each frame).
41
+
42
+ ## Drawing and the scene graph
43
+
44
+ - **Imperative.** `stage.draw(io)` walks the retained tree; each actor's `draw(io, m, alpha)` paints with its stage transform. Z order via `zIndex`, culling via `setCullingArea`, y-sort via `sortChildren`.
45
+ - **Functional.** `state.render().draw(io)`. `SceneNode.draw` is structural recursion that passes the parent matrix and alpha down; nothing is retained, so every frame is redrawn. The supercompiler fuses `render` into `draw`, so the tree is never built (test `state_view_draw_has_no_scenenode_alloc` in `compiler/wasm-codegen/tests/wasm_codegen/fusion_alloc_test.rs`). The one exception is the game-over overlay, whose `Filter` reads its child twice.
46
+ - **Pure.** `node.drawTree(io, m, tint)` walks the retained tree. Custom actors draw through `Actor.draw`.
47
+
48
+ Notes from the discussion ([decisions §5](decisions/2026-11-language-and-scene-decisions.md)): the functional picture already follows the textbook design (recursive ADT, matrix passed down, top-down hit paths, assets by handle). Structural sharing pays off only for parts that do not change (UI, terrain); where everything moves, rebuilding costs about the same and is simpler. Folding the tree into a flat command list is redundant because the host already batches draws. Zippers are not needed, since code edits `State` and rebuilds. Static subtree reuse is deferred (backlog).
49
+
50
+ ## Input handling
51
+
52
+ All three read input through one type, `Input` in `std/game/Input` (`PointerDown | PointerUp | PointerMove | KeyDown | KeyUp`), and `Input.poll(io, held)` is the only code that reads it from `io`. `Held` tracks held keys.
53
+
54
+ | | Imperative | Functional | Pure |
55
+ |---|---|---|---|
56
+ | Pointer | `stage.input(e)`: topmost actor under the pointer, then each parent, until one's `onInput` returns true | `Event(input)` message, then `render().hitId(x, y)`, `Ui.step`, `UiEvent`s | hit path, then `node.route` child first; `Button` reacts, `Custom` asks `actor.onInput` |
57
+ | Keyboard | a press sets `stage.focus`; keys bubble from it | `Event(KeyDown/KeyUp)` updates `State.held`; the game reads `held.isDown(..)` | a press focuses a node; unconsumed keys come back in `StageHandled.keys` |
58
+ | UI identity | integer ids | `Clickable(id, msg: M, child)`: a click becomes the game's own message | integer ids, `UiEvent(id, kind)` |
59
+ | Capture (press, drag off, release) | none | yes (`Ui`) | yes |
60
+
61
+ Typed messages exist only in the functional style: generic traits (`Actor[Msg]`) are not supported, and traits are gone entirely now (single inheritance replaced them). The cost is that every scene signature says `SceneNode[Msg]`.
62
+
63
+ ## Sprite animation
64
+
65
+ Source: the 2026-11-05 notes in [backlog.md](backlog.md) ("One way to build a game"); `std/game/functional/Anim.plum`.
66
+
67
+ - **Imperative.** The sprite mutates `frame` and `timer` in `act`, so it must be advanced exactly once per tick per sprite. It drifts if a tick is skipped or repeated, and a rewind has to restore the mutated fields.
68
+ - **Functional.** Playback is a function of time. A sprite stores only `clip` and `startedAt` and computes the frame in `draw`: `(tick - startedAt) / ticksPerFrame % frames`. Nothing is mutated per tick, there is no drift, and replay or rewind is free. `Anim` is plain data (`MoveBy | RotateBy | ScaleTo | Delay | Seq | Par | Forever`) and `Anim.at(anim, t)` returns the pose `t` seconds in; `Forever` repeats using `t - floor(t/lap)*lap`.
69
+ - **Pure.** `Action`s are stepped each frame (`action.step(pose, dt)`). Their `elapsed` and `current` fields are `mut` today, so animation state is stepped in place; a pure version needs a `stepAll` redesign (backlog FBIP worklist).
70
+
71
+ Functional gap noted in the backlog: constant `Anim` trees are rebuilt every frame in `game_state_view.plum` (`renderTitle`, the ship render); hoisting them into `State` or constants is open.
72
+
73
+ ## State machines
74
+
75
+ Source: the same backlog notes.
76
+
77
+ - **Imperative.** Typically `mut state: Int` plus loose fields (`timer`, `vy`) that are only valid in some states. Nothing stops reading `vy` while idle.
78
+ - **Functional.** An enum with per-state data (`Idle | Run | Jump(since, vy) | Land(since)`) and a `step(pose, input, tick) -> Pose` match. It is exhaustive and type-checked; the clip follows from the pose, and `since` doubles as the clip start.
79
+ - **Pure.** Either of the above; the actor held in `Custom` can carry an enum, but `act` mutates or replaces it.
80
+
81
+ Real functional costs: nested updates (above), and `step` must return its effects as data (pose plus `[SpawnHitbox(..)]`) that the world applies. This is more explicit and is also what makes it replayable.
82
+
83
+ ## Events and side effects
84
+
85
+ | | Imperative | Functional | Pure |
86
+ |---|---|---|---|
87
+ | Game events | callbacks and `onInput -> Bool` (consumed) | messages: `Msg = Tick | Resize | Restart | Event | RestartClicked ..`, handled in `State.update` | `UiEvent(id, kind)`, `Handled.consumed` |
88
+ | Sound, host calls | in the shell `update(io, ..)`, which compares score and lives before and after the step to decide which sound to play | the same in the example: the shell compares `t.score > clicked.score` after `State.update` | same |
89
+ | Purity | any method may read or write `io` | `State.update` and `render` take no `Io`; only the `World` shell does | `act` and `update` take no `Io`; `draw` does |
90
+
91
+ The examples play sounds by diffing state around an update. Returning effects as data (an event list from `update`) is the functional alternative and is not built.
92
+
93
+ ## Determinism, replay and rollback
94
+
95
+ Determinism comes from what may enter `update`, not from the style. Rules for all styles (design notes in [backlog.md](backlog.md), item "Deterministic games"; these are proposals, only some are in place):
96
+
97
+ 1. **Fixed timestep.** The host accumulates wall time and calls `update` with a constant `dt`; `draw` may interpolate. Today `plum_bridge.js` passes the real frame time, so this is not in place.
98
+ 2. **Input as a per-tick `InputFrame` value.** Passed in, never polled inside `update`; a replay is the list of frames. Today every example calls `Input.poll(io, ..)` inside the shell's `update(io, w, dt)`. Recording would save each step's `Polled.inputs`.
99
+ 3. **`update` takes no `Io`.** The compiler then proves it reads no clock, random or files and draws nothing. `draw(state, io)` keeps `Io` and cannot change state. The `Io` capability is in place; the split is not.
100
+ 4. **Seeded `Random` in state.** Each draw returns a new generator. The seed comes from `io` once at startup (`init` calls `io.randomSeed()` today) and is recorded with the replay.
101
+ 5. **No ordering by reference identity or host-seeded hashes.** The std `Map` hash is a fixed FNV-1a with insertion-ordered open addressing, so iteration depends only on the insert sequence; keep it so.
102
+ 6. **Floats.** wasm `f64` is IEEE 754 and gives the same results in every engine; do not hash NaN bits. `Math.sin/cos/sqrt/hypot` are written in Plum (`std/Math.plum`), not imported from the browser.
103
+ 7. **A per-tick state checksum.** Hash the state each tick and compare runs. Not built.
104
+
105
+ Other gaps from [game-scene-styles.md](game-scene-styles.md) section 4: host values in state (the functional game keeps `sw`, `sh` and gets `Resize` messages, so they must be in the input log or out of the simulation).
106
+
107
+ How each style stands under those rules:
108
+
109
+ | | Imperative | Functional | Pure |
110
+ |---|---|---|---|
111
+ | Deterministic under the rules | yes | yes | yes |
112
+ | Snapshot | deep copy or serialization of the mutable graph | keep the old value (persistent structures share memory) | explicit deep copy today, since the tree is updated in place when unique |
113
+ | Rollback or resimulation | restore the copy, replay inputs | old state plus logged inputs | restore the copy, replay inputs |
114
+ | Checksum | a graph walker that skips shared refs | hash of a value | walker over the `Stage` plus the `World` fields |
115
+ | Mutation order | part of the semantics through aliases (fragile) | not observable | not observable at the language level |
116
+ | Cost | none per tick | a new state per tick; FBIP removes it when the old value is not shared | none when updated in place |
117
+ | Replay log | inputs per tick | the `Msg` stream is already the log (`Tick`, `Resize`, `Restart`, `Event`, ..) | inputs per tick |
118
+
119
+ The functional style is almost free here: pure by construction, and a purity pass can check it because `update` has no `Io`. The recommendation in game-scene-styles.md and the backlog is to build deterministic simulation on the functional shape and port the design from `../kam_macroquad` (`main.rs` tick accumulator, `game/replay.rs`, `KaMRandom`).
120
+
121
+ ## Testing
122
+
123
+ - **Imperative.** The transform maths (`localMatrix`, `worldMatrix`, `flatten`, `hitNode`) never calls the host, so it runs under `plum test`. Tests that need their own `act`/`draw` records live in `examples/games/imperative/scene.plum`, not in std, because every actor kind in the program adds to dispatch cost (see Performance). The game example tests build the world and swap actors in by id (`placeOrb`, `swap`).
124
+ - **Functional.** `State.update(msg)` is a pure function: a test builds a state with `State.start(seed, 0, -1, -1, w, h)` and applies `Tick(..)` messages (`tickOf`, `withOrb` helpers in `game_state_view.plum`), with no `Io` and no host. This is the easiest style to test.
125
+ - **Pure.** Tests drive `advance(w, ax, ay, dt, inputs)` or `stage.frame(dt, inputs)` and inspect nodes by id.
126
+ - **All styles.** A fake or recording `Io` for tests and replays is planned but not built: the `Io` primitive makes it possible, and the backlog leaves the mechanism (a trait-like abstraction or language mocks) open.
127
+
128
+ ## Performance and benchmarks
129
+
130
+ Everything here is measured by the cited file. The direction (decisions §1) is "the most functional style, not the fastest": performance is reported, not chased, and the hard bar is staying well inside the frame budget (state/view p95 2.5 ms against a 4 ms bar at 120 Hz).
131
+
132
+ ### The three styles, 5.8k nodes (stress_scene, stress_pure_scene, stress_state_view)
133
+
134
+ Source: `docs/plans/2026-11-03-language-restrictions.benchmark.md` (two runs, fusion on).
135
+
136
+ | Style | avg ms (run 1 / 2) | p95 | update | draw |
137
+ |---|---|---|---|---|
138
+ | Imperative (`stress_scene`) | 1.703 / 1.697 | 1.9 / 1.8 | 0.039 | 1.27 |
139
+ | Pure (`stress_pure_scene`) | 1.714 / 1.716 | 1.9 | 0.167 | 1.21 |
140
+ | Functional (`stress_state_view`) | 1.755 / 1.742 | 2.0 | 0.033 | 1.36 |
141
+
142
+ The three are within about 3% and draw-dominated. Run D (docs/plans/2026-11-05-language-simplification-d.benchmark.md) measured `stress_scene` at 1.744 before and 1.725 after; the backlog quotes imperative 1.70, pure 1.71, functional 1.75.
143
+
144
+ How the gap closed, in order (each source file is `docs/plans/<name>.benchmark.md`):
145
+
146
+ | Step | Imperative | Pure | Functional | Source |
147
+ |---|---|---|---|---|
148
+ | Pure phase A (copy every actor each frame) | 1.67 | 1.92-1.98 (update 0.32 vs 0.037) | n/a | `2026-11-02-pure-scene` (missed the +10% bar) |
149
+ | `own` records (in-place) | 1.74 | 1.93-2.11 | 2.07-2.13 (List-based) | `2026-11-03-own-record`: copies were never the main cost |
150
+ | Recursive state/view, tail calls | 1.70 | 1.90 | 1.91 | same file |
151
+ | Zero-cost closures, allocation-free `Actions` | 1.83 | 1.78 | n/a | `2026-11-03-zero-cost-closures-scene-perf` (bar met) |
152
+ | Fusion on vs off | 1.72 vs 1.66 | 1.60 vs 1.60 | 1.68 vs 1.81 (-7%) | `2026-11-03-supercompiler` |
153
+ | `own` removed, uniqueness inference | 1.70 | 1.71 (update 0.167) | 1.75 | `2026-11-03-language-restrictions` |
154
+
155
+ Reading: fusion moves only the functional fixture outside noise, all of it in `draw` (1.45 to 1.34 ms) from view-to-draw fusion. In an earlier same-session run (`2026-11-03-state-view-parity`): mutable 1.93, pure 1.65, functional 2.07, with p95 2.90, 1.80, 2.50.
156
+
157
+ Pure `update` is 0.167 ms against 0.033-0.039 for the other two because `Orb` and `Mine` copy their shared `Node` each frame (their `x`/`y`/`rotation` stay immutable in std). That is a worklist item for FBIP.
158
+
159
+ ### Scene on a base `Actor` vs closures (imperative)
160
+
161
+ Source: `compiler/game/web-test/BENCHMARK.md` (formerly under plum-game), "Scene on `trait Actor` vs closure `Node`". `stress_world` is 20,000 sprites with culling and a y-sort every frame.
162
+
163
+ | Fixture | closure `Node` | boxed trait | box-free subtyped layout |
164
+ |---|---|---|---|
165
+ | `stress_scene`, 5.8k nodes | 1.60 | 1.69 | 1.68 |
166
+ | `stress_world`, 20k nodes | 1.09 | 2.09 (+92%) | 1.10 |
167
+
168
+ Boxed trait field reads from outside the trait cost a dispatch over every actor kind (about 30-40 ns against about 10 ns for a direct field read), which made the y-sort `update` 1.29 ms. The cost scaled with the number of kinds in the program (dropping two unused kinds took `update` from 1.65 to 1.29 ms). The subtyped layout (a base struct plus subtype structs, one `struct.get` per field) brought it back to 1.10 ms (`update` 0.37). Method dispatch is still a `ref.test` chain, so its cost grows with the number of kinds. Now that traits are gone, `Actor` is a base struct with this layout.
169
+
170
+ Culling (same file): 20k nodes with culling 1.15 ms, without 4.20 ms (3.7x); what remains with culling is `update` at about 0.42 ms, the O(n) sort scan.
171
+
172
+ ### Run C and run D (language simplification)
173
+
174
+ - **Run C** (no traits, single inheritance, native inline variants; `docs/plans/2026-11-05-language-simplification-c.results.md`): `stress_scene` 1.911 and 1.947 against a 2026-11-03 baseline of 1.70-1.75 (+11-12%); `stress_world` 1.409 and 1.411 against about 1.10 (+28%). Load average was about 6 (concurrent agents) and `stress_scene.plum` was ported in the same run. **The regression is unproven and an A/B on an idle machine against pre-run-C is still owed** (backlog, Benchmark item). Run D2 found that closure-specialisation hints had been empty since run C (fixed), which may explain part of it.
175
+ - **Run D** (Option/Result and postfix `?`; `docs/plans/2026-11-05-language-simplification-d.benchmark.md`, load average 5-30, baseline binary not re-run): `stress_scene` 1.744 to 1.725 ms (-1%); `stress_world` 1.594 to 1.180 ms (-26%, but the baseline ran under much heavier load). Loop probes, `plum run`, 5 runs, median:
176
+
177
+ | Probe | Before | After | Delta |
178
+ |---|---|---|---|
179
+ | `for` over a List, 1M | 28 ms | 49 ms | +75% |
180
+ | `for` over a Range, 1M | 6 ms | 8 ms | +33% |
181
+ | `Map.get` 1M on 1000 entries | 23 ms (`getOrNull`) | 27 ms (`get` + `unwrapOr`) | +17% |
182
+ | `Json.parse` 137,781 bytes x20 | 116 ms | 115 ms | -1% |
183
+
184
+ Cause (backlog): `next() -> Option[Pair[T, Self]]` allocates a `Some` cell for each step on top of the `Pair`, where the old `Pair?` was a nullable ref. Options not yet built: a niche layout where `Option[R]` of a GC ref is a nullable ref, and/or fusion or drive unboxing of the `Some(step) := it.next()` loop. First step: bisect 961bd21 against d7658cc back to back. This matters to games because every style loops over lists and slices in `update`.
185
+
186
+ ### Other measured facts
187
+
188
+ - A scene tree costs about 0.5 ms over flat immediate drawing at 5000 sprites (about 100 ns per node, all in `draw`; `update` goes from 0.00 to 0.07 ms): BENCHMARK.md analysis (c).
189
+ - Renderer: 16 texture slots plus SDF text cut `stress` from 1.58-1.67 ms to 0.98-1.03 ms and 52 draw calls to 1; interleaved textures 1.3 to 0.45 ms (BENCHMARK.md). The renderer is shared by all three styles.
190
+ - Unboxed variants: a 1M-cell `List` build plus length went from 1,238 ms to 192 ms (one allocation per cell instead of a box and a payload); `docs/plans/2026-11-03-unboxed-variants.benchmark.md`.
191
+ - GC: the 1M-list slowness under wasmtime was the dev-profile copying collector (fixed by compiling wasmtime at opt-level 3); V8 handled the same module in 128 ms, so browser games are unaffected (`docs/plans/2026-11-03-gc-report.md`).
192
+ - Closure specialisation: no `call_ref` in specialised copies, but the 1M List map/filter/fold timing barely moved (about 632 to 625 ms) because the cost was GC, not calls (zero-cost-closures benchmark).
193
+
194
+ ### Not measured
195
+
196
+ - A functional-style 20k-node world: no culling or sort fixture exists for it (a `stress_world_state_view` fixture is proposed in the backlog).
197
+ - Per-frame allocation and GC of `State` at that size. The only hint is the old List-based state/view, whose p99 roughly doubled (about 4.2 ms), read as GC from about 5,750 allocations per frame; the recursive version removed that tail (2.4 ms).
198
+ - Fusion-off numbers since the `PLUM_NO_FUSION` variable was removed: the harness passes no compiler flags, so `--no-fusion` cannot be used (backlog).
199
+ - GPU time, a low-end machine, a deterministic-sim overhead (tick accumulator, checksums).
200
+
201
+ ## Memory and allocation
202
+
203
+ - **Imperative.** Allocates once at setup; per frame it only mutates. Memory is a retained graph, so liveness is long.
204
+ - **Functional.** Allocates a new `State` (and list cells) each tick, and in principle a new tree each frame. The tree allocation is gone through fusion; the state allocation is not yet. FBIP is the planned fix: persistent collections updated in place when provably unique, `fip` functions as a checked guarantee, and a reference count used only as a uniqueness check if static proof falls short.
205
+ - **Pure.** Updates in place when the compiler proves uniqueness, otherwise copies. After `own` was removed, a recount found 27 update sites proved unique and 6 not (`2026-11-03-language-restrictions.benchmark.md`). Known unproven: `Orb` and `Mine` `act`, and the `goldAdv`/`advance` test helpers.
206
+
207
+ Uniqueness today covers spreads and method parameters, not field assignments. A known false negative: `adv(Cell(..))` inlined into `main` reports "s used after its spread" although `s.x` is only read inside the spread value.
208
+
209
+ FBIP worklist: the `mut` fields left are `Slice`, `Buffer`, `Map`, `Deque`, `JsonParser`, `Action` (`elapsed`, `current`), the imperative `Actor`, `Stage` and `Text.text`, the game shells, and `Ship`/`Orb`/`Mine`/`Star` (`clock`, `vx`, `vy`) in the pure game. Removing them is the end state in which imperative and functional converge: `update(state) -> State` written in place.
210
+
211
+ Other layout facts: `Array[S]` for a struct `S` is struct-of-arrays (about 3x on a 10k-particle loop); `Array[Int]`/`Array[Float]` are unboxed typed wasm arrays; `Map` is open addressing.
212
+
213
+ ## Game entry point
214
+
215
+ Today (all three examples): three free functions, `export fun init(io) -> World`, `export fun update(io, w, dt)` and `export fun draw(io, w)`; the host roots the `init` result and calls the other two each frame. All three examples keep the same `World` shell, which owns the host handles (sounds, textures) and the held keys.
216
+
217
+ Options for after the three styles collapse into one (user, 2026-11-05: "pick when the three game styles are collapsed into ONE way to build a game"). **Undecided.** Related idea, not scheduled: no top-level functions at all, with the entry on `primitive App = fun main(io: Io)`.
218
+
219
+ | Option | Shape | For | Against |
220
+ |---|---|---|---|
221
+ | Abstract `Game` base (user's preference so far) | `struct Game = abstract fun update(self, io, dt) -> Game; abstract fun draw(self, io)`; `World` embeds `Game` and overrides both; `App.main(io) = io.run(World(..))` | typed; a missing `draw` is a compile error; no magic export names; "init" is just constructing `World` | "more OOP" (user); uses the struct's single embed slot; `update` returns the base `Game`, not the concrete type |
222
+ | Functional `io.run` | `io.run[S](state: S, update: fn(S, Io, Float) -> S, draw: fn(S, Io))`, e.g. `io.run(World.start(io), World.update, World.draw)` | typed by the call; keeps the concrete type; no base class; state-in/state-out suits FBIP and deterministic sim; tests can swap `update` | the host stores state plus two fn refs |
223
+ | `App.init/update/draw` statics | name-only contract | simple | untyped |
224
+
225
+ Trade-off summary: embedding `Game` suits the imperative style (`update` returns `self`); the `io.run` shape suits the functional style and the determinism rules (an `update` that takes no `Io` is a different function type, so the rule can be a type). Either way the host calls two generated entry points, `game.update` and `game.draw`, and `update` returns the next value, which the host keeps.
226
+
227
+ ## Open questions and decisions pending
228
+
229
+ - **One way to build a game.** Collapse the three styles into one (backlog, user 2026-11-05, idea). Not decided. Pointers from this page: functional gives free replay and snapshots, a pure `update`, and the lowest test cost; imperative gives the least allocation until FBIP exists and the simplest mental model for scene-heavy games; pure sits between and has the weakest case (retire it? backlog "Retire PureScene?", decision record §4b).
230
+ - **Game entry shape** (above), chosen with the previous item.
231
+ - **Fixed timestep, `InputFrame`, `update` without `Io`.** Planned as the deterministic-games item, after FBIP. The user's order (2026-11-05): benchmark, FBIP, deterministic sim, kids' games.
232
+ - **Benchmark prerequisites.** Run C A/B, run D loop regressions (niche `Option` or unboxed iterators), a functional 20k-node `stress_world` fixture, and a `--no-fusion` switch in the harness.
233
+ - **FBIP.** Stops for user approval before it starts. Open: `fip` as a hard compile error, and the two-level ownership model from Pony (unique and mutable vs shared and immutable) as the visible face of FBIP replacing `mut` fields.
234
+ - **Structural `==`** on structs (deep equality, deferred) versus identity; this decides how `same(a, b)` works for imperative actors.
235
+ - **Events as data.** Return an event list from `update` (functional) instead of diffing state in the shell to play sounds. Not built.
236
+ - **Lens sugar** for deep functional updates, and spread from a field path (`Stage(..w.stage, x: 1)`). Proposals only.
237
+ - **Static subtree reuse** in functional scenes, deferred by the user as a later optimisation.
238
+ - **Input open questions** ([game-scene-styles.md](game-scene-styles.md)): whether a consumed pointer event still reaches parents, local vs world coordinates in `onInput`, and whether held keys live in `UiState` or each game's state.
239
+ - **Kids' games** (last in the queue) are functional-style; they would be the first games written after the choice.
docs/plans/2026-11-05-compiler-folder-move.md ADDED
@@ -0,0 +1,66 @@
1
+ # Move the Rust crates under compiler/, rename std and examples
2
+
3
+ **Goal:** every Rust crate lives under `compiler/` and `plum-std/` → `std/`, `plum-examples/` → `examples/` (user 2026-11-04 and 2026-11-05). No behaviour change: same tests, same wasm, same CLI behaviour from the repo root.
4
+ **Architecture:** workspace Cargo.toml, all crates, loader std lookup, plum-cli build.rs, plum-game (own Cargo.lock) + web-test, plum-tooling, scripts, Makefile.toml, README/docs.
5
+ **Tech stack:** Rust 2021, cargo, git.
6
+ **Detail:** contracts
7
+ **Target-min:** 20
8
+
9
+ ## Global constraints
10
+ - Work on `master` in the main checkout, default shared root `target/`. Don't commit. Use `git mv` (keeps history). Never run `plum format` on real files. No `sed -i -E` (macOS backups; use `sed -i ''` or a script).
11
+ - Layout (ruling, coordinator): `compiler/{core,checker,driver,wasm-codegen,cli,lsp,host,test-support,runtime,game}` from `plum-core`, `plum-checker`, `plum-driver`, `plum-wasm-codegen`, `plum-cli`, `plum-lsp`, `plum-host`, `plum-test-support`, `plum-runtime`, `plum-game`. Folder names drop the `plum-` prefix; package (crate) names stay `plum-core` etc., so `use plum_core::..` and `-p plum-cli` are unchanged. `plum-tooling/` stays at the top level (editor tooling, not compiler). The workspace `Cargo.toml`, `Cargo.lock` and `target/` stay at the root.
12
+ - `import std/X` resolves to `<lib>/std/X.plum` (was `<lib>/plum-std/X.plum`); `--lib-path` keeps meaning "the directory containing std/" (repo root).
13
+ - Historical docs (`docs/plans/**`, `docs/decisions/**`) are NOT rewritten.
14
+ - No `cargo test` (hook): `cargo build --tests`; per-crate binaries in `target/debug/deps` (newest by mtime, `/bin/ls -t`). Sweep: `cargo build -p plum-cli && target/debug/plum test std examples` (662).
15
+
16
+ ## Task graph
17
+ ```mermaid
18
+ flowchart LR
19
+ T1[git mv + Cargo + Rust paths]
20
+ T2[non-Rust paths: tooling, scripts, docs]
21
+ T3[verify incl. browser]
22
+ %% taskgraph
23
+ T1 --> T2
24
+ T2 --> T3
25
+ ```
26
+ Ruling: sequential [chain] - one tree-wide move, then the paths outside Rust, then verify - cost: three tasks in a row.
27
+
28
+ ## Package A: move
29
+ **Probe entry:** run=plum test std examples
30
+
31
+ ### T1 git mv, Cargo and every Rust path · Complexity 4 · Est. context 50k
32
+ **Owns:** compiler/**, Cargo.toml, Cargo.lock, plum-core/**, plum-checker/**, plum-driver/**, plum-wasm-codegen/**, plum-cli/**, plum-lsp/**, plum-host/**, plum-test-support/**, plum-runtime/**, plum-game/**, plum-std/**, plum-examples/**, std/**, examples/**
33
+ **Components:** 2
34
+ **LOC owned:** 120
35
+ **Tests:** fast
36
+ **Model:** sonnet
37
+ **Produces:** new layout builds and tests green: `cargo build --workspace --tests`, all binaries, `target/debug/plum test std examples` 662
38
+ e.g. `ls compiler` -> `checker cli core driver game host lsp runtime test-support wasm-codegen`; `target/debug/plum run examples/numbers.plum --lib-path .` works
39
+ **Notes:** `git mv` each folder; workspace `members` → `compiler/*` paths; every crate's `path = "../plum-x"` dep → `../x`; every `CARGO_MANIFEST_DIR/..` repo-root walk gains one `..`; the loader's std lookup and any `plum-std`/`plum-examples` string in Rust src/tests → `std`/`examples`; `compiler/cli/build.rs` paths to `plum-tooling/tree-sitter-plum` and `plum-tooling/vscode-plum` (`../../plum-tooling/..`); `compiler/game` has its own Cargo.lock/workspace and web-test crate — fix their relative paths to the root crates; the runtime manifest path used by `plum build`. `grep -rn 'plum-std\|plum-examples' --include='*.rs' --include='*.toml' compiler` must be empty afterwards.
40
+ **Test code:** the Produces commands green; the e.g. commands.
41
+
42
+ ### T2 non-Rust paths: tooling, scripts, docs · Complexity 2 · Est. context 30k
43
+ **Owns:** plum-tooling/**, scripts/**, Makefile.toml, README.md, AGENTS.md, docs/*.md, docs/game-scene-styles.md, docs/self-hosting.md, docs/backlog.md, .gitignore, std/**/*.plum, examples/**/*.plum, compiler/game/web-test/*.md
44
+ **Components:** 1
45
+ **LOC owned:** 60
46
+ **Tests:** fast
47
+ **Model:** sonnet
48
+ **Consumes:** new layout builds and tests green: `cargo build --workspace --tests`, all binaries, `target/debug/plum test std examples` 662 (T1)
49
+ **Produces:** no live reference to the old paths outside historical docs
50
+ e.g. `grep -rnE 'plum-(std|examples|core|checker|driver|wasm-codegen|cli|lsp|host|test-support|runtime|game)\b/' --exclude-dir=target --exclude-dir=.git --exclude-dir=docs/plans --exclude-dir=docs/decisions --exclude-dir='.superlite*' .` -> only crate NAMES (Cargo package names, `-p plum-cli`), no folder paths
51
+ **Notes:** Makefile.toml dist tasks (`compiler/cli`, binary still `plum`), scripts/*.sh, plum-tooling/vscode-plum/package.json and tree-sitter paths, comments in .plum files that name `plum-std/..`, README/docs file links, AGENTS.md if it names folders. `.gitignore` patterns that are folder-specific.
52
+ **Test code:** that grep clean; `cargo make --list-all-steps` (or `cargo make --print-steps dist-macos`) parses; the sweep still 662.
53
+
54
+ ### T3 verify including browser · Complexity 2 · Est. context 25k
55
+ **Owns:** docs/plans/2026-11-05-compiler-folder-move.results.md
56
+ **Components:** 1
57
+ **LOC owned:** 30
58
+ **Tests:** slow
59
+ **Model:** sonnet
60
+ **Consumes:** no live reference to the old paths outside historical docs (T2)
61
+ **Produces:** move results
62
+ **Notes:** `cargo build --workspace --tests` zero warnings; every binary; sweep 662; `plum compile` every compiler/game/web-test/fixtures/*.plum; browser: stress_scene + one game per style via `cargo run --release --manifest-path compiler/game/web-test/Cargo.toml -- <file> --measure 3 --headed --json`; `PLUM_EMBED_EDITOR=1 cargo build -p plum-cli` once (build.rs asset paths); wasm hashes for examples identical to before the move (record them before T1 with the old paths). Report red, don't fix.
63
+ **Test code:** results doc.
64
+
65
+ ## Review Focus
66
+ - A: file — nothing left behind at the old paths; git sees renames (not delete + add) for moved files.
docs/plans/2026-11-05-compiler-folder-move.results.md ADDED
@@ -0,0 +1,42 @@
1
+ # Compiler folder move: T3 verification results
2
+
3
+ Verdict: green. No red items. Details below.
4
+
5
+ ## 1. Build and tests
6
+
7
+ - `cargo build --workspace --tests`: zero Rust warnings (forced rebuild by touching every `compiler/*/src/lib.rs`). The only warnings are two pre-existing C `-Wunused-function` warnings from `tree-sitter-plum` `scanner.c` (`set_raw`, `set_bytes`).
8
+ - Test binaries (run from each crate's dir), all 0 failed:
9
+ - checker 146 + 5, cli 16 + 5, core 394 + 3, driver 3 + 0, host 3, lsp 20, test-support 7, wasm-codegen 212 + 3, tree-sitter-plum 1.
10
+ - `cargo build -p plum-cli && target/debug/plum test std examples`: `662 passed, 0 failed`, `65/65 files passed`.
11
+ - `plum compile` of all 9 `compiler/game/web-test/fixtures/*.plum` (interleave, rendertarget, stress, stress_pure_scene, stress_scene, stress_state_view, stress_state_view_deep, stress_world, text_sizes): all OK; outputs deleted.
12
+ - `compiler/game` own tests (`cargo build --tests --manifest-path compiler/game/Cargo.toml`, no Rust warnings): host_test 18 passed, plum_game 5 passed, plum_game (bin) 0 tests. All green. Note: `compiler/game` is a separate cargo project and builds into its own git-ignored `compiler/game/target`, not the root `target/`.
13
+
14
+ ## 2. PLUM_EMBED_EDITOR=1 cargo build -p plum-cli
15
+
16
+ Builds. Embedded assets in build OUT_DIR are all non-empty: `plum-grammar.so` 530096 B, `plum-lsp` 5854688 B, `vscode-extension.js` 788843 B. build.rs relative paths (`../lsp/src`, `../core/src`, `../checker/src`) resolve.
17
+
18
+ ## 3. Browser (`web-test --measure 3 --headed --json`)
19
+
20
+ All PASS, exit 0, backend gpu-webgpu, no console errors reported:
21
+
22
+ | game | frames | avg ms | fps |
23
+ |---|---|---|---|
24
+ | fixtures/stress_scene.plum | 359 | 8.33 | 120.0 |
25
+ | examples/games/functional/game_state_view.plum | 359 | 8.33 | 120.0 |
26
+ | examples/games/imperative/game.plum | 360 | 8.33 | 120.0 |
27
+ | examples/games/pure/game_pure_scene.plum | 359 | 8.34 | 120.0 |
28
+
29
+ ## 4. Wasm sameness (25 files in `.superlite/wasm-before-move.txt`)
30
+
31
+ - 19 files: byte-identical (same sha16) at the new `examples/` paths.
32
+ - 6 files differ at the new paths. The cause is the source path embedded in the wasm as panic text (`unwrap on None at <path>:line:col`):
33
+ - `examples/games/imperative/{game_loop,game_scene,game}.plum` embed their own path (`plum-examples/...` before, `examples/...` now).
34
+ - `examples/games/functional/game_state_view.plum`, `examples/games/functional/kids/balloon_pop.plum` and `examples/list_literal_probe.plum` embed `std/List.plum` (and `std/game/functional/Ui.plum`) where they embedded `plum-std/...` before.
35
+ - Confirmed by recompiling with the old layout in a scratch dir (`plum-examples/` copy, plus `plum-std/` copy symlinked as `std`). All 6 then hash identical to the before-move values. The differences are path strings only, not a codegen change.
36
+ - The scratch dir was deleted.
37
+
38
+ ## 5. Git layout
39
+
40
+ - `git status`: 205 `R ` and 56 `RM` entries (renames, the `M` is a content edit). No `D`/`A` pairs. Other changes are modified docs and config (`.gitignore`, `Cargo.toml`, `Makefile.toml`, `README.md`, `docs/*`, `scripts/wasm-hashes.sh`, `plum-tooling/vscode-plum/{package.json,src/extension.ts}`). Untracked: `.claude/`, `docs/imperative-vs-functional.md`, the plan doc and `io-primitive.progress.md`.
41
+ - `ls -d plum-*` shows only `plum-tooling`.
42
+ - `git ls-files` has 0 files under the old `plum-<crate>/` folders.
docs/plans/2026-11-05-io-primitive.progress.md ADDED
@@ -0,0 +1,9 @@
1
+ - start 086f1c8
2
+ Ruling: Io = i32 0; raw* methods exposed; extern methods only on primitive Io; top-level extern kept (ask user) - cost: an unused i32 param per io fn
3
+ Ruling: skip Wave 0 - cost: none
4
+ Ruling: Io extern imports keep module 'plum' (today's), not 'env' as the plan said; T4 adds a List[Io] / generic-container test - cost: none
5
+ Ruling: F-IO-1 - top-level `extern fun` is an error everywhere (host calls only as extern methods on primitive Io); fusion reorder bug fixed in drive.rs isProducer (pre-existing) with trace test fusion_keeps_io_calls_in_order - cost: top-level extern removed from the language (in line with user's no-top-level-fns direction)
6
+ Ruling: final-review progress file / plum-game Cargo.lock - bookkeeping, committed with this run - cost: none
7
+ Ruling: final-review protocol - wasm import lists (module, name, types) of all 34 examples+games+fixtures compared before/after in T3, T4 and F-IO-1: 0 diffs (scratch norm.py) - cost: no permanent snapshot test
8
+ Ruling: final-review state - closure capturing io + generic pass[T](x, io) (T2 e2e), Io in generic container Opt[Io] (T4 e2e); Io is i32, no Io struct type exists - cost: none
9
+ Ruling: final-review suite - T5: all binaries + sweep 662 + fixtures + 4 browser runs green - cost: none
docs/self-hosting.md CHANGED
@@ -27,7 +27,7 @@ The tree-sitter grammar stays for editors (Helix, VS Code).
27
27
 
28
28
  **Host (std `Io`).** Today: printing, files as `Str`, clock, random seed, HTTP. Missing: command-line arguments,
29
29
  exit code, environment variables, stderr, binary file read/write (`.wasm` output). All host calls belong on `Io`
30
- (`plum-std/Io.plum` is the only file allowed to call an `extern`); the first step is moving `Os`/`Time.now`/`Http`
30
+ (`std/Io.plum` is the only file allowed to call an `extern`); the first step is moving `Os`/`Time.now`/`Http`
31
31
  there (groundwork plan, part 1). New host imports wait for the `plum-host` crate (compiler cleanup phase 2a, T10).
32
32
 
33
33
  **Compiler and language.**
@@ -49,7 +49,7 @@ with `{{`/`}}` and `\n \t \r \\ \" \b \f \xNN \u0000` decoded. Comments are kept
49
49
  formatter. No INDENT/DEDENT tokens (see layout).
50
50
 
51
51
  **Layout: an offside rule in the parser, not the lexer.** Plum has indented blocks inside brackets
52
- (`each(|v|` + an indented body + `)`, `plum-examples/closures.plum`) and also multi-line argument lists, which a
52
+ (`each(|v|` + an indented body + `)`, `examples/closures.plum`) and also multi-line argument lists, which a
53
53
  Python-style lexer that ignores newlines inside brackets cannot tell apart. Instead the parser keeps the current block
54
54
  column: a token that starts a line at or left of it ends the expression or block; inside brackets the rule is off until
55
55
  a nested block (a closure body) turns it on again. This is the Haskell / F# approach and keeps the lexer context-free.
@@ -174,7 +174,7 @@ crash at the place it occurs, so unrelated tests keep running.
174
174
  - **ADTs + `match`, not visitors.** `enum Expr = | Var(..) | Call(..) | Match(..) ...`. A visitor is what OOP
175
175
  languages use for missing sum types.
176
176
  - **One "map over children" function** (`mapChildren(e, f)`) instead of a visitor; a pass handles the cases it cares
177
- about and sends everything else there. Cleanup phase 2's shared walker (`plum-core/src/visit.rs`) is the Rust twin.
177
+ about and sends everything else there. Cleanup phase 2's shared walker (`compiler/core/src/visit.rs`) is the Rust twin.
178
178
  - **Node ids + side tables.** Each expression and statement has an `Int` id; types, resolved names and spans live in
179
179
  `Slice`s indexed by id (cleanup phase 4). The AST stays immutable.
180
180
  - **An explicit state record** for the checker (`record Checker(mut types: .., mut errors: ..)`), like the game's
@@ -191,7 +191,7 @@ redesign.
191
191
  1. Groundwork (planned): `Io` holds all host calls; lexer; combinator core; AST; parser that parses every `.plum`
192
192
  file in the repo.
193
193
  2. Same output as Rust: an S-expression dump of the AST from both parsers, compared over every file. Needs a dump
194
- command in `plum-cli`, so it waits for cleanup phase 2a (which owns `plum-cli/src/main.rs`).
194
+ command in `plum-cli`, so it waits for cleanup phase 2a (which owns `compiler/cli/src/main.rs`).
195
195
  3. Checker in Plum: compare the inferred type of every node, and that both compilers reject the same files at the same positions (the messages differ: only Plum's are the new reports).
196
196
  4. Codegen + wasm encoder, no optimisations: the existing `plum test` suite (687 tests) must pass on the Plum
197
197
  compiler's output. Then port fusion, TRMC, reuse.
{plum-examples → examples}/arrays.plum RENAMED
File without changes
{plum-examples → examples}/basics.plum RENAMED
File without changes
{plum-examples → examples}/closures.plum RENAMED
File without changes
{plum-examples → examples}/control_flow.plum RENAMED
File without changes
{plum-examples → examples}/errors.plum RENAMED
File without changes
{plum-examples → examples}/functional.plum RENAMED
File without changes
{plum-examples → examples}/games/assets/gameover.wav RENAMED
File without changes
{plum-examples → examples}/games/assets/hit.wav RENAMED
File without changes
{plum-examples → examples}/games/assets/music.wav RENAMED
File without changes
{plum-examples → examples}/games/assets/pickup.wav RENAMED
File without changes
{plum-examples → examples}/games/assets/sprites.png RENAMED
File without changes
{plum-examples → examples}/games/functional/game_state_view.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Orb Run as components (`plum game plum-examples/games/functional/game_state_view.plum`): the same
1
+ # Orb Run as components (`plum game examples/games/functional/game_state_view.plum`): the same
2
2
  # game as game_pure_scene.plum (same arena, rules, sprites, sounds, keys and scoring),
3
3
  # written in the component style of Elm and React but purely functional. Steer the ship
4
4
  # with the arrow keys or WASD, collect the drifting orbs and avoid the mines. Three
{plum-examples → examples}/games/functional/kids/balloon_pop.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Balloon Pop (`plum game plum-examples/games/functional/kids/balloon_pop.plum`): a
1
+ # Balloon Pop (`plum game examples/games/functional/kids/balloon_pop.plum`): a
2
2
  # game for small children. Balloons float up the sky; tap or click one to pop it.
3
3
  # A popped balloon bursts into dots and its colour's name floats up ("Red!"), so the
4
4
  # child hears the pop and sees the word. Every 10 pops a "Hooray!" party with
{plum-examples → examples}/games/imperative/game.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Orb Run: a small arcade game for the game host (`plum game plum-examples/games/imperative/game.plum`).
1
+ # Orb Run: a small arcade game for the game host (`plum game examples/games/imperative/game.plum`).
2
2
  # Steer the ship with the arrow keys or WASD, collect the drifting orbs and avoid
3
3
  # the mines. Three lives; Enter or R restarts after the game is over. The music
4
4
  # loops from `init`; orbs, mines and the end of a run each play a sound.
{plum-examples → examples}/games/imperative/game_loop.plum RENAMED
File without changes
{plum-examples → examples}/games/imperative/game_scene.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Orb Run on the scene graph (`plum game plum-examples/games/imperative/game_scene.plum`): the same game as
1
+ # Orb Run on the scene graph (`plum game examples/games/imperative/game_scene.plum`): the same game as
2
2
  # game.plum (same arena, rules, sprites, sounds, keys and scoring), rebuilt in retained
3
3
  # mode on std/game/imperative/Scene. Steer the ship with the arrow keys or WASD, collect the
4
4
  # drifting orbs and avoid the mines. Three lives; Enter or R restarts after the game
{plum-examples → examples}/games/imperative/scene.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Clockwork: a scene-graph demo (`plum game plum-examples/games/imperative/scene.plum`). A sun, four
1
+ # Clockwork: a scene-graph demo (`plum game examples/games/imperative/scene.plum`). A sun, four
2
2
  # planets and their moons orbit by nesting rotating groups (std/game/imperative/Scene): each orbit
3
3
  # is a group whose rotation carries everything inside it, so a moon only ever
4
4
  # says "I sit 40 units from my planet" and the maths composes the rest. Click a
{plum-examples → examples}/games/pure/game_pure_scene.plum RENAMED
@@ -1,4 +1,4 @@
1
- # Orb Run on the pure scene graph (`plum game plum-examples/games/pure/game_pure_scene.plum`): the same
1
+ # Orb Run on the pure scene graph (`plum game examples/games/pure/game_pure_scene.plum`): the same
2
2
  # game as game_scene.plum (same arena, rules, sprites, sounds, keys and scoring), but the
3
3
  # scene is made of values. Steer the ship with the arrow keys or WASD, collect the
4
4
  # drifting orbs and avoid the mines. Three lives; Enter, R or the "Again" button
{plum-examples → examples}/games/tools/make_sounds.py RENAMED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Generates the Orb Run sound effects and music loop into ../assets.
3
3
 
4
- Standard library only: python3 plum-examples/games/tools/make_sounds.py
4
+ Standard library only: python3 examples/games/tools/make_sounds.py
5
5
  Everything is synthesized here, so the WAVs are original and reproducible.
6
6
  All files are mono, 16-bit, 22050 Hz.
7
7
  """
{plum-examples → examples}/games/tools/make_sprites.py RENAMED
File without changes
{plum-examples → examples}/generics.plum RENAMED
File without changes
{plum-examples → examples}/json.plum RENAMED
File without changes
{plum-examples → examples}/list_literal_probe.plum RENAMED
File without changes
{plum-examples → examples}/maps.plum RENAMED
File without changes
{plum-examples → examples}/match.plum RENAMED
File without changes
{plum-examples → examples}/numbers.plum RENAMED
@@ -91,7 +91,7 @@ primitive Shelf =
91
91
  # ==== numbers tests ====
92
92
 
93
93
  # Regression coverage for the `primitive Int` / `primitive Float` methods
94
- # (`plum-std/Int.plum`, `plum-std/Float.plum`), the `Math` namespace and the
94
+ # (`std/Int.plum`, `std/Float.plum`), the `Math` namespace and the
95
95
  # associated constants. `Int` and `Float` each have their own method table.
96
96
 
97
97
  test "abs keeps the receiver's own type"
@@ -129,7 +129,7 @@ test "pow computes integer powers exactly for both Int and Float self"
129
129
 
130
130
  test "log2/log10 agree between Int and the equivalent Float"
131
131
  # `log`/`log2`/`log10` are built on the Taylor-series `ln` approximation
132
- # (see `plum-std/Math.plum`'s `ln`), not exact libm — compare within a
132
+ # (see `std/Math.plum`'s `ln`), not exact libm — compare within a
133
133
  # small epsilon rather than requiring bit-exact equality.
134
134
  a := 8.log2()
135
135
  b := 8.0.log2()
{plum-examples → examples}/recursion.plum RENAMED
File without changes
{plum-examples → examples}/sequences.plum RENAMED
File without changes
{plum-examples → examples}/strings.plum RENAMED
File without changes
{plum-examples → examples}/structs_records.plum RENAMED
File without changes
{plum-examples → examples}/subtyping.plum RENAMED
File without changes
{plum-examples → examples}/unions.plum RENAMED
File without changes
plum-checker/Cargo.toml DELETED
@@ -1,13 +0,0 @@
1
- [package]
2
- name = "plum-checker"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [dependencies]
7
- plum-core = { path = "../plum-core" }
8
-
9
- [dev-dependencies]
10
- plum-test-support = { path = "../plum-test-support" }
11
- plum-driver = { path = "../plum-driver" }
12
- tree-sitter = "0.26"
13
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
plum-cli/Cargo.toml DELETED
@@ -1,25 +0,0 @@
1
- [package]
2
- name = "plum-cli"
3
- version = "0.1.0"
4
- edition = "2021"
5
- build = "build.rs"
6
-
7
- [[bin]]
8
- name = "plum"
9
- path = "src/main.rs"
10
-
11
- [dependencies]
12
- plum-core = { path = "../plum-core" }
13
- plum-checker = { path = "../plum-checker" }
14
- plum-driver = { path = "../plum-driver" }
15
- plum-wasm-codegen = { path = "../plum-wasm-codegen" }
16
- clap = { version = "4", features = ["derive"] }
17
- anyhow = "1"
18
- tree-sitter = "0.26"
19
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
20
- toml_edit = "0.22"
21
- wasmtime = "47"
22
- plum-host = { path = "../plum-host" }
23
-
24
- [dev-dependencies]
25
- plum-test-support = { path = "../plum-test-support" }
plum-core/Cargo.toml DELETED
@@ -1,19 +0,0 @@
1
- [package]
2
- name = "plum-core"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [dependencies]
7
- tree-sitter = "0.26"
8
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
9
- topiary-core = "0.7.3"
10
- topiary-tree-sitter-facade = "0.7.3"
11
-
12
- [dev-dependencies]
13
- plum-test-support = { path = "../plum-test-support" }
14
- tempfile = "3"
15
- plum-checker = { path = "../plum-checker" }
16
- plum-driver = { path = "../plum-driver" }
17
- plum-wasm-codegen = { path = "../plum-wasm-codegen" }
18
- wasmparser = "0.220"
19
- wasmtime = "47"
plum-driver/Cargo.toml DELETED
@@ -1,11 +0,0 @@
1
- [package]
2
- name = "plum-driver"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [dependencies]
7
- plum-core = { path = "../plum-core" }
8
- plum-checker = { path = "../plum-checker" }
9
-
10
- [dev-dependencies]
11
- plum-test-support = { path = "../plum-test-support" }
plum-game/web-test/README.md DELETED
@@ -1,28 +0,0 @@
1
- # plum-web-test
2
-
3
- Browser check for `plum game` builds, driven by [playwright-rs](https://crates.io/crates/playwright-rs) 0.20.
4
- Standalone crate (own `[workspace]`, not part of the root workspace).
5
-
6
- It builds `plum-cli`, exports the game with `plum game -o`, serves the output on
7
- 127.0.0.1:0, opens it in headless Chromium (software WebGPU), clicks the canvas once, then asserts:
8
- the page set `plumRenderer` (WebGPU started), the canvas is not one uniform colour, two
9
- screenshots 1 s apart differ (the game animates), and there are no page or console errors.
10
- AudioContext state is reported; a missing running context is only a warning unless
11
- `--require-audio` is given.
12
-
13
- ## One-time setup
14
-
15
- Downloads the Chromium and WebKit builds matching the bundled Playwright driver (about 180 MB, to
16
- `~/Library/Caches/ms-playwright` on macOS; the driver itself is fetched by the crate's build script):
17
-
18
- cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers
19
-
20
- ## Usage
21
-
22
- cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/games/imperative/game.plum --screenshot /tmp/orb-run.png
23
-
24
- Options: `--browser chromium|webkit` (default chromium; WebKit is Playwright's Safari-engine build and has WebGPU, so the same render assertions apply; a WebKit build without `navigator.gpu` is checked for the "WebGPU is not supported in this browser" message and a loadable plum.wasm instead), `--expect-unsupported-webgpu` (hides `navigator.gpu` in any browser and checks that path), `--no-module-introspection` (makes `WebAssembly.Module.imports/exports/customSections` throw the TypeError Safari gives for typed GC references), `--frames-wait SECS` (default 8), `--screenshot OUT.png`, `--assets DIR` (default: the nearest `assets` folder beside the source or in a parent, e.g. `plum-examples/games/assets`), `--require-audio`, `--json` (adds a one-line JSON object with the backend and frame stats), `--headed` (a real GPU window instead of headless software WebGPU), `--measure SECS` (records requestAnimationFrame intervals in the page and prints avg / p50 / p95 / p99 ms, fps and the mean per-frame update/draw/render split; `--json` adds `update_ms`, `draw_ms`, `render_ms`) and `--uncapped` (lifts vsync so the frame time is the work per frame), `--dpr N` (Playwright `deviceScaleFactor`; also checks the backing store is CSS size x ratio), `--lose-device` (destroys the GPU device from the page after a few frames and requires the `GPU device lost (...); restored` log and resumed rendering; the page exposes the device only when the harness asks) and `--expect-pixel x,y,r,g,b[,tol]` (repeatable; the screenshot pixel at CSS (x, y), default tolerance 2). `--json` also reports `draws`, the GPU draw calls per frame. `fixtures/interleave.plum`, `text_sizes.plum` and `rendertarget.plum` exercise multi-texture batching, SDF text and render targets/blend modes. `fixtures/stress.plum` and `fixtures/stress_scene.plum` are the scenes behind [BENCHMARK.md](BENCHMARK.md).
25
- Exit code is non-zero with a message on failure.
26
-
27
- `cargo test --manifest-path plum-game/web-test/Cargo.toml` runs the same check on
28
- `plum-examples/games/imperative/game.plum`, `plum-examples/games/imperative/game_scene.plum` and `plum-examples/games/imperative/scene.plum`, plus WebKit runs; each skips with a note if its browser is not installed.
plum-test-support/Cargo.toml DELETED
@@ -1,13 +0,0 @@
1
- [package]
2
- name = "plum-test-support"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [dependencies]
7
- plum-core = { path = "../plum-core" }
8
- plum-driver = { path = "../plum-driver" }
9
- plum-wasm-codegen = { path = "../plum-wasm-codegen" }
10
- tree-sitter = "0.26"
11
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
12
- wasmparser = "0.220"
13
- wasmtime = "47"
plum-tooling/vscode-plum/package.json CHANGED
@@ -41,7 +41,7 @@
41
41
  "plum.libPath": {
42
42
  "type": "string",
43
43
  "default": "",
44
- "description": "Directory containing plum-std (an `import std/X` resolves to `<libPath>/plum-std/X.plum`). Leave empty to use plum-lsp's own build-machine-baked default."
44
+ "description": "Directory containing std (an `import std/X` resolves to `<libPath>/std/X.plum`). Leave empty to use plum-lsp's own build-machine-baked default."
45
45
  }
46
46
  }
47
47
  }
plum-tooling/vscode-plum/src/extension.ts CHANGED
@@ -12,7 +12,7 @@ export function activate(context: ExtensionContext) {
12
12
  );
13
13
 
14
14
  // `plum-lsp` falls back to a build-machine-baked default (see
15
- // plum-lsp/src/main.rs) if `--lib-path` isn't given — this setting only
15
+ // compiler/lsp/src/main.rs) if `--lib-path` isn't given — this setting only
16
16
  // needs to be set when the workspace's std lib lives somewhere else.
17
17
  const configuredLibPath = workspace.getConfiguration("plum").get<string>("libPath");
18
18
  const args = configuredLibPath ? ["--lib-path", configuredLibPath] : [];
plum-wasm-codegen/Cargo.toml DELETED
@@ -1,17 +0,0 @@
1
- [package]
2
- name = "plum-wasm-codegen"
3
- version = "0.1.0"
4
- edition = "2021"
5
-
6
- [dependencies]
7
- wasm-encoder = "0.220"
8
- plum-core = { path = "../plum-core" }
9
- plum-checker = { path = "../plum-checker" }
10
- plum-driver = { path = "../plum-driver" }
11
-
12
- [dev-dependencies]
13
- plum-test-support = { path = "../plum-test-support" }
14
- wasmparser = "0.220"
15
- tree-sitter = "0.26"
16
- tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
17
- wasmtime = "47"
scripts/wasm-hashes.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # Usage: scripts/wasm-hashes.sh REV OUT
3
- # Compiles every plum-examples/**/*.plum (most have no `fun main`, so no filter) at git REV
3
+ # Compiles every examples/**/*.plum (most have no `fun main`, so no filter) at git REV
4
4
  # (one fresh `plum compile` process per file) and writes `<sha256> <relpath>` lines to OUT,
5
5
  # sorted by relpath. Files that fail to compile print `FAIL <relpath>`.
6
6
  set -u
@@ -20,7 +20,7 @@ PLUM="$TARGET/debug/plum"
20
20
 
21
21
  TMPOUT="$WT/lines.txt"; : > "$TMPOUT"
22
22
  cd "$WT/wt"
23
- find plum-examples -name '*.plum' | LC_ALL=C sort | while read -r f; do
23
+ find examples -name '*.plum' | LC_ALL=C sort | while read -r f; do
24
24
  if "$PLUM" compile "$f" -o "$WT/out.wasm" >/dev/null 2>&1; then
25
25
  echo "$(shasum -a 256 "$WT/out.wasm" | cut -d' ' -f1) $f" >> "$TMPOUT"
26
26
  else
{plum-std → std}/Array.plum RENAMED
File without changes
{plum-std → std}/Base64.plum RENAMED
File without changes
{plum-std → std}/Bool.plum RENAMED
File without changes
{plum-std → std}/Buffer.plum RENAMED
File without changes
{plum-std → std}/Byte.plum RENAMED
File without changes
{plum-std → std}/Deque.plum RENAMED
File without changes
{plum-std → std}/Float.plum RENAMED
File without changes
{plum-std → std}/Grid.plum RENAMED
File without changes
{plum-std → std}/Heap.plum RENAMED
File without changes
{plum-std → std}/Http.plum RENAMED
File without changes
{plum-std → std}/Int.plum RENAMED
File without changes
{plum-std → std}/Io.plum RENAMED
File without changes
{plum-std → std}/Json.plum RENAMED
File without changes
{plum-std → std}/List.plum RENAMED
File without changes
{plum-std → std}/Map.plum RENAMED
File without changes
{plum-std → std}/Math.plum RENAMED
File without changes
{plum-std → std}/Mem.plum RENAMED
File without changes
{plum-std → std}/Option.plum RENAMED
File without changes
{plum-std → std}/Os.plum RENAMED
File without changes
{plum-std → std}/Pair.plum RENAMED
File without changes
{plum-std → std}/Random.plum RENAMED
File without changes
{plum-std → std}/Result.plum RENAMED
File without changes
{plum-std → std}/Set.plum RENAMED
File without changes
{plum-std → std}/Slice.plum RENAMED
File without changes
{plum-std → std}/Str.plum RENAMED
File without changes
{plum-std → std}/Testing.plum RENAMED
@@ -15,7 +15,7 @@ import std/Result
15
15
  # assert Testing.expectTrue(isEven(4))
16
16
  #
17
17
  # These are statics returning `Bool` (`assert` only takes a bare
18
- # boolean expression — see `plum-core/parser.rs`'s `assert` rule), not a new
18
+ # boolean expression — see `compiler/core/parser.rs`'s `assert` rule), not a new
19
19
  # grammar form. They exist purely to make the condition read like an
20
20
  # assertion instead of a raw comparison.
21
21
  #
{plum-std → std}/Time.plum RENAMED
File without changes
{plum-std → std}/Uuid.plum RENAMED
File without changes
{plum-std → std}/game/Affine.plum RENAMED
File without changes
{plum-std → std}/game/Color.plum RENAMED
File without changes
{plum-std → std}/game/Game.plum RENAMED
File without changes
{plum-std → std}/game/Input.plum RENAMED
File without changes
{plum-std → std}/game/Vec2.plum RENAMED
File without changes
{plum-std → std}/game/functional/Anim.plum RENAMED
File without changes
{plum-std → std}/game/functional/FunctionalScene.plum RENAMED
File without changes
{plum-std → std}/game/functional/Ui.plum RENAMED
File without changes
{plum-std → std}/game/imperative/Scene.plum RENAMED
File without changes
{plum-std → std}/game/pure/Actions.plum RENAMED
@@ -166,7 +166,7 @@ fun actPose0() -> Pose =
166
166
  Pose(x: 0.0, y: 0.0, rotation: 0.0, scaleX: 1.0, scaleY: 1.0)
167
167
 
168
168
  # Golden values for actions, taken from the previous (pure, allocating) implementation
169
- # (`git show e04834f:plum-std/game/Actions.plum`) run on the same inputs: pose, spare
169
+ # (`git show e04834f:std/game/Actions.plum`) run on the same inputs: pose, spare
170
170
  # time and done flag at fixed checkpoints, stepping a fresh action with dt 0.016 x N
171
171
  # and some large dts that span several sub-actions.
172
172
  struct GoldRun(a: Action, p: Pose, spare: Float, done: Bool)
{plum-std → std}/game/pure/PureScene.plum RENAMED
File without changes
{plum-std → std}/game/pure/SceneInput.plum RENAMED
File without changes