plum
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
- .gitignore +3 -0
- Cargo.toml +1 -1
- Makefile.toml +5 -5
- README.md +38 -38
- compiler/checker/Cargo.toml +13 -0
- {plum-checker → compiler/checker}/src/lib.rs +1 -1
- {plum-checker → compiler/checker}/src/state.rs +0 -0
- {plum-checker → compiler/checker}/src/subtyping.rs +0 -0
- {plum-checker → compiler/checker}/src/types.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/checker_tests.rs +2 -2
- {plum-checker → compiler/checker}/tests/checker/closure_hint_test.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/diagnostic_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/explicit_generics_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/closure_types.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/io_capability.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/mod.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/mutability.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/optional_unpinned.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/pattern_casing.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/primitive_generics.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/types.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/features/variant_pattern.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/fusion_pipeline_test.rs +3 -3
- {plum-checker → compiler/checker}/tests/checker/inline_variants_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/io_primitive_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/main.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/question_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/checker/subtyping_tests.rs +0 -0
- {plum-checker → compiler/checker}/tests/fixtures/custom_actor_probe2.plum +0 -0
- compiler/cli/Cargo.toml +25 -0
- {plum-cli → compiler/cli}/build.rs +4 -4
- {plum-cli → compiler/cli}/src/editor.rs +9 -9
- {plum-cli → compiler/cli}/src/game.rs +3 -3
- {plum-cli → compiler/cli}/src/main.rs +2 -2
- {plum-cli → compiler/cli}/tests/cli/assert_once_test.rs +1 -1
- {plum-cli → compiler/cli}/tests/cli/compile_tests.rs +1 -1
- {plum-cli → compiler/cli}/tests/cli/crash_test.rs +2 -2
- {plum-cli → compiler/cli}/tests/cli/explain_layout_test.rs +1 -1
- {plum-cli → compiler/cli}/tests/cli/game_web_test.rs +2 -2
- {plum-cli → compiler/cli}/tests/cli/host_stub_test.rs +1 -1
- {plum-cli → compiler/cli}/tests/cli/main.rs +0 -0
- {plum-cli → compiler/cli}/tests/cli/test_scope_test.rs +2 -2
- compiler/core/Cargo.toml +19 -0
- {plum-core → compiler/core}/src/ast.rs +0 -0
- {plum-core → compiler/core}/src/builtin_usage.rs +0 -0
- {plum-core → compiler/core}/src/closure_escape.rs +0 -0
- {plum-core → compiler/core}/src/closure_spec.rs +0 -0
- {plum-core → compiler/core}/src/ctors.rs +0 -0
- {plum-core → compiler/core}/src/diag.rs +0 -0
- {plum-core → compiler/core}/src/drive.rs +0 -0
- {plum-core → compiler/core}/src/embeds.rs +0 -0
- {plum-core → compiler/core}/src/formatter.rs +1 -1
- {plum-core → compiler/core}/src/forms.rs +0 -0
- {plum-core → compiler/core}/src/generics.rs +0 -0
- {plum-core → compiler/core}/src/layout.rs +0 -0
- {plum-core → compiler/core}/src/lib.rs +0 -0
- {plum-core → compiler/core}/src/loader.rs +25 -17
- {plum-core → compiler/core}/src/lower.rs +0 -0
- {plum-core → compiler/core}/src/parser.rs +0 -0
- {plum-core → compiler/core}/src/purity.rs +0 -0
- {plum-core → compiler/core}/src/rename.rs +0 -0
- {plum-core → compiler/core}/src/reuse.rs +0 -0
- {plum-core → compiler/core}/src/session.rs +0 -0
- {plum-core → compiler/core}/src/simplify.rs +0 -0
- {plum-core → compiler/core}/src/subtyping.rs +0 -0
- {plum-core → compiler/core}/src/tail.rs +0 -0
- {plum-core → compiler/core}/src/tests_lower.rs +1 -1
- {plum-core → compiler/core}/src/trmc.rs +0 -0
- {plum-core → compiler/core}/src/unions.rs +0 -0
- {plum-core → compiler/core}/src/uniqueness.rs +0 -0
- {plum-core → compiler/core}/src/visit.rs +0 -0
- {plum-core → compiler/core}/src/whistle.rs +0 -0
- {plum-core → compiler/core}/tests/core/closure_escape_test.rs +1 -1
- {plum-core → compiler/core}/tests/core/closure_spec_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/closure_types_parse_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/diag_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/drive_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/for_next_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/formatter_test.rs +1 -1
- {plum-core → compiler/core}/tests/core/forms_list_range_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/fused_closure_generic_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/generics_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/inline_variants_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/layout_test.rs +4 -4
- {plum-core → compiler/core}/tests/core/list_literal_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/loader_test.rs +23 -5
- {plum-core → compiler/core}/tests/core/lower_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/main.rs +0 -0
- {plum-core → compiler/core}/tests/core/match_body_parse_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/override_parse_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/parser_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/purity_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/question_parse_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/record_embed_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/record_init_parse_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/record_init_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/rename_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/session_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/simplify_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/subtyping_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/tail_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/tests_lower_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/trmc_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/unions_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/uniqueness_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/visit_test.rs +0 -0
- {plum-core → compiler/core}/tests/core/whistle_test.rs +0 -0
- compiler/driver/Cargo.toml +11 -0
- {plum-driver → compiler/driver}/src/lib.rs +0 -0
- {plum-driver → compiler/driver}/tests/driver_tests.rs +0 -0
- {plum-game → compiler/game}/.gitignore +0 -0
- {plum-game → compiler/game}/Cargo.toml +3 -3
- {plum-game → compiler/game}/src/api.rs +1 -1
- {plum-game → compiler/game}/src/host.rs +0 -0
- {plum-game → compiler/game}/src/lib.rs +0 -0
- {plum-game → compiler/game}/src/main.rs +0 -0
- {plum-game → compiler/game}/src/recorder.rs +0 -0
- {plum-game → compiler/game}/tests/fixtures/assets_fixture.plum +0 -0
- {plum-game → compiler/game}/tests/fixtures/host_fixture.plum +0 -0
- {plum-game → compiler/game}/tests/fixtures/target_fixture.plum +0 -0
- {plum-game → compiler/game}/tests/host_test.rs +7 -7
- {plum-game → compiler/game}/web-test/BENCHMARK.md +4 -4
- {plum-game → compiler/game}/web-test/Cargo.toml +0 -0
- compiler/game/web-test/README.md +28 -0
- {plum-game → compiler/game}/web-test/examples/install-browsers.rs +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/sprites.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex0.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex1.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex2.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex3.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex4.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex5.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex6.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/assets/tex7.png +0 -0
- {plum-game → compiler/game}/web-test/fixtures/interleave.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/rendertarget.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/stress.plum +1 -1
- {plum-game → compiler/game}/web-test/fixtures/stress_pure_scene.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/stress_scene.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/stress_state_view.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/stress_state_view_deep.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/stress_world.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/text_sizes.plum +0 -0
- {plum-game → compiler/game}/web-test/fixtures/tools/make_textures.py +0 -0
- {plum-game → compiler/game}/web-test/src/lib.rs +1 -1
- {plum-game → compiler/game}/web-test/src/main.rs +1 -1
- {plum-game → compiler/game}/web-test/tests/web_check.rs +15 -15
- {plum-game → compiler/game}/web/index.html +0 -0
- {plum-game → compiler/game}/web/plum_bridge.js +0 -0
- {plum-host → compiler/host}/Cargo.toml +0 -0
- {plum-host → compiler/host}/src/lib.rs +2 -2
- {plum-lsp → compiler/lsp}/Cargo.toml +4 -4
- {plum-lsp → compiler/lsp}/src/completion.rs +2 -2
- {plum-lsp → compiler/lsp}/src/main.rs +7 -7
- {plum-runtime → compiler/runtime}/.gitignore +0 -0
- {plum-runtime → compiler/runtime}/Cargo.toml +1 -1
- {plum-runtime → compiler/runtime}/src/main.rs +0 -0
- compiler/test-support/Cargo.toml +13 -0
- {plum-test-support → compiler/test-support}/src/lib.rs +1 -1
- compiler/wasm-codegen/Cargo.toml +17 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/src/lib.rs +3 -3
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/codegen_tests.rs +3 -3
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/mod.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/reuse.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_inplace.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_mem/unique_spread.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/empty_record_embed.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/enum_init.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/inline_variants.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/mod.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_embed.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/record_init.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_records/unboxed_variants.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/closure_tail.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/error_tail.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/loop_break.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/mod.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_tail/tail.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_optional.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/closure_types.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_entry.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/io_primitive.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/list_and_shadow.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/mod.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/question.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/subtyping.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/e2e_types/type_param_static.rs +0 -0
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/examples_test.rs +1 -1
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/fusion_alloc_test.rs +1 -1
- {plum-wasm-codegen → compiler/wasm-codegen}/tests/wasm_codegen/main.rs +0 -0
- docs/backlog.md +4 -4
- docs/game-scene-styles.md +1 -1
- docs/imperative-vs-functional.md +239 -0
- docs/plans/2026-11-05-compiler-folder-move.md +66 -0
- docs/plans/2026-11-05-compiler-folder-move.results.md +42 -0
- docs/plans/2026-11-05-io-primitive.progress.md +9 -0
- docs/self-hosting.md +4 -4
- {plum-examples → examples}/arrays.plum +0 -0
- {plum-examples → examples}/basics.plum +0 -0
- {plum-examples → examples}/closures.plum +0 -0
- {plum-examples → examples}/control_flow.plum +0 -0
- {plum-examples → examples}/errors.plum +0 -0
- {plum-examples → examples}/functional.plum +0 -0
- {plum-examples → examples}/games/assets/gameover.wav +0 -0
- {plum-examples → examples}/games/assets/hit.wav +0 -0
- {plum-examples → examples}/games/assets/music.wav +0 -0
- {plum-examples → examples}/games/assets/pickup.wav +0 -0
- {plum-examples → examples}/games/assets/sprites.png +0 -0
- {plum-examples → examples}/games/functional/game_state_view.plum +1 -1
- {plum-examples → examples}/games/functional/kids/balloon_pop.plum +1 -1
- {plum-examples → examples}/games/imperative/game.plum +1 -1
- {plum-examples → examples}/games/imperative/game_loop.plum +0 -0
- {plum-examples → examples}/games/imperative/game_scene.plum +1 -1
- {plum-examples → examples}/games/imperative/scene.plum +1 -1
- {plum-examples → examples}/games/pure/game_pure_scene.plum +1 -1
- {plum-examples → examples}/games/tools/make_sounds.py +1 -1
- {plum-examples → examples}/games/tools/make_sprites.py +0 -0
- {plum-examples → examples}/generics.plum +0 -0
- {plum-examples → examples}/json.plum +0 -0
- {plum-examples → examples}/list_literal_probe.plum +0 -0
- {plum-examples → examples}/maps.plum +0 -0
- {plum-examples → examples}/match.plum +0 -0
- {plum-examples → examples}/numbers.plum +2 -2
- {plum-examples → examples}/recursion.plum +0 -0
- {plum-examples → examples}/sequences.plum +0 -0
- {plum-examples → examples}/strings.plum +0 -0
- {plum-examples → examples}/structs_records.plum +0 -0
- {plum-examples → examples}/subtyping.plum +0 -0
- {plum-examples → examples}/unions.plum +0 -0
- plum-checker/Cargo.toml +0 -13
- plum-cli/Cargo.toml +0 -25
- plum-core/Cargo.toml +0 -19
- plum-driver/Cargo.toml +0 -11
- plum-game/web-test/README.md +0 -28
- plum-test-support/Cargo.toml +0 -13
- plum-tooling/vscode-plum/package.json +1 -1
- plum-tooling/vscode-plum/src/extension.ts +1 -1
- plum-wasm-codegen/Cargo.toml +0 -17
- scripts/wasm-hashes.sh +2 -2
- {plum-std → std}/Array.plum +0 -0
- {plum-std → std}/Base64.plum +0 -0
- {plum-std → std}/Bool.plum +0 -0
- {plum-std → std}/Buffer.plum +0 -0
- {plum-std → std}/Byte.plum +0 -0
- {plum-std → std}/Deque.plum +0 -0
- {plum-std → std}/Float.plum +0 -0
- {plum-std → std}/Grid.plum +0 -0
- {plum-std → std}/Heap.plum +0 -0
- {plum-std → std}/Http.plum +0 -0
- {plum-std → std}/Int.plum +0 -0
- {plum-std → std}/Io.plum +0 -0
- {plum-std → std}/Json.plum +0 -0
- {plum-std → std}/List.plum +0 -0
- {plum-std → std}/Map.plum +0 -0
- {plum-std → std}/Math.plum +0 -0
- {plum-std → std}/Mem.plum +0 -0
- {plum-std → std}/Option.plum +0 -0
- {plum-std → std}/Os.plum +0 -0
- {plum-std → std}/Pair.plum +0 -0
- {plum-std → std}/Random.plum +0 -0
- {plum-std → std}/Result.plum +0 -0
- {plum-std → std}/Set.plum +0 -0
- {plum-std → std}/Slice.plum +0 -0
- {plum-std → std}/Str.plum +0 -0
- {plum-std → std}/Testing.plum +1 -1
- {plum-std → std}/Time.plum +0 -0
- {plum-std → std}/Uuid.plum +0 -0
- {plum-std → std}/game/Affine.plum +0 -0
- {plum-std → std}/game/Color.plum +0 -0
- {plum-std → std}/game/Game.plum +0 -0
- {plum-std → std}/game/Input.plum +0 -0
- {plum-std → std}/game/Vec2.plum +0 -0
- {plum-std → std}/game/functional/Anim.plum +0 -0
- {plum-std → std}/game/functional/FunctionalScene.plum +0 -0
- {plum-std → std}/game/functional/Ui.plum +0 -0
- {plum-std → std}/game/imperative/Scene.plum +0 -0
- {plum-std → std}/game/pure/Actions.plum +1 -1
- {plum-std → std}/game/pure/PureScene.plum +0 -0
- {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 = ["
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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>/
|
|
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
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 `
|
|
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
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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
|
|
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 `
|
|
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` (`
|
|
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
|
|
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 (`
|
|
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
|
-
`
|
|
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 [`
|
|
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 [`
|
|
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
|
|
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?** [`
|
|
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`](
|
|
1216
|
-
- **Scene** ([`game_scene.plum`](
|
|
1217
|
-
- **Pure scene** ([`game_pure_scene.plum`](
|
|
1218
|
-
- **State/view** ([`game_state_view.plum`](
|
|
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 `
|
|
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`](
|
|
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
|
|
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 [`
|
|
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 [`
|
|
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 [`
|
|
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
|
-
//
|
|
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 ["
|
|
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
|
|
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")).
|
|
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 = "
|
|
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", "
|
|
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 ["../
|
|
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!("../../
|
|
12
|
+
const INDEX_HTML: &str = include_str!("../../game/web/index.html");
|
|
13
|
-
const BRIDGE_JS: &str = include_str!("../../
|
|
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
|
-
.
|
|
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
|
-
.
|
|
567
|
+
.ancestors().nth(2)
|
|
568
568
|
.expect("plum-cli should live inside the workspace root")
|
|
569
|
-
.join("
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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")).
|
|
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
|
-
.
|
|
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 = "
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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>/
|
|
16
|
+
/// `import "std/option"` resolves to `<lib_path>/std/option.plum` — the
|
|
17
|
-
/// on-disk stdlib directory is `
|
|
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
|
|
145
|
+
fn stdDirOf(lib_path: &Path) -> PathBuf {
|
|
144
|
-
|
|
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>/
|
|
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!("
|
|
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 !
|
|
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 =
|
|
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` (`
|
|
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
|
-
"/../
|
|
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")).
|
|
80
|
+
let root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap();
|
|
81
81
|
let mut files = Vec::new();
|
|
82
|
-
plumFiles(&root.join("
|
|
82
|
+
plumFiles(&root.join("std"), &mut files);
|
|
83
|
-
plumFiles(&root.join("
|
|
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("
|
|
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("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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 = "../
|
|
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 = "../
|
|
14
|
+
plum-core = { path = "../core" }
|
|
15
|
-
plum-wasm-codegen = { path = "../
|
|
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 `
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
285
|
+
let mut a = load("examples/games/imperative/game_scene.plum");
|
|
286
|
-
let mut b = load("
|
|
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
|
|
84
|
+
cargo build --manifest-path compiler/game/web-test/Cargo.toml
|
|
85
|
-
|
|
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
|
|
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 `
|
|
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 (
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
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 `
|
|
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 `
|
|
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 = "../
|
|
11
|
+
plum-core = { path = "../core" }
|
|
12
|
-
plum-checker = { path = "../
|
|
12
|
+
plum-checker = { path = "../checker" }
|
|
13
|
-
plum-driver = { path = "../
|
|
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>/
|
|
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("
|
|
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 `
|
|
693
|
+
// CONTAINING `std/` (an `import std/Bool` resolves to
|
|
694
|
-
// `<lib_path>/
|
|
694
|
+
// `<lib_path>/std/Bool.plum` — see `plum_core::loader::loadImport`),
|
|
695
|
-
// not `
|
|
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("
|
|
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("
|
|
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("
|
|
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 = "../
|
|
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 `
|
|
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` (`
|
|
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` (`
|
|
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
|
-
/// `
|
|
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!("
|
|
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("../
|
|
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("
|
|
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 `
|
|
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`** — `
|
|
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 `
|
|
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 `
|
|
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
|
-
(`
|
|
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 + `)`, `
|
|
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 (`
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
# (`
|
|
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 `
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 `
|
|
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:
|
|
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
|