plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
8445080
— Peter John
2026-11-04T16:03:20+05:30
Compiler cleanup phase 1: dead code out, small duplicates merged, shared test-support crate
Generated files not shown: Cargo.lock
- Cargo.toml +1 -1
- docs/plans/2026-11-04-compiler-cleanup-phase1.incidents.jsonl +32 -0
- docs/plans/2026-11-04-compiler-cleanup-phase1.progress.md +31 -0
- docs/plans/2026-11-04-compiler-cleanup-phase1.report.json +287 -0
- docs/plans/2026-11-04-compiler-cleanup-phase1.report.md +41 -0
- docs/plans/2026-11-04-compiler-cleanup-phase1.results.md +19 -0
- plum-checker/Cargo.toml +1 -0
- plum-checker/src/lib.rs +61 -40
- plum-checker/src/types.rs +0 -20
- plum-checker/tests/checker_tests.rs +44 -57
- plum-checker/tests/error_union_tests.rs +2 -11
- plum-checker/tests/{closure_types_tests.rs → features/closure_types.rs} +1 -7
- plum-checker/tests/{io_capability_test.rs → features/io_capability.rs} +1 -7
- plum-checker/tests/features/main.rs +10 -0
- plum-checker/tests/{mutability_test.rs → features/mutability.rs} +0 -1
- plum-checker/tests/{optional_unpinned_tests.rs → features/optional_unpinned.rs} +2 -11
- plum-checker/tests/{pattern_casing_test.rs → features/pattern_casing.rs} +1 -7
- plum-checker/tests/{primitive_generics_test.rs → features/primitive_generics.rs} +1 -10
- plum-checker/tests/{types_tests.rs → features/types.rs} +0 -0
- plum-checker/tests/{variant_pattern_tests.rs → features/variant_pattern.rs} +1 -9
- plum-checker/tests/fusion_pipeline_test.rs +2 -9
- plum-checker/tests/inline_variants_tests.rs +1 -8
- plum-checker/tests/unions_tests.rs +1 -8
- plum-cli/Cargo.toml +3 -0
- plum-core/Cargo.toml +1 -0
- plum-core/src/closure_spec.rs +0 -4
- plum-core/src/drive.rs +0 -147
- plum-core/src/parser.rs +5 -9
- plum-core/src/simplify.rs +4 -8
- plum-core/src/uniqueness.rs +1 -3
- plum-core/tests/closure_escape_test.rs +1 -6
- plum-core/tests/closure_spec_test.rs +1 -11
- plum-core/tests/forms_generic_test.rs +2 -8
- plum-core/tests/inline_variants_test.rs +1 -8
- plum-core/tests/tail_test.rs +1 -8
- plum-core/tests/trmc_test.rs +1 -11
- plum-core/tests/union_lower_test.rs +1 -12
- plum-core/tests/unions_test.rs +1 -12
- plum-test-support/Cargo.toml +13 -0
- plum-test-support/src/lib.rs +158 -0
- plum-wasm-codegen/Cargo.toml +1 -0
- plum-wasm-codegen/src/lib.rs +2 -14
- plum-wasm-codegen/tests/closure_types_e2e_test.rs +0 -109
- plum-wasm-codegen/tests/codegen_tests.rs +1 -27
- plum-wasm-codegen/tests/e2e_mem/main.rs +4 -0
- plum-wasm-codegen/tests/{reuse_e2e_test.rs → e2e_mem/reuse.rs} +5 -28
- plum-wasm-codegen/tests/{unique_inplace_e2e_test.rs → e2e_mem/unique_inplace.rs} +1 -45
- plum-wasm-codegen/tests/{unique_spread_e2e_test.rs → e2e_mem/unique_spread.rs} +5 -28
- plum-wasm-codegen/tests/e2e_records/empty_record_embed.rs +32 -0
- plum-wasm-codegen/tests/{enum_init_e2e_test.rs → e2e_records/enum_init.rs} +1 -44
- plum-wasm-codegen/tests/{inline_variants_e2e_test.rs → e2e_records/inline_variants.rs} +1 -42
- plum-wasm-codegen/tests/e2e_records/main.rs +7 -0
- plum-wasm-codegen/tests/{record_embed_e2e_test.rs → e2e_records/record_embed.rs} +1 -45
- plum-wasm-codegen/tests/{record_init_e2e_test.rs → e2e_records/record_init.rs} +2 -45
- plum-wasm-codegen/tests/{unboxed_variants_e2e_test.rs → e2e_records/unboxed_variants.rs} +1 -38
- plum-wasm-codegen/tests/{closure_tail_e2e_test.rs → e2e_tail/closure_tail.rs} +1 -39
- plum-wasm-codegen/tests/{error_tail_e2e_test.rs → e2e_tail/error_tail.rs} +1 -39
- plum-wasm-codegen/tests/{loop_break_e2e_test.rs → e2e_tail/loop_break.rs} +1 -47
- plum-wasm-codegen/tests/e2e_tail/main.rs +5 -0
- plum-wasm-codegen/tests/{tail_calls_e2e_test.rs → e2e_tail/tail.rs} +24 -47
- plum-wasm-codegen/tests/e2e_types/closure_types.rs +50 -0
- plum-wasm-codegen/tests/{io_entry_e2e_test.rs → e2e_types/io_entry.rs} +7 -24
- plum-wasm-codegen/tests/e2e_types/main.rs +6 -0
- plum-wasm-codegen/tests/{traits_e2e_test.rs → e2e_types/traits.rs} +3 -54
- plum-wasm-codegen/tests/{type_param_static_test.rs → e2e_types/type_param_static.rs} +1 -44
- plum-wasm-codegen/tests/e2e_types/unions.rs +89 -0
- plum-wasm-codegen/tests/empty_record_embed_e2e_test.rs +0 -76
- plum-wasm-codegen/tests/examples_test.rs +4 -16
- plum-wasm-codegen/tests/fusion_alloc_test.rs +2 -11
- plum-wasm-codegen/tests/tail_call_e2e_test.rs +0 -79
- plum-wasm-codegen/tests/unions_e2e_test.rs +0 -138
- scripts/wasm-hashes.sh +31 -0
Cargo.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[workspace]
|
|
2
|
-
members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp"]
|
|
2
|
+
members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support"]
|
|
3
3
|
resolver = "2"
|
|
4
4
|
|
|
5
5
|
[profile.dev.package.wasmtime]
|
docs/plans/2026-11-04-compiler-cleanup-phase1.incidents.jsonl
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Agent","target":"batch (empty)"}
|
|
2
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Bash","target":"python3 - <<'EOF' import re p='docs/plans/2026-11-04-compiler-cleanup-phase1.md' s=open(p).read() a=s.index('### T8 e2e "}
|
|
3
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: touch plum-test-support/src/lib.rs && cargo test -p plum-test-support 21 | tail -20"}
|
|
4
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cargo test -p plum-test-support --lib 21 | tail -20"}
|
|
5
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cat /private/tmp/claude-501/-Users-pyrossh-Code-plum/819f2286-17d3-4e64-9597-f59666b9fd24/tasks/bnmm"}
|
|
6
|
+
{"file": "-", "line": 0, "context": "exit code 101: warning: [email protected]: plum-tooling/vscode-plum/out/extension.js not found (run `npm install && npm run build` there); `plum editor vscode` wil", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "15:46:02"}
|
|
7
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: sed -i '/grep -q .fun main/d; s/^# Compiles every plum-examples.*/# Compiles every plum-examples /**"}
|
|
8
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: touch plum-checker/src/lib.rs && cargo test -p plum-checker --lib 21 | grep -E ^test result|FAILED"}
|
|
9
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: touch plum-checker/src/lib.rs && cargo test -p plum-checker --test unions_tests --test fusion_pipeli"}
|
|
10
|
+
{"file": "docs/", "line": 0, "context": "T5: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/backlog.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "15:47:55"}
|
|
11
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T7", "agent": "pt_review", "kind": "verified", "time": "15:50:20"}
|
|
12
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T12", "agent": "pt_review", "kind": "verified", "time": "15:50:51"}
|
|
13
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T13", "agent": "pt_review", "kind": "verified", "time": "15:50:51"}
|
|
14
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T8", "agent": "pt_review", "kind": "verified", "time": "15:50:58"}
|
|
15
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T9", "agent": "pt_review", "kind": "verified", "time": "15:50:58"}
|
|
16
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T10", "agent": "pt_review", "kind": "verified", "time": "15:55:08"}
|
|
17
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Bash","target":"S=/Users/pyrossh/.claude/plugins/cache/superlite-dev/superlite/3.1.0/skills/swarm/scripts/superlite-cli; P=docs/plans/20"}
|
|
18
|
+
{"file": "", "line": 0, "context": "verified: seam T5↔T6", "agent": "pt_review", "kind": "verified", "time": "15:55:28"}
|
|
19
|
+
{"file": "plum-checker/src/lib.rs", "line": 1511, "context": "checkUnions empty-plan shortcut equivalent for errors/TypedInfo; lowered now Some for union-declaring non-formLoops programs (TRMC/tail marking now run). Benign.", "agent": "reviewer", "kind": "verified", "resolution": "open", "confidence": "medium", "time": "15:55:33"}
|
|
20
|
+
{"file": "plum-checker/src/lib.rs", "line": 700, "context": "primitive-name helpers identical incl Unit", "agent": "reviewer", "kind": "verified", "resolution": "fixed", "confidence": "high", "time": "15:55:33"}
|
|
21
|
+
{"file": "plum-core/src/parser.rs", "line": 850, "context": "forStmts/Scoper.ctors/drive/codegen deletions: no dangling refs, tests pass", "agent": "reviewer", "kind": "verified", "resolution": "fixed", "confidence": "high", "time": "15:55:33"}
|
|
22
|
+
{"file": "", "line": 0, "context": "verified: seam T3↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
23
|
+
{"file": "", "line": 0, "context": "verified: seam T2↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
24
|
+
{"file": "", "line": 0, "context": "verified: seam T4↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
25
|
+
{"file": "", "line": 0, "context": "verified: seam T7↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
26
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
27
|
+
{"file": "", "line": 0, "context": "verified: seam T6↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
28
|
+
{"file": "", "line": 0, "context": "verified: seam T13↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
29
|
+
{"file": "", "line": 0, "context": "verified: seam T8↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
30
|
+
{"file": "", "line": 0, "context": "verified: seam T10↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
31
|
+
{"file": "", "line": 0, "context": "verified: seam T12↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
|
32
|
+
{"file": "", "line": 0, "context": "verified: seam T9↔T11", "agent": "pt_review", "kind": "verified", "time": "16:01:50"}
|
docs/plans/2026-11-04-compiler-cleanup-phase1.progress.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
- start 996aea81a29bcff9604184dbd123b60781e46f5a
|
|
2
|
+
- unblocked: dispatched T0 T1 T2 T3 T4 T5 T6 T7 T10 at 15:40:09; waiting on fixes: T8 T9 T11
|
|
3
|
+
- Ruling: [scope] `Expr::Paren` removal moves to phase 2 with the shared walker - it touches 18 files across every crate and would collide with all phase-1 owners - cost if wrong: phase 2 slightly larger
|
|
4
|
+
- Ruling: [scope] plum-game/plum-runtime test helpers wait for phase 2's host crate - not workspace members - cost: ~2 duplicate helpers survive one more phase
|
|
5
|
+
- Ruling: [scope] 19 e2e files become 4 binaries (records/types/tail/mem), not 1-2 - keeps each task inside the size band - cost: 3 extra link steps
|
|
6
|
+
- unblocked: dispatched T8 T9 T11 T12 T13 at 15:42:59; waiting on fixes: none
|
|
7
|
+
- Ruling: accept plan at 15.9k chars and predicted 29 min - test migration is sequential behind the shared crate (T5) and verification needs everything - cost: slightly larger prime
|
|
8
|
+
- tests full passed=0 failed=1 skipped=0 seconds=65.2
|
|
9
|
+
- Ruling: T5 e.g. lines corrected - Source has items not fns; mismatch message for '= True' names True not Bool, test uses '-> Bool = 1' - cost: none
|
|
10
|
+
- checkpoint T5 80ed204fdb plum-test-support crate
|
|
11
|
+
- checkpoint T2 6ec1683e3c plum-core dead code
|
|
12
|
+
- checkpoint T4 a5f74176b7 codegen dead code
|
|
13
|
+
- Ruling: [scope] byte-identical wasm check replaced - T1 found the compiler is non-deterministic (same binary, same file, 3 different hashes; same size, ~255 bytes differ, likely HashMap order) - T11 instead compares `plum test` results and per-example wasm sizes before/after - cost if wrong: an ordering-only behaviour change slips through; determinism fix queued first in phase 2
|
|
14
|
+
- Ruling: T1 hashes all 25 examples, not only those with `fun main` (only 2 have one) - cost: none
|
|
15
|
+
- checkpoint T1 d0eaeeb582 wasm hash script
|
|
16
|
+
- checkpoint T3 31fefbd2f4 checker duplicates
|
|
17
|
+
- checkpoint T7 bd0143f4c9 codegen unit tests
|
|
18
|
+
- Ruling: keep e2e g1_typed_param - it also runs the program (checker test only type-checks) - cost: one near-duplicate test
|
|
19
|
+
- checkpoint T0 9ace3171f9 Wave 0 skeleton
|
|
20
|
+
- checkpoint T12 122cc05a3e e2e types binary
|
|
21
|
+
- checkpoint T13 a8d57938fe e2e memory binary
|
|
22
|
+
- checkpoint T8 f575ee0dfb e2e records binary
|
|
23
|
+
- checkpoint T9 844a96f391 e2e tail binary
|
|
24
|
+
- Ruling: plum-core tests that call parseSourceRaw keep local helpers (they already assert no syntax error) - a parseSourceRaw variant can join the support crate in phase 2 - cost: ~15 small local helpers remain
|
|
25
|
+
- checkpoint T10 5ecd118944 core and cli tests
|
|
26
|
+
- checkpoint T6 338bc5e4a3 checker tests
|
|
27
|
+
- Ruling: accept checkUnions empty-plan shortcut setting lowered=Some (union-enum programs without for loops now also get TRMC + tail marking, like every other path) - keeping None would need the extra full re-check this removes - cost if wrong: such programs' wasm changes; T11 size/plum-test comparison will show it
|
|
28
|
+
- checkpoint T11 fed9a1d99f verify phase 1
|
|
29
|
+
- tests full passed=767 failed=0 skipped=0 seconds=67.8
|
|
30
|
+
- Ruling: final-review finding 1 (run_tests exit 101) - stale 15:44 fast-mode summary from mid-T5 ('no tests found'); full suite re-run at 16:02: 767 passed, 0 failed - byte-identical check superseded by the earlier non-determinism ruling - cost: none
|
|
31
|
+
- Ruling: final-review finding 2 (T5 not checkpointed) - backlog edit went into docs commit 42452f8 first, then T5 checkpointed as 80ed204 - cost: none
|
docs/plans/2026-11-04-compiler-cleanup-phase1.report.json
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-04T15:40:03 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 20.317,
|
|
6
|
+
"actual": 20.593
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 0.933063,
|
|
10
|
+
"actual": 2.410947
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 59.92,
|
|
14
|
+
"actual": 63.187
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 11,
|
|
18
|
+
"actual": 12
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 2.410947,
|
|
24
|
+
"rework_usd": 0,
|
|
25
|
+
"coordinator_usd": 7.821102,
|
|
26
|
+
"total_usd": 10.232049
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 7,
|
|
30
|
+
"projected_peak": 6
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T5",
|
|
35
|
+
"T10",
|
|
36
|
+
"T11"
|
|
37
|
+
],
|
|
38
|
+
"projected": [
|
|
39
|
+
"T5",
|
|
40
|
+
"T8",
|
|
41
|
+
"T11"
|
|
42
|
+
],
|
|
43
|
+
"projected_min": 20.317
|
|
44
|
+
},
|
|
45
|
+
"model_mix": {
|
|
46
|
+
"haiku": {
|
|
47
|
+
"tasks": 1,
|
|
48
|
+
"cost_usd": 0.022088
|
|
49
|
+
},
|
|
50
|
+
"sonnet": {
|
|
51
|
+
"tasks": 9,
|
|
52
|
+
"cost_usd": 2.388859
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"coordinator_cost_by_model": {
|
|
56
|
+
"opus": 7.821102
|
|
57
|
+
},
|
|
58
|
+
"quality": {
|
|
59
|
+
"tests": {
|
|
60
|
+
"passed": 767,
|
|
61
|
+
"failed": 0,
|
|
62
|
+
"skipped": 0,
|
|
63
|
+
"seconds": 67.8,
|
|
64
|
+
"mode": "full",
|
|
65
|
+
"failures": [],
|
|
66
|
+
"skipped_tests": []
|
|
67
|
+
},
|
|
68
|
+
"first_time_pass": 0,
|
|
69
|
+
"first_time_n": 0,
|
|
70
|
+
"rework_dispatches": 0,
|
|
71
|
+
"open_incidents": 2
|
|
72
|
+
},
|
|
73
|
+
"tasks": [
|
|
74
|
+
{
|
|
75
|
+
"task": "T2",
|
|
76
|
+
"model": "sonnet",
|
|
77
|
+
"pred_min": 1.559,
|
|
78
|
+
"actual_min": 12.69,
|
|
79
|
+
"delta": {
|
|
80
|
+
"abs": 11.131,
|
|
81
|
+
"pct": 714.0
|
|
82
|
+
},
|
|
83
|
+
"cost_usd": 0.271694,
|
|
84
|
+
"first_time_pass": null
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"task": "T8",
|
|
88
|
+
"model": "sonnet",
|
|
89
|
+
"pred_min": 8.729,
|
|
90
|
+
"actual_min": 1.87,
|
|
91
|
+
"delta": {
|
|
92
|
+
"abs": -6.859,
|
|
93
|
+
"pct": -78.6
|
|
94
|
+
},
|
|
95
|
+
"cost_usd": 0.289316,
|
|
96
|
+
"first_time_pass": null
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"task": "T7",
|
|
100
|
+
"model": "sonnet",
|
|
101
|
+
"pred_min": 7.385,
|
|
102
|
+
"actual_min": 1.11,
|
|
103
|
+
"delta": {
|
|
104
|
+
"abs": -6.275,
|
|
105
|
+
"pct": -85.0
|
|
106
|
+
},
|
|
107
|
+
"cost_usd": 0.090003,
|
|
108
|
+
"first_time_pass": null
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"task": "T5",
|
|
112
|
+
"model": "sonnet",
|
|
113
|
+
"pred_min": 5.548,
|
|
114
|
+
"actual_min": 11.053,
|
|
115
|
+
"delta": {
|
|
116
|
+
"abs": 5.5053,
|
|
117
|
+
"pct": 99.2
|
|
118
|
+
},
|
|
119
|
+
"cost_usd": 0.385773,
|
|
120
|
+
"first_time_pass": null
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"task": "T1",
|
|
124
|
+
"model": "sonnet",
|
|
125
|
+
"pred_min": 6.712,
|
|
126
|
+
"actual_min": 8.478,
|
|
127
|
+
"delta": {
|
|
128
|
+
"abs": 1.7663,
|
|
129
|
+
"pct": 26.3
|
|
130
|
+
},
|
|
131
|
+
"cost_usd": 0.313338,
|
|
132
|
+
"first_time_pass": null
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"task": "T10",
|
|
136
|
+
"model": "sonnet",
|
|
137
|
+
"pred_min": 4.203,
|
|
138
|
+
"actual_min": 5.888,
|
|
139
|
+
"delta": {
|
|
140
|
+
"abs": 1.6853,
|
|
141
|
+
"pct": 40.1
|
|
142
|
+
},
|
|
143
|
+
"cost_usd": 0.219117,
|
|
144
|
+
"first_time_pass": null
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"task": "T6",
|
|
148
|
+
"model": "sonnet",
|
|
149
|
+
"pred_min": 5.412,
|
|
150
|
+
"actual_min": 5.108,
|
|
151
|
+
"delta": {
|
|
152
|
+
"abs": -0.3037,
|
|
153
|
+
"pct": -5.6
|
|
154
|
+
},
|
|
155
|
+
"cost_usd": 0.276448,
|
|
156
|
+
"first_time_pass": null
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"task": "T11",
|
|
160
|
+
"model": "sonnet",
|
|
161
|
+
"pred_min": 6.04,
|
|
162
|
+
"actual_min": 5.957,
|
|
163
|
+
"delta": {
|
|
164
|
+
"abs": -0.0833,
|
|
165
|
+
"pct": -1.4
|
|
166
|
+
},
|
|
167
|
+
"cost_usd": 0.28849,
|
|
168
|
+
"first_time_pass": null
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"task": "T0",
|
|
172
|
+
"model": "haiku",
|
|
173
|
+
"pred_min": null,
|
|
174
|
+
"actual_min": 9.258,
|
|
175
|
+
"delta": {
|
|
176
|
+
"abs": null,
|
|
177
|
+
"pct": null
|
|
178
|
+
},
|
|
179
|
+
"cost_usd": 0.022088,
|
|
180
|
+
"first_time_pass": null
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"task": "T12",
|
|
184
|
+
"model": "sonnet",
|
|
185
|
+
"pred_min": null,
|
|
186
|
+
"actual_min": 1.773,
|
|
187
|
+
"delta": {
|
|
188
|
+
"abs": null,
|
|
189
|
+
"pct": null
|
|
190
|
+
},
|
|
191
|
+
"cost_usd": 0.25468,
|
|
192
|
+
"first_time_pass": null
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"cost_table": {
|
|
196
|
+
"source": "est",
|
|
197
|
+
"usd": 2.5427,
|
|
198
|
+
"tokens": 8553342,
|
|
199
|
+
"wall_min": 29896518.2263,
|
|
200
|
+
"rows": [
|
|
201
|
+
{
|
|
202
|
+
"agent": "a0f08fad93c3a8e67",
|
|
203
|
+
"model": "sonnet",
|
|
204
|
+
"wall_min": 4.3542,
|
|
205
|
+
"tokens": 206023,
|
|
206
|
+
"usd": 0.1299
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"agent": "a104ba09f2726646e",
|
|
210
|
+
"model": "sonnet",
|
|
211
|
+
"wall_min": 1.9162,
|
|
212
|
+
"tokens": 694851,
|
|
213
|
+
"usd": 0.2893
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"agent": "a3e11bc139d96a8de",
|
|
217
|
+
"model": "haiku",
|
|
218
|
+
"wall_min": 8.1456,
|
|
219
|
+
"tokens": 1371320,
|
|
220
|
+
"usd": 0.0291
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"agent": "a51faf349e873c068",
|
|
224
|
+
"model": "sonnet",
|
|
225
|
+
"wall_min": 5.1723,
|
|
226
|
+
"tokens": 715219,
|
|
227
|
+
"usd": 0.2764
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"agent": "a8132d1ccbe13d970",
|
|
231
|
+
"model": "sonnet",
|
|
232
|
+
"wall_min": 1.829,
|
|
233
|
+
"tokens": 632984,
|
|
234
|
+
"usd": 0.2547
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"agent": "aad69da1f06adbe00",
|
|
238
|
+
"model": "opus",
|
|
239
|
+
"wall_min": 0.2053,
|
|
240
|
+
"tokens": 37415,
|
|
241
|
+
"usd": 0.1318
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"agent": "ab6c6d5f6caced738",
|
|
245
|
+
"model": "sonnet",
|
|
246
|
+
"wall_min": 29896447.3746,
|
|
247
|
+
"tokens": 0,
|
|
248
|
+
"usd": 0.0
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"agent": "ad8e2d9d21df14287",
|
|
252
|
+
"model": "sonnet",
|
|
253
|
+
"wall_min": 5.9192,
|
|
254
|
+
"tokens": 538763,
|
|
255
|
+
"usd": 0.2191
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"agent": "ae5842cae60f67b3c",
|
|
259
|
+
"model": "haiku",
|
|
260
|
+
"wall_min": 9.2599,
|
|
261
|
+
"tokens": 885529,
|
|
262
|
+
"usd": 0.0221
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"agent": "aea55d2c3dd89ff7b",
|
|
266
|
+
"model": "sonnet",
|
|
267
|
+
"wall_min": 4.5867,
|
|
268
|
+
"tokens": 692060,
|
|
269
|
+
"usd": 0.2426
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"agent": "afd99d239f08bd94f",
|
|
273
|
+
"model": "sonnet",
|
|
274
|
+
"wall_min": 20.6376,
|
|
275
|
+
"tokens": 1751810,
|
|
276
|
+
"usd": 0.6018
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"agent": "affa4d645ad13b2ff",
|
|
280
|
+
"model": "sonnet",
|
|
281
|
+
"wall_min": 8.8257,
|
|
282
|
+
"tokens": 1027368,
|
|
283
|
+
"usd": 0.3459
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
}
|
docs/plans/2026-11-04-compiler-cleanup-phase1.report.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-04T15:40:03 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 20.3 20.6 +0.3 (+1%)
|
|
7
|
+
agent cost (USD, +rework) 0.93 2.41 +1.48 (+158%)
|
|
8
|
+
task-minutes 59.9 63.2 +3.3 (+6%)
|
|
9
|
+
agents 11 12 +1 (+9%)
|
|
10
|
+
cost split: agents $2.41, rework $0.00, coordinator $7.82, total $10.23
|
|
11
|
+
concurrency: peak 7 at once (projected 6); critical path actual T5 T10 T11; projected T5 > T8 > T11 (20.3 min)
|
|
12
|
+
model mix: haiku 1 tasks $0.02, sonnet 9 tasks $2.39, coordinator opus $7.82
|
|
13
|
+
quality: tests 767 passed, 0 failed; first-time pass n/a; rework dispatches 0; open incidents 2
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T2 sonnet 1.6 12.7 +11.1 (+714%) $0.272
|
|
17
|
+
T8 sonnet 8.7 1.9 -6.9 (-79%) $0.289
|
|
18
|
+
T7 sonnet 7.4 1.1 -6.3 (-85%) $0.090
|
|
19
|
+
T5 sonnet 5.5 11.1 +5.5 (+99%) $0.386
|
|
20
|
+
T1 sonnet 6.7 8.5 +1.8 (+26%) $0.313
|
|
21
|
+
(+5 smaller misses; `superlite-cli report --json` lists all)
|
|
22
|
+
|
|
23
|
+
agent cost table:
|
|
24
|
+
| agent | model | wall_min | tokens | usd |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| a0f08fad93c3a8e67 | sonnet | 4.3542 | 206023 | 0.1299 |
|
|
27
|
+
| a104ba09f2726646e | sonnet | 1.9162 | 694851 | 0.2893 |
|
|
28
|
+
| a3e11bc139d96a8de | haiku | 8.1456 | 1371320 | 0.0291 |
|
|
29
|
+
| a51faf349e873c068 | sonnet | 5.1723 | 715219 | 0.2764 |
|
|
30
|
+
| a8132d1ccbe13d970 | sonnet | 1.829 | 632984 | 0.2547 |
|
|
31
|
+
| aad69da1f06adbe00 | opus | 0.2053 | 37415 | 0.1318 |
|
|
32
|
+
| ab6c6d5f6caced738 | sonnet | 29896447.3746 | 0 | 0.0 |
|
|
33
|
+
| ad8e2d9d21df14287 | sonnet | 5.9192 | 538763 | 0.2191 |
|
|
34
|
+
| ae5842cae60f67b3c | haiku | 9.2599 | 885529 | 0.0221 |
|
|
35
|
+
| aea55d2c3dd89ff7b | sonnet | 4.5867 | 692060 | 0.2426 |
|
|
36
|
+
| afd99d239f08bd94f | sonnet | 20.6376 | 1751810 | 0.6018 |
|
|
37
|
+
| affa4d645ad13b2ff | sonnet | 8.8257 | 1027368 | 0.3459 |
|
|
38
|
+
| totals | | 29896518.2263 | 8553342 | 2.5427 |
|
|
39
|
+
cost source: est
|
|
40
|
+
|
|
41
|
+
takeaway: ran 1% slower than projected; cost 158% over; biggest miss T2
|
docs/plans/2026-11-04-compiler-cleanup-phase1.results.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Compiler cleanup phase 1: results
|
|
2
|
+
|
|
3
|
+
Baseline 996aea8 (temp worktree) vs the current working tree (uncommitted).
|
|
4
|
+
|
|
5
|
+
## Wasm
|
|
6
|
+
The compiler is non-deterministic: the same binary gives different bytes, and slightly different sizes, on every run (hash-map iteration order). A byte hash comparison is therefore meaningless; per ruling, sizes and test results were compared instead.
|
|
7
|
+
|
|
8
|
+
- `plum test plum-std plum-examples`: baseline 63/63 files, 678 passed, 0 failed; current 63/63 files, 678 passed, 0 failed. Per-test output is identical.
|
|
9
|
+
- Wasm sizes of the 25 examples: 21 identical. 4 differ (baseline -> current): functional 29679 -> 29686, generics 30210 -> 30303, unboxed_variants 43361 -> 43360, unions 20873 -> 20846. The same binary shows the same spread on repeat runs (baseline functional 29659-29706, generics 30213-30276, unions 20829-20857; current unions 20817-20846), so these are run-to-run noise, not a change.
|
|
10
|
+
|
|
11
|
+
## Workspace tests
|
|
12
|
+
`cargo build --workspace --tests`, then every test binary (53) run from its crate dir: 766 passed, 0 failed. plum-test-support's own 5 tests pass, including `checkErrorsBool`.
|
|
13
|
+
|
|
14
|
+
## Counts
|
|
15
|
+
| | 996aea8 | now |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `#[test]` fns | 763 | 765 (+2: `primitive_names`) |
|
|
18
|
+
| test binaries (`tests/*.rs` + `tests/*/main.rs`) | 69 | 47 |
|
|
19
|
+
| Rust LOC (core, checker, codegen, cli, lsp, test-support) | 53882 | 52913 |
|
plum-checker/Cargo.toml
CHANGED
|
@@ -7,5 +7,6 @@ edition = "2021"
|
|
|
7
7
|
plum-core = { path = "../plum-core" }
|
|
8
8
|
|
|
9
9
|
[dev-dependencies]
|
|
10
|
+
plum-test-support = { path = "../plum-test-support" }
|
|
10
11
|
tree-sitter = "0.26"
|
|
11
12
|
tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
plum-checker/src/lib.rs
CHANGED
|
@@ -44,8 +44,6 @@ thread_local! {
|
|
|
44
44
|
/// generic arguments are pinned like a directly-returned expression's.
|
|
45
45
|
static TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
46
46
|
static MATCH_TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
47
|
-
/// Operator expressions (address) on non-primitive operands -> the synthetic method call
|
|
48
|
-
/// (`a + b` -> `a.add(b)`) they resolve to. Kept so the cloned arguments have stable addresses.
|
|
49
47
|
/// Key of the fn whose inferred error set the pre-pass is collecting (`None` otherwise).
|
|
50
48
|
static CUR_FN: RefCell<Option<String>> = const { RefCell::new(None) };
|
|
51
49
|
/// Error names contributed to `CUR_FN`'s set by the body being checked.
|
|
@@ -54,6 +52,8 @@ thread_local! {
|
|
|
54
52
|
static ERR_EDGES: RefCell<BTreeSet<(String, String)>> = const { RefCell::new(BTreeSet::new()) };
|
|
55
53
|
/// Error-union sites (`try`, `!`, `catch`, `?:`) recorded for codegen; `None` while collecting.
|
|
56
54
|
static ERR_SITES: RefCell<Option<HashMap<usize, ErrSite>>> = const { RefCell::new(None) };
|
|
55
|
+
/// Operator expressions (address) on non-primitive operands -> the synthetic method call
|
|
56
|
+
/// (`a + b` -> `a.add(b)`) they resolve to. Kept so the cloned arguments have stable addresses.
|
|
57
57
|
static OP_CALLS: RefCell<HashMap<usize, std::rc::Rc<ast::FnCall>>> = RefCell::new(HashMap::new());
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -651,12 +651,10 @@ pub fn nominalName(t: &PlumType) -> Option<&str> {
|
|
|
651
651
|
/// a method's receiver name) to its `PlumType`. Builtin primitives get their
|
|
652
652
|
/// dedicated variant; anything else is an unmodeled `TNamed`.
|
|
653
653
|
pub fn plumTypeFromName(name: &str) -> PlumType {
|
|
654
|
+
if let Some(t) = PlumType::fromPrimitiveName(name) {
|
|
655
|
+
return t;
|
|
656
|
+
}
|
|
654
657
|
match name {
|
|
655
|
-
"Int" => PlumType::TInt,
|
|
656
|
-
"Float" => PlumType::TFloat,
|
|
657
|
-
"Bool" => PlumType::TBool,
|
|
658
|
-
"Byte" => PlumType::TByte,
|
|
659
|
-
"Unit" => PlumType::TUnit,
|
|
660
658
|
other => match NAMED_ERR_SETS.with(|m| m.borrow().get(other).cloned()) {
|
|
661
659
|
Some(names) => PlumType::TErrName(names),
|
|
662
660
|
None => PlumType::TNamed(other.to_string()),
|
|
@@ -678,11 +676,7 @@ const OWNER_KEY: &str = "$owner";
|
|
|
678
676
|
fn plumTypeToAst(t: &PlumType) -> Option<ast::Type> {
|
|
679
677
|
let named = |name: &str, generics: Vec<ast::Type>| ast::Type { name: name.to_string(), generics };
|
|
680
678
|
Some(match t {
|
|
681
|
-
PlumType::TInt => named("Int", vec![]),
|
|
682
|
-
PlumType::TFloat => named("Float", vec![]),
|
|
683
|
-
PlumType::TBool => named("Bool", vec![]),
|
|
684
|
-
|
|
679
|
+
t if t.primitiveName().is_some() => named(t.primitiveName()?, vec![]),
|
|
685
|
-
PlumType::TUnit => named("Unit", vec![]),
|
|
686
680
|
PlumType::TNamed(n) | PlumType::TParam(n) => named(n, vec![]),
|
|
687
681
|
PlumType::TApp(n, args) => {
|
|
688
682
|
named(n, args.iter().map(plumTypeToAst).collect::<Option<Vec<_>>>()?)
|
|
@@ -712,11 +706,54 @@ pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
|
|
|
712
706
|
pub fn methodReceiverName(ty: &PlumType) -> Option<String> {
|
|
713
707
|
match ty {
|
|
714
708
|
PlumType::TNamed(name) | PlumType::TApp(name, _) => Some(name.clone()),
|
|
709
|
+
// `Unit` has no methods, so it is not a receiver.
|
|
710
|
+
PlumType::TUnit => None,
|
|
711
|
+
t => t.primitiveName().map(str::to_string),
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
impl PlumType {
|
|
716
|
+
/// The source name of a builtin primitive type (`Int`, `Float`, `Bool`, `Byte`, `Unit`).
|
|
717
|
+
pub fn primitiveName(&self) -> Option<&'static str> {
|
|
718
|
+
match self {
|
|
715
|
-
|
|
719
|
+
PlumType::TInt => Some("Int"),
|
|
716
|
-
|
|
720
|
+
PlumType::TFloat => Some("Float"),
|
|
717
|
-
|
|
721
|
+
PlumType::TBool => Some("Bool"),
|
|
718
|
-
|
|
722
|
+
PlumType::TByte => Some("Byte"),
|
|
723
|
+
PlumType::TUnit => Some("Unit"),
|
|
719
|
-
|
|
724
|
+
_ => None,
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/// The builtin primitive type written as `name`; `None` for any other name.
|
|
729
|
+
pub fn fromPrimitiveName(name: &str) -> Option<PlumType> {
|
|
730
|
+
Some(match name {
|
|
731
|
+
"Int" => PlumType::TInt,
|
|
732
|
+
"Float" => PlumType::TFloat,
|
|
733
|
+
"Bool" => PlumType::TBool,
|
|
734
|
+
"Byte" => PlumType::TByte,
|
|
735
|
+
"Unit" => PlumType::TUnit,
|
|
736
|
+
_ => return None,
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#[cfg(test)]
|
|
742
|
+
mod primitive_names {
|
|
743
|
+
use super::PlumType;
|
|
744
|
+
|
|
745
|
+
#[test]
|
|
746
|
+
fn names() {
|
|
747
|
+
assert_eq!(PlumType::TInt.primitiveName(), Some("Int"));
|
|
748
|
+
assert_eq!(PlumType::fromPrimitiveName("Bool"), Some(PlumType::TBool));
|
|
749
|
+
assert_eq!(PlumType::fromPrimitiveName("Str"), None);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#[test]
|
|
753
|
+
fn roundTrip() {
|
|
754
|
+
for t in [PlumType::TInt, PlumType::TFloat, PlumType::TBool, PlumType::TByte, PlumType::TUnit] {
|
|
755
|
+
assert_eq!(PlumType::fromPrimitiveName(t.primitiveName().unwrap()), Some(t));
|
|
756
|
+
}
|
|
720
757
|
}
|
|
721
758
|
}
|
|
722
759
|
|
|
@@ -1290,16 +1327,7 @@ pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
|
1290
1327
|
if let Some(lowered) = info.lowered.as_ref() {
|
|
1291
1328
|
let mut copy = (**lowered).clone();
|
|
1292
1329
|
if !plum_core::trmc::rewrite(&mut copy).is_empty() {
|
|
1293
|
-
let saved = plum_core::unions::takePlan();
|
|
1294
|
-
plum_core::unions::beginPlan();
|
|
1295
|
-
|
|
1330
|
+
recheckKeep(copy, &mut result, &mut info);
|
|
1296
|
-
plum_core::unions::takePlan();
|
|
1297
|
-
unions::restorePlan(&saved);
|
|
1298
|
-
if r2.is_ok() {
|
|
1299
|
-
i2.lowered = Some(std::rc::Rc::new(copy));
|
|
1300
|
-
result = r2;
|
|
1301
|
-
info = i2;
|
|
1302
|
-
}
|
|
1303
1331
|
}
|
|
1304
1332
|
}
|
|
1305
1333
|
}
|
|
@@ -1381,16 +1409,6 @@ pub fn recheckKeep(copy: ast::Source, result: &mut CheckResult<()>, info: &mut T
|
|
|
1381
1409
|
/// Receiver type names of the method calls of `lowered`, for `closure_spec` (a call site
|
|
1382
1410
|
/// whose receiver has no single known type gets no hint).
|
|
1383
1411
|
fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_spec::Hints {
|
|
1384
|
-
fn head(t: &PlumType) -> Option<String> {
|
|
1385
|
-
match t {
|
|
1386
|
-
PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n.clone()),
|
|
1387
|
-
PlumType::TInt => Some("Int".to_string()),
|
|
1388
|
-
PlumType::TFloat => Some("Float".to_string()),
|
|
1389
|
-
PlumType::TBool => Some("Bool".to_string()),
|
|
1390
|
-
PlumType::TByte => Some("Byte".to_string()),
|
|
1391
|
-
_ => None,
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
1412
|
let mut inst: HashMap<usize, Vec<&PlumType>> = HashMap::new();
|
|
1395
1413
|
for ((_, addr), t) in &info.inst_expr_types {
|
|
1396
1414
|
inst.entry(*addr).or_default().push(t);
|
|
@@ -1401,10 +1419,10 @@ fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_s
|
|
|
1401
1419
|
let addr = addr as usize;
|
|
1402
1420
|
let mut names: std::collections::HashSet<Option<String>> = std::collections::HashSet::new();
|
|
1403
1421
|
if let Some(t) = info.expr_types.get(&addr) {
|
|
1404
|
-
names.insert(
|
|
1422
|
+
names.insert(methodReceiverName(t));
|
|
1405
1423
|
}
|
|
1406
1424
|
for t in inst.get(&addr).into_iter().flatten() {
|
|
1407
|
-
names.insert(
|
|
1425
|
+
names.insert(methodReceiverName(t));
|
|
1408
1426
|
}
|
|
1409
1427
|
let name = match (names.len(), names.into_iter().next()) {
|
|
1410
1428
|
(1, Some(Some(n))) => n,
|
|
@@ -1490,7 +1508,10 @@ fn checkUnions(
|
|
|
1490
1508
|
// Report errors against the original, whose addresses codegen never sees.
|
|
1491
1509
|
(result, info)
|
|
1492
1510
|
} else if empty {
|
|
1511
|
+
// Nothing to lower: the first check already covers `copy`, keep it as the program.
|
|
1512
|
+
let mut info = info;
|
|
1513
|
+
info.lowered = Some(std::rc::Rc::new(copy));
|
|
1493
|
-
|
|
1514
|
+
(result, info)
|
|
1494
1515
|
} else {
|
|
1495
1516
|
plum_core::union_lower::applyPlan(&mut copy, &plan);
|
|
1496
1517
|
plum_core::unions::beginPlan();
|
plum-checker/src/types.rs
CHANGED
|
@@ -148,26 +148,6 @@ impl TypeScheme {
|
|
|
148
148
|
|
|
149
149
|
pub type TypeEnv = BTreeMap<String, TypeScheme>;
|
|
150
150
|
|
|
151
|
-
pub struct InferState {
|
|
152
|
-
pub counter: u64,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
impl InferState {
|
|
156
|
-
pub fn new() -> Self {
|
|
157
|
-
InferState { counter: 0 }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
pub fn freshVar(&mut self) -> String {
|
|
161
|
-
let name = format!("a{}", self.counter);
|
|
162
|
-
self.counter += 1;
|
|
163
|
-
name
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
pub fn freshType(&mut self) -> PlumType {
|
|
167
|
-
PlumType::TVar(self.freshVar())
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
151
|
#[derive(Debug, Clone)]
|
|
172
152
|
pub struct CheckError {
|
|
173
153
|
pub message: String,
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
use plum_checker::types::*;
|
|
3
3
|
use plum_checker::{checkSource, plumTypeFromAst, typedSource, unify, CheckCtx};
|
|
4
4
|
use plum_core::ast::Type as AstType;
|
|
5
|
-
use plum_core::
|
|
5
|
+
use plum_core::ast::*;
|
|
6
|
+
use plum_test_support::{parse, parse as parseClean};
|
|
6
7
|
|
|
7
8
|
fn emptyCtx() -> CheckCtx<'static> {
|
|
8
9
|
use std::sync::OnceLock;
|
|
@@ -22,16 +23,6 @@ fn emptyCtx() -> CheckCtx<'static> {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
fn parse(src: &str) -> Source {
|
|
26
|
-
let mut parser = tree_sitter::Parser::new();
|
|
27
|
-
parser
|
|
28
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
29
|
-
.unwrap();
|
|
30
|
-
let tree = parser.parse(src, None).unwrap();
|
|
31
|
-
let ap = AstParser::new(src);
|
|
32
|
-
ap.parseSource(tree.root_node())
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
#[test]
|
|
36
27
|
fn astTypeIntMapsToTint() {
|
|
37
28
|
let ast_ty = AstType {
|
|
@@ -66,16 +57,6 @@ fn unifyDifferentTypesErr() {
|
|
|
66
57
|
assert!(unify(&PlumType::TInt, &PlumType::TFloat, &ctx).is_err());
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
#[test]
|
|
70
|
-
fn freshVarsAreUnique() {
|
|
71
|
-
let mut state = InferState::new();
|
|
72
|
-
let a = state.freshVar();
|
|
73
|
-
let b = state.freshVar();
|
|
74
|
-
assert_ne!(a, b);
|
|
75
|
-
assert_eq!(a, "a0");
|
|
76
|
-
assert_eq!(b, "a1");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
60
|
#[test]
|
|
80
61
|
fn monoScheme() {
|
|
81
62
|
let scheme = TypeScheme::mono(PlumType::TInt);
|
|
@@ -107,21 +88,6 @@ enum Maybe =
|
|
|
107
88
|
// Roc-style tag unions + `Try` (T2). Sources define a local `Try` inline.
|
|
108
89
|
// ---------------------------------------------------------------------------
|
|
109
90
|
|
|
110
|
-
/// Like `parse`, but asserts the parse produced no error nodes.
|
|
111
|
-
fn parseClean(src: &str) -> Source {
|
|
112
|
-
let mut parser = tree_sitter::Parser::new();
|
|
113
|
-
parser
|
|
114
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
115
|
-
.unwrap();
|
|
116
|
-
let tree = parser.parse(src, None).unwrap();
|
|
117
|
-
assert!(
|
|
118
|
-
!tree.root_node().has_error(),
|
|
119
|
-
"embedded test source has parse errors:\n{}",
|
|
120
|
-
src
|
|
121
|
-
);
|
|
122
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
123
|
-
}
|
|
124
|
-
|
|
125
91
|
fn checkSrc(body: &str) -> Result<(), Vec<CheckError>> {
|
|
126
92
|
checkSource(&parseClean(body))
|
|
127
93
|
}
|
|
@@ -307,33 +273,54 @@ fn hasInferVar(t: &PlumType) -> bool {
|
|
|
307
273
|
}
|
|
308
274
|
}
|
|
309
275
|
|
|
276
|
+
/// Every `.plum` file under `dir`, recursively, sorted.
|
|
277
|
+
fn plumFiles(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
|
278
|
+
let mut entries: Vec<_> = std::fs::read_dir(dir)
|
|
279
|
+
.unwrap()
|
|
280
|
+
.map(|e| e.unwrap().path())
|
|
281
|
+
.collect();
|
|
282
|
+
entries.sort();
|
|
283
|
+
for path in entries {
|
|
284
|
+
if path.is_dir() {
|
|
285
|
+
plumFiles(&path, out);
|
|
286
|
+
} else if path.extension().and_then(|e| e.to_str()) == Some("plum") {
|
|
287
|
+
out.push(path);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
310
292
|
#[test]
|
|
311
293
|
fn typed_source_has_no_unresolved_infer_vars() {
|
|
294
|
+
let root = plum_test_support::repoRoot();
|
|
312
|
-
|
|
295
|
+
// Files that do not load or check standalone (library modules or fixtures that
|
|
296
|
+
// need a host); add a path suffix here with a reason rather than dropping the sweep.
|
|
297
|
+
let skip: &[&str] = &[];
|
|
313
|
-
let
|
|
298
|
+
let mut files = Vec::new();
|
|
299
|
+
for dir in ["plum-std", "plum-examples"] {
|
|
300
|
+
plumFiles(&root.join(dir), &mut files);
|
|
301
|
+
}
|
|
314
302
|
let mut bad = Vec::new();
|
|
303
|
+
for path in files {
|
|
315
|
-
|
|
304
|
+
if skip.iter().any(|s| path.ends_with(s)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
316
|
-
|
|
307
|
+
let ast = match plum_core::loadAndMerge(&path, &root) {
|
|
317
|
-
|
|
308
|
+
Ok(a) => a,
|
|
309
|
+
Err(e) => {
|
|
318
|
-
|
|
310
|
+
bad.push(format!("{}: load failed: {}", path.display(), e));
|
|
319
311
|
continue;
|
|
320
312
|
}
|
|
321
|
-
|
|
313
|
+
};
|
|
322
|
-
|
|
314
|
+
let info = match typedSource(&ast) {
|
|
323
|
-
|
|
315
|
+
Ok(i) => i,
|
|
324
|
-
|
|
316
|
+
Err(e) => {
|
|
325
|
-
bad.push(format!(
|
|
326
|
-
|
|
317
|
+
bad.push(format!("{}: check failed: {}", path.display(), e[0].message));
|
|
327
|
-
path.display(),
|
|
328
|
-
e[0].message
|
|
329
|
-
));
|
|
330
|
-
|
|
318
|
+
continue;
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
let n = info.expr_types.values().filter(|t| hasInferVar(t)).count();
|
|
334
|
-
if n > 0 {
|
|
335
|
-
bad.push(format!("{}: {} unresolved", path.display(), n));
|
|
336
319
|
}
|
|
320
|
+
};
|
|
321
|
+
let n = info.expr_types.values().filter(|t| hasInferVar(t)).count();
|
|
322
|
+
if n > 0 {
|
|
323
|
+
bad.push(format!("{}: {} unresolved", path.display(), n));
|
|
337
324
|
}
|
|
338
325
|
}
|
|
339
326
|
assert!(bad.is_empty(), "{:#?}", bad);
|
plum-checker/tests/error_union_tests.rs
CHANGED
|
@@ -2,17 +2,8 @@
|
|
|
2
2
|
//! Zig-style error unions: `!T`, `error.X`, `try` / `!`, `catch`, error-set
|
|
3
3
|
//! inference and pinned sets. Sources are self-contained (no std imports).
|
|
4
4
|
use plum_checker::{checkSource, typedSource, ErrSiteKind};
|
|
5
|
-
use plum_core::
|
|
5
|
+
use plum_core::AstParser;
|
|
6
|
-
|
|
7
|
-
fn parseClean(src: &str) -> Source {
|
|
8
|
-
|
|
6
|
+
use plum_test_support::parse as parseClean;
|
|
9
|
-
parser
|
|
10
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
11
|
-
.unwrap();
|
|
12
|
-
let tree = parser.parse(src, None).unwrap();
|
|
13
|
-
assert!(!tree.root_node().has_error(), "parse errors in:\n{}", src);
|
|
14
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
15
|
-
}
|
|
16
7
|
|
|
17
8
|
fn check(src: &str) -> Result<(), String> {
|
|
18
9
|
checkSource(&parseClean(src)).map_err(|errs| {
|
plum-checker/tests/{closure_types_tests.rs → features/closure_types.rs}
RENAMED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! Closure parameter types: declared (`|b: Box|`), inferred from the first call of a
|
|
3
2
|
//! closure stored in a local, or an error asking for the annotation.
|
|
4
3
|
use plum_checker::checkSource;
|
|
5
|
-
use plum_core::AstParser;
|
|
6
4
|
|
|
7
5
|
const BOX: &str = "record Box(v: Int) =\n fun get(self) -> Int = self.v\n\n";
|
|
8
6
|
|
|
9
7
|
fn check(body: &str) -> Result<(), Vec<String>> {
|
|
10
8
|
let src = format!("{BOX}{body}\n");
|
|
11
|
-
let
|
|
9
|
+
let source = plum_test_support::parse(&src);
|
|
12
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
13
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
14
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
15
|
-
let source = AstParser::new(&src).parseSource(tree.root_node());
|
|
16
10
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect())
|
|
17
11
|
}
|
|
18
12
|
|
plum-checker/tests/{io_capability_test.rs → features/io_capability.rs}
RENAMED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! The `Io` capability: host externs are callable only through `Io`.
|
|
3
2
|
use plum_checker::checkSource;
|
|
4
3
|
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
|
|
5
|
-
use plum_core::AstParser;
|
|
6
4
|
|
|
7
5
|
fn run(src: &str) -> Result<(), String> {
|
|
8
|
-
let mut
|
|
6
|
+
let mut source = plum_test_support::parse(src);
|
|
9
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
10
|
-
let tree = parser.parse(src, None).unwrap();
|
|
11
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
12
|
-
let mut source = AstParser::new(src).parseSource(tree.root_node());
|
|
13
7
|
let errs = hoistInlineVariants(&mut source);
|
|
14
8
|
assert!(errs.is_empty(), "{errs:?}");
|
|
15
9
|
let errs = resolveMembers(&mut source);
|
plum-checker/tests/features/main.rs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Small checker feature tests, one module per feature.
|
|
3
|
+
mod closure_types;
|
|
4
|
+
mod io_capability;
|
|
5
|
+
mod mutability;
|
|
6
|
+
mod optional_unpinned;
|
|
7
|
+
mod pattern_casing;
|
|
8
|
+
mod primitive_generics;
|
|
9
|
+
mod types;
|
|
10
|
+
mod variant_pattern;
|
plum-checker/tests/{mutability_test.rs → features/mutability.rs}
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! Field immutability: assigning a field not declared `mut` is an error.
|
|
3
2
|
use plum_checker::checkSource;
|
|
4
3
|
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
|
plum-checker/tests/{optional_unpinned_tests.rs → features/optional_unpinned.rs}
RENAMED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! Inline `if` / `match` with a `null` branch yield `T?`; a generic call whose type
|
|
3
2
|
//! params stay unknown is an error at the call.
|
|
4
3
|
use plum_checker::checkSource;
|
|
5
|
-
use plum_core::AstParser;
|
|
6
4
|
|
|
7
5
|
fn check(src: &str) -> Result<(), Vec<String>> {
|
|
8
|
-
let
|
|
6
|
+
let source = plum_test_support::parse(src);
|
|
9
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
10
|
-
let tree = parser.parse(src, None).unwrap();
|
|
11
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
12
|
-
let source = AstParser::new(src).parseSource(tree.root_node());
|
|
13
7
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect())
|
|
14
8
|
}
|
|
15
9
|
|
|
@@ -56,10 +50,7 @@ fn annotated_bare_tag_ok() {
|
|
|
56
50
|
#[test]
|
|
57
51
|
fn generic_funstar_wired() {
|
|
58
52
|
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/g3_generic_funstar.plum")).unwrap();
|
|
59
|
-
let
|
|
53
|
+
let source = plum_test_support::parse(&src);
|
|
60
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
61
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
62
|
-
let source = AstParser::new(&src).parseSource(tree.root_node());
|
|
63
54
|
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>());
|
|
64
55
|
assert!(r.is_ok(), "{r:?}");
|
|
65
56
|
}
|
plum-checker/tests/{pattern_casing_test.rs → features/pattern_casing.rs}
RENAMED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! Pattern casing: Capitalised = tag, lowercase = binder.
|
|
3
2
|
use plum_checker::checkSource;
|
|
4
3
|
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
|
|
5
|
-
use plum_core::AstParser;
|
|
6
4
|
|
|
7
5
|
const DEFS: &str = "\
|
|
8
6
|
enum Act = Stop | MoveBy(dx: Int, dy: Int)
|
|
@@ -13,11 +11,7 @@ enum Shape = Circle | Dot
|
|
|
13
11
|
|
|
14
12
|
fn run(body: &str) -> Result<(), String> {
|
|
15
13
|
let src = format!("{DEFS}\n{body}\n");
|
|
16
|
-
let mut
|
|
14
|
+
let mut source = plum_test_support::parse(&src);
|
|
17
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
18
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
19
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
20
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
21
15
|
let errs = hoistInlineVariants(&mut source);
|
|
22
16
|
assert!(errs.is_empty(), "{errs:?}");
|
|
23
17
|
let errs = resolveMembers(&mut source);
|
plum-checker/tests/{primitive_generics_test.rs → features/primitive_generics.rs}
RENAMED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
use plum_checker::{buildGlobalTables, checkSource};
|
|
3
2
|
|
|
4
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
5
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
6
|
-
}
|
|
7
|
-
|
|
8
3
|
fn load(name: &str, body: &str) -> plum_core::ast::Source {
|
|
9
|
-
let dir = std::env::temp_dir().join(format!("plum_prim_generics_{}", std::process::id()));
|
|
10
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
11
|
-
let path = dir.join(format!("{name}.plum"));
|
|
12
|
-
|
|
4
|
+
plum_test_support::load(name, body).unwrap()
|
|
13
|
-
plum_core::loadAndMerge(&path, &repoRoot()).unwrap()
|
|
14
5
|
}
|
|
15
6
|
|
|
16
7
|
#[test]
|
plum-checker/tests/{types_tests.rs → features/types.rs}
RENAMED
|
File without changes
|
plum-checker/tests/{variant_pattern_tests.rs → features/variant_pattern.rs}
RENAMED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! A bare variant name in a pattern: valid for payload-less variants and union-enum
|
|
3
2
|
//! members, a positioned error for an inline variant that has fields.
|
|
4
3
|
use plum_checker::checkSource;
|
|
5
4
|
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
|
|
6
|
-
use plum_core::AstParser;
|
|
7
5
|
|
|
8
6
|
const DEFS: &str = "\
|
|
9
7
|
enum Act = Stop | MoveBy(dx: Int, dy: Int)
|
|
@@ -14,13 +12,7 @@ enum Shape = Circle | Dot
|
|
|
14
12
|
|
|
15
13
|
fn run(body: &str) -> Result<(), Vec<String>> {
|
|
16
14
|
let src = format!("{DEFS}\n{body}\n");
|
|
17
|
-
let mut
|
|
15
|
+
let mut source = plum_test_support::parse(&src);
|
|
18
|
-
parser
|
|
19
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
20
|
-
.unwrap();
|
|
21
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
22
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
23
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
24
16
|
let errs = hoistInlineVariants(&mut source);
|
|
25
17
|
assert!(errs.is_empty(), "{errs:?}");
|
|
26
18
|
let errs = resolveMembers(&mut source);
|
plum-checker/tests/fusion_pipeline_test.rs
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
//! The supercompiler stage of `typedSource`: switches, explain notes, re-check fallback.
|
|
3
3
|
use plum_checker::{checkSource, fusionRewrite, recheckKeep, typedSource};
|
|
4
|
-
use plum_core::
|
|
4
|
+
use plum_core::ast::Source;
|
|
5
|
-
|
|
6
|
-
fn parse(src: &str) -> Source {
|
|
7
|
-
|
|
5
|
+
use plum_test_support::parse;
|
|
8
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
9
|
-
let tree = parser.parse(src, None).unwrap();
|
|
10
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
11
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
12
|
-
}
|
|
13
6
|
|
|
14
7
|
const SRC: &str = "\
|
|
15
8
|
enum Cells =
|
plum-checker/tests/inline_variants_tests.rs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
//! type whose fields can be read with `.field`.
|
|
5
5
|
use plum_checker::checkSource;
|
|
6
6
|
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan, UnionPlan};
|
|
7
|
-
use plum_core::AstParser;
|
|
8
7
|
|
|
9
8
|
const DEFS: &str = "\
|
|
10
9
|
enum Link[T] = Nil | Cons(head: T, tail: Link[T])
|
|
@@ -15,13 +14,7 @@ enum Shape = Circle | Dot
|
|
|
15
14
|
|
|
16
15
|
fn run(defs: &str, body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
17
16
|
let src = format!("{defs}\n{body}\n");
|
|
18
|
-
let mut
|
|
17
|
+
let mut source = plum_test_support::parse(&src);
|
|
19
|
-
parser
|
|
20
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
21
|
-
.unwrap();
|
|
22
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
23
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
24
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
25
18
|
let errs = hoistInlineVariants(&mut source);
|
|
26
19
|
assert!(errs.is_empty(), "{errs:?}");
|
|
27
20
|
let errs = resolveMembers(&mut source);
|
plum-checker/tests/unions_tests.rs
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
//! Valid-program behavior at runtime is covered by the Plum tests.
|
|
6
6
|
use plum_checker::checkSource;
|
|
7
7
|
use plum_core::unions::{beginPlan, resolveMembers, takePlan, UnionPlan};
|
|
8
|
-
use plum_core::AstParser;
|
|
9
8
|
|
|
10
9
|
const COERCE: &str = "\
|
|
11
10
|
record Circle(r: Float)
|
|
@@ -86,13 +85,7 @@ enum Js = Circle | Wrap[Js]
|
|
|
86
85
|
|
|
87
86
|
fn run(defs: &str, body: &str) -> (Result<(), Vec<String>>, UnionPlan) {
|
|
88
87
|
let src = format!("{defs}\n{body}\n");
|
|
89
|
-
let mut
|
|
88
|
+
let mut source = plum_test_support::parse(&src);
|
|
90
|
-
parser
|
|
91
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
92
|
-
.unwrap();
|
|
93
|
-
let tree = parser.parse(&src, None).unwrap();
|
|
94
|
-
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
95
|
-
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
96
89
|
let errs = resolveMembers(&mut source);
|
|
97
90
|
assert!(errs.is_empty(), "{errs:?}");
|
|
98
91
|
beginPlan();
|
plum-cli/Cargo.toml
CHANGED
|
@@ -19,3 +19,6 @@ tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
|
|
19
19
|
toml_edit = "0.22"
|
|
20
20
|
wasmtime = "47"
|
|
21
21
|
ureq = "2.10"
|
|
22
|
+
|
|
23
|
+
[dev-dependencies]
|
|
24
|
+
plum-test-support = { path = "../plum-test-support" }
|
plum-core/Cargo.toml
CHANGED
|
@@ -10,6 +10,7 @@ topiary-core = "0.7.3"
|
|
|
10
10
|
topiary-tree-sitter-facade = "0.7.3"
|
|
11
11
|
|
|
12
12
|
[dev-dependencies]
|
|
13
|
+
plum-test-support = { path = "../plum-test-support" }
|
|
13
14
|
tempfile = "3"
|
|
14
15
|
plum-checker = { path = "../plum-checker" }
|
|
15
16
|
plum-wasm-codegen = { path = "../plum-wasm-codegen" }
|
plum-core/src/closure_spec.rs
CHANGED
|
@@ -19,10 +19,6 @@ pub const MAX_COPIES: usize = 8;
|
|
|
19
19
|
/// method name) -> type name. Supplied by the checker, which knows the types.
|
|
20
20
|
pub type Hints = HashMap<(String, usize, usize, String), String>;
|
|
21
21
|
|
|
22
|
-
pub fn specialise(src: &mut Source) -> Vec<String> {
|
|
23
|
-
specialiseWith(src, &Hints::new())
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
/// The method call sites of `src` (hint key, address of the receiver expression), so the
|
|
27
23
|
/// checker can look the receivers' types up.
|
|
28
24
|
pub fn methodSites(src: &Source) -> Vec<((String, usize, usize, String), *const Expr)> {
|
plum-core/src/drive.rs
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
//! use; a result used twice is left alone (note "shared result").
|
|
9
9
|
//!
|
|
10
10
|
//! `drive` returns notes: "fused <name>", "shared result ...", "whistle ...", "cap ...".
|
|
11
|
-
#![allow(dead_code)]
|
|
12
11
|
use crate::ast::*;
|
|
13
12
|
use crate::simplify::simplifyBlock;
|
|
14
13
|
use crate::whistle::embeds;
|
|
@@ -2268,149 +2267,3 @@ fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
|
|
|
2268
2267
|
_ => vec![],
|
|
2269
2268
|
}
|
|
2270
2269
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
/// Scope-aware renamer: a declaration renames only the uses in its own scope (the rest of its
|
|
2274
|
-
/// block and nested blocks/closures); uses before it, or in sibling scopes, keep the outer name.
|
|
2275
|
-
struct Scoper<'a> {
|
|
2276
|
-
scopes: Vec<HashMap<String, String>>,
|
|
2277
|
-
k: usize,
|
|
2278
|
-
ctors: &'a HashMap<String, Vec<String>>,
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
impl Scoper<'_> {
|
|
2282
|
-
fn declare(&mut self, n: &mut String) {
|
|
2283
|
-
let new = format!("{}__s{}", n, self.k);
|
|
2284
|
-
self.scopes.last_mut().unwrap().insert(n.clone(), new.clone());
|
|
2285
|
-
*n = new;
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
fn lookup(&self, n: &mut String) {
|
|
2289
|
-
if let Some(new) = self.scopes.iter().rev().find_map(|s| s.get(n.as_str())) {
|
|
2290
|
-
*n = new.clone();
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
fn pattern(&mut self, p: &mut CasePattern) {
|
|
2295
|
-
match p {
|
|
2296
|
-
CasePattern::Name(n) => {
|
|
2297
|
-
if !isTagName(n) {
|
|
2298
|
-
self.declare(n)
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
CasePattern::Class { fields, .. } => fields.iter_mut().for_each(|f| self.pattern(f)),
|
|
2302
|
-
_ => {}
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
fn stmts(&mut self, stmts: &mut [Stmt]) {
|
|
2307
|
-
self.scopes.push(HashMap::new());
|
|
2308
|
-
for s in stmts {
|
|
2309
|
-
self.stmt(s);
|
|
2310
|
-
}
|
|
2311
|
-
self.scopes.pop();
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
fn stmt(&mut self, s: &mut Stmt) {
|
|
2315
|
-
match s {
|
|
2316
|
-
Stmt::Assign(a) => {
|
|
2317
|
-
for t in &mut a.targets {
|
|
2318
|
-
if let AssignTarget::Field(obj, _) = t {
|
|
2319
|
-
self.expr(obj);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
a.values.iter_mut().for_each(|v| self.expr(v));
|
|
2323
|
-
let decl = a.declare;
|
|
2324
|
-
for t in &mut a.targets {
|
|
2325
|
-
if let AssignTarget::Var(n) = t {
|
|
2326
|
-
if decl {
|
|
2327
|
-
self.declare(n)
|
|
2328
|
-
} else {
|
|
2329
|
-
self.lookup(n)
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
Stmt::ForLoop(f) => {
|
|
2335
|
-
self.expr(&mut f.source);
|
|
2336
|
-
self.scopes.push(HashMap::new());
|
|
2337
|
-
f.binders.iter_mut().for_each(|n| self.declare(n));
|
|
2338
|
-
for c in &mut f.clauses {
|
|
2339
|
-
match c {
|
|
2340
|
-
ForClause::If(e) => self.expr(e),
|
|
2341
|
-
ForClause::Bind(n, e) => {
|
|
2342
|
-
self.expr(e);
|
|
2343
|
-
self.declare(n);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
self.stmts(&mut f.body.stmts);
|
|
2348
|
-
self.scopes.pop();
|
|
2349
|
-
if let Some((n, decl)) = &mut f.result {
|
|
2350
|
-
if *decl {
|
|
2351
|
-
self.declare(n)
|
|
2352
|
-
} else {
|
|
2353
|
-
self.lookup(n)
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
Stmt::Match(m) => {
|
|
2358
|
-
m.subjects.iter_mut().for_each(|e| self.expr(e));
|
|
2359
|
-
for c in &mut m.cases {
|
|
2360
|
-
self.scopes.push(HashMap::new());
|
|
2361
|
-
c.patterns.iter_mut().for_each(|p| self.pattern(p));
|
|
2362
|
-
if let Some(g) = &mut c.guard {
|
|
2363
|
-
self.expr(g);
|
|
2364
|
-
}
|
|
2365
|
-
self.stmts(&mut c.body.stmts);
|
|
2366
|
-
self.scopes.pop();
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
_ => {
|
|
2370
|
-
for e in stmtExprs(s) {
|
|
2371
|
-
self.expr(e);
|
|
2372
|
-
}
|
|
2373
|
-
for b in stmtBlocks(s) {
|
|
2374
|
-
self.stmts(&mut b.stmts);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
fn expr(&mut self, e: &mut Expr) {
|
|
2381
|
-
match e {
|
|
2382
|
-
Expr::Var(n) => self.lookup(n),
|
|
2383
|
-
Expr::Bind(b) => {
|
|
2384
|
-
self.expr(&mut b.value);
|
|
2385
|
-
self.pattern(&mut b.case.patterns[0]);
|
|
2386
|
-
if let Some(g) = &mut b.case.guard {
|
|
2387
|
-
self.expr(g);
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
Expr::Closure(c) => {
|
|
2391
|
-
self.scopes.push(HashMap::new());
|
|
2392
|
-
c.params.iter_mut().for_each(|p| self.declare(p));
|
|
2393
|
-
self.stmts(&mut c.body.stmts);
|
|
2394
|
-
self.scopes.pop();
|
|
2395
|
-
}
|
|
2396
|
-
Expr::Catch(c) => {
|
|
2397
|
-
let CatchExpr { value, handler, .. } = &mut **c;
|
|
2398
|
-
self.expr(value);
|
|
2399
|
-
match handler {
|
|
2400
|
-
CatchHandler::Default(d) => self.expr(d),
|
|
2401
|
-
CatchHandler::Block { binder, body } => {
|
|
2402
|
-
self.scopes.push(HashMap::new());
|
|
2403
|
-
self.declare(binder);
|
|
2404
|
-
self.stmts(&mut body.stmts);
|
|
2405
|
-
self.scopes.pop();
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
_ => {
|
|
2410
|
-
for ch in childExprs(e) {
|
|
2411
|
-
self.expr(ch);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
}
|
plum-core/src/parser.rs
CHANGED
|
@@ -850,13 +850,15 @@ impl<'a> AstParser<'a> {
|
|
|
850
850
|
let mut stmts = Vec::new();
|
|
851
851
|
for n in node.named_children(&mut cursor) {
|
|
852
852
|
let u = self.unwrapExprNode(n);
|
|
853
|
+
// Every `for` stays a `Stmt::ForLoop` until `forms::lowerForms` knows which
|
|
854
|
+
// `fun*`s exist.
|
|
853
855
|
if u.kind() == "for" {
|
|
854
|
-
stmts.
|
|
856
|
+
stmts.push(Stmt::ForLoop(self.parseForLoop(u)));
|
|
855
857
|
} else if let Some(fl) = self.assignedFor(u) {
|
|
856
|
-
stmts.
|
|
858
|
+
stmts.push(Stmt::ForLoop(fl));
|
|
857
859
|
} else if let Some((mut fl, name)) = self.returnedFor(u) {
|
|
858
860
|
fl.result = Some((name.clone(), true));
|
|
859
|
-
stmts.
|
|
861
|
+
stmts.push(Stmt::ForLoop(fl));
|
|
860
862
|
stmts.push(Stmt::Return(Some(Expr::Var(name))));
|
|
861
863
|
} else if let Some(s) = self.parseStmt(n) {
|
|
862
864
|
stmts.push(s);
|
|
@@ -898,12 +900,6 @@ impl<'a> AstParser<'a> {
|
|
|
898
900
|
Some(fl)
|
|
899
901
|
}
|
|
900
902
|
|
|
901
|
-
/// Every `for` stays a `Stmt::ForLoop` until `forms::lowerForms` knows which
|
|
902
|
-
/// `fun*`s exist.
|
|
903
|
-
fn forStmts(&self, f: ForLoop) -> Vec<Stmt> {
|
|
904
|
-
vec![Stmt::ForLoop(f)]
|
|
905
|
-
}
|
|
906
|
-
|
|
907
903
|
fn parseStmt(&self, node: Node) -> Option<Stmt> {
|
|
908
904
|
let node = self.unwrapExprNode(node);
|
|
909
905
|
Some(match node.kind() {
|
plum-core/src/simplify.rs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
//! declined), never duplicated or dropped; every binder of an inlined body is renamed fresh.
|
|
5
5
|
//! Plum has no `match` expression: a match is a statement, so the match rewrites work on
|
|
6
6
|
//! blocks (`simplifyBlock`); `simplifyExpr` covers the expression-level rewrites.
|
|
7
|
-
#![allow(dead_code)]
|
|
8
7
|
use crate::ast::*;
|
|
9
8
|
use std::collections::{HashMap, HashSet};
|
|
10
9
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
@@ -348,7 +347,6 @@ impl<'a> Cx<'a> {
|
|
|
348
347
|
let mut binders: HashSet<String> = f.params.iter().map(|p| p.name.clone()).collect();
|
|
349
348
|
let mut assigned: HashSet<String> = HashSet::new();
|
|
350
349
|
let mut declared: HashSet<String> = HashSet::new();
|
|
351
|
-
let ctors = &self.ctors;
|
|
352
350
|
renameStmts(&mut body.stmts, &mut |n, kind| match kind {
|
|
353
351
|
Name::Decl => {
|
|
354
352
|
binders.insert(n.clone());
|
|
@@ -370,7 +368,7 @@ impl<'a> Cx<'a> {
|
|
|
370
368
|
let initial: HashMap<String, String> = f.params.iter().map(|p| &p.name)
|
|
371
369
|
.map(|b| (b.clone(), format!("{}__s{}", b, k)))
|
|
372
370
|
.collect();
|
|
373
|
-
Scoper { scopes: vec![initial], k
|
|
371
|
+
Scoper { scopes: vec![initial], k }.stmts(&mut body.stmts);
|
|
374
372
|
let mut subst: HashMap<String, Expr> = HashMap::new();
|
|
375
373
|
let mut binds = Vec::new();
|
|
376
374
|
let mut nontrivial = 0;
|
|
@@ -595,7 +593,6 @@ impl<'a> Cx<'a> {
|
|
|
595
593
|
let mut binders: HashSet<String> = binds.iter().filter_map(|(n, _)| n.clone()).collect();
|
|
596
594
|
let mut assigned: HashSet<String> = HashSet::new();
|
|
597
595
|
let mut declared: HashSet<String> = HashSet::new();
|
|
598
|
-
let ctors = &self.ctors;
|
|
599
596
|
renameStmts(&mut body.stmts, &mut |n, kind| match kind {
|
|
600
597
|
Name::Decl => {
|
|
601
598
|
binders.insert(n.clone());
|
|
@@ -617,7 +614,7 @@ impl<'a> Cx<'a> {
|
|
|
617
614
|
let initial: HashMap<String, String> = binds.iter().filter_map(|(n, _)| n.as_ref())
|
|
618
615
|
.map(|b| (b.clone(), format!("{}__s{}", b, k)))
|
|
619
616
|
.collect();
|
|
620
|
-
Scoper { scopes: vec![initial], k
|
|
617
|
+
Scoper { scopes: vec![initial], k }.stmts(&mut body.stmts);
|
|
621
618
|
let mut out = Vec::new();
|
|
622
619
|
let mut subst: HashMap<String, Expr> = HashMap::new();
|
|
623
620
|
for (i, (n, v)) in binds.into_iter().enumerate() {
|
|
@@ -1048,13 +1045,12 @@ fn stmtBlockRefs(s: &Stmt) -> Vec<&Block> {
|
|
|
1048
1045
|
|
|
1049
1046
|
/// Scope-aware renamer: a declaration renames only the uses in its own scope (the rest of its
|
|
1050
1047
|
/// block and nested blocks/closures); uses before it, or in sibling scopes, keep the outer name.
|
|
1051
|
-
struct Scoper
|
|
1048
|
+
struct Scoper {
|
|
1052
1049
|
scopes: Vec<HashMap<String, String>>,
|
|
1053
1050
|
k: usize,
|
|
1054
|
-
ctors: &'a HashMap<String, Vec<String>>,
|
|
1055
1051
|
}
|
|
1056
1052
|
|
|
1057
|
-
impl Scoper
|
|
1053
|
+
impl Scoper {
|
|
1058
1054
|
fn declare(&mut self, n: &mut String) {
|
|
1059
1055
|
let new = format!("{}__s{}", n, self.k);
|
|
1060
1056
|
self.scopes.last_mut().unwrap().insert(n.clone(), new.clone());
|
plum-core/src/uniqueness.rs
CHANGED
|
@@ -536,7 +536,7 @@ impl An {
|
|
|
536
536
|
return Err(why.unwrap_or_else(|| format!("a caller of {} may pass a shared {v}", u.name)));
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
-
let mut s = Scan { an: self, v, boundDepth: if param.is_some() { Some(0) } else { None }, err: None
|
|
539
|
+
let mut s = Scan { an: self, v, boundDepth: if param.is_some() { Some(0) } else { None }, err: None };
|
|
540
540
|
s.block(&u.block, true, 0);
|
|
541
541
|
if let Some(e) = s.err {
|
|
542
542
|
return Err(e);
|
|
@@ -597,8 +597,6 @@ struct Scan<'a> {
|
|
|
597
597
|
v: &'a str,
|
|
598
598
|
boundDepth: Option<usize>,
|
|
599
599
|
err: Option<String>,
|
|
600
|
-
#[allow(dead_code)]
|
|
601
|
-
selfUnit: usize,
|
|
602
600
|
}
|
|
603
601
|
|
|
604
602
|
impl<'a> Scan<'a> {
|
plum-core/tests/closure_escape_test.rs
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_core::closure_escape::nonEscapingFnParams;
|
|
3
|
-
use plum_core::AstParser;
|
|
4
3
|
|
|
5
4
|
fn parse(src: &str) -> plum_core::ast::Source {
|
|
6
|
-
|
|
5
|
+
plum_test_support::parse(src)
|
|
7
|
-
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
8
|
-
let tree = parser.parse(src, None).unwrap();
|
|
9
|
-
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
10
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
11
6
|
}
|
|
12
7
|
|
|
13
8
|
fn get(src: &str, key: &str) -> Vec<usize> {
|
plum-core/tests/closure_spec_test.rs
CHANGED
|
@@ -12,18 +12,8 @@ fn gcEngine() -> wasmtime::Engine {
|
|
|
12
12
|
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
16
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
20
|
-
let dir = std::env::temp_dir().join(format!("plum_cspec_{}_{}", name, std::process::id()));
|
|
21
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
22
|
-
let path = dir.join("main.plum");
|
|
23
|
-
std::fs::write(&path, src).unwrap();
|
|
24
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
25
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
26
|
-
|
|
16
|
+
plum_test_support::load(name, src).unwrap_or_else(|e| panic!("load: {e}"))
|
|
27
17
|
}
|
|
28
18
|
|
|
29
19
|
fn compile(name: &str, src: &str) -> Vec<u8> {
|
plum-core/tests/forms_generic_test.rs
CHANGED
|
@@ -4,16 +4,10 @@
|
|
|
4
4
|
//! (plum-core cannot run wasm; the checker end of this is covered by
|
|
5
5
|
//! `./target/debug/plum test` on the repro and plum-examples.)
|
|
6
6
|
use plum_core::ast::*;
|
|
7
|
-
use plum_core::
|
|
7
|
+
use plum_core::forms;
|
|
8
8
|
|
|
9
9
|
fn parse(src: &str) -> Source {
|
|
10
|
-
|
|
10
|
+
plum_test_support::parse(src)
|
|
11
|
-
parser
|
|
12
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
13
|
-
.unwrap();
|
|
14
|
-
let tree = parser.parse(src, None).unwrap();
|
|
15
|
-
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
16
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
17
11
|
}
|
|
18
12
|
|
|
19
13
|
const DEFS: &str = "\
|
plum-core/tests/inline_variants_test.rs
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_core::ast::*;
|
|
3
3
|
use plum_core::unions::*;
|
|
4
|
-
use plum_core::AstParser;
|
|
5
4
|
|
|
6
5
|
fn parse(src: &str) -> Source {
|
|
7
|
-
|
|
6
|
+
plum_test_support::parse(src)
|
|
8
|
-
parser
|
|
9
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
10
|
-
.unwrap();
|
|
11
|
-
let tree = parser.parse(src, None).unwrap();
|
|
12
|
-
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
13
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
14
7
|
}
|
|
15
8
|
|
|
16
9
|
fn enumOf<'a>(s: &'a Source, name: &str) -> &'a Enum {
|
plum-core/tests/tail_test.rs
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_core::ast::*;
|
|
3
3
|
use plum_core::tail::{markTailCalls, tailCalls, TailSite};
|
|
4
|
-
use plum_core::AstParser;
|
|
5
4
|
|
|
6
5
|
fn parse(src: &str) -> Source {
|
|
7
|
-
|
|
6
|
+
plum_test_support::parse(src)
|
|
8
|
-
parser
|
|
9
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
10
|
-
.unwrap();
|
|
11
|
-
let tree = parser.parse(src, None).unwrap();
|
|
12
|
-
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
13
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
14
7
|
}
|
|
15
8
|
|
|
16
9
|
/// Tail calls of the first fn named `name` (a method: the one with that name).
|
plum-core/tests/trmc_test.rs
CHANGED
|
@@ -30,18 +30,8 @@ fn gcEngine() -> wasmtime::Engine {
|
|
|
30
30
|
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
34
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
38
|
-
let dir = std::env::temp_dir().join(format!("plum_trmc_{}_{}", name, std::process::id()));
|
|
39
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
40
|
-
let path = dir.join("main.plum");
|
|
41
|
-
std::fs::write(&path, src).unwrap();
|
|
42
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
43
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
44
|
-
|
|
34
|
+
plum_test_support::load(name, src).unwrap_or_else(|e| panic!("load: {e}"))
|
|
45
35
|
}
|
|
46
36
|
|
|
47
37
|
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
plum-core/tests/union_lower_test.rs
CHANGED
|
@@ -2,20 +2,9 @@
|
|
|
2
2
|
use plum_core::ast::*;
|
|
3
3
|
use plum_core::union_lower::applyPlan;
|
|
4
4
|
use plum_core::unions::UnionPlan;
|
|
5
|
-
use plum_core::AstParser;
|
|
6
5
|
|
|
7
6
|
fn parse(src: &str) -> Source {
|
|
8
|
-
|
|
7
|
+
plum_test_support::parse(src)
|
|
9
|
-
parser
|
|
10
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
11
|
-
.unwrap();
|
|
12
|
-
let tree = parser.parse(src, None).unwrap();
|
|
13
|
-
assert!(
|
|
14
|
-
!tree.root_node().has_error(),
|
|
15
|
-
"{}",
|
|
16
|
-
tree.root_node().to_sexp()
|
|
17
|
-
);
|
|
18
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
19
8
|
}
|
|
20
9
|
|
|
21
10
|
fn fnNamed<'a>(s: &'a Source, name: &str) -> &'a Fn {
|
plum-core/tests/unions_test.rs
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_core::ast::*;
|
|
3
3
|
use plum_core::unions::*;
|
|
4
|
-
use plum_core::AstParser;
|
|
5
4
|
|
|
6
5
|
fn parse(src: &str) -> Source {
|
|
7
|
-
|
|
6
|
+
plum_test_support::parse(src)
|
|
8
|
-
parser
|
|
9
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
10
|
-
.unwrap();
|
|
11
|
-
let tree = parser.parse(src, None).unwrap();
|
|
12
|
-
assert!(
|
|
13
|
-
!tree.root_node().has_error(),
|
|
14
|
-
"{}",
|
|
15
|
-
tree.root_node().to_sexp()
|
|
16
|
-
);
|
|
17
|
-
AstParser::new(src).parseSource(tree.root_node())
|
|
18
7
|
}
|
|
19
8
|
|
|
20
9
|
fn enumOf<'a>(s: &'a Source, name: &str) -> &'a Enum {
|
plum-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 = "../plum-core" }
|
|
8
|
+
plum-checker = { path = "../plum-checker" }
|
|
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-test-support/src/lib.rs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//! Shared helpers for the compiler's test binaries (dev-dependency only).
|
|
2
|
+
#![allow(non_snake_case)]
|
|
3
|
+
|
|
4
|
+
use plum_core::ast::Source;
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
7
|
+
|
|
8
|
+
pub const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
9
|
+
|
|
10
|
+
pub fn repoRoot() -> PathBuf {
|
|
11
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn parseTree(src: &str) -> (tree_sitter::Tree, Source) {
|
|
15
|
+
let mut parser = tree_sitter::Parser::new();
|
|
16
|
+
parser
|
|
17
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
18
|
+
.unwrap();
|
|
19
|
+
let tree = parser.parse(src, None).unwrap();
|
|
20
|
+
let ast = plum_core::AstParser::new(src).parseSource(tree.root_node());
|
|
21
|
+
(tree, ast)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Parses `src` without asserting the tree is error free.
|
|
25
|
+
pub fn parseRaw(src: &str) -> Source {
|
|
26
|
+
parseTree(src).1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Parses `src`; panics with the source if the tree has syntax errors.
|
|
30
|
+
pub fn parse(src: &str) -> Source {
|
|
31
|
+
let (tree, ast) = parseTree(src);
|
|
32
|
+
assert!(
|
|
33
|
+
!tree.root_node().has_error(),
|
|
34
|
+
"embedded test source has parse errors:\n{src}"
|
|
35
|
+
);
|
|
36
|
+
ast
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
|
40
|
+
|
|
41
|
+
/// Loads `src` as `main.plum` through the real loader (imports resolve against the repo root).
|
|
42
|
+
pub fn load(name: &str, src: &str) -> Result<Source, String> {
|
|
43
|
+
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
44
|
+
let dir = std::env::temp_dir().join(format!("plum_ts_{}_{}_{}", name, std::process::id(), n));
|
|
45
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
46
|
+
let path = dir.join("main.plum");
|
|
47
|
+
std::fs::write(&path, src).unwrap();
|
|
48
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
49
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
50
|
+
r.map_err(|e| format!("{e}"))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// load + compileSource + validate; panics on any error.
|
|
54
|
+
pub fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
55
|
+
let source = load(name, src).unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
56
|
+
let bytes = plum_wasm_codegen::compileSource(&source)
|
|
57
|
+
.unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
58
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
59
|
+
bytes
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn engine() -> wasmtime::Engine {
|
|
63
|
+
let mut config = wasmtime::Config::new();
|
|
64
|
+
config.wasm_gc(true);
|
|
65
|
+
config.wasm_function_references(true);
|
|
66
|
+
config.wasm_tail_call(true);
|
|
67
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn instantiate(name: &str, src: &str) -> (wasmtime::Store<()>, wasmtime::Instance) {
|
|
71
|
+
let bytes = compile(name, src);
|
|
72
|
+
let engine = engine();
|
|
73
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
74
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
75
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
76
|
+
linker
|
|
77
|
+
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
78
|
+
.unwrap();
|
|
79
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
80
|
+
(store, instance)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Compiles `src` and runs its `main() -> Int`.
|
|
84
|
+
pub fn runInt(name: &str, src: &str) -> i64 {
|
|
85
|
+
let (mut store, instance) = instantiate(name, src);
|
|
86
|
+
let main = instance
|
|
87
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
88
|
+
.unwrap();
|
|
89
|
+
main.call(&mut store, ()).unwrap()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Compiles `src` and runs its `main() -> Str`, returning the string.
|
|
93
|
+
pub fn runStr(name: &str, src: &str) -> String {
|
|
94
|
+
let (mut store, instance) = instantiate(name, src);
|
|
95
|
+
let main = instance.get_func(&mut store, "main").expect("main export");
|
|
96
|
+
let mut results = [wasmtime::Val::I32(0)];
|
|
97
|
+
main.call(&mut store, &[], &mut results).unwrap();
|
|
98
|
+
let wasmtime::Val::AnyRef(Some(s)) = &results[0] else {
|
|
99
|
+
panic!("main did not return a Str");
|
|
100
|
+
};
|
|
101
|
+
let st = s.unwrap_struct(&store).unwrap();
|
|
102
|
+
let wasmtime::Val::AnyRef(Some(bytes_ref)) = st.field(&mut store, 0).unwrap() else {
|
|
103
|
+
panic!("Str.data is not a byte array");
|
|
104
|
+
};
|
|
105
|
+
let start = st.field(&mut store, 1).unwrap().unwrap_i64() as usize;
|
|
106
|
+
let len = st.field(&mut store, 2).unwrap().unwrap_i64() as usize;
|
|
107
|
+
let arr = bytes_ref.unwrap_array(&store).unwrap();
|
|
108
|
+
let mut buf = vec![0u8; arr.len(&store).unwrap() as usize];
|
|
109
|
+
arr.copy_to_i8_slice(&mut store, &mut buf).unwrap();
|
|
110
|
+
String::from_utf8_lossy(&buf[start..start + len]).into_owned()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// load + typedSource; the checker's messages (a load error is returned as one string).
|
|
114
|
+
pub fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
115
|
+
match load(name, src) {
|
|
116
|
+
Err(e) => vec![e],
|
|
117
|
+
Ok(source) => match plum_checker::typedSource(&source) {
|
|
118
|
+
Ok(_) => vec![],
|
|
119
|
+
Err(errs) => errs.iter().map(|e| e.message.clone()).collect(),
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[cfg(test)]
|
|
125
|
+
mod tests {
|
|
126
|
+
use super::*;
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn parseCounts() {
|
|
130
|
+
assert_eq!(parse("fun main() -> Int = 1").items.len(), 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
#[should_panic]
|
|
135
|
+
fn parseRejectsSyntaxErrors() {
|
|
136
|
+
parse("fun main( ->");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn runIntValue() {
|
|
141
|
+
assert_eq!(runInt("t1", &format!("{PRE}fun main() -> Int = 40 + 2")), 42);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn runStrValue() {
|
|
146
|
+
assert_eq!(
|
|
147
|
+
runStr("t3", &format!("{PRE}fun main() -> Str = \"hi\"")),
|
|
148
|
+
"hi"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn checkErrorsBool() {
|
|
154
|
+
let errs = checkErrors("t2", &format!("{PRE}fun main() -> Bool = 1"));
|
|
155
|
+
assert_eq!(errs.len(), 1, "{errs:?}");
|
|
156
|
+
assert!(errs[0].contains("Bool"), "{errs:?}");
|
|
157
|
+
}
|
|
158
|
+
}
|
plum-wasm-codegen/Cargo.toml
CHANGED
|
@@ -9,6 +9,7 @@ plum-core = { path = "../plum-core" }
|
|
|
9
9
|
plum-checker = { path = "../plum-checker" }
|
|
10
10
|
|
|
11
11
|
[dev-dependencies]
|
|
12
|
+
plum-test-support = { path = "../plum-test-support" }
|
|
12
13
|
wasmparser = "0.220"
|
|
13
14
|
tree-sitter = "0.26"
|
|
14
15
|
tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -26,7 +26,6 @@ enum TypeEntry {
|
|
|
26
26
|
/// share ONE index space regardless of kind — a `Passive` segment's index (needed by
|
|
27
27
|
/// `array.new_data`) is its position among ALL segments, active or passive.
|
|
28
28
|
enum DataSegmentEntry {
|
|
29
|
-
Active(u32, Vec<u8>),
|
|
30
29
|
Passive(Vec<u8>),
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -134,11 +133,6 @@ impl WasmModule {
|
|
|
134
133
|
idx
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
pub fn addDataSegment(&mut self, offset: u32, data: &[u8]) {
|
|
138
|
-
self.data_segments
|
|
139
|
-
.push(DataSegmentEntry::Active(offset, data.to_vec()));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
136
|
/// Adds a passive segment (no implicit memory-init offset) and returns its index
|
|
143
137
|
/// in the shared active/passive data-segment index space — the index `array.new_data`
|
|
144
138
|
/// needs to reference it. Passive segments require a `DataCountSection` (see `finish`).
|
|
@@ -153,7 +147,7 @@ impl WasmModule {
|
|
|
153
147
|
/// export is callable — needed because `struct.new` (and therefore constructing
|
|
154
148
|
/// any GC singleton, like the pre-allocated payload-free enum variants) is not
|
|
155
149
|
/// allowed inside a `global`'s own const-expr initializer (confirmed empirically
|
|
156
|
-
///
|
|
150
|
+
/// `wasmtimeGcConfigAllowsStructNewInGlobalConstExpr` fails), so those
|
|
157
151
|
/// globals are declared `mutable` with a `ref.null` initial value and populated
|
|
158
152
|
/// here instead.
|
|
159
153
|
pub fn setStartFunction(&mut self, func_idx: u32) {
|
|
@@ -314,10 +308,6 @@ impl WasmModule {
|
|
|
314
308
|
let mut data = DataSection::new();
|
|
315
309
|
for entry in &self.data_segments {
|
|
316
310
|
match entry {
|
|
317
|
-
DataSegmentEntry::Active(offset, bytes) => {
|
|
318
|
-
let offset_expr = ConstExpr::i32_const(*offset as i32);
|
|
319
|
-
data.active(0, &offset_expr, bytes.iter().copied());
|
|
320
|
-
}
|
|
321
311
|
DataSegmentEntry::Passive(bytes) => {
|
|
322
312
|
data.passive(bytes.iter().copied());
|
|
323
313
|
}
|
|
@@ -4484,9 +4474,7 @@ struct Collector<'a> {
|
|
|
4484
4474
|
/// type before any `struct.get` on it validates, and that narrowed value needs
|
|
4485
4475
|
/// its OWN local (declared with the concrete variant's ref type) distinct from
|
|
4486
4476
|
/// the original wide-typed subject local, which stays declared at the
|
|
4487
|
-
/// supertype's type for the whole function.
|
|
4477
|
+
/// supertype's type for the whole function.
|
|
4488
|
-
/// patterns nested inside another constructor pattern's fields, depth >= 1,
|
|
4489
|
-
/// since a plain i32 pointer needed no per-pattern static type at all.)
|
|
4490
4478
|
nested_class_scratch: HashMap<usize, u32>,
|
|
4491
4479
|
/// Slot number (the `u32` values in `nested_class_scratch`) -> the variant name
|
|
4492
4480
|
/// it narrows to, so its scratch local can be declared with that variant's exact
|
plum-wasm-codegen/tests/closure_types_e2e_test.rs
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use plum_wasm_codegen::compileSource;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_closure_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
45
|
-
let source = load(name, src).expect("load");
|
|
46
|
-
match plum_checker::checkSource(&source) {
|
|
47
|
-
Ok(()) => Vec::new(),
|
|
48
|
-
Err(e) => e.into_iter().map(|x| x.message).collect(),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const BOX: &str = "import std/Int\n\nrecord Box(v: Int) =\n fun get(self) -> Int = self.v\n\n";
|
|
53
|
-
|
|
54
|
-
#[test]
|
|
55
|
-
fn g1_typed_param() {
|
|
56
|
-
let src = format!("{BOX}fun ap(f: fn(Box) -> Int) -> Int = f(Box(v: 2))\n\nfun main() -> Int =\n ap(|b: Box| b.get())\n");
|
|
57
|
-
assert_eq!(runInt("g1", &src), 2);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[test]
|
|
61
|
-
fn g2_local_closure_inferred_from_first_call() {
|
|
62
|
-
let src = format!("{BOX}fun main() -> Int =\n get := |b| b.get()\n get(Box(v: 2))\n");
|
|
63
|
-
assert_eq!(runInt("g2", &src), 2);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#[test]
|
|
67
|
-
fn g2_never_called_is_error() {
|
|
68
|
-
let src = format!("{BOX}fun main() -> Int =\n get := |b| b.get()\n 1\n");
|
|
69
|
-
let errs = checkErrors("g2n", &src);
|
|
70
|
-
assert!(
|
|
71
|
-
errs.iter().any(|e| e.contains("cannot infer the type of b; write |b: Type|")),
|
|
72
|
-
"{errs:?}"
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
#[test]
|
|
77
|
-
fn g5_returned_closure() {
|
|
78
|
-
let src = "import std/Int\n\nfun adder(n: Int) -> fn(Int) -> Int =\n |x| x + n\n\nfun main() -> Int =\n add1 := adder(1)\n add1(2)\n";
|
|
79
|
-
assert_eq!(runInt("g5", src), 3);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// `adder(1)(2)` (call on a call result) does not parse: the grammar has no call-on-call form.
|
|
83
|
-
|
|
84
|
-
#[test]
|
|
85
|
-
fn adder_two_levels() {
|
|
86
|
-
let src = "import std/Int\n\nfun adder(n: Int) -> fn(Int) -> fn(Int) -> Int =\n |x| |y| x + y + n\n\nfun main() -> Int =\n f := adder(1)\n g := f(2)\n g(3)\n";
|
|
87
|
-
assert_eq!(runInt("two", src), 6);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const LINK: &str = "import std/Int\n\nenum Link[T] = Nil | Cons(head: T, tail: Link[T])\n\nfun len(xs: Link[T]) -> Int = match xs\n Nil => 0\n Cons(_, t) => 1 + len(t)\n\n";
|
|
91
|
-
|
|
92
|
-
/// A bare `Nil` whose `T` is fixed by a LATER argument gets the call's `Link[Int]`.
|
|
93
|
-
#[test]
|
|
94
|
-
fn bare_tag_fixed_by_later_arg() {
|
|
95
|
-
let src = format!("{LINK}fun push(xs: Link[T], x: T) -> Link[T] = Cons(head: x, tail: xs)\n\nfun main() -> Int =\n len(push(push(Nil, 1), 2))\n");
|
|
96
|
-
assert_eq!(runInt("bare_later", &src), 2);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#[test]
|
|
100
|
-
fn bare_tag_fixed_by_other_link_arg() {
|
|
101
|
-
let src = format!("{LINK}fun two(a: Link[T], b: Link[T]) -> Int = len(a) + len(b)\n\nfun mk(n: Int) -> Link[Int] = Cons(head: n, tail: Nil)\n\nfun main() -> Int =\n two(Nil, mk(2)) * 10 + two(mk(1), Nil)\n");
|
|
102
|
-
assert_eq!(runInt("bare_other", &src), 11);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
#[test]
|
|
106
|
-
fn curried_closure_runs() {
|
|
107
|
-
let src = "import std/Int\n\nfun curry() -> fn(Int) -> fn(Int) -> Int =\n |x| |y| x + y\n\nfun main() -> Int =\n add2 := curry()\n f := add2(2)\n f(3)\n";
|
|
108
|
-
assert_eq!(runInt("curried", src), 5);
|
|
109
|
-
}
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
use plum_core::AstParser;
|
|
3
2
|
use plum_wasm_codegen::{compileSource, compileTestSource};
|
|
4
3
|
use wasm_encoder::Encode;
|
|
5
4
|
|
|
6
|
-
fn parse(src: &str) -> plum_core::ast::Source {
|
|
7
|
-
let mut parser = tree_sitter::Parser::new();
|
|
8
|
-
parser
|
|
9
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
10
|
-
.unwrap();
|
|
11
|
-
let tree = parser.parse(src, None).unwrap();
|
|
12
|
-
let ap = AstParser::new(src);
|
|
13
|
-
ap.parseSource(tree.root_node())
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/// The wasm-gc migration (docs/superpowers/plans/2026-07-25-wasm-gc-migration.md)
|
|
17
|
-
/// needs both `wasm_gc` and `wasm_function_references` enabled — confirmed
|
|
18
|
-
/// empirically (see the `wasmtimeGcConfig*` tests below) rather than assumed from
|
|
19
|
-
/// docs, since wasmtime's own doc comment on `Config::wasm_gc` warns its GC support
|
|
20
|
-
/// is still in progress. Every test that instantiates/runs compiled output uses
|
|
21
|
-
|
|
5
|
+
use plum_test_support::{engine as gcEngine, parse, repoRoot};
|
|
22
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
23
|
-
let mut config = wasmtime::Config::new();
|
|
24
|
-
config.wasm_gc(true);
|
|
25
|
-
config.wasm_function_references(true);
|
|
26
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
27
|
-
}
|
|
28
6
|
|
|
29
7
|
#[test]
|
|
30
8
|
fn outputValidates() {
|
|
@@ -1059,10 +1037,6 @@ fun main() -> Int =
|
|
|
1059
1037
|
|
|
1060
1038
|
// ---- mixed types via enums (T4) ----
|
|
1061
1039
|
|
|
1062
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
1063
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
1040
|
/// Compiles `src` as a real file (so `import std/...` resolves against the repo's
|
|
1067
1041
|
/// `plum-std`) and returns the wasm instance's `main` export plus its store.
|
|
1068
1042
|
fn runMerged<R: wasmtime::WasmResults>(name: &str, src: &str) -> R {
|
plum-wasm-codegen/tests/e2e_mem/main.rs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
mod reuse;
|
|
3
|
+
mod unique_inplace;
|
|
4
|
+
mod unique_spread;
|
plum-wasm-codegen/tests/{reuse_e2e_test.rs → e2e_mem/reuse.rs}
RENAMED
|
@@ -1,37 +1,17 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{engine, PRE};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
2
|
fn load(name: &str, src: &str, reuse: bool) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_reuse_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
let mut r =
|
|
3
|
+
let mut r = plum_test_support::load(name, src)?;
|
|
23
4
|
if reuse {
|
|
24
5
|
plum_core::reuse::markReuse(&mut r);
|
|
25
6
|
}
|
|
26
7
|
Ok(r)
|
|
27
8
|
}
|
|
28
9
|
|
|
29
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
30
10
|
fn runInt(name: &str, src: &str, reuse: bool) -> i64 {
|
|
31
11
|
let source = load(name, src, reuse).expect("load");
|
|
32
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
12
|
+
let bytes = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
33
13
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
34
|
-
let engine =
|
|
14
|
+
let engine = engine();
|
|
35
15
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
36
16
|
let mut store = wasmtime::Store::new(&engine, ());
|
|
37
17
|
let mut linker = wasmtime::Linker::new(&engine);
|
|
@@ -45,11 +25,10 @@ fn runInt(name: &str, src: &str, reuse: bool) -> i64 {
|
|
|
45
25
|
main.call(&mut store, ()).unwrap()
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
|
|
49
28
|
/// Number of `struct.new` instructions in the whole module.
|
|
50
29
|
fn structNewCount(name: &str, src: &str, reuse: bool) -> usize {
|
|
51
30
|
let source = load(name, src, reuse).expect("load");
|
|
52
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
31
|
+
let bytes = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
53
32
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
54
33
|
let mut n = 0;
|
|
55
34
|
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
@@ -65,8 +44,6 @@ fn structNewCount(name: &str, src: &str, reuse: bool) -> usize {
|
|
|
65
44
|
n
|
|
66
45
|
}
|
|
67
46
|
|
|
68
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
69
|
-
|
|
70
47
|
const INC: &str = "
|
|
71
48
|
enum Link =
|
|
72
49
|
| Nil
|
plum-wasm-codegen/tests/{unique_inplace_e2e_test.rs → e2e_mem/unique_inplace.rs}
RENAMED
|
@@ -1,46 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::*;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_own_inplace_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
2
|
|
|
45
3
|
/// True when the real pipeline (checker `typedSource`, fusion on) flags the spread in
|
|
46
4
|
/// fn `name` as unique, i.e. proved written in place by uniqueness alone.
|
|
@@ -57,8 +15,6 @@ fn spreadIsUnique(name: &str, fnName: &str, src: &str) -> bool {
|
|
|
57
15
|
.expect("fn kept")
|
|
58
16
|
}
|
|
59
17
|
|
|
60
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
61
|
-
|
|
62
18
|
fn prog(body: &str) -> String {
|
|
63
19
|
format!("{PRE}{body}")
|
|
64
20
|
}
|
plum-wasm-codegen/tests/{unique_spread_e2e_test.rs → e2e_mem/unique_spread.rs}
RENAMED
|
@@ -1,25 +1,6 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{engine, PRE};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
2
|
fn load(name: &str, src: &str, unique: bool) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_unique_spread_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
let mut r =
|
|
3
|
+
let mut r = plum_test_support::load(name, src)?;
|
|
23
4
|
if unique {
|
|
24
5
|
for it in r.items.iter_mut() {
|
|
25
6
|
if let plum_core::ast::Item::Fn(f) = it {
|
|
@@ -36,12 +17,11 @@ fn load(name: &str, src: &str, unique: bool) -> Result<plum_core::ast::Source, S
|
|
|
36
17
|
Ok(r)
|
|
37
18
|
}
|
|
38
19
|
|
|
39
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
40
20
|
fn runInt(name: &str, src: &str, unique: bool) -> i64 {
|
|
41
21
|
let source = load(name, src, unique).expect("load");
|
|
42
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
22
|
+
let bytes = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
43
23
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
44
|
-
let engine =
|
|
24
|
+
let engine = engine();
|
|
45
25
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
46
26
|
let mut store = wasmtime::Store::new(&engine, ());
|
|
47
27
|
let mut linker = wasmtime::Linker::new(&engine);
|
|
@@ -55,11 +35,10 @@ fn runInt(name: &str, src: &str, unique: bool) -> i64 {
|
|
|
55
35
|
main.call(&mut store, ()).unwrap()
|
|
56
36
|
}
|
|
57
37
|
|
|
58
|
-
|
|
59
38
|
/// Number of `struct.new` instructions in the whole module.
|
|
60
39
|
fn structNewCount(name: &str, src: &str, unique: bool) -> usize {
|
|
61
40
|
let source = load(name, src, unique).expect("load");
|
|
62
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
41
|
+
let bytes = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
63
42
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
64
43
|
let mut n = 0;
|
|
65
44
|
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
@@ -75,8 +54,6 @@ fn structNewCount(name: &str, src: &str, unique: bool) -> usize {
|
|
|
75
54
|
n
|
|
76
55
|
}
|
|
77
56
|
|
|
78
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
79
|
-
|
|
80
57
|
const SPREAD: &str = "
|
|
81
58
|
record Cell(x: Int, y: Int)
|
|
82
59
|
|
plum-wasm-codegen/tests/e2e_records/empty_record_embed.rs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
use plum_test_support::{runInt, PRE};
|
|
2
|
+
|
|
3
|
+
fn prog(body: &str) -> String {
|
|
4
|
+
format!("{PRE}{body}")
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn field_less_record_embedding_field_less_trait_dispatches_in_a_slice() {
|
|
9
|
+
let src = prog("\
|
|
10
|
+
trait Actor =
|
|
11
|
+
fun act(self, n: Int) -> Int = n
|
|
12
|
+
fun draw(self) -> Int = 1
|
|
13
|
+
|
|
14
|
+
record Panel(Actor) =
|
|
15
|
+
override fun draw(self) -> Int = 10
|
|
16
|
+
|
|
17
|
+
record Ship(Actor, speed: Int) =
|
|
18
|
+
override fun act(self, n: Int) -> Int = n + self.speed
|
|
19
|
+
override fun draw(self) -> Int = 100
|
|
20
|
+
|
|
21
|
+
fun main() -> Int =
|
|
22
|
+
xs: Slice[Actor] := Slice[Actor]()
|
|
23
|
+
xs.push(Panel())
|
|
24
|
+
xs.push(Ship(speed: 5))
|
|
25
|
+
total := 0
|
|
26
|
+
for i := range 2
|
|
27
|
+
total = total + xs.at(i).draw() + xs.at(i).act(1000)
|
|
28
|
+
total
|
|
29
|
+
");
|
|
30
|
+
// Panel: draw 10 + act 1000; Ship: draw 100 + act 1005.
|
|
31
|
+
assert_eq!(runInt("slice", &src), 2115);
|
|
32
|
+
}
|
plum-wasm-codegen/tests/{enum_init_e2e_test.rs → e2e_records/enum_init.rs}
RENAMED
|
@@ -1,48 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
use plum_test_support::{load, runInt};
|
|
2
2
|
use plum_wasm_codegen::compileSource;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_enum_init_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
3
|
const LINK: &str = "enum Link[T] =
|
|
47
4
|
| Nil
|
|
48
5
|
| Cons(head: T, tail: Link[T])
|
plum-wasm-codegen/tests/{inline_variants_e2e_test.rs → e2e_records/inline_variants.rs}
RENAMED
|
@@ -1,45 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{load, runInt};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_inline_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
2
|
|
|
44
3
|
fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
45
4
|
let source = load(name, src).expect("load");
|
plum-wasm-codegen/tests/e2e_records/main.rs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
mod empty_record_embed;
|
|
3
|
+
mod enum_init;
|
|
4
|
+
mod inline_variants;
|
|
5
|
+
mod record_embed;
|
|
6
|
+
mod record_init;
|
|
7
|
+
mod unboxed_variants;
|
plum-wasm-codegen/tests/{record_embed_e2e_test.rs → e2e_records/record_embed.rs}
RENAMED
|
@@ -1,48 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{runInt, PRE};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_record_embed_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
2
|
|
|
47
3
|
fn prog(body: &str) -> String {
|
|
48
4
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/{record_init_e2e_test.rs → e2e_records/record_init.rs}
RENAMED
|
@@ -1,48 +1,5 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{runInt, PRE};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_record_init_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
|
|
2
|
+
use plum_test_support::load;
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
3
|
|
|
47
4
|
fn prog(body: &str) -> String {
|
|
48
5
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/{unboxed_variants_e2e_test.rs → e2e_records/unboxed_variants.rs}
RENAMED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
1
|
//! Unboxed layout: union-enum members and inline variants are wasm-gc subtypes
|
|
3
2
|
//! of the enum, so constructing one allocates exactly one struct.
|
|
4
|
-
use
|
|
3
|
+
use plum_test_support::{compile, runInt};
|
|
5
4
|
use wasmparser::{Operator, Parser, Payload};
|
|
6
5
|
|
|
7
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
8
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
12
|
-
let dir = std::env::temp_dir().join(format!("plum_unboxed_{}_{}", name, std::process::id()));
|
|
13
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
14
|
-
let path = dir.join("main.plum");
|
|
15
|
-
std::fs::write(&path, src).unwrap();
|
|
16
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
17
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
18
|
-
let source = r.unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
19
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
20
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
21
|
-
bytes
|
|
22
|
-
}
|
|
23
|
-
|
|
24
6
|
/// Number of `struct.new` instructions in the exported `main` function.
|
|
25
7
|
fn structNewsInMain(bytes: &[u8]) -> usize {
|
|
26
8
|
let mut imports = 0u32;
|
|
@@ -58,25 +40,6 @@ fn structNewsInMain(bytes: &[u8]) -> usize {
|
|
|
58
40
|
n
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
62
|
-
let bytes = compile(name, src);
|
|
63
|
-
let mut config = wasmtime::Config::new();
|
|
64
|
-
config.wasm_gc(true);
|
|
65
|
-
config.wasm_function_references(true);
|
|
66
|
-
let engine = wasmtime::Engine::new(&config).expect("engine");
|
|
67
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
68
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
69
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
70
|
-
linker
|
|
71
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
72
|
-
.unwrap();
|
|
73
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
74
|
-
let main = instance
|
|
75
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
76
|
-
.unwrap();
|
|
77
|
-
main.call(&mut store, ()).unwrap()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
43
|
const HEAD: &str = "\
|
|
81
44
|
import std/Str
|
|
82
45
|
import std/Int
|
plum-wasm-codegen/tests/{closure_tail_e2e_test.rs → e2e_tail/closure_tail.rs}
RENAMED
|
@@ -1,42 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{compile, runInt};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
config.wasm_tail_call(true);
|
|
9
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_tail_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
-
bytes
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
30
|
-
let bytes = compile(name, src);
|
|
31
|
-
let engine = gcEngine();
|
|
32
|
-
let module = wasmtime::Module::new(&engine, &bytes).expect("module");
|
|
33
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
34
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
35
|
-
linker.define_unknown_imports_as_traps(&module).unwrap();
|
|
36
|
-
let inst = linker.instantiate(&mut store, &module).expect("instantiate");
|
|
37
|
-
let main = inst.get_typed_func::<(), i64>(&mut store, "main").expect("main");
|
|
38
|
-
main.call(&mut store, ()).expect("run")
|
|
39
|
-
}
|
|
40
2
|
|
|
41
3
|
const HEAD: &str = "import std/Int\n\n";
|
|
42
4
|
|
plum-wasm-codegen/tests/{error_tail_e2e_test.rs → e2e_tail/error_tail.rs}
RENAMED
|
@@ -1,42 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{compile, runInt};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
config.wasm_tail_call(true);
|
|
9
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_tail_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
-
bytes
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
30
|
-
let bytes = compile(name, src);
|
|
31
|
-
let engine = gcEngine();
|
|
32
|
-
let module = wasmtime::Module::new(&engine, &bytes).expect("module");
|
|
33
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
34
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
35
|
-
linker.define_unknown_imports_as_traps(&module).unwrap();
|
|
36
|
-
let inst = linker.instantiate(&mut store, &module).expect("instantiate");
|
|
37
|
-
let main = inst.get_typed_func::<(), i64>(&mut store, "main").expect("main");
|
|
38
|
-
main.call(&mut store, ()).expect("run")
|
|
39
|
-
}
|
|
40
2
|
|
|
41
3
|
fn countReturnCalls(bytes: &[u8]) -> usize {
|
|
42
4
|
let mut n = 0;
|
plum-wasm-codegen/tests/{loop_break_e2e_test.rs → e2e_tail/loop_break.rs}
RENAMED
|
@@ -1,50 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{runInt, PRE};
|
|
3
|
-
|
|
4
|
-
fn tailEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
config.wasm_tail_call(true);
|
|
9
|
-
wasmtime::Engine::new(&config).expect("engine with GC and tail calls should construct")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_tail_calls_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
let mut v = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
|
|
26
|
-
v.validate_all(&bytes).expect("valid wasm");
|
|
27
|
-
bytes
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
31
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
32
|
-
let bytes = compile(name, src);
|
|
33
|
-
let engine = tailEngine();
|
|
34
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
35
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
36
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
37
|
-
linker
|
|
38
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
39
|
-
.unwrap();
|
|
40
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
41
|
-
let main = instance
|
|
42
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
43
|
-
.unwrap();
|
|
44
|
-
main.call(&mut store, ()).unwrap()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
48
2
|
|
|
49
3
|
fn prog(body: &str) -> String {
|
|
50
4
|
format!("{PRE}{body}")
|
plum-wasm-codegen/tests/e2e_tail/main.rs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
mod closure_tail;
|
|
3
|
+
mod error_tail;
|
|
4
|
+
mod loop_break;
|
|
5
|
+
mod tail;
|
plum-wasm-codegen/tests/{tail_calls_e2e_test.rs → e2e_tail/tail.rs}
RENAMED
|
@@ -1,50 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{compile, runInt, PRE};
|
|
3
|
-
|
|
4
|
-
fn tailEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
config.wasm_tail_call(true);
|
|
9
|
-
wasmtime::Engine::new(&config).expect("engine with GC and tail calls should construct")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_tail_calls_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
let mut v = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
|
|
26
|
-
v.validate_all(&bytes).expect("valid wasm");
|
|
27
|
-
bytes
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
31
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
32
|
-
let bytes = compile(name, src);
|
|
33
|
-
let engine = tailEngine();
|
|
34
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
35
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
36
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
37
|
-
linker
|
|
38
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
39
|
-
.unwrap();
|
|
40
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
41
|
-
let main = instance
|
|
42
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
43
|
-
.unwrap();
|
|
44
|
-
main.call(&mut store, ()).unwrap()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
48
2
|
|
|
49
3
|
fn prog(body: &str) -> String {
|
|
50
4
|
format!("{PRE}{body}")
|
|
@@ -281,3 +235,26 @@ fun main() -> Int = isEven(10)
|
|
|
281
235
|
assert!(ops.returnCalls >= 1, "isEven should return_call isOdd");
|
|
282
236
|
assert!(ops.calls.is_empty());
|
|
283
237
|
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
const HEAD: &str = "import std/Int\n\n";
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn tail_call_in_match_arm() {
|
|
244
|
+
let src = format!("{HEAD}enum Step =\n | Go(n: Int)\n | Stop\n\nfun run(s: Step, acc: Int) -> Int = match s\n Go(n) => run(if n == 0 then Stop else Go(n - 1), acc + 1)\n Stop => acc\n\nfun main() -> Int = run(Go(1000000), 0)\n");
|
|
245
|
+
assert_eq!(runInt("match", &src), 1000001);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn non_tail_union_wrap_not_tail() {
|
|
250
|
+
// The callee returns Int but the fn returns Int?: the call is wrapped on the way
|
|
251
|
+
// out, so it must stay a normal call and still produce the right value.
|
|
252
|
+
let src = format!("{HEAD}fun inner(n: Int) -> Int = n + 1\n\nfun outer(n: Int) -> Int? = inner(n)\n\nfun main() -> Int =\n v := outer(41)\n v ?: 0\n");
|
|
253
|
+
assert_eq!(runInt("wrap", &src), 42);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn big_link_built_and_summed() {
|
|
258
|
+
let src = format!("{HEAD}enum Link =\n | Nil\n | Cons(head: Int, tail: Link)\n\nfun build(n: Int, acc: Link) -> Link = if n == 0 then acc else build(n - 1, Cons(n, acc))\n\nfun sum(l: Link, acc: Int) -> Int = match l\n Nil => acc\n Cons(h, t) => sum(t, acc + h)\n\nfun main() -> Int = sum(build(200000, Nil), 0)\n");
|
|
259
|
+
assert_eq!(runInt("link", &src), 200000i64 * 200001 / 2);
|
|
260
|
+
}
|
plum-wasm-codegen/tests/e2e_types/closure_types.rs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
use plum_test_support::*;
|
|
2
|
+
|
|
3
|
+
const BOX: &str = "import std/Int\n\nrecord Box(v: Int) =\n fun get(self) -> Int = self.v\n\n";
|
|
4
|
+
|
|
5
|
+
#[test]
|
|
6
|
+
fn g1_typed_param() {
|
|
7
|
+
let src = format!("{BOX}fun ap(f: fn(Box) -> Int) -> Int = f(Box(v: 2))\n\nfun main() -> Int =\n ap(|b: Box| b.get())\n");
|
|
8
|
+
assert_eq!(runInt("g1", &src), 2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[test]
|
|
12
|
+
fn g2_local_closure_inferred_from_first_call() {
|
|
13
|
+
let src = format!("{BOX}fun main() -> Int =\n get := |b| b.get()\n get(Box(v: 2))\n");
|
|
14
|
+
assert_eq!(runInt("g2", &src), 2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn g5_returned_closure() {
|
|
19
|
+
let src = "import std/Int\n\nfun adder(n: Int) -> fn(Int) -> Int =\n |x| x + n\n\nfun main() -> Int =\n add1 := adder(1)\n add1(2)\n";
|
|
20
|
+
assert_eq!(runInt("g5", src), 3);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// `adder(1)(2)` (call on a call result) does not parse: the grammar has no call-on-call form.
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn adder_two_levels() {
|
|
27
|
+
let src = "import std/Int\n\nfun adder(n: Int) -> fn(Int) -> fn(Int) -> Int =\n |x| |y| x + y + n\n\nfun main() -> Int =\n f := adder(1)\n g := f(2)\n g(3)\n";
|
|
28
|
+
assert_eq!(runInt("two", src), 6);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LINK: &str = "import std/Int\n\nenum Link[T] = Nil | Cons(head: T, tail: Link[T])\n\nfun len(xs: Link[T]) -> Int = match xs\n Nil => 0\n Cons(_, t) => 1 + len(t)\n\n";
|
|
32
|
+
|
|
33
|
+
/// A bare `Nil` whose `T` is fixed by a LATER argument gets the call's `Link[Int]`.
|
|
34
|
+
#[test]
|
|
35
|
+
fn bare_tag_fixed_by_later_arg() {
|
|
36
|
+
let src = format!("{LINK}fun push(xs: Link[T], x: T) -> Link[T] = Cons(head: x, tail: xs)\n\nfun main() -> Int =\n len(push(push(Nil, 1), 2))\n");
|
|
37
|
+
assert_eq!(runInt("bare_later", &src), 2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn bare_tag_fixed_by_other_link_arg() {
|
|
42
|
+
let src = format!("{LINK}fun two(a: Link[T], b: Link[T]) -> Int = len(a) + len(b)\n\nfun mk(n: Int) -> Link[Int] = Cons(head: n, tail: Nil)\n\nfun main() -> Int =\n two(Nil, mk(2)) * 10 + two(mk(1), Nil)\n");
|
|
43
|
+
assert_eq!(runInt("bare_other", &src), 11);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn curried_closure_runs() {
|
|
48
|
+
let src = "import std/Int\n\nfun curry() -> fn(Int) -> fn(Int) -> Int =\n |x| |y| x + y\n\nfun main() -> Int =\n add2 := curry()\n f := add2(2)\n f(3)\n";
|
|
49
|
+
assert_eq!(runInt("curried", src), 5);
|
|
50
|
+
}
|
plum-wasm-codegen/tests/{io_entry_e2e_test.rs → e2e_types/io_entry.rs}
RENAMED
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::{compile, engine};
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
2
|
|
|
11
3
|
fn build(name: &str, src: &str) -> (wasmtime::Engine, wasmtime::Module) {
|
|
12
|
-
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
|
13
|
-
let dir = std::env::temp_dir().join(format!("plum_io_entry_{}_{}", name, std::process::id()));
|
|
14
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
15
|
-
let path = dir.join("main.plum");
|
|
16
|
-
std::fs::write(&path, src).unwrap();
|
|
17
|
-
let source = plum_core::loadAndMerge(&path, &root);
|
|
18
|
-
let
|
|
4
|
+
let bytes = compile(name, src);
|
|
19
|
-
let source = source.unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
20
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
21
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
22
|
-
let engine =
|
|
5
|
+
let engine = engine();
|
|
23
6
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
24
7
|
(engine, module)
|
|
25
8
|
}
|
|
@@ -32,12 +15,12 @@ fn instantiate(engine: &wasmtime::Engine, module: &wasmtime::Module) -> (wasmtim
|
|
|
32
15
|
(store, instance)
|
|
33
16
|
}
|
|
34
17
|
|
|
35
|
-
const
|
|
18
|
+
const PRE_IO: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Io\n\n";
|
|
36
19
|
|
|
37
20
|
#[test]
|
|
38
21
|
fn entry_point_receives_io() {
|
|
39
22
|
let src = format!(
|
|
40
|
-
"{
|
|
23
|
+
"{PRE_IO}export fun update(io: Io, n: Int) -> Int =\n w := io.screenWidth()\n n + 1\n\nfun main(io: Io) -> Int =\n h := io.screenHeight()\n 41\n"
|
|
41
24
|
);
|
|
42
25
|
let (engine, module) = build("recv", &src);
|
|
43
26
|
let (mut store, instance) = instantiate(&engine, &module);
|
|
@@ -49,8 +32,8 @@ fn entry_point_receives_io() {
|
|
|
49
32
|
|
|
50
33
|
#[test]
|
|
51
34
|
fn export_abi_unchanged_with_io_param() {
|
|
52
|
-
let withIo = format!("{
|
|
35
|
+
let withIo = format!("{PRE_IO}export fun update(io: Io, n: Int, f: Float) -> Int =\n w := io.screenWidth()\n n\n");
|
|
53
|
-
let without = format!("{
|
|
36
|
+
let without = format!("{PRE_IO}export fun update(n: Int, f: Float) -> Int = n\n");
|
|
54
37
|
let sig = |name: &str, src: &str| {
|
|
55
38
|
let (_, module) = build(name, src);
|
|
56
39
|
let e = module.exports().find(|e| e.name() == "update").unwrap();
|
plum-wasm-codegen/tests/e2e_types/main.rs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
mod closure_types;
|
|
3
|
+
mod io_entry;
|
|
4
|
+
mod traits;
|
|
5
|
+
mod type_param_static;
|
|
6
|
+
mod unions;
|
plum-wasm-codegen/tests/{traits_e2e_test.rs → e2e_types/traits.rs}
RENAMED
|
@@ -1,48 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::*;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_traits_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
2
|
|
|
47
3
|
fn prog(body: &str) -> String {
|
|
48
4
|
format!("{PRE}{body}")
|
|
@@ -245,13 +201,6 @@ fn opsOfExport(bytes: &[u8], name: &str) -> Vec<String> {
|
|
|
245
201
|
ops
|
|
246
202
|
}
|
|
247
203
|
|
|
248
|
-
fn compileOk(name: &str, src: &str) -> Vec<u8> {
|
|
249
|
-
let source = load(name, src).expect("load");
|
|
250
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
251
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
252
|
-
bytes
|
|
253
|
-
}
|
|
254
|
-
|
|
255
204
|
const ACTORS: &str = "\
|
|
256
205
|
trait Actor(mut x: Int, mut y: Int) =
|
|
257
206
|
fun hit(self) -> Int
|
|
@@ -271,7 +220,7 @@ fun readY(a: Actor) -> Int = a.y
|
|
|
271
220
|
fun main() -> Int =
|
|
272
221
|
readY(Box(x: 1, y: 7, w: 3))
|
|
273
222
|
"));
|
|
274
|
-
let bytes =
|
|
223
|
+
let bytes = compile("direct_read", &src);
|
|
275
224
|
let ops = opsOfExport(&bytes, "readY");
|
|
276
225
|
assert!(ops.iter().any(|o| o.starts_with("StructGet")), "{ops:?}");
|
|
277
226
|
assert!(!ops.iter().any(|o| o.starts_with("Call") || o.starts_with("RefTest") || o.starts_with("RefCast")), "{ops:?}");
|
|
@@ -293,7 +242,7 @@ fun main() -> Int =
|
|
|
293
242
|
same = 1
|
|
294
243
|
same * 100 + b.y
|
|
295
244
|
"));
|
|
296
|
-
let bytes =
|
|
245
|
+
let bytes = compile("identity", &src);
|
|
297
246
|
let ops = opsOfExport(&bytes, "asActor");
|
|
298
247
|
assert!(!ops.iter().any(|o| o.starts_with("StructNew")), "{ops:?}");
|
|
299
248
|
assert_eq!(runInt("identity_run", &src), 109);
|
plum-wasm-codegen/tests/{type_param_static_test.rs → e2e_types/type_param_static.rs}
RENAMED
|
@@ -1,47 +1,4 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use
|
|
1
|
+
use plum_test_support::*;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
12
|
-
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
|
13
|
-
let dir = std::env::temp_dir().join(format!("plum_tps_{}_{}", name, std::process::id()));
|
|
14
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
15
|
-
let path = dir.join("main.plum");
|
|
16
|
-
std::fs::write(&path, src).unwrap();
|
|
17
|
-
let r = plum_core::loadAndMerge(&path, &root);
|
|
18
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
19
|
-
r.map_err(|e| format!("{e}"))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
23
|
-
let source = load(name, src).expect("load");
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
-
let engine = gcEngine();
|
|
27
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
28
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
29
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
30
|
-
linker
|
|
31
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
32
|
-
.unwrap();
|
|
33
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
34
|
-
let main = instance.get_typed_func::<(), i64>(&mut store, "main").unwrap();
|
|
35
|
-
main.call(&mut store, ()).unwrap()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
39
|
-
let source = load(name, src).expect("load");
|
|
40
|
-
match plum_checker::checkSource(&source) {
|
|
41
|
-
Ok(()) => Vec::new(),
|
|
42
|
-
Err(e) => e.into_iter().map(|x| x.message).collect(),
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
2
|
|
|
46
3
|
#[test]
|
|
47
4
|
fn static_on_type_param() {
|
plum-wasm-codegen/tests/e2e_types/unions.rs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use plum_test_support::*;
|
|
2
|
+
|
|
3
|
+
const DEFS: &str = "\
|
|
4
|
+
import std/Str
|
|
5
|
+
import std/Int
|
|
6
|
+
import std/Float
|
|
7
|
+
import std/Slice
|
|
8
|
+
|
|
9
|
+
struct Circle(r: Float) =
|
|
10
|
+
fun area(self) -> Float = self.r * self.r * 3.0
|
|
11
|
+
|
|
12
|
+
record Rectangle(w: Float, h: Float) =
|
|
13
|
+
fun area(self) -> Float = self.w * self.h
|
|
14
|
+
|
|
15
|
+
enum Shape = Circle | Rectangle
|
|
16
|
+
|
|
17
|
+
";
|
|
18
|
+
|
|
19
|
+
fn withDefs(body: &str) -> String {
|
|
20
|
+
format!("{DEFS}{body}")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn member_into_optional_and_error_unions() {
|
|
25
|
+
let src = withDefs(
|
|
26
|
+
"\
|
|
27
|
+
fun opt() -> Shape? =
|
|
28
|
+
Circle(r: 2.0)
|
|
29
|
+
|
|
30
|
+
fun bang() -> !Shape =
|
|
31
|
+
Rectangle(w: 2.0, h: 2.0)
|
|
32
|
+
|
|
33
|
+
fun typed() -> ParseError!Shape =
|
|
34
|
+
Circle(r: 1.0)
|
|
35
|
+
|
|
36
|
+
fun main() -> Int =
|
|
37
|
+
a := opt() ?: Rectangle(w: 0.0, h: 0.0)
|
|
38
|
+
b := bang() catch Rectangle(w: 0.0, h: 0.0)
|
|
39
|
+
c := typed() catch Rectangle(w: 0.0, h: 0.0)
|
|
40
|
+
Int(a.area() + b.area() + c.area())
|
|
41
|
+
",
|
|
42
|
+
);
|
|
43
|
+
let src = src.replace(
|
|
44
|
+
"import std/Slice\n",
|
|
45
|
+
"import std/Slice\n\nerror ParseError = Bad | Worse\n",
|
|
46
|
+
);
|
|
47
|
+
assert_eq!(runInt("optional", &src), 19);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn invariant_list_rejected() {
|
|
52
|
+
let src = withDefs(
|
|
53
|
+
"\
|
|
54
|
+
fun take(xs: Slice[Shape]) -> Int =
|
|
55
|
+
1
|
|
56
|
+
|
|
57
|
+
fun go(cs: Slice[Circle]) -> Int =
|
|
58
|
+
take(cs)
|
|
59
|
+
",
|
|
60
|
+
);
|
|
61
|
+
assert!(!checkErrors("invariant", &src).is_empty());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn variadic_dispatch_is_an_error() {
|
|
66
|
+
let src = "\
|
|
67
|
+
import std/Str
|
|
68
|
+
import std/Int
|
|
69
|
+
import std/Float
|
|
70
|
+
|
|
71
|
+
record Pa(v: Int) =
|
|
72
|
+
fun sum(self, xs: ...Int) -> Int = self.v
|
|
73
|
+
|
|
74
|
+
record Pb(v: Int) =
|
|
75
|
+
fun sum(self, xs: ...Int) -> Int = self.v
|
|
76
|
+
|
|
77
|
+
enum Pair = Pa | Pb
|
|
78
|
+
|
|
79
|
+
fun go(p: Pair) -> Int =
|
|
80
|
+
p.sum(1, 2)
|
|
81
|
+
";
|
|
82
|
+
let errs = checkErrors("variadic", src);
|
|
83
|
+
assert!(
|
|
84
|
+
errs.iter().any(|e| e.contains(
|
|
85
|
+
"Pair.sum: auto-dispatch can't forward a variadic parameter; define sum on Pair"
|
|
86
|
+
)),
|
|
87
|
+
"{errs:?}"
|
|
88
|
+
);
|
|
89
|
+
}
|
plum-wasm-codegen/tests/empty_record_embed_e2e_test.rs
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use plum_wasm_codegen::compileSource;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_empty_record_embed_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
|
-
|
|
47
|
-
fn prog(body: &str) -> String {
|
|
48
|
-
format!("{PRE}{body}")
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#[test]
|
|
52
|
-
fn field_less_record_embedding_field_less_trait_dispatches_in_a_slice() {
|
|
53
|
-
let src = prog("\
|
|
54
|
-
trait Actor =
|
|
55
|
-
fun act(self, n: Int) -> Int = n
|
|
56
|
-
fun draw(self) -> Int = 1
|
|
57
|
-
|
|
58
|
-
record Panel(Actor) =
|
|
59
|
-
override fun draw(self) -> Int = 10
|
|
60
|
-
|
|
61
|
-
record Ship(Actor, speed: Int) =
|
|
62
|
-
override fun act(self, n: Int) -> Int = n + self.speed
|
|
63
|
-
override fun draw(self) -> Int = 100
|
|
64
|
-
|
|
65
|
-
fun main() -> Int =
|
|
66
|
-
xs: Slice[Actor] := Slice[Actor]()
|
|
67
|
-
xs.push(Panel())
|
|
68
|
-
xs.push(Ship(speed: 5))
|
|
69
|
-
total := 0
|
|
70
|
-
for i := range 2
|
|
71
|
-
total = total + xs.at(i).draw() + xs.at(i).act(1000)
|
|
72
|
-
total
|
|
73
|
-
");
|
|
74
|
-
// Panel: draw 10 + act 1000; Ship: draw 100 + act 1005.
|
|
75
|
-
assert_eq!(runInt("slice", &src), 2115);
|
|
76
|
-
}
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
+
use plum_test_support::{engine, repoRoot};
|
|
2
3
|
use plum_wasm_codegen::compileSource;
|
|
3
4
|
|
|
4
5
|
fn examplesDir() -> std::path::PathBuf {
|
|
5
6
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../plum-examples")
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
fn libPath() -> std::path::PathBuf {
|
|
9
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
/// Loads `name` via the REAL compilation path (`loadAndMerge`, resolving its
|
|
13
10
|
/// own `import`s against the repo root — see `plum-core::loader::loadAndMerge`'s
|
|
14
11
|
/// doc comment) rather than parsing it in isolation — several example files
|
|
@@ -16,19 +13,10 @@ fn libPath() -> std::path::PathBuf {
|
|
|
16
13
|
/// single-file parse would leave those names undeclared.
|
|
17
14
|
fn parseFile(name: &str) -> plum_core::ast::Source {
|
|
18
15
|
let path = examplesDir().join(name);
|
|
19
|
-
plum_core::loadAndMerge(&path, &
|
|
16
|
+
plum_core::loadAndMerge(&path, &repoRoot())
|
|
20
17
|
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e))
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
/// See the identically-named helper in codegen_tests.rs for why both features
|
|
24
|
-
/// are needed (part of the wasm-gc migration, docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
|
|
25
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
26
|
-
let mut config = wasmtime::Config::new();
|
|
27
|
-
config.wasm_gc(true);
|
|
28
|
-
config.wasm_function_references(true);
|
|
29
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
fn assertCompiles(name: &str) -> Vec<u8> {
|
|
33
21
|
let source = parseFile(name);
|
|
34
22
|
let bytes =
|
|
@@ -55,7 +43,7 @@ fn assertCompiles(name: &str) -> Vec<u8> {
|
|
|
55
43
|
#[test]
|
|
56
44
|
fn ioExampleCompilesAndRunsCorrectly() {
|
|
57
45
|
let bytes = assertCompiles("basics.plum");
|
|
58
|
-
let engine =
|
|
46
|
+
let engine = engine();
|
|
59
47
|
let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
|
|
60
48
|
let mut store = wasmtime::Store::new(&engine, ());
|
|
61
49
|
|
|
@@ -186,7 +174,7 @@ fn const_table_1000_entries() {
|
|
|
186
174
|
std::fs::create_dir_all(&dir).unwrap();
|
|
187
175
|
let path = dir.join("t.plum");
|
|
188
176
|
std::fs::write(&path, src).unwrap();
|
|
189
|
-
let source = plum_core::loadAndMerge(&path, &
|
|
177
|
+
let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("1000-const file should load");
|
|
190
178
|
let bytes = compileSource(&source).expect("1000-const file should compile");
|
|
191
179
|
assert!(wasmparser::validate(&bytes).is_ok());
|
|
192
180
|
std::fs::remove_dir_all(&dir).ok();
|
plum-wasm-codegen/tests/fusion_alloc_test.rs
CHANGED
|
@@ -2,25 +2,16 @@
|
|
|
2
2
|
//! Fusion allocation checks: compile through the real pipeline (checker `typedSource`
|
|
3
3
|
//! runs the supercompiler stage) and count `struct.new` of cell types per function.
|
|
4
4
|
use plum_wasm_codegen::compileSource;
|
|
5
|
+
use plum_test_support::repoRoot;
|
|
5
6
|
use std::collections::HashMap;
|
|
6
7
|
use std::collections::HashSet;
|
|
7
8
|
|
|
8
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
9
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
fn loadFile(path: &std::path::Path) -> plum_core::ast::Source {
|
|
13
10
|
plum_core::loadAndMerge(path, &repoRoot()).unwrap_or_else(|e| panic!("load: {e}"))
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_fusion_alloc_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
|
|
14
|
+
plum_test_support::load(name, src).unwrap_or_else(|e| panic!("load: {e}"))
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
fn compile(source: &plum_core::ast::Source, fusion: bool) -> Vec<u8> {
|
plum-wasm-codegen/tests/tail_call_e2e_test.rs
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use plum_wasm_codegen::compileSource;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
config.wasm_tail_call(true);
|
|
9
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
13
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
17
|
-
let dir = std::env::temp_dir().join(format!("plum_tail_{}_{}", name, std::process::id()));
|
|
18
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
19
|
-
let path = dir.join("main.plum");
|
|
20
|
-
std::fs::write(&path, src).unwrap();
|
|
21
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
22
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
23
|
-
let source = r.unwrap_or_else(|e| panic!("load: {e}"));
|
|
24
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
25
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
26
|
-
bytes
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
30
|
-
let bytes = compile(name, src);
|
|
31
|
-
let engine = gcEngine();
|
|
32
|
-
let module = wasmtime::Module::new(&engine, &bytes).expect("module");
|
|
33
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
34
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
35
|
-
linker.define_unknown_imports_as_traps(&module).unwrap();
|
|
36
|
-
let inst = linker.instantiate(&mut store, &module).expect("instantiate");
|
|
37
|
-
let main = inst.get_typed_func::<(), i64>(&mut store, "main").expect("main");
|
|
38
|
-
main.call(&mut store, ()).expect("run")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const HEAD: &str = "import std/Int\n\n";
|
|
42
|
-
|
|
43
|
-
#[test]
|
|
44
|
-
fn self_tail_call_deep() {
|
|
45
|
-
let src = format!("{HEAD}fun count(n: Int, acc: Int) -> Int = if n == 0 then acc else count(n - 1, acc + 1)\n\nfun main() -> Int = count(1000000, 0)\n");
|
|
46
|
-
assert_eq!(runInt("self", &src), 1000000);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#[test]
|
|
50
|
-
fn mutual_tail_call_deep() {
|
|
51
|
-
let src = format!("{HEAD}fun even(n: Int) -> Int = if n == 0 then 1 else odd(n - 1)\n\nfun odd(n: Int) -> Int = if n == 0 then 0 else even(n - 1)\n\nfun main() -> Int = even(1000000)\n");
|
|
52
|
-
assert_eq!(runInt("mutual", &src), 1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#[test]
|
|
56
|
-
fn tail_call_in_match_arm() {
|
|
57
|
-
let src = format!("{HEAD}enum Step =\n | Go(n: Int)\n | Stop\n\nfun run(s: Step, acc: Int) -> Int = match s\n Go(n) => run(if n == 0 then Stop else Go(n - 1), acc + 1)\n Stop => acc\n\nfun main() -> Int = run(Go(1000000), 0)\n");
|
|
58
|
-
assert_eq!(runInt("match", &src), 1000001);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
#[test]
|
|
62
|
-
fn non_tail_union_wrap_not_tail() {
|
|
63
|
-
// The callee returns Int but the fn returns Int?: the call is wrapped on the way
|
|
64
|
-
// out, so it must stay a normal call and still produce the right value.
|
|
65
|
-
let src = format!("{HEAD}fun inner(n: Int) -> Int = n + 1\n\nfun outer(n: Int) -> Int? = inner(n)\n\nfun main() -> Int =\n v := outer(41)\n v ?: 0\n");
|
|
66
|
-
assert_eq!(runInt("wrap", &src), 42);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
#[test]
|
|
70
|
-
fn closure_tail_call() {
|
|
71
|
-
let src = format!("{HEAD}fun spin(f: fn(Int) -> Int, n: Int) -> Int = if n == 0 then 0 else spin(f, f(n) - 1)\n\nfun viaClosure(g: fn(Int) -> Int, x: Int) -> Int = g(x)\n\nfun main() -> Int = spin(|n| n, 1000000) + viaClosure(|x| x + 1, 1)\n");
|
|
72
|
-
assert_eq!(runInt("closure", &src), 2);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#[test]
|
|
76
|
-
fn big_link_built_and_summed() {
|
|
77
|
-
let src = format!("{HEAD}enum Link =\n | Nil\n | Cons(head: Int, tail: Link)\n\nfun build(n: Int, acc: Link) -> Link = if n == 0 then acc else build(n - 1, Cons(n, acc))\n\nfun sum(l: Link, acc: Int) -> Int = match l\n Nil => acc\n Cons(h, t) => sum(t, acc + h)\n\nfun main() -> Int = sum(build(200000, Nil), 0)\n");
|
|
78
|
-
assert_eq!(runInt("link", &src), 200000i64 * 200001 / 2);
|
|
79
|
-
}
|
plum-wasm-codegen/tests/unions_e2e_test.rs
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#![allow(non_snake_case)]
|
|
2
|
-
use plum_wasm_codegen::compileSource;
|
|
3
|
-
|
|
4
|
-
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
-
let mut config = wasmtime::Config::new();
|
|
6
|
-
config.wasm_gc(true);
|
|
7
|
-
config.wasm_function_references(true);
|
|
8
|
-
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
-
let dir = std::env::temp_dir().join(format!("plum_unions_{}_{}", name, std::process::id()));
|
|
17
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
-
let path = dir.join("main.plum");
|
|
19
|
-
std::fs::write(&path, src).unwrap();
|
|
20
|
-
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
-
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
-
r.map_err(|e| format!("{e}"))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
-
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
-
let source = load(name, src).expect("load");
|
|
28
|
-
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
-
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
-
let engine = gcEngine();
|
|
31
|
-
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
-
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
-
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
-
linker
|
|
35
|
-
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
-
.unwrap();
|
|
37
|
-
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
-
let main = instance
|
|
39
|
-
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
-
.unwrap();
|
|
41
|
-
main.call(&mut store, ()).unwrap()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
45
|
-
let source = load(name, src).expect("load");
|
|
46
|
-
match plum_checker::checkSource(&source) {
|
|
47
|
-
Ok(()) => Vec::new(),
|
|
48
|
-
Err(e) => e.into_iter().map(|x| x.message).collect(),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const DEFS: &str = "\
|
|
53
|
-
import std/Str
|
|
54
|
-
import std/Int
|
|
55
|
-
import std/Float
|
|
56
|
-
import std/Slice
|
|
57
|
-
|
|
58
|
-
struct Circle(r: Float) =
|
|
59
|
-
fun area(self) -> Float = self.r * self.r * 3.0
|
|
60
|
-
|
|
61
|
-
record Rectangle(w: Float, h: Float) =
|
|
62
|
-
fun area(self) -> Float = self.w * self.h
|
|
63
|
-
|
|
64
|
-
enum Shape = Circle | Rectangle
|
|
65
|
-
|
|
66
|
-
";
|
|
67
|
-
|
|
68
|
-
fn withDefs(body: &str) -> String {
|
|
69
|
-
format!("{DEFS}{body}")
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
#[test]
|
|
73
|
-
fn member_into_optional_and_error_unions() {
|
|
74
|
-
let src = withDefs(
|
|
75
|
-
"\
|
|
76
|
-
fun opt() -> Shape? =
|
|
77
|
-
Circle(r: 2.0)
|
|
78
|
-
|
|
79
|
-
fun bang() -> !Shape =
|
|
80
|
-
Rectangle(w: 2.0, h: 2.0)
|
|
81
|
-
|
|
82
|
-
fun typed() -> ParseError!Shape =
|
|
83
|
-
Circle(r: 1.0)
|
|
84
|
-
|
|
85
|
-
fun main() -> Int =
|
|
86
|
-
a := opt() ?: Rectangle(w: 0.0, h: 0.0)
|
|
87
|
-
b := bang() catch Rectangle(w: 0.0, h: 0.0)
|
|
88
|
-
c := typed() catch Rectangle(w: 0.0, h: 0.0)
|
|
89
|
-
Int(a.area() + b.area() + c.area())
|
|
90
|
-
",
|
|
91
|
-
);
|
|
92
|
-
let src = src.replace(
|
|
93
|
-
"import std/Slice\n",
|
|
94
|
-
"import std/Slice\n\nerror ParseError = Bad | Worse\n",
|
|
95
|
-
);
|
|
96
|
-
assert_eq!(runInt("optional", &src), 19);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#[test]
|
|
100
|
-
fn invariant_list_rejected() {
|
|
101
|
-
let src = withDefs(
|
|
102
|
-
"\
|
|
103
|
-
fun take(xs: Slice[Shape]) -> Int =
|
|
104
|
-
1
|
|
105
|
-
|
|
106
|
-
fun go(cs: Slice[Circle]) -> Int =
|
|
107
|
-
take(cs)
|
|
108
|
-
",
|
|
109
|
-
);
|
|
110
|
-
assert!(!checkErrors("invariant", &src).is_empty());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
#[test]
|
|
114
|
-
fn variadic_dispatch_is_an_error() {
|
|
115
|
-
let src = "\
|
|
116
|
-
import std/Str
|
|
117
|
-
import std/Int
|
|
118
|
-
import std/Float
|
|
119
|
-
|
|
120
|
-
record Pa(v: Int) =
|
|
121
|
-
fun sum(self, xs: ...Int) -> Int = self.v
|
|
122
|
-
|
|
123
|
-
record Pb(v: Int) =
|
|
124
|
-
fun sum(self, xs: ...Int) -> Int = self.v
|
|
125
|
-
|
|
126
|
-
enum Pair = Pa | Pb
|
|
127
|
-
|
|
128
|
-
fun go(p: Pair) -> Int =
|
|
129
|
-
p.sum(1, 2)
|
|
130
|
-
";
|
|
131
|
-
let errs = checkErrors("variadic", src);
|
|
132
|
-
assert!(
|
|
133
|
-
errs.iter().any(|e| e.contains(
|
|
134
|
-
"Pair.sum: auto-dispatch can't forward a variadic parameter; define sum on Pair"
|
|
135
|
-
)),
|
|
136
|
-
"{errs:?}"
|
|
137
|
-
);
|
|
138
|
-
}
|
scripts/wasm-hashes.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: scripts/wasm-hashes.sh REV OUT
|
|
3
|
+
# Compiles every plum-examples/**/*.plum (most have no `fun main`, so no filter) at git REV
|
|
4
|
+
# (one fresh `plum compile` process per file) and writes `<sha256> <relpath>` lines to OUT,
|
|
5
|
+
# sorted by relpath. Files that fail to compile print `FAIL <relpath>`.
|
|
6
|
+
set -u
|
|
7
|
+
REV="${1:?usage: wasm-hashes.sh REV OUT}"
|
|
8
|
+
OUT="${2:?usage: wasm-hashes.sh REV OUT}"
|
|
9
|
+
case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
|
|
10
|
+
REPO="$(git rev-parse --show-toplevel)"
|
|
11
|
+
BASE="${TMPDIR:-/tmp}"; BASE="${BASE%/}"
|
|
12
|
+
TARGET="${CARGO_TARGET_DIR:-$BASE/plum-wasm-hashes-target}"
|
|
13
|
+
WT="$(mktemp -d "$BASE/plum-wasm-hashes.XXXXXX")"
|
|
14
|
+
trap 'git -C "$REPO" worktree remove --force "$WT/wt" >/dev/null 2>&1; rm -rf "$WT"' EXIT
|
|
15
|
+
|
|
16
|
+
git -C "$REPO" worktree add --detach "$WT/wt" "$REV" >/dev/null 2>&1 || { echo "cannot checkout $REV" >&2; exit 1; }
|
|
17
|
+
(cd "$WT/wt" && CARGO_TARGET_DIR="$TARGET" cargo build -q -p plum-cli) || { echo "build failed" >&2; exit 1; }
|
|
18
|
+
PLUM="$TARGET/debug/plum"
|
|
19
|
+
[ -x "$PLUM" ] || { echo "no plum binary at $PLUM" >&2; exit 1; }
|
|
20
|
+
|
|
21
|
+
TMPOUT="$WT/lines.txt"; : > "$TMPOUT"
|
|
22
|
+
cd "$WT/wt"
|
|
23
|
+
find plum-examples -name '*.plum' | LC_ALL=C sort | while read -r f; do
|
|
24
|
+
if "$PLUM" compile "$f" -o "$WT/out.wasm" >/dev/null 2>&1; then
|
|
25
|
+
echo "$(shasum -a 256 "$WT/out.wasm" | cut -d' ' -f1) $f" >> "$TMPOUT"
|
|
26
|
+
else
|
|
27
|
+
echo "FAIL $f" >> "$TMPOUT"
|
|
28
|
+
fi
|
|
29
|
+
rm -f "$WT/out.wasm"
|
|
30
|
+
done
|
|
31
|
+
LC_ALL=C sort -k2 "$TMPOUT" > "$OUT"
|