plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
086f1c8
— Peter John
2026-11-05T18:05:48+05:30
Compiler cleanup phase 3b: pass-list driver and Diagnostic errors
Generated files not shown: Cargo.lock
- Cargo.toml +1 -1
- docs/backlog.md +1 -1
- docs/plans/2026-11-05-compiler-cleanup-phase3b.md +106 -0
- docs/plans/2026-11-05-compiler-cleanup-phase3b.results.md +11 -0
- docs/plans/2026-11-05-explicit-fn-generics.progress.md +5 -0
- plum-checker/Cargo.toml +1 -0
- plum-checker/src/lib.rs +47 -231
- plum-checker/src/state.rs +35 -4
- plum-checker/src/types.rs +19 -32
- plum-checker/tests/checker/checker_tests.rs +9 -4
- plum-checker/tests/checker/closure_hint_test.rs +2 -2
- plum-checker/tests/checker/diagnostic_tests.rs +34 -0
- plum-checker/tests/checker/explicit_generics_tests.rs +1 -1
- plum-checker/tests/checker/features/io_capability.rs +1 -1
- plum-checker/tests/checker/features/mutability.rs +1 -1
- plum-checker/tests/checker/features/pattern_casing.rs +1 -1
- plum-checker/tests/checker/features/variant_pattern.rs +1 -1
- plum-checker/tests/checker/fusion_pipeline_test.rs +12 -18
- plum-checker/tests/checker/inline_variants_tests.rs +1 -1
- plum-checker/tests/checker/main.rs +1 -0
- plum-checker/tests/checker/question_tests.rs +2 -2
- plum-checker/tests/checker/subtyping_tests.rs +4 -4
- plum-cli/Cargo.toml +1 -0
- plum-cli/src/game.rs +1 -1
- plum-cli/src/main.rs +39 -28
- plum-cli/tests/cli/compile_tests.rs +16 -0
- plum-cli/tests/cli/crash_test.rs +1 -1
- plum-core/Cargo.toml +1 -0
- plum-core/src/diag.rs +50 -0
- plum-core/src/drive.rs +28 -9
- plum-core/src/lib.rs +4 -1
- plum-core/src/loader.rs +71 -48
- plum-core/src/lower.rs +15 -9
- plum-core/src/parser.rs +46 -24
- plum-core/tests/core/closure_escape_test.rs +2 -2
- plum-core/tests/core/closure_spec_test.rs +2 -2
- plum-core/tests/core/diag_test.rs +37 -0
- plum-core/tests/core/inline_variants_test.rs +21 -8
- plum-core/tests/core/layout_test.rs +2 -2
- plum-core/tests/core/loader_test.rs +43 -40
- plum-core/tests/core/lower_test.rs +4 -4
- plum-core/tests/core/main.rs +1 -0
- plum-core/tests/core/session_test.rs +2 -2
- plum-core/tests/core/trmc_test.rs +6 -4
- plum-driver/Cargo.toml +11 -0
- plum-driver/src/lib.rs +187 -0
- plum-driver/tests/driver_tests.rs +75 -0
- plum-game/tests/host_test.rs +1 -1
- plum-lsp/Cargo.toml +1 -0
- plum-lsp/src/main.rs +40 -26
- plum-test-support/Cargo.toml +1 -1
- plum-test-support/src/lib.rs +12 -6
- plum-wasm-codegen/Cargo.toml +1 -0
- plum-wasm-codegen/src/lib.rs +18 -53
- plum-wasm-codegen/tests/wasm_codegen/codegen_tests.rs +16 -13
- plum-wasm-codegen/tests/wasm_codegen/e2e_mem/reuse.rs +4 -4
- plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_inplace.rs +1 -1
- plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_spread.rs +2 -2
- plum-wasm-codegen/tests/wasm_codegen/e2e_records/enum_init.rs +1 -1
- plum-wasm-codegen/tests/wasm_codegen/e2e_types/subtyping.rs +2 -2
- plum-wasm-codegen/tests/wasm_codegen/examples_test.rs +4 -4
- plum-wasm-codegen/tests/wasm_codegen/fusion_alloc_test.rs +7 -7
Cargo.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[workspace]
|
|
2
|
-
members = ["plum-core", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support", "plum-host"]
|
|
2
|
+
members = ["plum-core", "plum-driver", "plum-cli", "plum-checker", "plum-wasm-codegen", "plum-lsp", "plum-test-support", "plum-host"]
|
|
3
3
|
resolver = "2"
|
|
4
4
|
|
|
5
5
|
[profile.dev]
|
docs/backlog.md
CHANGED
|
@@ -4,7 +4,7 @@ Open work, in the order the user queued it, plus smaller items found during buil
|
|
|
4
4
|
|
|
5
5
|
## Planned (in order)
|
|
6
6
|
|
|
7
|
-
**Current order (user 2026-11-05):** explicit fn generics (
|
|
7
|
+
**Current order (user 2026-11-05):** explicit fn generics (done faa4d7e) → cleanup phase 3b (pass list, positioned errors; running) → Io primitive + extern methods → move Rust crates under `compiler/` → cleanup phase 4 (node ids, typed codegen; also: `inferExpr` returns `Diagnostic` with spans, removing 3b's registered-message match for undefined names) → phase 5 (file splits, enum kinds) → phase 6 (tests in Plum) → benchmark (incl. iterator unboxing / niche Option, run C game A/B) → FBIP (stop for user approval) → deterministic sim → kids' games. The items below keep their detail; this line wins on order.
|
|
8
8
|
|
|
9
9
|
1. **Compiler cleanup and architecture** — `docs/plans/2026-11-04-compiler-cleanup.review.md` (written in another session; meant to run after language-restrictions merged). Approved 2026-11-04; running on branch `superlite/compiler-cleanup`, phase 1 first.
|
|
10
10
|
- **Explicit function generics** (user 2026-11-04: right after cleanup phase 2) — functions declare their generics (`fun map[B](...)`) instead of every single capital letter being a generic; allows longer generic names and one-letter concrete types/tags. Grammar change + about 140 signatures in std/examples. Phase 2's "generics by scope" makes this mostly a grammar + signature rewrite.
|
docs/plans/2026-11-05-compiler-cleanup-phase3b.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Compiler cleanup, phase 3b: pass-list driver and positioned errors
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-04-compiler-cleanup.review.md (phase 3 rows "Pass list" and "Positioned errors")
|
|
4
|
+
**Goal:** (1) A small driver owns the order of the post-check passes (closure specialisation, fusion, TRMC, tail marking) and the "adopt a rewrite only if it re-checks" rule; the checker only checks. (2) One error type `Diagnostic { file, line, column, kind, message }` replaces `Result<_, String>` error plumbing between parser/loader, checker, driver, codegen, CLI and LSP; no behaviour is decided by matching message text. No language change; compiled wasm for every example is byte-identical before/after.
|
|
5
|
+
**Architecture:** plum-core (new `diag.rs`, parser, loader, drive), plum-checker, new crate plum-driver, plum-wasm-codegen, plum-cli, plum-lsp, plum-test-support.
|
|
6
|
+
**Tech stack:** Rust 2021.
|
|
7
|
+
**Detail:** contracts
|
|
8
|
+
**Target-min:** 35
|
|
9
|
+
|
|
10
|
+
## Global constraints
|
|
11
|
+
- Work on `master` in the main checkout, default shared `target/`. Don't commit. Never run `plum format` on real files. No `sed -i -E`.
|
|
12
|
+
- No `cargo test` (hook): `cargo build --tests`, one test binary per crate in `target/debug/deps` (`core-*`, `checker-*`, `cli-*`, `wasm_codegen-*`, plus lib unit tests; newest by mtime `/bin/ls -t`). Plum sweep `cargo build -p plum-cli && target/debug/plum test plum-std plum-examples` (662).
|
|
13
|
+
- Exact sub-expression positions need spans (phase 4, node ids). Here a diagnostic takes the most specific position the AST already has (FnCall/Stmt line+column, else the enclosing item) and always the item's `file`.
|
|
14
|
+
- User-visible error text keeps its wording; the format becomes `file:line:column: message` (kind decides the prefix where one exists today, e.g. `type error:`). Tests asserting message text keep passing (update only the prefix/format parts).
|
|
15
|
+
- Byte-identical wasm: before T1, compile every plum-examples/**/*.plum with `target/debug/plum compile` into a scratch dir and keep the hashes; T6 compares.
|
|
16
|
+
|
|
17
|
+
## Task graph
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
T1[Diagnostic type + parser/loader]
|
|
21
|
+
T2[checker on Diagnostic, kinds not text]
|
|
22
|
+
T3[plum-driver: pass list]
|
|
23
|
+
T4[codegen + CLI on Diagnostic]
|
|
24
|
+
T5[LSP + test-support on Diagnostic]
|
|
25
|
+
T6[verify]
|
|
26
|
+
%% taskgraph
|
|
27
|
+
T1 --> T2
|
|
28
|
+
T2 --> T3
|
|
29
|
+
T3 --> T4
|
|
30
|
+
T3 --> T5
|
|
31
|
+
T4 --> T6
|
|
32
|
+
T5 --> T6
|
|
33
|
+
```
|
|
34
|
+
Ruling: sequential [chain] - T1-T3 rewrite the same error plumbing and the checker entry points in order - cost: T1-T3 run alone.
|
|
35
|
+
|
|
36
|
+
## Package A: errors and driver
|
|
37
|
+
**Probe entry:** run=plum test plum-std plum-examples
|
|
38
|
+
|
|
39
|
+
### T1 Diagnostic type, parser and loader · Complexity 3 · Est. context 40k
|
|
40
|
+
**Owns:** plum-core/src/diag.rs, plum-core/src/lib.rs, plum-core/src/parser.rs, plum-core/src/loader.rs, plum-core/src/lower.rs, plum-core/tests/core/diag_test.rs, plum-core/tests/core/main.rs, plum-core/tests/core/loader_test.rs
|
|
41
|
+
**Components:** 1
|
|
42
|
+
**LOC owned:** 120
|
|
43
|
+
**Tests:** fast
|
|
44
|
+
**Model:** sonnet
|
|
45
|
+
**Produces:** `plum_core::diag::Diagnostic { file: String, line: usize, column: usize, kind: DiagKind, message: String }`, `DiagKind::{Parse, Load, Resolution, Type, Codegen}`, `impl Display` = `file:line:column: message`; loader/lower return `Result<Source, Vec<Diagnostic>>`
|
|
46
|
+
e.g. loading a file with `fun f( = 1` -> `[Diagnostic{file: "main.plum", line: 1, column: 7, kind: Parse, ..}]`; a missing import -> kind Load naming the import
|
|
47
|
+
**Notes:** keep a thin `String`-returning wrapper where a caller outside plum-core needs it until T4/T5 switch (mark it `// removed in T4`). Parser placeholders (`unwrap_or(Expr::Int(0))` etc. on malformed nodes) become Parse diagnostics.
|
|
48
|
+
**Test code:** diag_test: Display format; the two e.g. cases; loader_test green; all binaries + sweep green.
|
|
49
|
+
|
|
50
|
+
### T2 checker on Diagnostic, kinds instead of text · Complexity 4 · Est. context 50k
|
|
51
|
+
**Owns:** plum-checker/src/*.rs, plum-checker/tests/checker/*.rs, plum-checker/tests/checker/features/*.rs
|
|
52
|
+
**Components:** 1
|
|
53
|
+
**LOC owned:** 150
|
|
54
|
+
**Tests:** fast
|
|
55
|
+
**Consumes:** `plum_core::diag::Diagnostic { file: String, line: usize, column: usize, kind: DiagKind, message: String }`, `DiagKind::{Parse, Load, Resolution, Type, Codegen}`, `impl Display` = `file:line:column: message`; loader/lower return `Result<Source, Vec<Diagnostic>>` (T1)
|
|
56
|
+
**Produces:** `CheckError` is `Diagnostic` (type alias or replaced); every checker error carries its item's file and the most specific known position; `isResolutionError` text matching replaced by `DiagKind::Resolution`
|
|
57
|
+
e.g. `fun f() -> Int = g()` in main.plum -> one Diagnostic{file: "main.plum", line: 1, kind: Resolution, message: "undefined name 'g'"..}
|
|
58
|
+
**Test code:** checker binary green (message assertions keep wording); a test that a resolution error in an uninstantiated generic body is still reported (the old text-match behaviour) via kind.
|
|
59
|
+
|
|
60
|
+
### T3 plum-driver: the pass list · Complexity 5 · Est. context 50k
|
|
61
|
+
**Owns:** plum-driver/**, Cargo.toml, plum-checker/src/lib.rs, plum-checker/src/state.rs
|
|
62
|
+
**Components:** 1
|
|
63
|
+
**LOC owned:** 160
|
|
64
|
+
**Tests:** fast
|
|
65
|
+
**Consumes:** `CheckError` is `Diagnostic` (type alias or replaced); every checker error carries its item's file and the most specific known position; `isResolutionError` text matching replaced by `DiagKind::Resolution` (T2)
|
|
66
|
+
**Produces:** `plum_driver::check(source: &Source, opts: &PassOptions) -> Result<TypedInfo, Vec<Diagnostic>>` running `[ClosureSpec, Fusion, Trmc, TailMarks]` in order, each rewrite adopted only if it re-checks (dropped ones reported under `--explain`); plum-checker exposes only checking (`checkProgram`) plus what the passes need
|
|
67
|
+
e.g. `plum_driver::check(&src, &PassOptions{fusion: false, ..})` -> no `$`-named fns; default -> same lowered program as today's `typedSourceWith`
|
|
68
|
+
**Notes:** move `typedSourceWith`'s body, `adoptPre`, `recheckKeep`, `fusionRewrite`, `closureHints`, `preUnionSource` into the driver; `FusionOptions` becomes `PassOptions`. Keep `plum_checker::typedSource`/`typedSourceWith` as deprecated thin calls into... nothing — they can't call the driver (cycle): instead switch every caller in T4/T5; in T3 leave them working by keeping the old functions until T4/T5 land, then T6 deletes them.
|
|
69
|
+
**Test code:** plum-driver unit tests: the e.g. cases, a rewrite that fails re-check is dropped and the previous program kept; all binaries + sweep green.
|
|
70
|
+
|
|
71
|
+
## Package B: callers
|
|
72
|
+
**Probe entry:** run=plum test plum-std plum-examples
|
|
73
|
+
|
|
74
|
+
### T4 codegen and CLI on Diagnostic · Complexity 4 · Est. context 45k
|
|
75
|
+
**Owns:** plum-wasm-codegen/src/*.rs, plum-wasm-codegen/Cargo.toml, plum-wasm-codegen/tests/wasm_codegen/**/*.rs, plum-cli/src/*.rs, plum-cli/tests/cli/*.rs, plum-core/src/drive.rs
|
|
76
|
+
**Components:** 2
|
|
77
|
+
**LOC owned:** 120
|
|
78
|
+
**Tests:** fast
|
|
79
|
+
**Consumes:** `plum_driver::check(source: &Source, opts: &PassOptions) -> Result<TypedInfo, Vec<Diagnostic>>` running `[ClosureSpec, Fusion, Trmc, TailMarks]` in order, each rewrite adopted only if it re-checks (dropped ones reported under `--explain`); plum-checker exposes only checking (`checkProgram`) plus what the passes need (T3)
|
|
80
|
+
**Produces:** `plum_wasm_codegen::compile(source, opts) -> Result<Output, Vec<Diagnostic>>` via plum-driver; CLI prints diagnostics; `nameFileInErrors` and `starts_with("type error")` gone; drive.rs's `why.starts_with("consumer ")` becomes an enum
|
|
81
|
+
e.g. `plum compile bad.plum` with an undefined name -> stderr `bad.plum:3:5: type error: undefined name 'g'` and exit 1
|
|
82
|
+
**Test code:** cli test for the e.g. output; wasm_codegen + cli binaries green.
|
|
83
|
+
|
|
84
|
+
### T5 LSP and test-support on Diagnostic · Complexity 3 · Est. context 35k
|
|
85
|
+
**Owns:** plum-lsp/src/*.rs, plum-lsp/Cargo.toml, plum-test-support/src/*.rs, plum-test-support/Cargo.toml
|
|
86
|
+
**Components:** 1
|
|
87
|
+
**LOC owned:** 80
|
|
88
|
+
**Tests:** fast
|
|
89
|
+
**Consumes:** `plum_driver::check(source: &Source, opts: &PassOptions) -> Result<TypedInfo, Vec<Diagnostic>>` running `[ClosureSpec, Fusion, Trmc, TailMarks]` in order, each rewrite adopted only if it re-checks (dropped ones reported under `--explain`); plum-checker exposes only checking (`checkProgram`) plus what the passes need (T3)
|
|
90
|
+
**Produces:** LSP publishes diagnostics from `Diagnostic` positions (no message parsing); test-support uses plum-driver
|
|
91
|
+
e.g. an LSP document with `g()` undefined -> one LSP diagnostic at the call's line/column
|
|
92
|
+
**Test code:** plum-lsp tests green incl. `readmeSnippetsHaveNoSyntaxDiagnostics`; all binaries green.
|
|
93
|
+
|
|
94
|
+
### T6 verify and remove leftovers · Complexity 2 · Est. context 30k
|
|
95
|
+
**Owns:** docs/plans/2026-11-05-compiler-cleanup-phase3b.results.md, plum-checker/src/lib.rs, plum-core/src/*.rs
|
|
96
|
+
**Components:** 1
|
|
97
|
+
**LOC owned:** 40
|
|
98
|
+
**Tests:** slow
|
|
99
|
+
**Consumes:** `plum_wasm_codegen::compile(source, opts) -> Result<Output, Vec<Diagnostic>>` via plum-driver; CLI prints diagnostics; `nameFileInErrors` and `starts_with("type error")` gone; drive.rs's `why.starts_with("consumer ")` becomes an enum (T4) · LSP publishes diagnostics from `Diagnostic` positions (no message parsing); test-support uses plum-driver (T5)
|
|
100
|
+
**Produces:** phase 3b results
|
|
101
|
+
**Notes:** delete the deprecated `typedSource`/`typedSourceWith` and any `// removed in T4` wrappers; grep src for `.contains("`/`.starts_with("` on error messages (none may decide behaviour). Byte-compare example wasm against the pre-T1 hashes (coordinator has them in .superlite/progress.md).
|
|
102
|
+
**Test code:** workspace build zero warnings; all binaries; sweep 662; fixtures compile; wasm hashes identical (list any difference as a finding).
|
|
103
|
+
|
|
104
|
+
## Review Focus
|
|
105
|
+
- A: state — a dropped rewrite never leaves a half-adopted TypedInfo; pass order unchanged.
|
|
106
|
+
- B: protocol — CLI exit codes and stderr format; LSP positions 0- vs 1-based.
|
docs/plans/2026-11-05-compiler-cleanup-phase3b.results.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Compiler cleanup phase 3b: results
|
|
2
|
+
|
|
3
|
+
- Temporary shims deleted: loader `loadAndMerge`/`loadAndMergeWithOverrides`, lower `perFile`/`frontEnd`, `Diagnostic::legacyString`, `legacyJoin`, checker `typedSource`/`typedSourceWith`/`FusionOptions` and the old pass bodies (`preUnionSource`, `adoptPre`, `fusionRewrite`, `recheckKeep`), codegen `compileSource` and `typeCheckWith`. `CompileOptions.fusion` is now `passes: plum_driver::PassOptions`. `plum_driver::fusionNotes` is the public fusion body. Tests use `plum_driver::check`, the `*Diag` loader/lower functions, and `plum_test_support::compileSource`. `CheckErrorExt` stays: it is the constructor set for checker diagnostics, not a shim.
|
|
4
|
+
- plum-driver: a TRMC rewrite that fails its re-check now prints the same "dropped" note under `--explain` as fusion and closure specialisation.
|
|
5
|
+
- The only error-message match that decides behaviour is the registered undefined-name match (`isResolutionMessage`); phase 4 removes it.
|
|
6
|
+
- `cargo build --workspace --tests`: zero warnings from our crates.
|
|
7
|
+
- Test binaries (all green): core 391, checker 141, cli 16, wasm_codegen 207, plum_lsp 20 (incl. `typeDiagnosticUsesTheDiagnosticPosition`), driver 3, plum_test_support 7, plum_core 3, plum_wasm_codegen 3, plum 5, plum_checker 5.
|
|
8
|
+
- `plum test plum-std plum-examples`: 662 passed, 0 failed (65/65 files).
|
|
9
|
+
- All 9 fixtures in `plum-game/web-test/fixtures` compile.
|
|
10
|
+
- wasm hashes of the 25 plum-examples are identical to `.superlite/wasm-before-3b.txt`.
|
|
11
|
+
- `git diff --stat faa4d7e`: 55 files changed, 575 insertions(+), 612 deletions(-).
|
docs/plans/2026-11-05-explicit-fn-generics.progress.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
- start 949dabf
|
|
2
|
+
Ruling: skip Wave 0 - cost: none
|
|
3
|
+
Ruling: user 2026-11-05: rename Float.Euler -> Float.E in T8 (after T7 frees single letters); Float.Epsilon unchanged - cost: none
|
|
4
|
+
Ruling: final-review build-speed.progress.md - coordinator-written copy of the previous run's notes, checkpoint refusals are bookkeeping; committed with this run - cost: none
|
|
5
|
+
Ruling: final-review shadowing (fun map[T] in struct Box[T]) - cannot occur: redeclaring an owner generic is an error with a test (T3) - cost: none
|
plum-checker/Cargo.toml
CHANGED
|
@@ -8,5 +8,6 @@ plum-core = { path = "../plum-core" }
|
|
|
8
8
|
|
|
9
9
|
[dev-dependencies]
|
|
10
10
|
plum-test-support = { path = "../plum-test-support" }
|
|
11
|
+
plum-driver = { path = "../plum-driver" }
|
|
11
12
|
tree-sitter = "0.26"
|
|
12
13
|
tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
plum-checker/src/lib.rs
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
mod state;
|
|
6
6
|
pub mod subtyping;
|
|
7
7
|
pub mod types;
|
|
8
|
+
pub use types::{CheckErrorExt, DiagKind};
|
|
8
9
|
|
|
9
10
|
use plum_core::ast;
|
|
10
11
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
11
|
-
use state::{resetCheck, withCheck};
|
|
12
|
+
use state::{registerResolution, resetCheck, setCurrentFile, withCheck};
|
|
12
13
|
use types::{CheckError, CheckResult, PlumType, TypeEnv, TypeScheme};
|
|
13
14
|
|
|
14
15
|
|
|
@@ -267,7 +268,11 @@ pub fn restoreScope(saved: SavedScope) {
|
|
|
267
268
|
/// duration of `body`.
|
|
268
269
|
pub fn withFnScope<R>(f: &ast::Fn, owner: &[String], body: impl FnOnce() -> R) -> R {
|
|
269
270
|
let saved = setScope(Scope::of(owner, f), Some((f.file.clone(), f.line, f.name.clone())));
|
|
271
|
+
let prevFile = (!f.file.is_empty()).then(|| setCurrentFile(&f.file));
|
|
270
272
|
let r = body();
|
|
273
|
+
if let Some(prev) = prevFile {
|
|
274
|
+
setCurrentFile(&prev);
|
|
275
|
+
}
|
|
271
276
|
restoreScope(saved);
|
|
272
277
|
r
|
|
273
278
|
}
|
|
@@ -393,7 +398,7 @@ fn freshOwner(generics: &[String]) -> BTreeMap<String, PlumType> {
|
|
|
393
398
|
}
|
|
394
399
|
|
|
395
400
|
/// Remembers a variable binding made at any call site (variable names are
|
|
396
|
-
/// globally unique) so `
|
|
401
|
+
/// globally unique) so `checkProgram` can resolve recorded types at the end.
|
|
397
402
|
/// Applies the bindings recorded so far at any call site (e.g. `out := List()`
|
|
398
403
|
/// then `out.push(x)` bound `_t1`) to a type, so a later `.field` or method
|
|
399
404
|
/// call on a still-variable-typed value (`out.at(0).x`) sees the real type.
|
|
@@ -679,9 +684,15 @@ fn wrapHint(expected: &PlumType, actual: &PlumType, ctx: &CheckCtx) -> &'static
|
|
|
679
684
|
}
|
|
680
685
|
|
|
681
686
|
pub fn lookup(env: &TypeEnv, name: &str) -> Result<PlumType, String> {
|
|
682
|
-
env.get(name)
|
|
683
|
-
|
|
687
|
+
env.get(name).map(|s| *s.body.clone()).ok_or_else(|| undefinedName(name))
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/// The "undefined name" error text; registered, so the `CheckError` built from it (or from
|
|
691
|
+
/// a message that wraps it) is a `DiagKind::Resolution`.
|
|
692
|
+
fn undefinedName(name: &str) -> String {
|
|
684
|
-
|
|
693
|
+
let message = format!("undefined name '{}'", name);
|
|
694
|
+
registerResolution(&message);
|
|
695
|
+
message
|
|
685
696
|
}
|
|
686
697
|
|
|
687
698
|
/// `(receiver type, method name) -> TFun` for every `name<Receiver>(...)` method.
|
|
@@ -902,24 +913,6 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
902
913
|
checkSourceFull(source).0
|
|
903
914
|
}
|
|
904
915
|
|
|
905
|
-
/// Type-checks the original source like `checkSource` and also returns the
|
|
906
|
-
/// instantiated type of every expression (keyed by expression address) and the
|
|
907
|
-
/// set of generic instantiations (see `TypedInfo`) for codegen.
|
|
908
|
-
/// Switches for the supercompiler stage of `typedSource`.
|
|
909
|
-
#[derive(Clone, Copy, Debug)]
|
|
910
|
-
pub struct FusionOptions {
|
|
911
|
-
/// Run the fusion stage (`--no-fusion` clears it).
|
|
912
|
-
pub enabled: bool,
|
|
913
|
-
/// Print each fusion decision to stderr (`--explain-fusion`).
|
|
914
|
-
pub explain: bool,
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
impl Default for FusionOptions {
|
|
918
|
-
fn default() -> Self {
|
|
919
|
-
FusionOptions { enabled: true, explain: false }
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
916
|
/// Whether `obj_ty.field` is declared immutable (a record or enum field without `mut`).
|
|
924
917
|
/// `None` for structs (value-local assignment stays allowed) and unknown types.
|
|
925
918
|
fn immutableFieldOwner(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Option<String> {
|
|
@@ -943,142 +936,9 @@ fn immutableFieldOwner(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Option
|
|
|
943
936
|
}
|
|
944
937
|
}
|
|
945
938
|
|
|
946
|
-
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
947
|
-
typedSourceWith(source, FusionOptions::default())
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/// Like `typedSource` with explicit fusion options (no thread-local read).
|
|
951
|
-
pub fn typedSourceWith(source: &ast::Source, fusion: FusionOptions) -> CheckResult<TypedInfo> {
|
|
952
|
-
let (mut result, mut info) = checkSourceFull(source);
|
|
953
|
-
// Closure specialisation and fusion run on the checked program BEFORE union lowering
|
|
954
|
-
// (`pre`: own generics applied, `for` loops lowered), so the passes never see the
|
|
955
|
-
// hidden `List__Cons(Cons(..))` wrappers. Each rewrite is re-checked (with union
|
|
956
|
-
// lowering) and adopted only if it checks; on failure the previous program stays.
|
|
957
|
-
if result.is_ok() {
|
|
958
|
-
let mut pre = preUnionSource(source);
|
|
959
|
-
// Closure specialisation: copies of callees with the closure argument inlined.
|
|
960
|
-
{
|
|
961
|
-
// `info` is keyed by the addresses of the program it checked (`lowered`, or
|
|
962
|
-
// `source` itself), not of the clone `pre`; hints are keyed by call position,
|
|
963
|
-
// so read them off the checked program and apply them to its twin `pre`.
|
|
964
|
-
let hints = match info.lowered.clone() {
|
|
965
|
-
Some(checked) => closureHints(&checked, &info),
|
|
966
|
-
None => closureHints(source, &info),
|
|
967
|
-
};
|
|
968
|
-
let mut copy = pre.clone();
|
|
969
|
-
if !plum_core::closure_spec::specialiseWith(&mut copy, &hints).is_empty()
|
|
970
|
-
&& adoptPre(©, &mut result, &mut info, "closure specialisation", fusion.explain)
|
|
971
|
-
{
|
|
972
|
-
pre = copy;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
// Supercompiler stage: purity -> simplify -> drive -> uniqueness -> reuse.
|
|
976
|
-
// `--no-fusion` skips it.
|
|
977
|
-
if fusion.enabled {
|
|
978
|
-
let mut copy = pre.clone();
|
|
979
|
-
let notes = fusionRewrite(&mut copy);
|
|
980
|
-
if fusion.explain {
|
|
981
|
-
for n in ¬es {
|
|
982
|
-
eprintln!("fusion: {n}");
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
if !notes.is_empty() {
|
|
986
|
-
adoptPre(©, &mut result, &mut info, "fusion", fusion.explain);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
// Tail recursion modulo cons: rewrite the lowered program, then check the
|
|
991
|
-
// rewrite again (types are recorded per expression address). If the rewrite
|
|
992
|
-
// does not check, keep the original program.
|
|
993
|
-
if result.is_ok() {
|
|
994
|
-
if let Some(lowered) = info.lowered.as_ref() {
|
|
995
|
-
let mut copy = (**lowered).clone();
|
|
996
|
-
if !plum_core::trmc::rewrite(&mut copy).is_empty() {
|
|
997
|
-
recheckKeep(copy, &mut result, &mut info);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
// Mark tail calls on the final lowered program codegen compiles. Flags are set
|
|
1002
|
-
// in place, so the address-keyed type info stays valid.
|
|
1003
|
-
if let Some(lowered) = info.lowered.as_mut().and_then(std::rc::Rc::get_mut) {
|
|
1004
|
-
plum_core::tail::markTailCalls(lowered);
|
|
1005
|
-
}
|
|
1006
|
-
result.map(|()| info)
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
/// The checked program before union lowering: own generics applied and `for` loops lowered.
|
|
1010
|
-
fn preUnionSource(source: &ast::Source) -> ast::Source {
|
|
1011
|
-
let mut cur = source.clone();
|
|
1012
|
-
while let Some(applied) = applyOwnGenerics(&cur) {
|
|
1013
|
-
cur = applied;
|
|
1014
|
-
}
|
|
1015
|
-
if cur.formLoops {
|
|
1016
|
-
plum_core::forms::begin(&cur);
|
|
1017
|
-
let _ = checkSourceOnce(&cur);
|
|
1018
|
-
cur = plum_core::forms::finish(&cur);
|
|
1019
|
-
}
|
|
1020
|
-
cur
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/// Checks `copy` (a pre-union program) with union lowering; adopts the result and info only
|
|
1024
|
-
/// if it checks. Returns whether it was adopted.
|
|
1025
|
-
/// A rewrite that does not re-check is dropped; with `explain` the dropped stage and the
|
|
1026
|
-
/// first re-check errors are printed, so a lost optimisation cannot hide.
|
|
1027
|
-
fn adoptPre(copy: &ast::Source, result: &mut CheckResult<()>, info: &mut TypedInfo, stage: &str, explain: bool) -> bool {
|
|
1028
|
-
// Type info is keyed by expression address: check the very program kept as `lowered`.
|
|
1029
|
-
let kept = std::rc::Rc::new(copy.clone());
|
|
1030
|
-
let (r2, mut i2) = checkSourceOnce(&kept);
|
|
1031
|
-
if r2.is_ok() {
|
|
1032
|
-
if i2.lowered.is_none() {
|
|
1033
|
-
i2.lowered = Some(kept);
|
|
1034
|
-
}
|
|
1035
|
-
*result = r2;
|
|
1036
|
-
*info = i2;
|
|
1037
|
-
true
|
|
1038
|
-
} else {
|
|
1039
|
-
if explain {
|
|
1040
|
-
eprintln!("fusion: {stage} dropped: the rewritten program failed its re-check");
|
|
1041
|
-
if let Err(errs) = &r2 {
|
|
1042
|
-
for e in errs.iter().take(3) {
|
|
1043
|
-
eprintln!("fusion: {}:{}: {}", e.line, e.column, e.message);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
false
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
/// Runs the fusion passes on `copy` and returns every note (empty = nothing changed).
|
|
1052
|
-
pub fn fusionRewrite(copy: &mut ast::Source) -> Vec<String> {
|
|
1053
|
-
let pure = plum_core::purity::pureFns(copy);
|
|
1054
|
-
let mut notes = plum_core::simplify::simplify(copy, &pure);
|
|
1055
|
-
notes.extend(plum_core::drive::drive(copy, &pure));
|
|
1056
|
-
let rep = plum_core::uniqueness::markUnique(copy);
|
|
1057
|
-
notes.extend(rep.proved.iter().map(|l| format!("unique spread at {l}")));
|
|
1058
|
-
notes.extend(rep.unproved.iter().map(|(l, why)| format!("uniqueness: not in place {l}: {why}")));
|
|
1059
|
-
notes.retain(|n| !n.is_empty());
|
|
1060
|
-
notes.extend(plum_core::reuse::markReuse(copy));
|
|
1061
|
-
notes.retain(|n| !n.is_empty());
|
|
1062
|
-
notes
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/// Re-checks the rewritten `copy`; adopts it (result and info) only if it checks,
|
|
1066
|
-
/// otherwise the original `result`/`info` stay. Returns whether it was adopted.
|
|
1067
|
-
pub fn recheckKeep(copy: ast::Source, result: &mut CheckResult<()>, info: &mut TypedInfo) -> bool {
|
|
1068
|
-
let (r2, mut i2) = checkSourceOnce(©);
|
|
1069
|
-
if r2.is_ok() {
|
|
1070
|
-
i2.lowered = Some(std::rc::Rc::new(copy));
|
|
1071
|
-
*result = r2;
|
|
1072
|
-
*info = i2;
|
|
1073
|
-
true
|
|
1074
|
-
} else {
|
|
1075
|
-
false
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
939
|
/// Receiver type names of the method calls of `lowered`, for `closure_spec` (a call site
|
|
1080
940
|
/// whose receiver has no single known type gets no hint).
|
|
1081
|
-
fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_spec::Hints {
|
|
941
|
+
pub fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_spec::Hints {
|
|
1082
942
|
let mut inst: HashMap<usize, Vec<&PlumType>> = HashMap::new();
|
|
1083
943
|
for ((_, addr), t) in &info.inst_expr_types {
|
|
1084
944
|
inst.entry(*addr).or_default().push(t);
|
|
@@ -1116,6 +976,12 @@ fn closureHints(lowered: &ast::Source, info: &TypedInfo) -> plum_core::closure_s
|
|
|
1116
976
|
hints
|
|
1117
977
|
}
|
|
1118
978
|
|
|
979
|
+
/// Checks `source` (own generics applied, `for` loops lowered) and returns the result with the
|
|
980
|
+
/// typed program info. Checking only: the rewrite passes live in `plum-driver`.
|
|
981
|
+
pub fn checkProgram(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
982
|
+
checkSourceFull(source)
|
|
983
|
+
}
|
|
984
|
+
|
|
1119
985
|
fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1120
986
|
if let Some(applied) = applyOwnGenerics(source) {
|
|
1121
987
|
// Check (and let codegen compile) the copy with bare own-generic names applied.
|
|
@@ -1212,6 +1078,12 @@ fn ioCapabilityErrors(source: &ast::Source) -> Vec<CheckError> {
|
|
|
1212
1078
|
errors
|
|
1213
1079
|
}
|
|
1214
1080
|
|
|
1081
|
+
/// One check of exactly `source` (no own-generics application, no loop lowering first); what
|
|
1082
|
+
/// the driver uses to re-check a rewritten program.
|
|
1083
|
+
pub fn checkOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1084
|
+
checkSourceOnce(source)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1215
1087
|
fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1216
1088
|
resetCheck();
|
|
1217
1089
|
withCheck(|s| {
|
|
@@ -1255,7 +1127,7 @@ fn checkSourceOnce(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1255
1127
|
/// Returns a copy of `source` with every such type annotation (params, returns,
|
|
1256
1128
|
/// annotated locals, explicit construction generics, fields) applied,
|
|
1257
1129
|
/// so the checker and codegen both see the same `TApp`; `None` if there is none.
|
|
1258
|
-
fn applyOwnGenerics(source: &ast::Source) -> Option<ast::Source> {
|
|
1130
|
+
pub fn applyOwnGenerics(source: &ast::Source) -> Option<ast::Source> {
|
|
1259
1131
|
let own: HashMap<&str, Vec<ast::Type>> = source
|
|
1260
1132
|
.items
|
|
1261
1133
|
.iter()
|
|
@@ -1700,6 +1572,7 @@ fn constUseError(n: &str, env: &TypeEnv, ctx: &CheckCtx) -> Option<String> {
|
|
|
1700
1572
|
}
|
|
1701
1573
|
|
|
1702
1574
|
fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1575
|
+
setCurrentFile(&source.file);
|
|
1703
1576
|
// Generic items are checked once, structurally (`TApp`/`TParam`), on the
|
|
1704
1577
|
// ORIGINAL source.
|
|
1705
1578
|
let mut errors: Vec<CheckError> = checkPrimitives(source);
|
|
@@ -1814,7 +1687,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1814
1687
|
}
|
|
1815
1688
|
let mut local_errors = checkFn(f, &global_env, &ctx);
|
|
1816
1689
|
if !fnGenericNames(f, &ctx).is_empty() {
|
|
1817
|
-
local_errors.retain(|e|
|
|
1690
|
+
local_errors.retain(|e| e.kind == types::DiagKind::Resolution);
|
|
1818
1691
|
drainCalls();
|
|
1819
1692
|
} else {
|
|
1820
1693
|
registerCalls(&mut inst, &fn_by_key, &ctx, None);
|
|
@@ -1826,6 +1699,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1826
1699
|
for item in &source.items {
|
|
1827
1700
|
if let ast::Item::Test(t) = item {
|
|
1828
1701
|
withCheck(|s| { let l = &s.cur_pos; l.set((t.line, t.column)) });
|
|
1702
|
+
let prevFile = (!t.file.is_empty()).then(|| setCurrentFile(&t.file));
|
|
1829
1703
|
let mut env = global_env.clone();
|
|
1830
1704
|
// A test may use `io` when std/Io is loaded (the test wrapper passes it).
|
|
1831
1705
|
if source.items.iter().any(|i| matches!(i, ast::Item::Enum(e) if e.name == "Io")) {
|
|
@@ -1840,6 +1714,9 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1840
1714
|
&ctx,
|
|
1841
1715
|
);
|
|
1842
1716
|
errors.append(&mut block_errors);
|
|
1717
|
+
if let Some(prev) = prevFile {
|
|
1718
|
+
setCurrentFile(&prev);
|
|
1719
|
+
}
|
|
1843
1720
|
if !t.file.is_empty() && t.file != source.file {
|
|
1844
1721
|
// An imported file's tests are never compiled (see `desugarTestsToFns`):
|
|
1845
1722
|
// their calls must not instantiate generic fns the program does not use.
|
|
@@ -1885,7 +1762,7 @@ fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1885
1762
|
};
|
|
1886
1763
|
let full = format!("{}{}", chain, msg);
|
|
1887
1764
|
if seen_msgs.insert(full.clone()) {
|
|
1888
|
-
errors.push(CheckError:
|
|
1765
|
+
errors.push(CheckError { message: full, ..e });
|
|
1889
1766
|
}
|
|
1890
1767
|
}
|
|
1891
1768
|
registerCalls(&mut inst, &fn_by_key, &ctx, Some(id));
|
|
@@ -1907,14 +1784,8 @@ struct InstState {
|
|
|
1907
1784
|
file: String,
|
|
1908
1785
|
}
|
|
1909
1786
|
|
|
1910
|
-
///
|
|
1787
|
+
/// (A generic body keeps its `DiagKind::Resolution` errors even when uninstantiated; every
|
|
1911
|
-
///
|
|
1788
|
+
/// other error is a type error found per instantiation.)
|
|
1912
|
-
fn isResolutionError(msg: &str) -> bool {
|
|
1913
|
-
msg.contains("undefined name")
|
|
1914
|
-
|| msg.contains("extern")
|
|
1915
|
-
|| msg.contains("missing a body")
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
1789
|
fn drainCalls() -> Vec<CallSite> {
|
|
1919
1790
|
withCheck(|s| { let c = &s.calls; {
|
|
1920
1791
|
c.borrow_mut()
|
|
@@ -2104,7 +1975,7 @@ fn checkFnBody(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckEr
|
|
|
2104
1975
|
match (f.is_extern, &f.body) {
|
|
2105
1976
|
(true, ast::FnBody::Extern) => {
|
|
2106
1977
|
if f.type_param.is_some() {
|
|
2107
|
-
errors.push(CheckError::
|
|
1978
|
+
errors.push(CheckError::resolution(
|
|
2108
1979
|
f.line,
|
|
2109
1980
|
f.column,
|
|
2110
1981
|
format!(
|
|
@@ -2116,7 +1987,7 @@ fn checkFnBody(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckEr
|
|
|
2116
1987
|
return errors;
|
|
2117
1988
|
}
|
|
2118
1989
|
(true, _) => {
|
|
2119
|
-
errors.push(CheckError::
|
|
1990
|
+
errors.push(CheckError::resolution(
|
|
2120
1991
|
f.line,
|
|
2121
1992
|
f.column,
|
|
2122
1993
|
format!("fn '{}': extern fn must not have a body", f.name),
|
|
@@ -2136,7 +2007,7 @@ fn checkFnBody(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckEr
|
|
|
2136
2007
|
}
|
|
2137
2008
|
return errors;
|
|
2138
2009
|
}
|
|
2139
|
-
errors.push(CheckError::
|
|
2010
|
+
errors.push(CheckError::resolution(
|
|
2140
2011
|
f.line,
|
|
2141
2012
|
f.column,
|
|
2142
2013
|
format!("fn '{}': missing a body (mark it abstract or extern)", f.name),
|
|
@@ -4589,9 +4460,11 @@ fn inferExprInner(
|
|
|
4589
4460
|
}
|
|
4590
4461
|
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
|
4591
4462
|
// `k => v` desugars to `Pair(k, v)`; without std/Map there is no Pair.
|
|
4592
|
-
Err(_) if call.name == "Pair" =>
|
|
4463
|
+
Err(_) if call.name == "Pair" => {
|
|
4593
|
-
"undefined name 'Pair' — `k => v` builds a std Pair; import std/Map".to_string()
|
|
4464
|
+
let message = "undefined name 'Pair' — `k => v` builds a std Pair; import std/Map".to_string();
|
|
4465
|
+
registerResolution(&message);
|
|
4466
|
+
Err(message)
|
|
4594
|
-
|
|
4467
|
+
}
|
|
4595
4468
|
Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
|
|
4596
4469
|
}
|
|
4597
4470
|
}
|
|
@@ -4687,60 +4560,3 @@ fn checkStaticUse(
|
|
|
4687
4560
|
}
|
|
4688
4561
|
Ok(())
|
|
4689
4562
|
}
|
|
4690
|
-
|
|
4691
|
-
#[cfg(test)]
|
|
4692
|
-
mod fusion_options_tests {
|
|
4693
|
-
use super::*;
|
|
4694
|
-
use plum_test_support::parse;
|
|
4695
|
-
|
|
4696
|
-
const SRC: &str = "\
|
|
4697
|
-
enum Cells =
|
|
4698
|
-
| Cons(head: Int, tail: Cells)
|
|
4699
|
-
| Nil
|
|
4700
|
-
|
|
4701
|
-
fun mapCells(xs: Cells) -> Cells = match xs
|
|
4702
|
-
Cons(h, t) => Cons(head: h * 2, tail: mapCells(t))
|
|
4703
|
-
Nil => Nil
|
|
4704
|
-
|
|
4705
|
-
fun sumL(xs: Cells) -> Int = match xs
|
|
4706
|
-
Cons(h, t) => h + sumL(t)
|
|
4707
|
-
Nil => 0
|
|
4708
|
-
|
|
4709
|
-
fun total(xs: Cells) -> Int = sumL(mapCells(xs))
|
|
4710
|
-
|
|
4711
|
-
fun count(n: Int) -> Int =
|
|
4712
|
-
s := 0
|
|
4713
|
-
for i := range n
|
|
4714
|
-
s = s + i
|
|
4715
|
-
s
|
|
4716
|
-
";
|
|
4717
|
-
|
|
4718
|
-
fn fnNames(info: &TypedInfo) -> Vec<String> {
|
|
4719
|
-
info.lowered
|
|
4720
|
-
.as_ref()
|
|
4721
|
-
.unwrap()
|
|
4722
|
-
.items
|
|
4723
|
-
.iter()
|
|
4724
|
-
.filter_map(|i| match i {
|
|
4725
|
-
ast::Item::Fn(f) => Some(f.name.clone()),
|
|
4726
|
-
_ => None,
|
|
4727
|
-
})
|
|
4728
|
-
.collect()
|
|
4729
|
-
}
|
|
4730
|
-
|
|
4731
|
-
#[test]
|
|
4732
|
-
fn defaultOptionsMatchTypedSource() {
|
|
4733
|
-
let src = parse(SRC);
|
|
4734
|
-
let a = typedSource(&src).expect("checks");
|
|
4735
|
-
let b = typedSourceWith(&src, FusionOptions::default()).expect("checks");
|
|
4736
|
-
assert_eq!(fnNames(&a), fnNames(&b));
|
|
4737
|
-
assert!(fnNames(&b).iter().any(|n| n.contains("sumL$mapCells")));
|
|
4738
|
-
}
|
|
4739
|
-
|
|
4740
|
-
#[test]
|
|
4741
|
-
fn disabledHasNoFusedFns() {
|
|
4742
|
-
let src = parse(SRC);
|
|
4743
|
-
let info = typedSourceWith(&src, FusionOptions { enabled: false, explain: false }).expect("checks");
|
|
4744
|
-
assert!(!fnNames(&info).iter().any(|n| n.contains('$')), "{:?}", fnNames(&info));
|
|
4745
|
-
}
|
|
4746
|
-
}
|
plum-checker/src/state.rs
CHANGED
|
@@ -18,7 +18,7 @@ pub(crate) struct CheckState {
|
|
|
18
18
|
pub(crate) closure_hint: RefCell<Option<(Vec<PlumType>, bool)>>,
|
|
19
19
|
/// Counter for the fresh `_t<N>` inference variables minted per generic call site.
|
|
20
20
|
pub(crate) fresh: Cell<u32>,
|
|
21
|
-
/// When set, `inferExprWith` records every expression's type here (see `
|
|
21
|
+
/// When set, `inferExprWith` records every expression's type here (see `checkProgram`).
|
|
22
22
|
pub(crate) record_binds: RefCell<Option<Binds>>,
|
|
23
23
|
pub(crate) record: RefCell<Option<HashMap<usize, PlumType>>>,
|
|
24
24
|
/// Generic-call sites seen while checking one body (see `CallSite`); `None` outside `checkSourceInner`.
|
|
@@ -51,6 +51,11 @@ pub(crate) struct CheckState {
|
|
|
51
51
|
/// Generic names in scope in the fn (or enum) whose types are being converted, and
|
|
52
52
|
/// where it is (file, line, fn name) for the implicit-generic report.
|
|
53
53
|
pub(crate) scope: RefCell<plum_core::generics::Scope>,
|
|
54
|
+
/// The file of the declaration being checked, stamped on every error made meanwhile.
|
|
55
|
+
pub(crate) cur_file: RefCell<String>,
|
|
56
|
+
/// Exact messages of resolution errors (see `registerResolution`): an error whose text
|
|
57
|
+
/// contains one is a `DiagKind::Resolution`.
|
|
58
|
+
pub(crate) resolution_msgs: RefCell<Vec<String>>,
|
|
54
59
|
pub(crate) scope_site: RefCell<Option<(String, usize, String)>>,
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -65,6 +70,30 @@ pub(crate) fn withCheck<R>(f: impl FnOnce(&CheckState) -> R) -> R {
|
|
|
65
70
|
CHECK.with(|c| f(&c.borrow()))
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
/// The file of the declaration being checked (empty outside one).
|
|
74
|
+
pub(crate) fn currentFile() -> String {
|
|
75
|
+
withCheck(|s| s.cur_file.borrow().clone())
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub(crate) fn setCurrentFile(file: &str) -> String {
|
|
79
|
+
withCheck(|s| std::mem::replace(&mut *s.cur_file.borrow_mut(), file.to_string()))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Records a message created as a resolution error, so the `CheckError` later built from it
|
|
83
|
+
/// (usually wrapped in a longer message) is classified by where it came from.
|
|
84
|
+
pub(crate) fn registerResolution(message: &str) {
|
|
85
|
+
withCheck(|s| {
|
|
86
|
+
let mut m = s.resolution_msgs.borrow_mut();
|
|
87
|
+
if !m.iter().any(|x| x == message) {
|
|
88
|
+
m.push(message.to_string());
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub(crate) fn isResolutionMessage(message: &str) -> bool {
|
|
94
|
+
withCheck(|s| s.resolution_msgs.borrow().iter().any(|r| message.contains(r.as_str())))
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/// Return every field to its initial value. Must not run inside `withCheck`.
|
|
69
98
|
pub(crate) fn resetCheck() {
|
|
70
99
|
CHECK.with(|c| *c.borrow_mut() = CheckState::default());
|
|
@@ -94,12 +123,14 @@ mod tests {
|
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
#[test]
|
|
97
|
-
fn
|
|
126
|
+
fn checkProgramIsRepeatable() {
|
|
98
127
|
let src = plum_test_support::parse(
|
|
99
128
|
"fun id[T](x: T) -> T = x\nfun main() -> Int =\n a := id(1)\n b := id(true)\n a\n",
|
|
100
129
|
);
|
|
101
|
-
let first = crate::
|
|
130
|
+
let (r1, first) = crate::checkProgram(&src);
|
|
102
|
-
let second = crate::
|
|
131
|
+
let (r2, second) = crate::checkProgram(&src);
|
|
132
|
+
r1.expect("checks");
|
|
133
|
+
r2.expect("checks");
|
|
103
134
|
// generated names (closure copies, `_t<N>` vars) must not depend on earlier checks
|
|
104
135
|
let names = |i: &crate::TypedInfo| format!("{:?}", i.lowered.as_ref().map(|l| l.items.len()));
|
|
105
136
|
assert_eq!(names(&first), names(&second));
|
plum-checker/src/types.rs
CHANGED
|
@@ -85,42 +85,29 @@ impl TypeScheme {
|
|
|
85
85
|
|
|
86
86
|
pub type TypeEnv = BTreeMap<String, TypeScheme>;
|
|
87
87
|
|
|
88
|
+
/// A checker error is a `Diagnostic`; the position is the enclosing declaration's (see
|
|
89
|
+
/// `ast::Fn`/`ast::Enum`/`ast::Test`) unless a more specific one is known, `(0, 0)` when unknown.
|
|
90
|
+
pub type CheckError = plum_core::diag::Diagnostic;
|
|
88
|
-
|
|
91
|
+
pub use plum_core::diag::DiagKind;
|
|
92
|
+
|
|
93
|
+
/// Constructors for `CheckError` that fill the file from the declaration being checked
|
|
94
|
+
/// (`state::CheckState::cur_file`) and the kind from where the error is created.
|
|
89
|
-
pub
|
|
95
|
+
pub trait CheckErrorExt: Sized {
|
|
90
|
-
pub message: String,
|
|
91
|
-
/// 1-based source position the error is about — the position of the
|
|
92
|
-
/// enclosing `fn`/`enum`/`test` declaration (see `ast::Fn`/`ast::Enum`/
|
|
93
|
-
///
|
|
96
|
+
/// A type error at `line:column` of the declaration being checked.
|
|
97
|
+
fn at(line: usize, column: usize, message: String) -> Self;
|
|
94
|
-
///
|
|
98
|
+
/// A name-resolution or declaration-structure error (kept even in an uninstantiated
|
|
95
|
-
///
|
|
99
|
+
/// generic body).
|
|
96
|
-
pub line: usize,
|
|
97
|
-
|
|
100
|
+
fn resolution(line: usize, column: usize, message: String) -> Self;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
impl CheckError {
|
|
103
|
+
impl CheckErrorExt for CheckError {
|
|
101
|
-
/// A `CheckError` with no known position — equivalent to the old
|
|
102
|
-
/// `CheckError { message }` literal, kept as the default for error sites
|
|
103
|
-
/// that aren't (yet) scoped to a single declaration.
|
|
104
|
-
|
|
104
|
+
fn at(line: usize, column: usize, message: String) -> Self {
|
|
105
|
+
let kind = if crate::state::isResolutionMessage(&message) { DiagKind::Resolution } else { DiagKind::Type };
|
|
105
|
-
CheckError
|
|
106
|
+
CheckError::new(kind, crate::state::currentFile(), line, column, message)
|
|
106
|
-
message,
|
|
107
|
-
line: 0,
|
|
108
|
-
column: 0,
|
|
109
|
-
}
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
fn resolution(line: usize, column: usize, message: String) -> Self {
|
|
113
|
-
CheckError {
|
|
114
|
-
message,
|
|
115
|
-
line,
|
|
116
|
-
column,
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
impl std::fmt::Display for CheckError {
|
|
122
|
-
|
|
110
|
+
CheckError::new(DiagKind::Resolution, crate::state::currentFile(), line, column, message)
|
|
123
|
-
write!(f, "{}", self.message)
|
|
124
111
|
}
|
|
125
112
|
}
|
|
126
113
|
|
plum-checker/tests/checker/checker_tests.rs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_checker::types::*;
|
|
3
|
-
use plum_checker::{checkSource, plumTypeFromAst,
|
|
3
|
+
use plum_checker::{checkSource, plumTypeFromAst, unify, CheckCtx, TypedInfo};
|
|
4
4
|
use plum_core::ast::Type as AstType;
|
|
5
5
|
use plum_core::ast::*;
|
|
6
6
|
use plum_test_support::{parse, parse as parseClean};
|
|
7
7
|
|
|
8
|
+
/// The checker plus the default pass list.
|
|
9
|
+
fn typedSource(source: &Source) -> Result<TypedInfo, Vec<plum_core::Diagnostic>> {
|
|
10
|
+
plum_driver::check(source, &plum_driver::PassOptions::default())
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
fn emptyCtx() -> CheckCtx<'static> {
|
|
9
14
|
use std::sync::OnceLock;
|
|
10
15
|
static METHODS: OnceLock<plum_checker::MethodEnv> = OnceLock::new();
|
|
@@ -233,7 +238,7 @@ fn instantiation_set_listed() {
|
|
|
233
238
|
"{}fun main() -> Int =\n a := Slice(items: 1).map(|n| n * 2)\n b := Slice(items: \"x\").map(|s| s)\n 0\n",
|
|
234
239
|
GENERIC_DEFS
|
|
235
240
|
);
|
|
236
|
-
let info =
|
|
241
|
+
let info = typedSource(&parseClean(&src)).unwrap_or_else(|e| panic!("{:?}", e));
|
|
237
242
|
let has = |t: PlumType| {
|
|
238
243
|
info.instantiations
|
|
239
244
|
.iter()
|
|
@@ -295,10 +300,10 @@ fn typed_source_has_no_unresolved_infer_vars() {
|
|
|
295
300
|
if skip.iter().any(|s| path.ends_with(s)) {
|
|
296
301
|
continue;
|
|
297
302
|
}
|
|
298
|
-
let ast = match plum_core::
|
|
303
|
+
let ast = match plum_core::loadAndMergeDiag(&path, &root) {
|
|
299
304
|
Ok(a) => a,
|
|
300
305
|
Err(e) => {
|
|
301
|
-
bad.push(format!("{}: load failed: {}", path.display(), e));
|
|
306
|
+
bad.push(format!("{}: load failed: {}", path.display(), e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")));
|
|
302
307
|
continue;
|
|
303
308
|
}
|
|
304
309
|
};
|
plum-checker/tests/checker/closure_hint_test.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
//! Closure specialisation resolves a method call by the receiver's checked type, not
|
|
3
3
|
//! by the method name being unique: `Box.apply` and `Bag.apply` both exist here.
|
|
4
|
-
use
|
|
4
|
+
use plum_driver::{check, PassOptions};
|
|
5
5
|
use plum_core::ast::{Expr, Item};
|
|
6
6
|
use plum_core::visit::walkRef;
|
|
7
7
|
use plum_test_support::parse;
|
|
@@ -18,7 +18,7 @@ fun useBox(b: Box) -> Int = b.apply(|x| x + 1)
|
|
|
18
18
|
|
|
19
19
|
/// Method names `useBox` calls, in the checked (and specialised) program fusion would see.
|
|
20
20
|
fn calledByUse(src: &str) -> Vec<String> {
|
|
21
|
-
let info =
|
|
21
|
+
let info = check(&parse(src), &PassOptions { fusion: false, ..PassOptions::default() }).expect("checks");
|
|
22
22
|
let low = info.lowered.as_ref().expect("closure specialisation adopted a rewrite");
|
|
23
23
|
let f = low
|
|
24
24
|
.items
|
plum-checker/tests/checker/diagnostic_tests.rs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! Checker errors are `Diagnostic`s: they carry the item's file and a kind.
|
|
3
|
+
use plum_checker::{checkSource, DiagKind};
|
|
4
|
+
|
|
5
|
+
fn errors(src: &str) -> Vec<plum_checker::types::CheckError> {
|
|
6
|
+
let mut source = plum_test_support::parseRawSource(src);
|
|
7
|
+
source.stampFile("main.plum");
|
|
8
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
9
|
+
checkSource(&source).unwrap_err()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn undefinedNameIsAResolutionDiagnosticWithItsFile() {
|
|
14
|
+
let es = errors("fun f() -> Int = g\n\nfun main() -> Int = f()\n");
|
|
15
|
+
assert!(es.iter().all(|e| e.file == "main.plum"), "{es:?}");
|
|
16
|
+
let e = es.iter().find(|e| e.message.contains("undefined name 'g'")).expect("undefined name");
|
|
17
|
+
assert_eq!(e.kind, DiagKind::Resolution, "{e:?}");
|
|
18
|
+
assert_eq!(e.line, 1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn typeMismatchIsATypeDiagnostic() {
|
|
23
|
+
let es = errors("fun main() -> Int = true\n");
|
|
24
|
+
assert!(es.iter().all(|e| e.kind == DiagKind::Type && e.file == "main.plum"), "{es:?}");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn resolutionErrorInAnUninstantiatedGenericBodyIsStillReported() {
|
|
29
|
+
let es = errors("fun id[T](x: T) -> T = nope\n\nfun main() -> Int = 1\n");
|
|
30
|
+
assert!(
|
|
31
|
+
es.iter().any(|e| e.kind == DiagKind::Resolution && e.message.contains("undefined name 'nope'")),
|
|
32
|
+
"{es:?}"
|
|
33
|
+
);
|
|
34
|
+
}
|
plum-checker/tests/checker/explicit_generics_tests.rs
CHANGED
|
@@ -4,7 +4,7 @@ use plum_checker::checkSource;
|
|
|
4
4
|
|
|
5
5
|
fn run(src: &str) -> Result<(), String> {
|
|
6
6
|
let mut source = plum_test_support::parseRawSource(src);
|
|
7
|
-
plum_core::lower::
|
|
7
|
+
plum_core::lower::frontEndDiag(&mut source).map_err(|e| format!("lower: {e:?}"))?;
|
|
8
8
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "))
|
|
9
9
|
}
|
|
10
10
|
|
plum-checker/tests/checker/features/io_capability.rs
CHANGED
|
@@ -3,7 +3,7 @@ use plum_checker::checkSource;
|
|
|
3
3
|
|
|
4
4
|
fn run(src: &str) -> Result<(), String> {
|
|
5
5
|
let mut source = plum_test_support::parse(src);
|
|
6
|
-
plum_core::lower::
|
|
6
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
7
7
|
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "));
|
|
8
8
|
r
|
|
9
9
|
}
|
plum-checker/tests/checker/features/mutability.rs
CHANGED
|
@@ -3,7 +3,7 @@ use plum_checker::checkSource;
|
|
|
3
3
|
|
|
4
4
|
fn run(src: &str) -> Result<(), String> {
|
|
5
5
|
let mut source = plum_test_support::parseRawSource(src);
|
|
6
|
-
plum_core::lower::
|
|
6
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
7
7
|
let r = checkSource(&source)
|
|
8
8
|
.map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "));
|
|
9
9
|
r
|
plum-checker/tests/checker/features/pattern_casing.rs
CHANGED
|
@@ -9,7 +9,7 @@ enum Shape = Ring(r: Float) | Dot
|
|
|
9
9
|
fn run(body: &str) -> Result<(), String> {
|
|
10
10
|
let src = format!("{DEFS}\n{body}\n");
|
|
11
11
|
let mut source = plum_test_support::parse(&src);
|
|
12
|
-
plum_core::lower::
|
|
12
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
13
13
|
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "));
|
|
14
14
|
r
|
|
15
15
|
}
|
plum-checker/tests/checker/features/variant_pattern.rs
CHANGED
|
@@ -9,7 +9,7 @@ enum Shape = Ring(r: Float) | Dot
|
|
|
9
9
|
fn run(body: &str) -> Result<(), Vec<String>> {
|
|
10
10
|
let src = format!("{DEFS}\n{body}\n");
|
|
11
11
|
let mut source = plum_test_support::parse(&src);
|
|
12
|
-
plum_core::lower::
|
|
12
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
13
13
|
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
14
14
|
r
|
|
15
15
|
}
|
plum-checker/tests/checker/fusion_pipeline_test.rs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
//! The supercompiler stage of `
|
|
2
|
+
//! The supercompiler stage of `plum_driver::check`: switches, explain notes, re-check fallback.
|
|
3
|
-
use plum_checker::{checkSource,
|
|
3
|
+
use plum_checker::{checkSource, TypedInfo};
|
|
4
|
+
use plum_core::Diagnostic;
|
|
5
|
+
use plum_driver::{fusionNotes, PassOptions};
|
|
4
6
|
use plum_core::ast::Source;
|
|
5
7
|
use plum_test_support::parse;
|
|
6
8
|
|
|
@@ -27,6 +29,11 @@ fun count(n: Int) -> Int =
|
|
|
27
29
|
s
|
|
28
30
|
";
|
|
29
31
|
|
|
32
|
+
/// The checker plus the default pass list.
|
|
33
|
+
fn typedSource(source: &Source) -> Result<TypedInfo, Vec<Diagnostic>> {
|
|
34
|
+
plum_driver::check(source, &PassOptions::default())
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
fn lowered(src: &str) -> Source {
|
|
31
38
|
let info = typedSource(&parse(src)).expect("checks");
|
|
32
39
|
(**info.lowered.as_ref().unwrap()).clone()
|
|
@@ -51,23 +58,10 @@ fn fused_site_is_in_lowered_program() {
|
|
|
51
58
|
#[test]
|
|
52
59
|
fn rewritten_program_still_checks() {
|
|
53
60
|
let mut copy = lowered(SRC);
|
|
54
|
-
|
|
61
|
+
fusionNotes(&mut copy);
|
|
55
62
|
assert!(checkSource(©).is_ok());
|
|
56
63
|
// an already-fused program yields no further notes
|
|
57
|
-
assert!(
|
|
64
|
+
assert!(fusionNotes(&mut copy).is_empty());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[test]
|
|
61
|
-
fn failed_recheck_keeps_original() {
|
|
62
|
-
let src = parse(SRC);
|
|
63
|
-
let mut info = typedSource(&src).expect("checks");
|
|
64
|
-
let before = std::rc::Rc::as_ptr(info.lowered.as_ref().unwrap());
|
|
65
|
-
let mut result = Ok(());
|
|
66
|
-
// A copy that cannot type-check: `total` now returns a Bool for an Int.
|
|
67
|
-
let bad = parse(&SRC.replace("sumL(mapCells(xs))", "True"));
|
|
68
|
-
assert!(!recheckKeep(bad, &mut result, &mut info));
|
|
69
|
-
assert!(result.is_ok());
|
|
70
|
-
assert_eq!(std::rc::Rc::as_ptr(info.lowered.as_ref().unwrap()), before);
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
fn plumBin() -> Option<std::path::PathBuf> {
|
|
@@ -128,7 +122,7 @@ fn fusion_runs_without_loops_or_unions() {
|
|
|
128
122
|
fn explain_lists_unproved_uniqueness() {
|
|
129
123
|
let src = "struct Pt(x: Int, y: Int)\nfun bad(s: Pt) -> Pt =\n k := s\n Pt(..s, x: 1)\n";
|
|
130
124
|
let mut copy = parse(src);
|
|
131
|
-
let notes =
|
|
125
|
+
let notes = fusionNotes(&mut copy);
|
|
132
126
|
let n = notes.iter().find(|n| n.contains("not in place")).unwrap_or_else(|| panic!("{notes:?}"));
|
|
133
127
|
assert!(n.contains("aliased"), "{n}");
|
|
134
128
|
}
|
plum-checker/tests/checker/inline_variants_tests.rs
CHANGED
|
@@ -13,7 +13,7 @@ struct Circle(r: Float)
|
|
|
13
13
|
fn run(defs: &str, body: &str) -> Result<(), Vec<String>> {
|
|
14
14
|
let src = format!("{defs}\n{body}\n");
|
|
15
15
|
let mut source = plum_test_support::parse(&src);
|
|
16
|
-
plum_core::lower::
|
|
16
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
17
17
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect())
|
|
18
18
|
}
|
|
19
19
|
|
plum-checker/tests/checker/main.rs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//! Single integration-test binary for plum-checker (one link instead of one per file).
|
|
3
3
|
mod checker_tests;
|
|
4
4
|
mod closure_hint_test;
|
|
5
|
+
mod diagnostic_tests;
|
|
5
6
|
mod explicit_generics_tests;
|
|
6
7
|
mod features;
|
|
7
8
|
mod fusion_pipeline_test;
|
plum-checker/tests/checker/question_tests.rs
CHANGED
|
@@ -24,7 +24,7 @@ enum ErrB =
|
|
|
24
24
|
fn run(body: &str) -> Result<(), Vec<String>> {
|
|
25
25
|
let src = format!("{DEFS}\n{body}\n");
|
|
26
26
|
let mut source = plum_test_support::parse(&src);
|
|
27
|
-
plum_core::lower::
|
|
27
|
+
plum_core::lower::frontEndDiag(&mut source).expect("front end");
|
|
28
28
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect())
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -221,7 +221,7 @@ fn removed_forms_fail_to_compile() {
|
|
|
221
221
|
let failed = match parsed {
|
|
222
222
|
Err(_) => true,
|
|
223
223
|
Ok(mut source) => {
|
|
224
|
-
plum_core::lower::
|
|
224
|
+
plum_core::lower::frontEndDiag(&mut source).is_err() || checkSource(&source).is_err()
|
|
225
225
|
}
|
|
226
226
|
};
|
|
227
227
|
assert!(failed, "should fail to compile: {body}");
|
plum-checker/tests/checker/subtyping_tests.rs
CHANGED
|
@@ -20,7 +20,7 @@ struct Plain(q: Int)
|
|
|
20
20
|
fn run(body: &str) -> Result<(), String> {
|
|
21
21
|
let src = format!("{DEFS}\n{body}\n");
|
|
22
22
|
let mut source = plum_test_support::parseRawSource(&src);
|
|
23
|
-
plum_core::lower::
|
|
23
|
+
plum_core::lower::frontEndDiag(&mut source).map_err(|e| format!("lower: {e:?}"))?;
|
|
24
24
|
checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect::<Vec<_>>().join(" | "))
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -86,7 +86,7 @@ fn noWildcardMessageIsClear() {
|
|
|
86
86
|
fn abstractStructMethodAccepted() {
|
|
87
87
|
let src = "struct Actor(x: Int) =\n abstract fun draw(self) -> Int\n\nstruct Ship(Actor(x)) =\n override fun draw(self) -> Int = 2\n\nfun f(a: Actor) -> Int = a.draw()\n";
|
|
88
88
|
let mut source = plum_test_support::parseRawSource(src);
|
|
89
|
-
plum_core::lower::
|
|
89
|
+
plum_core::lower::frontEndDiag(&mut source).unwrap();
|
|
90
90
|
checkSource(&source).unwrap();
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -94,14 +94,14 @@ fn abstractStructMethodAccepted() {
|
|
|
94
94
|
fn bodilessTopLevelFnStillErrors() {
|
|
95
95
|
let src = "fun g() -> Int\n";
|
|
96
96
|
let mut source = plum_test_support::parseRawSource(src);
|
|
97
|
-
plum_core::lower::
|
|
97
|
+
plum_core::lower::frontEndDiag(&mut source).unwrap();
|
|
98
98
|
let e = checkSource(&source).unwrap_err();
|
|
99
99
|
assert!(e.iter().any(|x| x.message.contains("missing a body")));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
fn checkErrs(src: &str) -> Vec<String> {
|
|
103
103
|
let mut source = plum_test_support::parseRawSource(src);
|
|
104
|
-
plum_core::lower::
|
|
104
|
+
plum_core::lower::frontEndDiag(&mut source).unwrap();
|
|
105
105
|
checkSource(&source).unwrap_err().into_iter().map(|x| x.message).collect()
|
|
106
106
|
}
|
|
107
107
|
|
plum-cli/Cargo.toml
CHANGED
|
@@ -11,6 +11,7 @@ path = "src/main.rs"
|
|
|
11
11
|
[dependencies]
|
|
12
12
|
plum-core = { path = "../plum-core" }
|
|
13
13
|
plum-checker = { path = "../plum-checker" }
|
|
14
|
+
plum-driver = { path = "../plum-driver" }
|
|
14
15
|
plum-wasm-codegen = { path = "../plum-wasm-codegen" }
|
|
15
16
|
clap = { version = "4", features = ["derive"] }
|
|
16
17
|
anyhow = "1"
|
plum-cli/src/game.rs
CHANGED
|
@@ -227,7 +227,7 @@ pub fn cmdGame(
|
|
|
227
227
|
port: Option<u16>,
|
|
228
228
|
noOpen: bool,
|
|
229
229
|
lib_path: Option<PathBuf>,
|
|
230
|
-
fusion:
|
|
230
|
+
fusion: plum_driver::PassOptions,
|
|
231
231
|
) -> Result<()> {
|
|
232
232
|
let lib_path = lib_path.unwrap_or_else(repoRoot);
|
|
233
233
|
let wasm = crate::compileToWasm(&file, &lib_path, fusion)?;
|
plum-cli/src/main.rs
CHANGED
|
@@ -9,6 +9,7 @@ use std::process;
|
|
|
9
9
|
use anyhow::{Context, Result};
|
|
10
10
|
use clap::{Parser, Subcommand};
|
|
11
11
|
|
|
12
|
+
use plum_core::diag::{DiagKind, Diagnostic};
|
|
12
13
|
use plum_core::formatSource;
|
|
13
14
|
|
|
14
15
|
mod editor;
|
|
@@ -138,14 +139,31 @@ fn main() {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/// The `--no-fusion` / `--explain-fusion` flags of a command, as explicit fusion options.
|
|
141
|
-
fn fusionFlags(explain: bool, no_fusion: bool) ->
|
|
142
|
+
fn fusionFlags(explain: bool, no_fusion: bool) -> plum_driver::PassOptions {
|
|
142
|
-
|
|
143
|
+
plum_driver::PassOptions { fusion: !no_fusion, explain, ..plum_driver::PassOptions::default() }
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
///
|
|
146
|
+
/// One diagnostic as `file:line:col: <kind>: message`. `entry` names the file when the
|
|
146
|
-
///
|
|
147
|
+
/// diagnostic carries none (positions then refer to the merged entry source).
|
|
147
|
-
fn
|
|
148
|
+
fn renderDiag(d: &Diagnostic, entry: &std::path::Path) -> String {
|
|
149
|
+
let prefix = match d.kind {
|
|
150
|
+
DiagKind::Parse => "parse error: ",
|
|
151
|
+
DiagKind::Type | DiagKind::Resolution => "type error: ",
|
|
152
|
+
DiagKind::Codegen => "codegen error: ",
|
|
153
|
+
DiagKind::Load => "",
|
|
154
|
+
};
|
|
155
|
+
let file = if d.file.is_empty() { entry.display().to_string() } else { d.file.clone() };
|
|
156
|
+
if d.line > 0 {
|
|
157
|
+
format!("{}:{}:{}: {}{}", file, d.line, d.column, prefix, d.message)
|
|
158
|
+
} else if d.kind == DiagKind::Load || (d.file.is_empty() && d.kind == DiagKind::Codegen) {
|
|
159
|
+
format!("{}{}", prefix, d.message)
|
|
160
|
+
} else {
|
|
161
|
+
format!("{}: {}{}", file, prefix, d.message)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn renderAll(ds: &[Diagnostic], entry: &std::path::Path) -> String {
|
|
148
|
-
|
|
166
|
+
ds.iter().map(|d| renderDiag(d, entry)).collect::<Vec<_>>().join("\n")
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
fn run() -> Result<()> {
|
|
@@ -225,25 +243,24 @@ fn cmdFormat(file: Option<std::path::PathBuf>, check: bool, use_stdin: bool) ->
|
|
|
225
243
|
fn compileToWasm(
|
|
226
244
|
file: &std::path::Path,
|
|
227
245
|
lib_path: &std::path::Path,
|
|
228
|
-
fusion:
|
|
246
|
+
fusion: plum_driver::PassOptions,
|
|
229
247
|
) -> Result<Vec<u8>> {
|
|
230
|
-
let ast = plum_core::
|
|
248
|
+
let ast = plum_core::loadAndMergeDiag(file, lib_path).map_err(|e| anyhow::anyhow!("{}", renderAll(&e, file)))?;
|
|
231
249
|
|
|
232
|
-
plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: false, fusion })
|
|
250
|
+
plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: false, passes: fusion })
|
|
251
|
+
.map(|o| o.wasm)
|
|
233
|
-
|
|
252
|
+
.map_err(|ds| {
|
|
234
|
-
eprintln!("{}",
|
|
253
|
+
eprintln!("{}", renderAll(&ds, file));
|
|
235
254
|
process::exit(1);
|
|
236
|
-
}
|
|
237
|
-
anyhow::anyhow!("codegen error: {e}")
|
|
238
|
-
|
|
255
|
+
})
|
|
239
256
|
}
|
|
240
257
|
|
|
241
258
|
fn compileToTestWasm(
|
|
242
259
|
file: &std::path::Path,
|
|
243
260
|
lib_path: &std::path::Path,
|
|
244
|
-
fusion:
|
|
261
|
+
fusion: plum_driver::PassOptions,
|
|
245
262
|
) -> Result<(Vec<u8>, Vec<String>)> {
|
|
246
|
-
let ast = plum_core::
|
|
263
|
+
let ast = plum_core::loadAndMergeDiag(file, lib_path).map_err(|e| anyhow::anyhow!("{}", renderAll(&e, file)))?;
|
|
247
264
|
|
|
248
265
|
// Returns an error (rather than exiting the process directly, as
|
|
249
266
|
// `compileToWasm`'s equivalent check does for the single-file
|
|
@@ -252,13 +269,7 @@ fn compileToTestWasm(
|
|
|
252
269
|
// continue to the rest of the directory instead of the whole run
|
|
253
270
|
// dying on the first file with a type error. Checked once, on the
|
|
254
271
|
// test-desugared source codegen then compiles.
|
|
255
|
-
let out = plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: true, fusion }).map_err(|
|
|
272
|
+
let out = plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: true, passes: fusion }).map_err(|ds| anyhow::anyhow!("{}", renderAll(&ds, file)))?;
|
|
256
|
-
if e.starts_with("type error") {
|
|
257
|
-
anyhow::anyhow!(nameFileInErrors(&e, file))
|
|
258
|
-
} else {
|
|
259
|
-
anyhow::anyhow!("codegen error: {e}")
|
|
260
|
-
}
|
|
261
|
-
})?;
|
|
262
273
|
let compiled = (out.wasm, out.test_names);
|
|
263
274
|
// Debug aid: `PLUM_DUMP_WASM=out.wasm plum test file.plum` keeps the module.
|
|
264
275
|
if let Some(path) = std::env::var_os("PLUM_DUMP_WASM") {
|
|
@@ -272,7 +283,7 @@ fn cmdCompile(
|
|
|
272
283
|
output: Option<std::path::PathBuf>,
|
|
273
284
|
lib_path: std::path::PathBuf,
|
|
274
285
|
) -> Result<()> {
|
|
275
|
-
let wasm_bytes = compileToWasm(&file, &lib_path,
|
|
286
|
+
let wasm_bytes = compileToWasm(&file, &lib_path, plum_driver::PassOptions::default())?;
|
|
276
287
|
|
|
277
288
|
let out_path = output.unwrap_or_else(|| file.with_extension("wasm"));
|
|
278
289
|
fs::write(&out_path, &wasm_bytes)
|
|
@@ -285,7 +296,7 @@ fn cmdCompile(
|
|
|
285
296
|
fn cmdRun(
|
|
286
297
|
file: std::path::PathBuf,
|
|
287
298
|
lib_path: std::path::PathBuf,
|
|
288
|
-
fusion:
|
|
299
|
+
fusion: plum_driver::PassOptions,
|
|
289
300
|
) -> Result<()> {
|
|
290
301
|
let wasm_bytes = compileToWasm(&file, &lib_path, fusion)?;
|
|
291
302
|
|
|
@@ -356,7 +367,7 @@ fn collectPlumFiles(path: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
|
|
|
356
367
|
fn cmdTest(
|
|
357
368
|
paths: Vec<std::path::PathBuf>,
|
|
358
369
|
lib_path: std::path::PathBuf,
|
|
359
|
-
fusion:
|
|
370
|
+
fusion: plum_driver::PassOptions,
|
|
360
371
|
) -> Result<()> {
|
|
361
372
|
// A single plain file keeps the exact original single-file UX (no
|
|
362
373
|
// `=== path ===` header, no aggregate summary line, no "N/1 files
|
|
@@ -438,7 +449,7 @@ fn cmdTest(
|
|
|
438
449
|
fn runTestFile(
|
|
439
450
|
file: &std::path::Path,
|
|
440
451
|
lib_path: &std::path::Path,
|
|
441
|
-
fusion:
|
|
452
|
+
fusion: plum_driver::PassOptions,
|
|
442
453
|
) -> Result<(usize, usize)> {
|
|
443
454
|
let (wasm_bytes, names) = compileToTestWasm(file, lib_path, fusion)?;
|
|
444
455
|
if let Ok(path) = std::env::var("PLUM_DUMP_WASM") {
|
|
@@ -562,7 +573,7 @@ fn cmdBuild(
|
|
|
562
573
|
file: std::path::PathBuf,
|
|
563
574
|
output: Option<std::path::PathBuf>,
|
|
564
575
|
lib_path: std::path::PathBuf,
|
|
565
|
-
fusion:
|
|
576
|
+
fusion: plum_driver::PassOptions,
|
|
566
577
|
) -> Result<()> {
|
|
567
578
|
let wasm_bytes = compileToWasm(&file, &lib_path, fusion)?;
|
|
568
579
|
|
plum-cli/tests/cli/compile_tests.rs
CHANGED
|
@@ -107,3 +107,19 @@ fn returnBodySuggestedByTheParseErrorHintCompiles() {
|
|
|
107
107
|
String::from_utf8_lossy(&output.stderr)
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn undefinedNameIsReportedAsFileLineColumnTypeError() {
|
|
113
|
+
let dir = freshTempDir("undefined_name");
|
|
114
|
+
let src_path = dir.join("bad.plum");
|
|
115
|
+
std::fs::write(&src_path, "fun f(a: Unit) -> Unit =\n b\n").unwrap();
|
|
116
|
+
let out = Command::new(plumBin())
|
|
117
|
+
.args(["compile", src_path.to_str().unwrap(), "-o", dir.join("bad.wasm").to_str().unwrap()])
|
|
118
|
+
.output()
|
|
119
|
+
.expect("failed to run plum compile");
|
|
120
|
+
assert_eq!(out.status.code(), Some(1));
|
|
121
|
+
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
122
|
+
let line = stderr.lines().find(|l| l.contains("undefined name")).unwrap_or_else(|| panic!("{stderr}"));
|
|
123
|
+
assert!(line.contains("bad.plum:2:3: type error: "), "{line}");
|
|
124
|
+
assert!(line.contains("undefined name 'b'"), "{line}");
|
|
125
|
+
}
|
plum-cli/tests/cli/crash_test.rs
CHANGED
|
@@ -61,5 +61,5 @@ fn int_literal_overflow_error_has_position() {
|
|
|
61
61
|
all.contains("integer literal out of range"),
|
|
62
62
|
"output: {all}"
|
|
63
63
|
);
|
|
64
|
-
assert!(all.contains("lit.plum
|
|
64
|
+
assert!(all.contains("lit.plum:4:"), "output: {all}");
|
|
65
65
|
}
|
plum-core/Cargo.toml
CHANGED
|
@@ -13,6 +13,7 @@ topiary-tree-sitter-facade = "0.7.3"
|
|
|
13
13
|
plum-test-support = { path = "../plum-test-support" }
|
|
14
14
|
tempfile = "3"
|
|
15
15
|
plum-checker = { path = "../plum-checker" }
|
|
16
|
+
plum-driver = { path = "../plum-driver" }
|
|
16
17
|
plum-wasm-codegen = { path = "../plum-wasm-codegen" }
|
|
17
18
|
wasmparser = "0.220"
|
|
18
19
|
wasmtime = "47"
|
plum-core/src/diag.rs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! The one error value between parser, loader, lowering, checker, driver, codegen and tools.
|
|
2
|
+
use std::fmt;
|
|
3
|
+
|
|
4
|
+
/// What produced a diagnostic. Callers decide behaviour from the kind, never from the text.
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
6
|
+
pub enum DiagKind {
|
|
7
|
+
/// Syntax: a token tree-sitter could not fit, a malformed node, a misplaced top-level form.
|
|
8
|
+
Parse,
|
|
9
|
+
/// Reading or resolving files: unreadable file, missing import, duplicate declaration.
|
|
10
|
+
Load,
|
|
11
|
+
/// A name that does not resolve, or a declaration rule (imports, extension rules).
|
|
12
|
+
Resolution,
|
|
13
|
+
/// A type or lowering error.
|
|
14
|
+
Type,
|
|
15
|
+
/// A failure while generating code.
|
|
16
|
+
Codegen,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
20
|
+
pub struct Diagnostic {
|
|
21
|
+
/// The file the error is in; empty when unknown.
|
|
22
|
+
pub file: String,
|
|
23
|
+
/// 1-based; 0 when no position is known.
|
|
24
|
+
pub line: usize,
|
|
25
|
+
pub column: usize,
|
|
26
|
+
pub kind: DiagKind,
|
|
27
|
+
pub message: String,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Diagnostic {
|
|
31
|
+
pub fn new(kind: DiagKind, file: impl Into<String>, line: usize, column: usize, message: impl Into<String>) -> Self {
|
|
32
|
+
Diagnostic { file: file.into(), line, column, kind, message: message.into() }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// A diagnostic with no position (and no file).
|
|
36
|
+
pub fn bare(kind: DiagKind, message: impl Into<String>) -> Self {
|
|
37
|
+
Diagnostic::new(kind, "", 0, 0, message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl fmt::Display for Diagnostic {
|
|
42
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
43
|
+
match (self.file.is_empty(), self.line) {
|
|
44
|
+
(true, 0) => write!(f, "{}", self.message),
|
|
45
|
+
(true, l) => write!(f, "{}:{}: {}", l, self.column, self.message),
|
|
46
|
+
(false, 0) => write!(f, "{}: {}", self.file, self.message),
|
|
47
|
+
(false, l) => write!(f, "{}:{}:{}: {}", self.file, l, self.column, self.message),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
plum-core/src/drive.rs
CHANGED
|
@@ -33,6 +33,25 @@ pub fn drive(src: &mut Source, pure: &HashSet<String>) -> Vec<String> {
|
|
|
33
33
|
d.notes
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/// Why a consumer could not be unfolded as a one-expression wrapper.
|
|
37
|
+
enum Unfold {
|
|
38
|
+
/// Its body is not one expression: no wrapper, so no candidate unless handed a just-built value.
|
|
39
|
+
NotAConsumer,
|
|
40
|
+
Other(String),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl From<String> for Unfold {
|
|
44
|
+
fn from(s: String) -> Self {
|
|
45
|
+
Unfold::Other(s)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl From<&str> for Unfold {
|
|
50
|
+
fn from(s: &str) -> Self {
|
|
51
|
+
Unfold::Other(s.to_string())
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
#[derive(Clone)]
|
|
37
56
|
struct Call {
|
|
38
57
|
key: String,
|
|
@@ -912,7 +931,7 @@ impl Driver {
|
|
|
912
931
|
}
|
|
913
932
|
// a fn that neither matches nor wraps is no consumer: not a candidate,
|
|
914
933
|
// unless it is handed a just-built value it could have taken apart
|
|
915
|
-
Err(
|
|
934
|
+
Err(Unfold::NotAConsumer) => {
|
|
916
935
|
if let Some(t) = &built {
|
|
917
936
|
self.note(format!(
|
|
918
937
|
"not specialised: {} on a just-built {}: its body is not one match on that parameter",
|
|
@@ -920,7 +939,7 @@ impl Driver {
|
|
|
920
939
|
));
|
|
921
940
|
}
|
|
922
941
|
}
|
|
923
|
-
Err(why) => self.note(format!("not fused: {} of {}: {}", c.key, pk, why)),
|
|
942
|
+
Err(Unfold::Other(why)) => self.note(format!("not fused: {} of {}: {}", c.key, pk, why)),
|
|
924
943
|
}
|
|
925
944
|
continue;
|
|
926
945
|
};
|
|
@@ -1268,10 +1287,10 @@ impl Driver {
|
|
|
1268
1287
|
|
|
1269
1288
|
/// A non-recursive consumer whose body is one expression, instantiated at the call
|
|
1270
1289
|
/// (its own effects stay; arguments must be pure or used at most once).
|
|
1271
|
-
fn unfold(&mut self, c: &Call) -> Result<Expr,
|
|
1290
|
+
fn unfold(&mut self, c: &Call) -> Result<Expr, Unfold> {
|
|
1272
1291
|
let b = bodyBlock(&c.f).ok_or("extern consumer")?;
|
|
1273
1292
|
let [Stmt::Expr(body, ..)] = &b.stmts[..] else {
|
|
1274
|
-
return Err(
|
|
1293
|
+
return Err(Unfold::NotAConsumer);
|
|
1275
1294
|
};
|
|
1276
1295
|
let mut recursive = false;
|
|
1277
1296
|
walkRef(body, &mut |x| match x {
|
|
@@ -1284,22 +1303,22 @@ impl Driver {
|
|
|
1284
1303
|
_ => {}
|
|
1285
1304
|
});
|
|
1286
1305
|
if recursive {
|
|
1287
|
-
return Err(format!("wrapper {} is recursive", c.key));
|
|
1306
|
+
return Err(format!("wrapper {} is recursive", c.key).into());
|
|
1288
1307
|
}
|
|
1289
1308
|
if c.args.len() != pnames(&c.f).len() {
|
|
1290
|
-
return Err("argument count".to_string());
|
|
1309
|
+
return Err("argument count".to_string().into());
|
|
1291
1310
|
}
|
|
1292
1311
|
let (names, rb) = self.renamed(&c.f, "$u").ok_or("extern consumer")?;
|
|
1293
1312
|
let mut stmts = rb.stmts;
|
|
1294
|
-
let Some(Stmt::Expr(mut body, ..)) = stmts.pop() else { return Err("wrapper body".
|
|
1313
|
+
let Some(Stmt::Expr(mut body, ..)) = stmts.pop() else { return Err("wrapper body".into()) };
|
|
1295
1314
|
for (n, a) in names.iter().zip(&c.args) {
|
|
1296
1315
|
let uses = cnt(&body, n, true);
|
|
1297
1316
|
let trivial = matches!(a, Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) | Expr::TypeName(_));
|
|
1298
1317
|
if !self.argPure(a) {
|
|
1299
|
-
return Err(format!("argument of wrapper {} has effects", c.key));
|
|
1318
|
+
return Err(format!("argument of wrapper {} has effects", c.key).into());
|
|
1300
1319
|
}
|
|
1301
1320
|
if uses > 1 && !trivial {
|
|
1302
|
-
return Err(format!("wrapper {} uses an argument {} times", c.key, uses));
|
|
1321
|
+
return Err(format!("wrapper {} uses an argument {} times", c.key, uses).into());
|
|
1303
1322
|
}
|
|
1304
1323
|
}
|
|
1305
1324
|
let map: HashMap<&String, &Expr> = names.iter().zip(c.args.iter()).collect();
|
plum-core/src/lib.rs
CHANGED
|
@@ -7,6 +7,7 @@ pub mod builtin_usage;
|
|
|
7
7
|
pub mod closure_escape;
|
|
8
8
|
pub mod closure_spec;
|
|
9
9
|
pub mod ctors;
|
|
10
|
+
pub mod diag;
|
|
10
11
|
pub mod drive;
|
|
11
12
|
pub mod embeds;
|
|
12
13
|
pub mod formatter;
|
|
@@ -33,7 +34,9 @@ pub mod whistle;
|
|
|
33
34
|
pub use formatter::{
|
|
34
35
|
formatSource, formatSourceWithOpts, inlineSingleStatementCaseBodies, FormatterError,
|
|
35
36
|
};
|
|
37
|
+
pub use diag::{DiagKind, Diagnostic};
|
|
36
38
|
pub use loader::{
|
|
39
|
+
loadAndMergeDiag, loadAndMergeWithOverridesDiag,
|
|
37
|
-
|
|
40
|
+
syntaxErrors, SyntaxError, SyntaxErrorKind,
|
|
38
41
|
};
|
|
39
42
|
pub use parser::AstParser;
|
plum-core/src/loader.rs
CHANGED
|
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
|
|
3
3
|
|
|
4
4
|
use crate::ast::{Import, Item, PrimitiveDecl, Source, BUILTIN_PRIMITIVES};
|
|
5
5
|
use crate::builtin_usage::usedBuiltinTypeNames;
|
|
6
|
+
use crate::diag::{DiagKind, Diagnostic};
|
|
6
7
|
use crate::parser::AstParser;
|
|
7
8
|
|
|
8
9
|
/// Resolves `entry`'s `import` statements (transitively, and tolerating
|
|
@@ -15,8 +16,19 @@ use crate::parser::AstParser;
|
|
|
15
16
|
/// `import "std/option"` resolves to `<lib_path>/plum-std/option.plum` — the
|
|
16
17
|
/// on-disk stdlib directory is `plum-std`, but source files keep writing
|
|
17
18
|
/// `std/...` imports, so the `std/` prefix is remapped here.
|
|
18
|
-
pub fn
|
|
19
|
+
pub fn loadAndMergeDiag(entry: &Path, lib_path: &Path) -> Result<Source, Vec<Diagnostic>> {
|
|
19
|
-
|
|
20
|
+
loadAndMergeWithOverridesDiag(entry, lib_path, &HashMap::new())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// What the loader's internal steps fail with.
|
|
24
|
+
type Loaded_<T> = Result<T, Vec<Diagnostic>>;
|
|
25
|
+
|
|
26
|
+
fn load(message: String) -> Vec<Diagnostic> {
|
|
27
|
+
vec![Diagnostic::bare(DiagKind::Load, message)]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn resolution(message: String) -> Vec<Diagnostic> {
|
|
31
|
+
vec![Diagnostic::bare(DiagKind::Resolution, message)]
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
/// Same as [`loadAndMerge`], but `overrides` (keyed by canonicalized path)
|
|
@@ -27,11 +39,11 @@ pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
|
|
|
27
39
|
/// This exists for editor tooling (see `plum-lsp`): an LSP client's `entry`
|
|
28
40
|
/// document is often unsaved, so its on-disk content is stale or, for a
|
|
29
41
|
/// brand-new file, doesn't exist at all yet.
|
|
30
|
-
pub fn
|
|
42
|
+
pub fn loadAndMergeWithOverridesDiag(
|
|
31
43
|
entry: &Path,
|
|
32
44
|
lib_path: &Path,
|
|
33
45
|
overrides: &HashMap<PathBuf, String>,
|
|
34
|
-
) -> Result<Source,
|
|
46
|
+
) -> Result<Source, Vec<Diagnostic>> {
|
|
35
47
|
// `entry` itself may not exist on disk yet (a new, never-saved buffer),
|
|
36
48
|
// so canonicalize its parent instead and rejoin the file name — same
|
|
37
49
|
// trick `std::fs::canonicalize` can't do for us since it requires the
|
|
@@ -71,7 +83,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
71
83
|
formLoops: false,
|
|
72
84
|
primitives: prims,
|
|
73
85
|
};
|
|
74
|
-
crate::lower::
|
|
86
|
+
crate::lower::frontEndDiag(&mut merged)?;
|
|
75
87
|
Ok(merged)
|
|
76
88
|
}
|
|
77
89
|
|
|
@@ -81,7 +93,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
81
93
|
/// stateless namespace, so it leaves the namespace list. Outside std this
|
|
82
94
|
/// form is an error. A built-in's `primitive` block is extended the same way
|
|
83
95
|
/// with no extra rule: its methods are flat items.
|
|
84
|
-
fn dropExtensions(items: &[Item], prims: &mut Vec<PrimitiveDecl>) ->
|
|
96
|
+
fn dropExtensions(items: &[Item], prims: &mut Vec<PrimitiveDecl>) -> Loaded_<()> {
|
|
85
97
|
let declared = |n: &str| items.iter().any(|i| matches!(i, Item::Enum(e) if e.name == n));
|
|
86
98
|
let bad = USER_PRIMS.with(|u| {
|
|
87
99
|
u.borrow()
|
|
@@ -90,17 +102,19 @@ fn dropExtensions(items: &[Item], prims: &mut Vec<PrimitiveDecl>) -> Result<(),
|
|
|
90
102
|
.map(|(n, f)| (n.clone(), f.clone()))
|
|
91
103
|
});
|
|
92
104
|
if let Some((n, f)) = bad {
|
|
93
|
-
return Err(
|
|
105
|
+
return Err(vec![Diagnostic::new(
|
|
106
|
+
DiagKind::Resolution,
|
|
107
|
+
f.display().to_string(),
|
|
108
|
+
0,
|
|
109
|
+
0,
|
|
94
|
-
"
|
|
110
|
+
format!("primitive {} extends a declared type and is only allowed in std", n),
|
|
95
|
-
f.display(),
|
|
96
|
-
n
|
|
97
|
-
));
|
|
111
|
+
)]);
|
|
98
112
|
}
|
|
99
113
|
prims.retain(|p| !declared(&p.name));
|
|
100
114
|
Ok(())
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
fn canonicalizeMaybeMissing(path: &Path) ->
|
|
117
|
+
fn canonicalizeMaybeMissing(path: &Path) -> Loaded_<PathBuf> {
|
|
104
118
|
if let Ok(canon) = std::fs::canonicalize(path) {
|
|
105
119
|
return Ok(canon);
|
|
106
120
|
}
|
|
@@ -109,13 +123,13 @@ fn canonicalizeMaybeMissing(path: &Path) -> Result<PathBuf, String> {
|
|
|
109
123
|
.filter(|p| !p.as_os_str().is_empty())
|
|
110
124
|
.unwrap_or(Path::new("."));
|
|
111
125
|
let file_name = path.file_name().ok_or_else(|| {
|
|
112
|
-
format!(
|
|
126
|
+
load(format!(
|
|
113
127
|
"cannot read entry file '{}': not a file path",
|
|
114
128
|
path.display()
|
|
115
|
-
)
|
|
129
|
+
))
|
|
116
130
|
})?;
|
|
117
131
|
let parent_canon = std::fs::canonicalize(parent)
|
|
118
|
-
.map_err(|e| format!("cannot read entry file '{}': {}", path.display(), e))?;
|
|
132
|
+
.map_err(|e| load(format!("cannot read entry file '{}': {}", path.display(), e)))?;
|
|
119
133
|
Ok(parent_canon.join(file_name))
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -131,18 +145,18 @@ fn isStdFile(path: &Path) -> bool {
|
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
/// `import std/x` -> `<lib_path>/plum-std/x.plum`, canonicalized.
|
|
134
|
-
fn resolveImport(import: &Import, lib_path: &Path) ->
|
|
148
|
+
fn resolveImport(import: &Import, lib_path: &Path) -> Loaded_<PathBuf> {
|
|
135
149
|
let resolved_path = match import.path.strip_prefix("std/") {
|
|
136
150
|
Some(rest) => format!("plum-std/{}", rest),
|
|
137
151
|
None => import.path.clone(),
|
|
138
152
|
};
|
|
139
153
|
let target = lib_path.join(format!("{}.plum", resolved_path));
|
|
140
154
|
std::fs::canonicalize(&target).map_err(|_| {
|
|
141
|
-
format!(
|
|
155
|
+
load(format!(
|
|
142
156
|
"import '{}': no such file '{}'",
|
|
143
157
|
import.path,
|
|
144
158
|
target.display()
|
|
145
|
-
)
|
|
159
|
+
))
|
|
146
160
|
})
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -154,7 +168,7 @@ fn collectImport(
|
|
|
154
168
|
overrides: &HashMap<PathBuf, String>,
|
|
155
169
|
visited: &mut HashSet<PathBuf>,
|
|
156
170
|
files: &mut Vec<Loaded>,
|
|
157
|
-
) ->
|
|
171
|
+
) -> Loaded_<()> {
|
|
158
172
|
let target_canon = resolveImport(import, lib_path)?;
|
|
159
173
|
if !visited.insert(target_canon.clone()) {
|
|
160
174
|
// Already loaded — a diamond import or a cycle.
|
|
@@ -187,7 +201,7 @@ fn declaredTypes(source: &Source) -> Vec<String> {
|
|
|
187
201
|
/// on its own, e.g. `std/Int` importing `std/Pair`). The std type is renamed in
|
|
188
202
|
/// every std file (`Pair` -> `PairStd`), so std code keeps its own type and the
|
|
189
203
|
/// user's name means the user's type. A directly imported one stays a duplicate.
|
|
190
|
-
fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) ->
|
|
204
|
+
fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) -> Loaded_<()> {
|
|
191
205
|
let mut user: HashSet<String> = HashSet::new();
|
|
192
206
|
let mut direct: HashSet<PathBuf> = HashSet::new();
|
|
193
207
|
let mut all: HashSet<String> = HashSet::new();
|
|
@@ -234,8 +248,8 @@ fn shadowStdTypes(files: &mut [Loaded], lib_path: &Path) -> Result<(), String> {
|
|
|
234
248
|
|
|
235
249
|
/// `text` with every `type_identifier` token named in `rename` replaced (comments and
|
|
236
250
|
/// string text untouched); `None` when none occurs.
|
|
237
|
-
fn renameTypeIdents(text: &str, rename: &HashMap<String, String>) ->
|
|
251
|
+
fn renameTypeIdents(text: &str, rename: &HashMap<String, String>) -> Loaded_<Option<String>> {
|
|
238
|
-
let tree = treeOf(text)?;
|
|
252
|
+
let tree = treeOf(text).map_err(load)?;
|
|
239
253
|
let mut spans: Vec<(usize, usize, &str)> = Vec::new();
|
|
240
254
|
let mut cursor = tree.root_node().walk();
|
|
241
255
|
let mut stack = vec![tree.root_node()];
|
|
@@ -271,7 +285,7 @@ fn renameTypeIdents(text: &str, rename: &HashMap<String, String>) -> Result<Opti
|
|
|
271
285
|
/// `import std/Int` / `import std/Float` / `import std/Str` — except in
|
|
272
286
|
/// `Int.plum`/`Float.plum`/`Str.plum` themselves, which declare the type
|
|
273
287
|
/// rather than import it.
|
|
274
|
-
fn checkBuiltinImports(path: &Path, source: &Source) ->
|
|
288
|
+
fn checkBuiltinImports(path: &Path, source: &Source) -> Loaded_<()> {
|
|
275
289
|
checkExtends(path, source)?;
|
|
276
290
|
let used = usedBuiltinTypeNames(source);
|
|
277
291
|
for name in ["Int", "Float", "Str"] {
|
|
@@ -292,12 +306,12 @@ fn checkBuiltinImports(path: &Path, source: &Source) -> Result<(), String> {
|
|
|
292
306
|
let import_path = format!("std/{}", name);
|
|
293
307
|
let already_imported = source.imports.iter().any(|imp| imp.path == import_path);
|
|
294
308
|
if !already_imported {
|
|
295
|
-
return Err(format!(
|
|
309
|
+
return Err(resolution(format!(
|
|
296
310
|
"'{}' uses type '{}' but doesn't import it: add `import {}`",
|
|
297
311
|
path.display(),
|
|
298
312
|
name,
|
|
299
313
|
import_path
|
|
300
|
-
));
|
|
314
|
+
)));
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
Ok(())
|
|
@@ -311,11 +325,11 @@ fn displayPath(path: &Path) -> String {
|
|
|
311
325
|
rel.as_deref().unwrap_or(path).display().to_string()
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
fn readText(path: &Path, overrides: &HashMap<PathBuf, String>) ->
|
|
328
|
+
fn readText(path: &Path, overrides: &HashMap<PathBuf, String>) -> Loaded_<String> {
|
|
315
329
|
match overrides.get(path) {
|
|
316
330
|
Some(text) => Ok(text.clone()),
|
|
317
331
|
None => std::fs::read_to_string(path)
|
|
318
|
-
.map_err(|e| format!("cannot read '{}': {}", path.display(), e)),
|
|
332
|
+
.map_err(|e| load(format!("cannot read '{}': {}", path.display(), e))),
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
335
|
|
|
@@ -327,8 +341,12 @@ fn treeOf(text: &str) -> Result<tree_sitter::Tree, String> {
|
|
|
327
341
|
parser.parse(text, None).ok_or_else(|| "parse failed".to_string())
|
|
328
342
|
}
|
|
329
343
|
|
|
344
|
+
fn treeOfIn(text: &str, path: &Path) -> Loaded_<tree_sitter::Tree> {
|
|
345
|
+
treeOf(text).map_err(|e| vec![Diagnostic::bare(DiagKind::Parse, format!("{} for '{}'", e, path.display()))])
|
|
346
|
+
}
|
|
347
|
+
|
|
330
|
-
fn parseText(path: &Path, text: &str) ->
|
|
348
|
+
fn parseText(path: &Path, text: &str) -> Loaded_<Source> {
|
|
331
|
-
let tree =
|
|
349
|
+
let tree = treeOfIn(text, path)?;
|
|
332
350
|
let root = tree.root_node();
|
|
333
351
|
// `Parser::parse` only returns `None` on a timeout/cancellation — an
|
|
334
352
|
// unparseable token still produces a tree, just with an `ERROR` node
|
|
@@ -338,20 +356,25 @@ fn parseText(path: &Path, text: &str) -> Result<Source, String> {
|
|
|
338
356
|
// anywhere in the file silently discarded every top-level item after
|
|
339
357
|
// it — no parse error, just fewer items than the file actually has.
|
|
340
358
|
if let Some(err) = syntaxErrors(root, &text).into_iter().next() {
|
|
341
|
-
return Err(
|
|
359
|
+
return Err(vec![Diagnostic::new(
|
|
342
|
-
|
|
360
|
+
DiagKind::Parse,
|
|
343
|
-
path.display(),
|
|
361
|
+
path.display().to_string(),
|
|
344
362
|
err.start.row + 1,
|
|
345
363
|
err.start.column + 1,
|
|
346
364
|
err.message(),
|
|
347
|
-
));
|
|
365
|
+
)]);
|
|
348
366
|
}
|
|
349
367
|
let ap = AstParser::new(&text);
|
|
350
368
|
let mut source = ap.parseSourceRaw(root);
|
|
369
|
+
// A malformed node the grammar let through: report the first one.
|
|
370
|
+
if let Some(mut d) = ap.diagnostics().into_iter().next() {
|
|
371
|
+
d.file = path.display().to_string();
|
|
372
|
+
return Err(vec![d]);
|
|
373
|
+
}
|
|
351
374
|
source.stampFile(&displayPath(path));
|
|
352
|
-
source
|
|
353
|
-
|
|
375
|
+
source.checkTopLevelOrder().map_err(|e| {
|
|
354
|
-
|
|
376
|
+
vec![Diagnostic::new(DiagKind::Parse, path.display().to_string(), 0, 0, e)]
|
|
377
|
+
})?;
|
|
355
378
|
Ok(source)
|
|
356
379
|
}
|
|
357
380
|
|
|
@@ -631,10 +654,10 @@ fn mergeItems(
|
|
|
631
654
|
items: &mut Vec<Item>,
|
|
632
655
|
names: &mut HashMap<String, PathBuf>,
|
|
633
656
|
prims: &mut Vec<PrimitiveDecl>,
|
|
634
|
-
) ->
|
|
657
|
+
) -> Loaded_<()> {
|
|
635
658
|
// Removed forms, then inline variants as hidden records (per-file lowering).
|
|
636
659
|
let mut hoisted = source.clone();
|
|
637
|
-
crate::lower::
|
|
660
|
+
crate::lower::perFileDiag(&mut hoisted)?;
|
|
638
661
|
let source = &hoisted;
|
|
639
662
|
// Parse-time literal errors (out-of-range integers) of every merged file.
|
|
640
663
|
MERGED_LIT_ERRORS.with(|e| e.borrow_mut().extend(source.formErrors.iter().cloned()));
|
|
@@ -676,23 +699,23 @@ fn mergeItems(
|
|
|
676
699
|
.get(&key)
|
|
677
700
|
.map(|f| f.display().to_string())
|
|
678
701
|
.unwrap_or_default();
|
|
679
|
-
return Err(format!(
|
|
702
|
+
return Err(load(format!(
|
|
680
703
|
"duplicate constant '{}': defined in {}:{} and {}:{}",
|
|
681
704
|
c.name,
|
|
682
705
|
prev_file,
|
|
683
706
|
prev.line,
|
|
684
707
|
file.display(),
|
|
685
708
|
c.line
|
|
686
|
-
));
|
|
709
|
+
)));
|
|
687
710
|
}
|
|
688
711
|
}
|
|
689
712
|
if let Some(existing_file) = names.get(&key) {
|
|
690
|
-
return Err(format!(
|
|
713
|
+
return Err(load(format!(
|
|
691
714
|
"duplicate declaration '{}': declared in both '{}' and '{}'",
|
|
692
715
|
key,
|
|
693
716
|
existing_file.display(),
|
|
694
717
|
file.display()
|
|
695
|
-
));
|
|
718
|
+
)));
|
|
696
719
|
}
|
|
697
720
|
names.insert(key, file.to_path_buf());
|
|
698
721
|
items.push(item.clone());
|
|
@@ -719,17 +742,17 @@ fn itemNameKey(item: &Item) -> String {
|
|
|
719
742
|
|
|
720
743
|
/// A `primitive` named after a compiler built-in declares that type's API, so
|
|
721
744
|
/// it is only for std; any other name is a stateless namespace, usable anywhere.
|
|
722
|
-
fn checkExtends(path: &Path, source: &Source) ->
|
|
745
|
+
fn checkExtends(path: &Path, source: &Source) -> Loaded_<()> {
|
|
723
746
|
let inStd = path.components().any(|c| c.as_os_str() == "plum-std");
|
|
724
747
|
for p in &source.primitives {
|
|
725
748
|
if !inStd && BUILTIN_PRIMITIVES.contains(&p.name.as_str()) {
|
|
726
|
-
return Err(
|
|
749
|
+
return Err(vec![Diagnostic::new(
|
|
727
|
-
|
|
750
|
+
DiagKind::Resolution,
|
|
728
|
-
path.display(),
|
|
751
|
+
path.display().to_string(),
|
|
729
752
|
p.line,
|
|
730
753
|
p.column,
|
|
731
|
-
p.name
|
|
754
|
+
format!("primitive {} declares a built-in type and is only allowed in std", p.name),
|
|
732
|
-
));
|
|
755
|
+
)]);
|
|
733
756
|
}
|
|
734
757
|
}
|
|
735
758
|
Ok(())
|
plum-core/src/lower.rs
CHANGED
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
use crate::ast::Source;
|
|
2
|
+
use crate::diag::{DiagKind, Diagnostic};
|
|
3
|
+
|
|
4
|
+
fn diags(kind: DiagKind, errors: Vec<String>) -> Vec<Diagnostic> {
|
|
5
|
+
errors.into_iter().map(|m| Diagnostic::bare(kind, m)).collect()
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
/// Per-file steps, run on each file before it is merged (and by single-file
|
|
4
9
|
/// `parseSource`): removed-form errors and inline-variant checks. Inline variants stay
|
|
5
10
|
/// ordinary named-field enum variants.
|
|
6
|
-
pub fn
|
|
11
|
+
pub fn perFileDiag(source: &mut Source) -> Result<(), Vec<Diagnostic>> {
|
|
7
12
|
let legacy: Vec<String> = crate::unions::legacyErrors(source);
|
|
8
13
|
if !legacy.is_empty() {
|
|
9
|
-
return Err(
|
|
14
|
+
return Err(diags(DiagKind::Parse, legacy));
|
|
10
15
|
}
|
|
11
16
|
let inlineErrors = crate::unions::inlineVariantErrors(source);
|
|
12
17
|
if !inlineErrors.is_empty() {
|
|
13
|
-
return Err(
|
|
18
|
+
return Err(diags(DiagKind::Type, inlineErrors));
|
|
14
19
|
}
|
|
15
20
|
Ok(())
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/// Front-end lowering of a merged program: embeds, constructors, inline-variant
|
|
19
24
|
/// type checks, union-enum errors, then forms. The first failing stage returns its
|
|
20
|
-
/// errors
|
|
25
|
+
/// errors.
|
|
21
|
-
pub fn
|
|
26
|
+
pub fn frontEndDiag(source: &mut Source) -> Result<(), Vec<Diagnostic>> {
|
|
22
27
|
crate::session::begin();
|
|
23
28
|
let embedErrors = crate::embeds::lowerEmbeds(source);
|
|
24
29
|
if !embedErrors.is_empty() {
|
|
25
|
-
return Err(
|
|
30
|
+
return Err(diags(DiagKind::Type, embedErrors));
|
|
26
31
|
}
|
|
27
32
|
let ctorErrors = crate::ctors::lowerCtors(source);
|
|
28
33
|
if !ctorErrors.is_empty() {
|
|
29
|
-
return Err(
|
|
34
|
+
return Err(diags(DiagKind::Type, ctorErrors));
|
|
30
35
|
}
|
|
31
36
|
let typeErrors = crate::unions::inlineVariantTypeErrors(source);
|
|
32
37
|
if !typeErrors.is_empty() {
|
|
33
|
-
return Err(
|
|
38
|
+
return Err(diags(DiagKind::Type, typeErrors));
|
|
34
39
|
}
|
|
35
40
|
let memberErrors = crate::unions::unionMemberErrors(source);
|
|
36
41
|
if !memberErrors.is_empty() {
|
|
37
|
-
return Err(
|
|
42
|
+
return Err(diags(DiagKind::Type, memberErrors));
|
|
38
43
|
}
|
|
39
44
|
crate::forms::prepareForms(source);
|
|
40
45
|
Ok(())
|
|
41
46
|
}
|
|
47
|
+
|
plum-core/src/parser.rs
CHANGED
|
@@ -6,6 +6,8 @@ pub struct AstParser<'a> {
|
|
|
6
6
|
/// Errors found while parsing literals (out-of-range integers); reported by
|
|
7
7
|
/// the checker through `Source::formErrors`.
|
|
8
8
|
litErrors: std::cell::RefCell<Vec<(usize, usize, String)>>,
|
|
9
|
+
/// Malformed nodes that got a placeholder instead of a real value.
|
|
10
|
+
malformed: std::cell::RefCell<Vec<crate::diag::Diagnostic>>,
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
impl<'a> AstParser<'a> {
|
|
@@ -13,9 +15,29 @@ impl<'a> AstParser<'a> {
|
|
|
13
15
|
AstParser {
|
|
14
16
|
source: source.as_bytes(),
|
|
15
17
|
litErrors: std::cell::RefCell::new(Vec::new()),
|
|
18
|
+
malformed: std::cell::RefCell::new(Vec::new()),
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
/// The malformed nodes met so far (each got a placeholder expression); empty for any
|
|
23
|
+
/// tree the loader's syntax check accepted.
|
|
24
|
+
pub fn diagnostics(&self) -> Vec<crate::diag::Diagnostic> {
|
|
25
|
+
self.malformed.borrow().clone()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The placeholder for a missing child of `node`, recorded as a Parse diagnostic.
|
|
29
|
+
fn missing(&self, node: Node) -> Expr {
|
|
30
|
+
let pos = node.start_position();
|
|
31
|
+
self.malformed.borrow_mut().push(crate::diag::Diagnostic::new(
|
|
32
|
+
crate::diag::DiagKind::Parse,
|
|
33
|
+
"",
|
|
34
|
+
pos.row + 1,
|
|
35
|
+
pos.column + 1,
|
|
36
|
+
format!("incomplete {}", node.kind().replace('_', " ")),
|
|
37
|
+
));
|
|
38
|
+
Expr::Int(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
fn text(&self, node: Node) -> String {
|
|
20
42
|
node.utf8_text(self.source).unwrap_or("").to_string()
|
|
21
43
|
}
|
|
@@ -64,8 +86,8 @@ impl<'a> AstParser<'a> {
|
|
|
64
86
|
pub fn parseSource(&self, node: Node) -> Source {
|
|
65
87
|
let mut source = self.parseSourceRaw(node);
|
|
66
88
|
// Single-file path: errors are discarded; callers needing them call `lower::perFile` and `lower::frontEnd`.
|
|
67
|
-
let _ = crate::lower::
|
|
89
|
+
let _ = crate::lower::perFileDiag(&mut source);
|
|
68
|
-
let _ = crate::lower::
|
|
90
|
+
let _ = crate::lower::frontEndDiag(&mut source);
|
|
69
91
|
source
|
|
70
92
|
}
|
|
71
93
|
|
|
@@ -499,7 +521,7 @@ impl<'a> AstParser<'a> {
|
|
|
499
521
|
let unwrapped = self.unwrapExprNode(n);
|
|
500
522
|
self.parseExpression(unwrapped)
|
|
501
523
|
})
|
|
502
|
-
.
|
|
524
|
+
.unwrap_or_else(|| self.missing(node));
|
|
503
525
|
Const {
|
|
504
526
|
name,
|
|
505
527
|
value,
|
|
@@ -666,7 +688,7 @@ impl<'a> AstParser<'a> {
|
|
|
666
688
|
let u = self.unwrapExprNode(n);
|
|
667
689
|
self.parseExpression(u)
|
|
668
690
|
})
|
|
669
|
-
.
|
|
691
|
+
.unwrap_or_else(|| self.missing(node)),
|
|
670
692
|
),
|
|
671
693
|
"assert" => Stmt::Assert(self.parseCheck(node)),
|
|
672
694
|
"while" => Stmt::While(self.parseWhile(node)),
|
|
@@ -784,7 +806,7 @@ impl<'a> AstParser<'a> {
|
|
|
784
806
|
let u = self.unwrapExprNode(*n);
|
|
785
807
|
self.parseExpression(u)
|
|
786
808
|
})
|
|
787
|
-
.
|
|
809
|
+
.unwrap_or_else(|| self.missing(node));
|
|
788
810
|
let body = named
|
|
789
811
|
.last()
|
|
790
812
|
.filter(|n| n.kind() == "body")
|
|
@@ -803,7 +825,7 @@ impl<'a> AstParser<'a> {
|
|
|
803
825
|
let u = self.unwrapExprNode(*n);
|
|
804
826
|
self.parseExpression(u)
|
|
805
827
|
})
|
|
806
|
-
.
|
|
828
|
+
.unwrap_or_else(|| self.missing(node));
|
|
807
829
|
let body = named
|
|
808
830
|
.get(1)
|
|
809
831
|
.filter(|n| n.kind() == "body")
|
|
@@ -836,7 +858,7 @@ impl<'a> AstParser<'a> {
|
|
|
836
858
|
let u = self.unwrapExprNode(*n);
|
|
837
859
|
self.parseExpression(u)
|
|
838
860
|
})
|
|
839
|
-
.
|
|
861
|
+
.unwrap_or_else(|| self.missing(node));
|
|
840
862
|
let body = named
|
|
841
863
|
.last()
|
|
842
864
|
.filter(|n| n.kind() == "body")
|
|
@@ -951,7 +973,7 @@ impl<'a> AstParser<'a> {
|
|
|
951
973
|
let u = self.unwrapExprNode(n);
|
|
952
974
|
self.parseExpression(u)
|
|
953
975
|
})
|
|
954
|
-
.
|
|
976
|
+
.unwrap_or_else(|| self.missing(node));
|
|
955
977
|
Expr::Not(Box::new(arg))
|
|
956
978
|
}
|
|
957
979
|
"boolean_operator" => self.parseBoolOp(node),
|
|
@@ -988,7 +1010,7 @@ impl<'a> AstParser<'a> {
|
|
|
988
1010
|
value: node
|
|
989
1011
|
.child_by_field_name("value")
|
|
990
1012
|
.map(|n| self.parseExpression(self.unwrapExprNode(n)))
|
|
991
|
-
.
|
|
1013
|
+
.unwrap_or_else(|| self.missing(node)),
|
|
992
1014
|
}))
|
|
993
1015
|
}
|
|
994
1016
|
"closure" => Expr::Closure(Box::new(self.parseClosure(node))),
|
|
@@ -1062,7 +1084,7 @@ impl<'a> AstParser<'a> {
|
|
|
1062
1084
|
let value = node
|
|
1063
1085
|
.child_by_field_name("value")
|
|
1064
1086
|
.map(|n| self.parsePrimaryExpression(self.unwrapExprNode(n)))
|
|
1065
|
-
.
|
|
1087
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1066
1088
|
Expr::Bang(Box::new(BangExpr {
|
|
1067
1089
|
value,
|
|
1068
1090
|
line: pos.row + 1,
|
|
@@ -1080,7 +1102,7 @@ impl<'a> AstParser<'a> {
|
|
|
1080
1102
|
let u = self.unwrapExprNode(n);
|
|
1081
1103
|
self.parseExpression(u)
|
|
1082
1104
|
})
|
|
1083
|
-
.
|
|
1105
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1084
1106
|
inner
|
|
1085
1107
|
}
|
|
1086
1108
|
"string" => Expr::String(self.parseString(node)),
|
|
@@ -1121,14 +1143,14 @@ impl<'a> AstParser<'a> {
|
|
|
1121
1143
|
let u = self.unwrapExprNode(*n);
|
|
1122
1144
|
self.parsePrimaryExpression(u)
|
|
1123
1145
|
})
|
|
1124
|
-
.
|
|
1146
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1125
1147
|
let right = named
|
|
1126
1148
|
.last()
|
|
1127
1149
|
.map(|n| {
|
|
1128
1150
|
let u = self.unwrapExprNode(*n);
|
|
1129
1151
|
self.parsePrimaryExpression(u)
|
|
1130
1152
|
})
|
|
1131
|
-
.
|
|
1153
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1132
1154
|
Expr::Binary(Box::new(BinaryExpr { op, left, right }))
|
|
1133
1155
|
}
|
|
1134
1156
|
|
|
@@ -1153,7 +1175,7 @@ impl<'a> AstParser<'a> {
|
|
|
1153
1175
|
let u = self.unwrapExprNode(n);
|
|
1154
1176
|
self.parsePrimaryExpression(u)
|
|
1155
1177
|
})
|
|
1156
|
-
.
|
|
1178
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1157
1179
|
Expr::Unary(Box::new(UnaryExpr { op, operand }))
|
|
1158
1180
|
}
|
|
1159
1181
|
|
|
@@ -1171,14 +1193,14 @@ impl<'a> AstParser<'a> {
|
|
|
1171
1193
|
let u = self.unwrapExprNode(*n);
|
|
1172
1194
|
self.parseExpression(u)
|
|
1173
1195
|
})
|
|
1174
|
-
.
|
|
1196
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1175
1197
|
let right = named
|
|
1176
1198
|
.last()
|
|
1177
1199
|
.map(|n| {
|
|
1178
1200
|
let u = self.unwrapExprNode(*n);
|
|
1179
1201
|
self.parseExpression(u)
|
|
1180
1202
|
})
|
|
1181
|
-
.
|
|
1203
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1182
1204
|
Expr::Bool(Box::new(BoolExpr { op, left, right }))
|
|
1183
1205
|
}
|
|
1184
1206
|
|
|
@@ -1201,14 +1223,14 @@ impl<'a> AstParser<'a> {
|
|
|
1201
1223
|
let u = self.unwrapExprNode(*n);
|
|
1202
1224
|
self.parseExpression(u)
|
|
1203
1225
|
})
|
|
1204
|
-
.
|
|
1226
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1205
1227
|
let right = named
|
|
1206
1228
|
.last()
|
|
1207
1229
|
.map(|n| {
|
|
1208
1230
|
let u = self.unwrapExprNode(*n);
|
|
1209
1231
|
self.parseExpression(u)
|
|
1210
1232
|
})
|
|
1211
|
-
.
|
|
1233
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1212
1234
|
Expr::Compare(Box::new(CompareExpr { op, left, right }))
|
|
1213
1235
|
}
|
|
1214
1236
|
|
|
@@ -1220,7 +1242,7 @@ impl<'a> AstParser<'a> {
|
|
|
1220
1242
|
let u = self.unwrapExprNode(n);
|
|
1221
1243
|
self.parseExpression(u)
|
|
1222
1244
|
})
|
|
1223
|
-
.
|
|
1245
|
+
.unwrap_or_else(|| self.missing(node))
|
|
1224
1246
|
};
|
|
1225
1247
|
Expr::InlineIf(Box::new(InlineIf {
|
|
1226
1248
|
condition: field("condition"),
|
|
@@ -1240,7 +1262,7 @@ impl<'a> AstParser<'a> {
|
|
|
1240
1262
|
let u = self.unwrapExprNode(*n);
|
|
1241
1263
|
self.parsePrimaryExpression(u)
|
|
1242
1264
|
})
|
|
1243
|
-
.
|
|
1265
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1244
1266
|
let member = named.get(1).map(|n| self.text(*n)).unwrap_or_default();
|
|
1245
1267
|
let attr = match named.get(2) {
|
|
1246
1268
|
Some(args_node) => {
|
|
@@ -1304,7 +1326,7 @@ impl<'a> AstParser<'a> {
|
|
|
1304
1326
|
if pairs > 0 && !mixed {
|
|
1305
1327
|
let mut out = call(Expr::TypeName("Map".to_string()), "empty", vec![]);
|
|
1306
1328
|
for p in elems {
|
|
1307
|
-
let part = |i: u32| p.named_child(i).map(expr).
|
|
1329
|
+
let part = |i: u32| p.named_child(i).map(expr).unwrap_or_else(|| self.missing(node));
|
|
1308
1330
|
out = call(out, "with", vec![part(0), part(1)]);
|
|
1309
1331
|
}
|
|
1310
1332
|
return out;
|
|
@@ -1313,7 +1335,7 @@ impl<'a> AstParser<'a> {
|
|
|
1313
1335
|
let mut out = Expr::TypeName("Nil".to_string());
|
|
1314
1336
|
for e in elems.into_iter().rev() {
|
|
1315
1337
|
let value = match e.kind() {
|
|
1316
|
-
"pair_argument" => e.named_child(0).map(expr).
|
|
1338
|
+
"pair_argument" => e.named_child(0).map(expr).unwrap_or_else(|| self.missing(node)),
|
|
1317
1339
|
_ => expr(e),
|
|
1318
1340
|
};
|
|
1319
1341
|
out = Expr::ClassCall(ClassCall {
|
|
@@ -1339,7 +1361,7 @@ impl<'a> AstParser<'a> {
|
|
|
1339
1361
|
let u = self.unwrapExprNode(n);
|
|
1340
1362
|
self.parsePrimaryExpression(u)
|
|
1341
1363
|
})
|
|
1342
|
-
.
|
|
1364
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1343
1365
|
Expr::Question(Box::new(value))
|
|
1344
1366
|
}
|
|
1345
1367
|
|
|
@@ -1384,7 +1406,7 @@ impl<'a> AstParser<'a> {
|
|
|
1384
1406
|
let u = self.unwrapExprNode(n);
|
|
1385
1407
|
self.parseExpression(u)
|
|
1386
1408
|
})
|
|
1387
|
-
.
|
|
1409
|
+
.unwrap_or_else(|| self.missing(node));
|
|
1388
1410
|
Arg::Keyword { name, value }
|
|
1389
1411
|
}
|
|
1390
1412
|
"pair_argument" => {
|
plum-core/tests/core/closure_escape_test.rs
CHANGED
|
@@ -91,9 +91,9 @@ fn std_list_combinators_non_escaping() {
|
|
|
91
91
|
let path = dir.join("main.plum");
|
|
92
92
|
std::fs::write(&path, "import std/Int\nimport std/List\n\nfun main() =\n [1]\n").unwrap();
|
|
93
93
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
|
94
|
-
let r = plum_core::
|
|
94
|
+
let r = plum_core::loadAndMergeDiag(&path, &root);
|
|
95
95
|
let _ = std::fs::remove_dir_all(&dir);
|
|
96
|
-
let m = nonEscapingFnParams(&r.unwrap_or_else(|e| panic!("load: {e}")));
|
|
96
|
+
let m = nonEscapingFnParams(&r.unwrap_or_else(|e| panic!("load: {e:?}")));
|
|
97
97
|
assert_eq!(m.get("List.map"), Some(&vec![0]), "{m:?}");
|
|
98
98
|
assert_eq!(m.get("List.filter"), Some(&vec![0]));
|
|
99
99
|
assert_eq!(m.get("List.fold"), Some(&vec![1]));
|
plum-core/tests/core/closure_spec_test.rs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! Closure specialisation (`plum_core::closure_spec`): which calls get a copy of the
|
|
3
3
|
//! callee with the closure inlined, and that the program still computes the same thing
|
|
4
4
|
//! (compiled and run through the real pipeline, where the pass is wired in).
|
|
5
|
-
use
|
|
5
|
+
use plum_test_support::compileSource;
|
|
6
6
|
|
|
7
7
|
fn gcEngine() -> wasmtime::Engine {
|
|
8
8
|
let mut config = wasmtime::Config::new();
|
|
@@ -40,7 +40,7 @@ fn runInt(name: &str, src: &str) -> i64 {
|
|
|
40
40
|
|
|
41
41
|
fn lowered(name: &str, src: &str) -> std::rc::Rc<plum_core::ast::Source> {
|
|
42
42
|
let source = load(name, src);
|
|
43
|
-
let info =
|
|
43
|
+
let info = plum_driver::check(&source, &plum_driver::PassOptions::default()).unwrap_or_else(|e| panic!("check: {e:?}"));
|
|
44
44
|
info.lowered.expect("lowered program")
|
|
45
45
|
}
|
|
46
46
|
|
plum-core/tests/core/diag_test.rs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::diag::{DiagKind, Diagnostic};
|
|
3
|
+
use plum_core::loadAndMergeDiag;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use tempfile::TempDir;
|
|
6
|
+
|
|
7
|
+
fn load(src: &str) -> Vec<Diagnostic> {
|
|
8
|
+
let dir = TempDir::new().unwrap();
|
|
9
|
+
let entry = dir.path().join("main.plum");
|
|
10
|
+
fs::write(&entry, src).unwrap();
|
|
11
|
+
loadAndMergeDiag(&entry, dir.path()).unwrap_err()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn displayIsFileLineColumnMessage() {
|
|
16
|
+
let d = Diagnostic::new(DiagKind::Type, "main.plum", 3, 5, "undefined name 'g'");
|
|
17
|
+
assert_eq!(d.to_string(), "main.plum:3:5: undefined name 'g'");
|
|
18
|
+
assert_eq!(Diagnostic::bare(DiagKind::Type, "oops").to_string(), "oops");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn syntaxErrorIsAPositionedParseDiagnostic() {
|
|
23
|
+
let ds = load("fun f( = 1\n");
|
|
24
|
+
assert_eq!(ds.len(), 1, "{ds:?}");
|
|
25
|
+
let d = &ds[0];
|
|
26
|
+
assert_eq!(d.kind, DiagKind::Parse);
|
|
27
|
+
assert_eq!((d.line, d.column), (1, 6), "{d:?}");
|
|
28
|
+
assert!(d.file.ends_with("main.plum"), "{d:?}");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn missingImportIsALoadDiagnosticNamingIt() {
|
|
33
|
+
let ds = load("import std/Nope\n\nfun f(u: Unit) -> Unit = u\n");
|
|
34
|
+
assert_eq!(ds.len(), 1, "{ds:?}");
|
|
35
|
+
assert_eq!(ds[0].kind, DiagKind::Load);
|
|
36
|
+
assert!(ds[0].message.contains("std/Nope"), "{:?}", ds[0]);
|
|
37
|
+
}
|
plum-core/tests/core/inline_variants_test.rs
CHANGED
|
@@ -86,7 +86,7 @@ fn mixed_tags_and_inline_variants_stay_variants() {
|
|
|
86
86
|
#[test]
|
|
87
87
|
fn per_file_keeps_inline_variants() {
|
|
88
88
|
let mut s = plum_test_support::parseRawSource("enum Link[T] = Nil | Cons(head: T, tail: Link[T])\n");
|
|
89
|
-
assert_eq!(
|
|
89
|
+
assert_eq!(perFile(&mut s), Ok(()));
|
|
90
90
|
assert_eq!(names(&s), vec!["Link"]);
|
|
91
91
|
let l = enumOf(&s, "Link");
|
|
92
92
|
assert_eq!(l.variants[1].name, "Cons");
|
|
@@ -97,25 +97,38 @@ fn per_file_keeps_inline_variants() {
|
|
|
97
97
|
fn per_file_errors() {
|
|
98
98
|
let mut s = plum_test_support::parseRawSource("enum Opt[T] = Nn | Sm(T)\n");
|
|
99
99
|
assert_eq!(
|
|
100
|
-
|
|
100
|
+
perFile(&mut s),
|
|
101
101
|
Err("variant Sm(T) needs named fields: Sm(value: T)".to_string())
|
|
102
102
|
);
|
|
103
103
|
let mut s = plum_test_support::parseRawSource("struct Cons(a: Int)\nenum Link = Nil | Cons(head: Int)\n");
|
|
104
|
-
assert!(
|
|
104
|
+
assert!(perFile(&mut s).unwrap_err().contains("already declared"));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
#[test]
|
|
108
108
|
fn union_enum_rejected() {
|
|
109
109
|
let mut s = plum_test_support::parseRawSource("struct Circle(r: Int)\nenum Shape = Circle | Dot\n");
|
|
110
|
-
assert_eq!(
|
|
110
|
+
assert_eq!(perFile(&mut s), Ok(()));
|
|
111
111
|
assert_eq!(
|
|
112
|
-
|
|
112
|
+
frontEnd(&mut s),
|
|
113
113
|
Err("enum Shape lists struct Circle as a member; union enums are removed \u{2014} declare it inline: Circle(..)".to_string())
|
|
114
114
|
);
|
|
115
115
|
let mut s = plum_test_support::parseRawSource("enum Num = Int | Dot\n");
|
|
116
|
-
assert!(
|
|
116
|
+
assert!(frontEnd(&mut s).unwrap_err().contains("lists type Int as a member"));
|
|
117
117
|
let mut s = plum_test_support::parseRawSource("enum Color = Red | Green\n");
|
|
118
|
-
assert_eq!(
|
|
118
|
+
assert_eq!(frontEnd(&mut s), Ok(()));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The checks' messages, joined: the tests assert on the text, not on the positions.
|
|
122
|
+
fn messages(r: Result<(), Vec<plum_core::Diagnostic>>) -> Result<(), String> {
|
|
123
|
+
r.map_err(|ds| ds.iter().map(|d| d.message.clone()).collect::<Vec<_>>().join("\n"))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn perFile(s: &mut Source) -> Result<(), String> {
|
|
127
|
+
messages(plum_core::lower::perFileDiag(s))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn frontEnd(s: &mut Source) -> Result<(), String> {
|
|
131
|
+
messages(plum_core::lower::frontEndDiag(s))
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
fn loadSrc(src: &str) -> Result<Source, String> {
|
|
@@ -124,7 +137,7 @@ fn loadSrc(src: &str) -> Result<Source, String> {
|
|
|
124
137
|
std::fs::create_dir_all(&lib).unwrap();
|
|
125
138
|
let entry = lib.join("main.plum");
|
|
126
139
|
std::fs::write(&entry, src).unwrap();
|
|
127
|
-
plum_core::
|
|
140
|
+
plum_core::loadAndMergeDiag(&entry, &lib).map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n"))
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
#[test]
|
plum-core/tests/core/layout_test.rs
CHANGED
|
@@ -85,9 +85,9 @@ fn std_and_examples_layouts() {
|
|
|
85
85
|
let mut boxed = 0;
|
|
86
86
|
let mut known: Vec<(String, Layout)> = Vec::new();
|
|
87
87
|
for f in &files {
|
|
88
|
-
let s = match plum_core::
|
|
88
|
+
let s = match plum_core::loadAndMergeDiag(f, root) {
|
|
89
89
|
Ok(s) => s,
|
|
90
|
-
Err(e) => panic!("{}: {e}", f.display()),
|
|
90
|
+
Err(e) => panic!("{}: {e:?}", f.display()),
|
|
91
91
|
};
|
|
92
92
|
let all = enums(&s);
|
|
93
93
|
for e in all.iter().filter(|e| e.isStruct) {
|
plum-core/tests/core/loader_test.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
use plum_core::
|
|
2
|
+
use plum_core::loadAndMergeDiag;
|
|
3
3
|
use std::fs;
|
|
4
4
|
use tempfile::TempDir;
|
|
5
5
|
|
|
@@ -35,7 +35,7 @@ fn importSeesImportedDeclarations() {
|
|
|
35
35
|
"module fixtures\n\nimport helper\n\nfun main(u: Unit) -> Unit =\n helperFn(u)\n",
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
-
let merged =
|
|
38
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("loadAndMerge failed");
|
|
39
39
|
let names: Vec<&str> = merged
|
|
40
40
|
.items
|
|
41
41
|
.iter()
|
|
@@ -76,7 +76,7 @@ fn transitiveImportSurfacesGrandchildDeclarations() {
|
|
|
76
76
|
"module fixtures\n\nimport b\n\nfun main(u: Unit) -> Unit =\n bFn(u)\n",
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
-
let merged =
|
|
79
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("loadAndMerge failed");
|
|
80
80
|
let names: Vec<&str> = merged
|
|
81
81
|
.items
|
|
82
82
|
.iter()
|
|
@@ -117,7 +117,7 @@ fn diamondImportIncludesSharedDependencyOnce() {
|
|
|
117
117
|
"module fixtures\n\nimport b\nimport c\n\nfun main(u: Unit) -> Unit =\n bFn(cFn(u))\n",
|
|
118
118
|
);
|
|
119
119
|
|
|
120
|
-
let merged =
|
|
120
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("loadAndMerge failed");
|
|
121
121
|
let d_count = merged
|
|
122
122
|
.items
|
|
123
123
|
.iter()
|
|
@@ -145,7 +145,7 @@ fn importCycleResolvesWithoutHangingOrErroring() {
|
|
|
145
145
|
"module fixtures\n\nimport b\n\nfun main(u: Unit) -> Unit =\n bFn(u)\n",
|
|
146
146
|
);
|
|
147
147
|
|
|
148
|
-
let merged =
|
|
148
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("import cycle should resolve, not error");
|
|
149
149
|
let names: Vec<&str> = merged
|
|
150
150
|
.items
|
|
151
151
|
.iter()
|
|
@@ -168,9 +168,9 @@ fn unresolvableImportIsAClearError() {
|
|
|
168
168
|
"module fixtures\n\nimport does_not_exist\n\nfun main(u: Unit) -> Unit =\n u\n",
|
|
169
169
|
);
|
|
170
170
|
|
|
171
|
-
let result =
|
|
171
|
+
let result = loadAndMergeDiag(&entry, &lib_path);
|
|
172
172
|
assert!(result.is_err());
|
|
173
|
-
let msg = result.unwrap_err();
|
|
173
|
+
let msg = errText(result.unwrap_err());
|
|
174
174
|
assert!(
|
|
175
175
|
msg.contains("does_not_exist"),
|
|
176
176
|
"expected error to name the missing import, got: {}",
|
|
@@ -193,9 +193,9 @@ fn duplicateTopLevelNameAcrossFilesIsAClearError() {
|
|
|
193
193
|
"module fixtures\n\nimport helper\n\nfun sharedFn(u: Unit) -> Unit =\n u\n",
|
|
194
194
|
);
|
|
195
195
|
|
|
196
|
-
let result =
|
|
196
|
+
let result = loadAndMergeDiag(&entry, &lib_path);
|
|
197
197
|
assert!(result.is_err());
|
|
198
|
-
let msg = result.unwrap_err();
|
|
198
|
+
let msg = errText(result.unwrap_err());
|
|
199
199
|
assert!(
|
|
200
200
|
msg.contains("sharedFn"),
|
|
201
201
|
"expected error to name the duplicate 'sharedFn', got: {}",
|
|
@@ -237,7 +237,7 @@ fun main(u: Unit) -> Unit =
|
|
|
237
237
|
",
|
|
238
238
|
);
|
|
239
239
|
|
|
240
|
-
let merged =
|
|
240
|
+
let merged = loadAndMergeDiag(&entry, &lib_path)
|
|
241
241
|
.expect("same method name on different receivers should not collide");
|
|
242
242
|
let method_count = merged
|
|
243
243
|
.items
|
|
@@ -251,6 +251,11 @@ fun main(u: Unit) -> Unit =
|
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/// Diagnostics as the plain text a user sees.
|
|
255
|
+
fn errText(ds: Vec<plum_core::Diagnostic>) -> String {
|
|
256
|
+
ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
#[test]
|
|
255
260
|
fn removedSyntaxIsAParseError() {
|
|
256
261
|
for src in [
|
|
@@ -263,8 +268,8 @@ fn removedSyntaxIsAParseError() {
|
|
|
263
268
|
let dir = TempDir::new().unwrap();
|
|
264
269
|
let entry = dir.path().join("main.plum");
|
|
265
270
|
fs::write(&entry, src).unwrap();
|
|
266
|
-
let
|
|
271
|
+
let errs = loadAndMergeDiag(&entry, dir.path()).unwrap_err();
|
|
267
|
-
assert!(
|
|
272
|
+
assert!(errs.iter().all(|d| d.kind == plum_core::DiagKind::Parse), "{src}: {errs:?}");
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
275
|
|
|
@@ -272,7 +277,7 @@ fn parseErrorOf(src: &str) -> String {
|
|
|
272
277
|
let dir = TempDir::new().unwrap();
|
|
273
278
|
let entry = dir.path().join("main.plum");
|
|
274
279
|
fs::write(&entry, src).unwrap();
|
|
275
|
-
|
|
280
|
+
errText(loadAndMergeDiag(&entry, dir.path()).unwrap_err())
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
#[test]
|
|
@@ -281,7 +286,7 @@ fn parseErrorBlamesTheTokenNotTheWholeFunction() {
|
|
|
281
286
|
"import std/Float\n\nexport fun update(w: Float, dt: Float) =\n ()\n\nfun ok() =\n _ := 1.0\n",
|
|
282
287
|
);
|
|
283
288
|
assert!(
|
|
284
|
-
err.contains("
|
|
289
|
+
err.contains(":4:3: unexpected '()'"),
|
|
285
290
|
"{err}"
|
|
286
291
|
);
|
|
287
292
|
assert!(
|
|
@@ -295,7 +300,7 @@ fn parseErrorBlamesTheTokenNotTheWholeFunction() {
|
|
|
295
300
|
fn parseErrorOnStrayTokenMidExpression() {
|
|
296
301
|
let err = parseErrorOf("fun f() =\n x := 1 + * 2\n");
|
|
297
302
|
assert!(
|
|
298
|
-
err.contains("
|
|
303
|
+
err.contains(":2:12: unexpected '*'"),
|
|
299
304
|
"{err}"
|
|
300
305
|
);
|
|
301
306
|
assert!(!err.contains("hint:"), "{err}");
|
|
@@ -304,35 +309,35 @@ fn parseErrorOnStrayTokenMidExpression() {
|
|
|
304
309
|
#[test]
|
|
305
310
|
fn parseErrorOnMissingClosingDelimiter() {
|
|
306
311
|
let err = parseErrorOf("fun f() =\n x := (1 + 2\n");
|
|
307
|
-
assert!(err.contains("
|
|
312
|
+
assert!(err.contains(":2:8: unclosed '('"), "{err}");
|
|
308
313
|
let err = parseErrorOf("fun f() =\n x := foo(1, 2\n y := 3\n\nfun g() =\n z := 1\n");
|
|
309
|
-
assert!(err.contains("
|
|
314
|
+
assert!(err.contains(":3:3: unexpected 'y'"), "{err}");
|
|
310
315
|
}
|
|
311
316
|
|
|
312
317
|
#[test]
|
|
313
318
|
fn unclosedOpenerAtEndOfFileIsReportedAtTheOpener() {
|
|
314
319
|
let err = parseErrorOf("import std/Int\n\nfun f(a: Int) -> Int =\n g(a, 2\n");
|
|
315
|
-
assert!(err.contains("
|
|
320
|
+
assert!(err.contains(":4:4: unclosed '('"), "{err}");
|
|
316
321
|
let err = parseErrorOf("fun f() =\n x := [1, 2\n");
|
|
317
|
-
assert!(err.contains("
|
|
322
|
+
assert!(err.contains(":2:8: unclosed '['"), "{err}");
|
|
318
323
|
let err = parseErrorOf("fun f() =\n x := g(h(1), 2\n");
|
|
319
|
-
assert!(err.contains("
|
|
324
|
+
assert!(err.contains(":2:9: unclosed '('"), "{err}");
|
|
320
325
|
let err = parseErrorOf("fun f() =\n x := { 1 + 2\n");
|
|
321
|
-
assert!(err.contains("
|
|
326
|
+
assert!(err.contains(":2:8: unclosed '{'"), "{err}");
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
#[test]
|
|
325
330
|
fn parseErrorAtEndOfFile() {
|
|
326
331
|
let err = parseErrorOf("fun f() =\n x := 1 +\n");
|
|
327
332
|
assert!(
|
|
328
|
-
err.contains("
|
|
333
|
+
err.contains(":2:11: unexpected end of file"),
|
|
329
334
|
"{err}"
|
|
330
335
|
);
|
|
331
336
|
let err = parseErrorOf("fun f() =\n x := foo(1, 2");
|
|
332
|
-
assert!(err.contains("
|
|
337
|
+
assert!(err.contains(":2:11: unclosed '('"), "{err}");
|
|
333
338
|
let err = parseErrorOf("fun f() =\n if true\n x := 1\n else\n");
|
|
334
339
|
assert!(
|
|
335
|
-
err.contains("
|
|
340
|
+
err.contains(":4:7: unexpected end of file"),
|
|
336
341
|
"{err}"
|
|
337
342
|
);
|
|
338
343
|
}
|
|
@@ -346,7 +351,7 @@ fn duplicate_const_in_one_body() {
|
|
|
346
351
|
"main",
|
|
347
352
|
"module fixtures\n\nprimitive Consts =\n Pi = true\n Pi = false\n",
|
|
348
353
|
);
|
|
349
|
-
let msg =
|
|
354
|
+
let msg = errText(loadAndMergeDiag(&entry, &lib_path).unwrap_err());
|
|
350
355
|
assert!(msg.contains("duplicate constant 'Consts.Pi'"), "{}", msg);
|
|
351
356
|
}
|
|
352
357
|
|
|
@@ -364,7 +369,7 @@ fn assoc_const_names_are_per_type_across_modules() {
|
|
|
364
369
|
"main",
|
|
365
370
|
"module fixtures\n\nimport helper\n\nprimitive Beta =\n Flag = false\n",
|
|
366
371
|
);
|
|
367
|
-
let merged =
|
|
372
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).unwrap();
|
|
368
373
|
let names: Vec<String> = merged
|
|
369
374
|
.items
|
|
370
375
|
.iter()
|
|
@@ -389,7 +394,7 @@ fn primitive_builtin_std_only() {
|
|
|
389
394
|
"primitive Int =\n fun f(self) -> Byte =\n self\n",
|
|
390
395
|
)
|
|
391
396
|
.unwrap();
|
|
392
|
-
let err =
|
|
397
|
+
let err = errText(loadAndMergeDiag(&entry, dir.path()).unwrap_err());
|
|
393
398
|
assert!(err.contains("only allowed in std"), "{err}");
|
|
394
399
|
// A namespace is fine anywhere.
|
|
395
400
|
let ok = dir.path().join("ns.plum");
|
|
@@ -398,7 +403,7 @@ fn primitive_builtin_std_only() {
|
|
|
398
403
|
"primitive Util =\n Enabled = true\n fun f() -> Unit =\n todo\n",
|
|
399
404
|
)
|
|
400
405
|
.unwrap();
|
|
401
|
-
assert!(
|
|
406
|
+
assert!(loadAndMergeDiag(&ok, dir.path()).is_ok());
|
|
402
407
|
// Inside std, a built-in: fine.
|
|
403
408
|
let std = dir.path().join("plum-std");
|
|
404
409
|
fs::create_dir_all(&std).unwrap();
|
|
@@ -408,7 +413,7 @@ fn primitive_builtin_std_only() {
|
|
|
408
413
|
"primitive Byte =\n fun f(self) -> Byte =\n self\n",
|
|
409
414
|
)
|
|
410
415
|
.unwrap();
|
|
411
|
-
assert!(
|
|
416
|
+
assert!(loadAndMergeDiag(&byte, dir.path()).is_ok());
|
|
412
417
|
}
|
|
413
418
|
|
|
414
419
|
#[test]
|
|
@@ -425,7 +430,7 @@ fn source_and_fns_carry_their_file_path() {
|
|
|
425
430
|
"main",
|
|
426
431
|
"module fixtures\n\nimport std/Util\n\nfun mainFn(b: Bool) -> Bool =\n b\n",
|
|
427
432
|
);
|
|
428
|
-
let source =
|
|
433
|
+
let source = loadAndMergeDiag(&entry, &lib_path).unwrap();
|
|
429
434
|
assert!(source.file.ends_with("main.plum"), "{}", source.file);
|
|
430
435
|
let fileOf = |name: &str| {
|
|
431
436
|
source
|
|
@@ -464,7 +469,7 @@ fn outOfRangeLiteralInEntryOrImportSurvivesTheMerge() {
|
|
|
464
469
|
"main",
|
|
465
470
|
"module fixtures\n\nimport helper\n\nfun main(u: Unit) -> Unit =\n big(u)\n",
|
|
466
471
|
);
|
|
467
|
-
let merged =
|
|
472
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("loadAndMerge failed");
|
|
468
473
|
assert!(
|
|
469
474
|
merged
|
|
470
475
|
.formErrors
|
|
@@ -484,7 +489,7 @@ fn inlineVariantStaysAVariant() {
|
|
|
484
489
|
"main",
|
|
485
490
|
"module fixtures\n\nenum Shape =\n | Circle(r: Unit)\n | Dot\n",
|
|
486
491
|
);
|
|
487
|
-
let merged =
|
|
492
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).unwrap();
|
|
488
493
|
assert!(!merged.items.iter().any(|i| matches!(i, plum_core::ast::Item::Enum(e) if e.name == "Circle")));
|
|
489
494
|
let shape = merged.items.iter().find_map(|i| match i {
|
|
490
495
|
plum_core::ast::Item::Enum(e) if e.name == "Shape" => Some(e),
|
|
@@ -502,11 +507,9 @@ fn legacySingleSelfVariantIsAnError() {
|
|
|
502
507
|
"main",
|
|
503
508
|
"module fixtures\n\nenum User =\n | User(name: Unit)\n",
|
|
504
509
|
);
|
|
505
|
-
let err =
|
|
510
|
+
let err = loadAndMergeDiag(&entry, &lib_path).unwrap_err();
|
|
506
|
-
assert_eq!(
|
|
511
|
+
assert_eq!(err.len(), 1, "{err:?}");
|
|
507
|
-
err,
|
|
508
|
-
|
|
512
|
+
assert_eq!(err[0].message, "enum User has a single variant named User — write struct User(...)");
|
|
509
|
-
);
|
|
510
513
|
}
|
|
511
514
|
|
|
512
515
|
#[test]
|
|
@@ -523,7 +526,7 @@ fn nestedStdImportResolvesToASubdirectory() {
|
|
|
523
526
|
"main",
|
|
524
527
|
"module fixtures\n\nimport std/game/Util\n\nfun mainFn(b: Bool) -> Bool =\n helperFn(b)\n",
|
|
525
528
|
);
|
|
526
|
-
let source =
|
|
529
|
+
let source = loadAndMergeDiag(&entry, &lib_path).expect("nested std import loads");
|
|
527
530
|
assert!(source.items.iter().any(
|
|
528
531
|
|i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "helperFn" && f.file.ends_with("game/Util.plum"))
|
|
529
532
|
));
|
|
@@ -532,7 +535,7 @@ fn nestedStdImportResolvesToASubdirectory() {
|
|
|
532
535
|
"other",
|
|
533
536
|
"module fixtures\n\nimport std/Util\n\nfun f(b: Bool) -> Bool =\n b\n",
|
|
534
537
|
);
|
|
535
|
-
assert!(
|
|
538
|
+
assert!(loadAndMergeDiag(&missing, &lib_path).is_err());
|
|
536
539
|
}
|
|
537
540
|
|
|
538
541
|
/// Import-free programs (every plum-examples file imports std, so small inline programs are used):
|
|
@@ -554,7 +557,7 @@ fn single_file_equals_loader() {
|
|
|
554
557
|
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
555
558
|
let tree = parser.parse(src, None).unwrap();
|
|
556
559
|
let single = plum_core::AstParser::new(src).parseSource(tree.root_node());
|
|
557
|
-
let merged =
|
|
560
|
+
let merged = loadAndMergeDiag(&entry, dir.path()).expect("loadAndMerge failed");
|
|
558
561
|
// The loader stamps each item with its path; blank those out before comparing.
|
|
559
562
|
let norm = |x: &plum_core::ast::Source| {
|
|
560
563
|
// the loader drops `topLevelOrder` after merging; items are the lowered program
|
plum-core/tests/core/lower_test.rs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
use plum_core::lower::
|
|
2
|
+
use plum_core::lower::frontEndDiag;
|
|
3
3
|
|
|
4
4
|
#[test]
|
|
5
5
|
fn lowersEmbeds() {
|
|
6
6
|
let mut s = plum_test_support::parseRawSource(
|
|
7
7
|
"struct Actor(x: Int) =\n fun up(self) -> Int = 0\n\nstruct Hut(Actor(x), k: Int) =\n override fun up(self) -> Int = 1\n",
|
|
8
8
|
);
|
|
9
|
-
assert_eq!(
|
|
9
|
+
assert_eq!(frontEndDiag(&mut s), Ok(()));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
#[test]
|
|
@@ -22,6 +22,6 @@ fn second_embed_errors() {
|
|
|
22
22
|
let mut s = plum_test_support::parseRawSource(
|
|
23
23
|
"struct Aa(x: Int)\nstruct Bb(y: Int)\nstruct Ss(Aa(x), Bb(y))\n",
|
|
24
24
|
);
|
|
25
|
-
let e =
|
|
25
|
+
let e = frontEndDiag(&mut s).unwrap_err();
|
|
26
|
-
assert!(e.contains("struct Ss embeds Aa and Bb; a struct can embed only one struct"), "{e}");
|
|
26
|
+
assert!(e.iter().any(|d| d.message.contains("struct Ss embeds Aa and Bb; a struct can embed only one struct")), "{e:?}");
|
|
27
27
|
}
|
plum-core/tests/core/main.rs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
mod closure_escape_test;
|
|
2
2
|
mod closure_spec_test;
|
|
3
3
|
mod closure_types_parse_test;
|
|
4
|
+
mod diag_test;
|
|
4
5
|
mod drive_test;
|
|
5
6
|
mod for_next_test;
|
|
6
7
|
mod formatter_test;
|
plum-core/tests/core/session_test.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
-
use plum_core::lower::
|
|
2
|
+
use plum_core::lower::frontEndDiag;
|
|
3
3
|
|
|
4
4
|
const SRC: &str = "\
|
|
5
5
|
enum Shape =
|
|
@@ -13,7 +13,7 @@ fun main() =
|
|
|
13
13
|
|
|
14
14
|
fn lowered() -> String {
|
|
15
15
|
let mut s = plum_test_support::parseRawSource(SRC);
|
|
16
|
-
|
|
16
|
+
frontEndDiag(&mut s).unwrap();
|
|
17
17
|
format!("{:?}", s.items)
|
|
18
18
|
}
|
|
19
19
|
|
plum-core/tests/core/trmc_test.rs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
//! Tail recursion modulo cons (`plum_core::trmc`): which fns are rewritten, and
|
|
3
3
|
//! that the rewrite computes what the recursion would (compiled and run through
|
|
4
4
|
//! the real pipeline, which is where `trmc::rewrite` is wired in).
|
|
5
|
-
use
|
|
5
|
+
use plum_driver::PassOptions;
|
|
6
6
|
use plum_wasm_codegen::CompileOptions;
|
|
7
7
|
|
|
8
8
|
fn compileWith(source: &plum_core::ast::Source, fusion: bool) -> Result<Vec<u8>, String> {
|
|
9
|
-
let opts = CompileOptions {
|
|
9
|
+
let opts = CompileOptions { passes: plum_driver::PassOptions { fusion: fusion, explain: false, ..plum_driver::PassOptions::default() }, ..Default::default() };
|
|
10
|
-
plum_wasm_codegen::compile(source, &opts)
|
|
10
|
+
plum_wasm_codegen::compile(source, &opts)
|
|
11
|
+
.map(|o| o.wasm)
|
|
12
|
+
.map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n"))
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
fn compileSource(source: &plum_core::ast::Source) -> Result<Vec<u8>, String> {
|
|
@@ -20,7 +22,7 @@ fn compileUnfused(source: &plum_core::ast::Source) -> Result<Vec<u8>, String> {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
fn typedSource(source: &plum_core::ast::Source) -> plum_checker::types::CheckResult<plum_checker::TypedInfo> {
|
|
23
|
-
|
|
25
|
+
plum_driver::check(source, &PassOptions::default())
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
fn gcEngine() -> wasmtime::Engine {
|
plum-driver/Cargo.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "plum-driver"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
plum-core = { path = "../plum-core" }
|
|
8
|
+
plum-checker = { path = "../plum-checker" }
|
|
9
|
+
|
|
10
|
+
[dev-dependencies]
|
|
11
|
+
plum-test-support = { path = "../plum-test-support" }
|
plum-driver/src/lib.rs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//! The post-check pass list. `plum_checker` only checks; this crate owns the order of the
|
|
2
|
+
//! rewrite passes (closure specialisation, fusion, TRMC, tail marks) and the one rule they
|
|
3
|
+
//! share: a rewrite is adopted only if the rewritten program checks again.
|
|
4
|
+
#![allow(non_snake_case)]
|
|
5
|
+
|
|
6
|
+
use plum_checker::{checkOnce, checkProgram, TypedInfo};
|
|
7
|
+
use plum_core::ast::Source;
|
|
8
|
+
use plum_core::closure_spec::Hints;
|
|
9
|
+
use plum_core::diag::Diagnostic;
|
|
10
|
+
use std::rc::Rc;
|
|
11
|
+
|
|
12
|
+
/// Which passes run, and whether dropped or applied rewrites are explained on stderr.
|
|
13
|
+
#[derive(Clone, Copy, Debug)]
|
|
14
|
+
pub struct PassOptions {
|
|
15
|
+
pub closureSpec: bool,
|
|
16
|
+
/// The supercompiler stage (`--no-fusion` clears it).
|
|
17
|
+
pub fusion: bool,
|
|
18
|
+
pub trmc: bool,
|
|
19
|
+
/// Print each fusion decision and each dropped rewrite (`--explain-fusion`).
|
|
20
|
+
pub explain: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl Default for PassOptions {
|
|
24
|
+
fn default() -> Self {
|
|
25
|
+
PassOptions { closureSpec: true, fusion: true, trmc: true, explain: false }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// What a pass rewrites.
|
|
30
|
+
#[derive(Clone, Copy, PartialEq, Debug)]
|
|
31
|
+
pub enum Target {
|
|
32
|
+
/// The checked program before union lowering (own generics applied, `for` loops lowered),
|
|
33
|
+
/// so the passes never see the hidden `List__Cons(Cons(..))` wrappers.
|
|
34
|
+
Pre,
|
|
35
|
+
/// The final lowered program codegen compiles.
|
|
36
|
+
Lowered,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// What a rewrite may read besides the program.
|
|
40
|
+
pub struct Env<'a> {
|
|
41
|
+
pub explain: bool,
|
|
42
|
+
/// Receiver types of method calls, read off the checked program (see `closureHints`).
|
|
43
|
+
pub hints: &'a Hints,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// One rewrite pass: changes the program in place, returns whether anything changed.
|
|
47
|
+
pub struct Pass {
|
|
48
|
+
pub name: &'static str,
|
|
49
|
+
pub target: Target,
|
|
50
|
+
pub needsHints: bool,
|
|
51
|
+
pub enabled: fn(&PassOptions) -> bool,
|
|
52
|
+
pub rewrite: fn(&mut Source, &Env) -> bool,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn closureSpec(copy: &mut Source, env: &Env) -> bool {
|
|
56
|
+
!plum_core::closure_spec::specialiseWith(copy, env.hints).is_empty()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Purity -> simplify -> drive -> uniqueness -> reuse; returns every note (empty = nothing changed).
|
|
60
|
+
pub fn fusionNotes(copy: &mut Source) -> Vec<String> {
|
|
61
|
+
let pure = plum_core::purity::pureFns(copy);
|
|
62
|
+
let mut notes = plum_core::simplify::simplify(copy, &pure);
|
|
63
|
+
notes.extend(plum_core::drive::drive(copy, &pure));
|
|
64
|
+
let rep = plum_core::uniqueness::markUnique(copy);
|
|
65
|
+
notes.extend(rep.proved.iter().map(|l| format!("unique spread at {l}")));
|
|
66
|
+
notes.extend(rep.unproved.iter().map(|(l, why)| format!("uniqueness: not in place {l}: {why}")));
|
|
67
|
+
notes.retain(|n| !n.is_empty());
|
|
68
|
+
notes.extend(plum_core::reuse::markReuse(copy));
|
|
69
|
+
notes.retain(|n| !n.is_empty());
|
|
70
|
+
notes
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn fusion(copy: &mut Source, env: &Env) -> bool {
|
|
74
|
+
let notes = fusionNotes(copy);
|
|
75
|
+
if env.explain {
|
|
76
|
+
for n in ¬es {
|
|
77
|
+
eprintln!("fusion: {n}");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
!notes.is_empty()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Tail recursion modulo cons.
|
|
84
|
+
fn trmc(copy: &mut Source, _: &Env) -> bool {
|
|
85
|
+
!plum_core::trmc::rewrite(copy).is_empty()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// The rewrite passes, in the order they run. Tail marks (flags set in place on the final
|
|
89
|
+
/// program) run last and need no re-check.
|
|
90
|
+
pub const PASSES: [Pass; 3] = [
|
|
91
|
+
Pass { name: "closure specialisation", target: Target::Pre, needsHints: true, enabled: |o| o.closureSpec, rewrite: closureSpec },
|
|
92
|
+
Pass { name: "fusion", target: Target::Pre, needsHints: false, enabled: |o| o.fusion, rewrite: fusion },
|
|
93
|
+
Pass { name: "trmc", target: Target::Lowered, needsHints: false, enabled: |o| o.trmc, rewrite: trmc },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/// The state the passes thread through: the pre-union program and the checked result.
|
|
97
|
+
struct State {
|
|
98
|
+
pre: Source,
|
|
99
|
+
result: Result<(), Vec<Diagnostic>>,
|
|
100
|
+
info: TypedInfo,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Checks `source`, then runs the enabled passes of `PASSES` in order, each adopted only if
|
|
104
|
+
/// it re-checks, then marks tail calls. A dropped rewrite leaves the previous program and
|
|
105
|
+
/// its `TypedInfo` untouched (with `explain` the drop and its first errors are printed).
|
|
106
|
+
pub fn check(source: &Source, opts: &PassOptions) -> Result<TypedInfo, Vec<Diagnostic>> {
|
|
107
|
+
check_with(source, opts, &PASSES)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// `check` with an explicit pass list.
|
|
111
|
+
pub fn check_with(source: &Source, opts: &PassOptions, passes: &[Pass]) -> Result<TypedInfo, Vec<Diagnostic>> {
|
|
112
|
+
let (result, info) = checkProgram(source);
|
|
113
|
+
let mut st = State { pre: preUnionSource(source), result, info };
|
|
114
|
+
if st.result.is_ok() {
|
|
115
|
+
for pass in passes.iter().filter(|p| (p.enabled)(opts)) {
|
|
116
|
+
runPass(pass, source, opts, &mut st);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Flags are set in place, so the address-keyed type info stays valid.
|
|
120
|
+
if let Some(lowered) = st.info.lowered.as_mut().and_then(Rc::get_mut) {
|
|
121
|
+
plum_core::tail::markTailCalls(lowered);
|
|
122
|
+
}
|
|
123
|
+
st.result.map(|()| st.info)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn runPass(pass: &Pass, original: &Source, opts: &PassOptions, st: &mut State) {
|
|
127
|
+
// Type info is keyed by the addresses of the program it checked (`lowered`, or the
|
|
128
|
+
// original source itself), not of the clone `pre`; hints are keyed by call position, so
|
|
129
|
+
// read them off the checked program and apply them to its twin.
|
|
130
|
+
let hints = if pass.needsHints {
|
|
131
|
+
match st.info.lowered.clone() {
|
|
132
|
+
Some(checked) => plum_checker::closureHints(&checked, &st.info),
|
|
133
|
+
None => plum_checker::closureHints(original, &st.info),
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
Hints::new()
|
|
137
|
+
};
|
|
138
|
+
let env = Env { explain: opts.explain, hints: &hints };
|
|
139
|
+
let mut copy = match pass.target {
|
|
140
|
+
Target::Pre => st.pre.clone(),
|
|
141
|
+
Target::Lowered => match st.info.lowered.as_ref() {
|
|
142
|
+
Some(l) => (**l).clone(),
|
|
143
|
+
None => return,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
if !(pass.rewrite)(&mut copy, &env) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Type info is keyed by expression address: check the very program kept as `lowered`.
|
|
150
|
+
let kept = Rc::new(copy);
|
|
151
|
+
let (r2, mut i2) = checkOnce(&kept);
|
|
152
|
+
match r2 {
|
|
153
|
+
Ok(()) => {
|
|
154
|
+
if pass.target == Target::Pre {
|
|
155
|
+
st.pre = (*kept).clone();
|
|
156
|
+
}
|
|
157
|
+
// A pre-union program is lowered by its check; an already lowered one is kept as is.
|
|
158
|
+
if pass.target == Target::Lowered || i2.lowered.is_none() {
|
|
159
|
+
i2.lowered = Some(kept);
|
|
160
|
+
}
|
|
161
|
+
st.result = Ok(());
|
|
162
|
+
st.info = i2;
|
|
163
|
+
}
|
|
164
|
+
Err(errs) => {
|
|
165
|
+
if opts.explain {
|
|
166
|
+
eprintln!("fusion: {} dropped: the rewritten program failed its re-check", pass.name);
|
|
167
|
+
for e in errs.iter().take(3) {
|
|
168
|
+
eprintln!("fusion: {}:{}: {}", e.line, e.column, e.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// The checked program before union lowering: own generics applied and `for` loops lowered.
|
|
176
|
+
fn preUnionSource(source: &Source) -> Source {
|
|
177
|
+
let mut cur = source.clone();
|
|
178
|
+
while let Some(applied) = plum_checker::applyOwnGenerics(&cur) {
|
|
179
|
+
cur = applied;
|
|
180
|
+
}
|
|
181
|
+
if cur.formLoops {
|
|
182
|
+
plum_core::forms::begin(&cur);
|
|
183
|
+
let _ = checkOnce(&cur);
|
|
184
|
+
cur = plum_core::forms::finish(&cur);
|
|
185
|
+
}
|
|
186
|
+
cur
|
|
187
|
+
}
|
plum-driver/tests/driver_tests.rs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::ast::{Item, Source};
|
|
3
|
+
use plum_driver::{check, check_with, Env, Pass, PassOptions, Target, PASSES};
|
|
4
|
+
|
|
5
|
+
const SRC: &str = "\
|
|
6
|
+
import std/Int
|
|
7
|
+
import std/List
|
|
8
|
+
|
|
9
|
+
fun total(xs: List[Int]) -> Int = xs.map(|x| x + 1).fold(0, |a, x| a + x)
|
|
10
|
+
|
|
11
|
+
fun main() -> Int = total([1, 2, 3])
|
|
12
|
+
";
|
|
13
|
+
|
|
14
|
+
fn load(name: &str, src: &str) -> Source {
|
|
15
|
+
plum_test_support::load(name, src).expect("loads")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn fnNames(s: &Source) -> Vec<String> {
|
|
19
|
+
s.items
|
|
20
|
+
.iter()
|
|
21
|
+
.filter_map(|i| if let Item::Fn(f) = i { Some(f.name.clone()) } else { None })
|
|
22
|
+
.collect()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn loweredNames(src: &Source, opts: &PassOptions) -> Vec<String> {
|
|
26
|
+
let info = check(src, opts).expect("checks");
|
|
27
|
+
fnNames(info.lowered.as_ref().expect("lowered"))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
fn noPassesMeansNoGeneratedFns() {
|
|
32
|
+
let src = load("drv_none", SRC);
|
|
33
|
+
let opts = PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false };
|
|
34
|
+
let names = loweredNames(&src, &opts);
|
|
35
|
+
assert!(names.iter().all(|n| !n.contains('$')), "{names:?}");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn defaultPassesSpecialise() {
|
|
40
|
+
let src = load("drv_default", SRC);
|
|
41
|
+
let names = loweredNames(&src, &PassOptions::default());
|
|
42
|
+
assert!(names.iter().any(|n| n.contains('$')), "the default passes specialise: {names:?}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// A rewrite that breaks the program (a call to a fn that takes another type) fails its
|
|
46
|
+
/// re-check and is dropped: the result equals the run without it.
|
|
47
|
+
fn breakProgram(copy: &mut Source, _: &Env) -> bool {
|
|
48
|
+
for item in &mut copy.items {
|
|
49
|
+
if let Item::Fn(f) = item {
|
|
50
|
+
if f.name == "main" {
|
|
51
|
+
f.returns = Some(plum_core::ast::Type { name: "Bool".to_string(), generics: vec![] });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn aRewriteThatFailsItsRecheckIsDropped() {
|
|
61
|
+
let src = load("drv_drop", SRC);
|
|
62
|
+
let opts = PassOptions::default();
|
|
63
|
+
let bad = Pass {
|
|
64
|
+
name: "break",
|
|
65
|
+
target: Target::Pre,
|
|
66
|
+
needsHints: false,
|
|
67
|
+
enabled: |_| true,
|
|
68
|
+
rewrite: breakProgram,
|
|
69
|
+
};
|
|
70
|
+
let mut passes: Vec<Pass> = vec![bad];
|
|
71
|
+
passes.extend(PASSES.into_iter());
|
|
72
|
+
let with = fnNames(check_with(&src, &opts, &passes).expect("still checks").lowered.as_ref().unwrap());
|
|
73
|
+
let without = loweredNames(&src, &opts);
|
|
74
|
+
assert_eq!(with, without);
|
|
75
|
+
}
|
plum-game/tests/host_test.rs
CHANGED
|
@@ -8,7 +8,7 @@ fn root() -> PathBuf {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
fn compile(src_path: &Path) -> Vec<u8> {
|
|
11
|
-
let source = plum_core::
|
|
11
|
+
let source = plum_core::loadAndMergeDiag(src_path, &root()).expect("load");
|
|
12
12
|
plum_wasm_codegen::compile(&source, &plum_wasm_codegen::CompileOptions::default()).expect("compile").wasm
|
|
13
13
|
}
|
|
14
14
|
|
plum-lsp/Cargo.toml
CHANGED
|
@@ -10,6 +10,7 @@ path = "src/main.rs"
|
|
|
10
10
|
[dependencies]
|
|
11
11
|
plum-core = { path = "../plum-core" }
|
|
12
12
|
plum-checker = { path = "../plum-checker" }
|
|
13
|
+
plum-driver = { path = "../plum-driver" }
|
|
13
14
|
tree-sitter = "0.26"
|
|
14
15
|
tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
|
15
16
|
tower-lsp = "0.20"
|
plum-lsp/src/main.rs
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
// 1. Syntax errors — read directly off the tree-sitter parse tree (ERROR /
|
|
4
4
|
// MISSING nodes), which carries real byte/line/column spans.
|
|
5
5
|
//
|
|
6
|
-
// 2. Type errors — from `
|
|
6
|
+
// 2. Type errors — from `plum_driver::check`. A `plum_core::Diagnostic` carries a
|
|
7
|
-
// `line`/`column`
|
|
7
|
+
// 1-based `line`/`column`; the diagnostic range runs from that column to the
|
|
8
|
-
// declaration (not the exact sub-expression/token within it — the
|
|
9
|
-
// checker doesn't track spans that fine yet), so the diagnostic range
|
|
10
|
-
//
|
|
8
|
+
// end of its line.
|
|
11
9
|
//
|
|
12
10
|
// Both are republished on every `did_open`/`did_change`/`did_save`. Type
|
|
13
|
-
// checking goes through `plum_core::
|
|
11
|
+
// checking goes through `plum_core::loadAndMergeWithOverridesDiag`, passing the
|
|
14
12
|
// live in-memory buffer for the edited document while every other
|
|
15
13
|
// (transitively imported) file still reads from disk — so it reflects
|
|
16
14
|
// unsaved edits, not just the last save.
|
|
@@ -82,7 +80,7 @@ impl Backend {
|
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
/// Diagnostics from `plum_checker::checkSource`, run against the live
|
|
85
|
-
/// in-memory buffer (via `
|
|
83
|
+
/// in-memory buffer (via `loadAndMergeWithOverridesDiag`) rather than
|
|
86
84
|
/// whatever's on disk, so an unsaved edit is reflected immediately.
|
|
87
85
|
/// Every other (transitively imported) file still resolves from disk.
|
|
88
86
|
/// Returns `None` (rather than an error) if the document has no on-disk
|
|
@@ -102,7 +100,7 @@ impl Backend {
|
|
|
102
100
|
let canon = std::fs::canonicalize(&path).unwrap_or(path.clone());
|
|
103
101
|
let mut overrides = HashMap::new();
|
|
104
102
|
overrides.insert(canon, text.to_string());
|
|
105
|
-
plum_core::
|
|
103
|
+
plum_core::loadAndMergeWithOverridesDiag(&path, &self.libPath, &overrides).ok()
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
async fn completionItems(&self, uri: &Url, pos: Position) -> Vec<CompletionItem> {
|
|
@@ -308,28 +306,28 @@ fn typeDiagnosticsFor(path: &Path, libPath: &Path, text: &str) -> Option<Vec<Dia
|
|
|
308
306
|
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
309
307
|
let mut overrides = HashMap::new();
|
|
310
308
|
overrides.insert(canon, text.to_string());
|
|
311
|
-
let ast = plum_core::
|
|
309
|
+
let ast = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides).ok()?;
|
|
312
310
|
let lastLine = text.lines().count().max(1) as u32 - 1;
|
|
311
|
+
let opts = plum_driver::PassOptions { closureSpec: false, fusion: false, trmc: false, explain: false };
|
|
313
|
-
match
|
|
312
|
+
match plum_driver::check(&ast, &opts) {
|
|
314
|
-
Ok(
|
|
313
|
+
Ok(_) => Some(Vec::new()),
|
|
315
|
-
Err(
|
|
314
|
+
Err(diags) => Some(
|
|
316
|
-
|
|
315
|
+
diags
|
|
317
316
|
.into_iter()
|
|
318
|
-
.map(|
|
|
317
|
+
.map(|d| {
|
|
319
|
-
// `
|
|
318
|
+
// `plum_core::Diagnostic` positions are 1-based (0 = unknown); LSP's
|
|
320
|
-
// enclosing declaration, not a sub-expression — clamp
|
|
321
|
-
//
|
|
319
|
+
// are 0-based. The range runs from the reported column to the end of
|
|
322
|
-
// editor still shows something sensible even if the
|
|
323
|
-
// line
|
|
320
|
+
// its line, clamped to the document.
|
|
324
|
-
let line =
|
|
321
|
+
let line = d.line.saturating_sub(1).min(lastLine as usize) as u32;
|
|
322
|
+
let col = d.column.saturating_sub(1) as u32;
|
|
325
323
|
Diagnostic {
|
|
326
324
|
range: Range {
|
|
327
|
-
start: Position::new(line,
|
|
325
|
+
start: Position::new(line, col),
|
|
328
326
|
end: Position::new(line, u32::MAX),
|
|
329
327
|
},
|
|
330
328
|
severity: Some(DiagnosticSeverity::ERROR),
|
|
331
329
|
source: Some("plum-check".into()),
|
|
332
|
-
message:
|
|
330
|
+
message: d.message,
|
|
333
331
|
..Default::default()
|
|
334
332
|
}
|
|
335
333
|
})
|
|
@@ -350,7 +348,7 @@ fn memberCompletionFor(
|
|
|
350
348
|
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
351
349
|
let mut overrides = HashMap::new();
|
|
352
350
|
overrides.insert(canon, text.to_string());
|
|
353
|
-
let Ok(merged) = plum_core::
|
|
351
|
+
let Ok(merged) = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides) else {
|
|
354
352
|
return Vec::new();
|
|
355
353
|
};
|
|
356
354
|
let tree = Backend::parseTree(text);
|
|
@@ -534,7 +532,7 @@ fn resolveTarget(
|
|
|
534
532
|
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
535
533
|
let mut overrides = HashMap::new();
|
|
536
534
|
overrides.insert(canon, text.to_string());
|
|
537
|
-
let merged = plum_core::
|
|
535
|
+
let merged = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides).ok()?;
|
|
538
536
|
if s > 0 && lineText.as_bytes()[s - 1] == b'.' {
|
|
539
537
|
let before = &lineText[..s - 1];
|
|
540
538
|
let rs = before
|
|
@@ -622,7 +620,7 @@ fn inheritedDefinition(
|
|
|
622
620
|
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
623
621
|
let mut overrides = HashMap::new();
|
|
624
622
|
overrides.insert(canon.clone(), text.to_string());
|
|
625
|
-
let merged = plum_core::
|
|
623
|
+
let merged = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides).ok()?;
|
|
626
624
|
let (file, line, needle) = match loc {
|
|
627
625
|
Some((f, l, _)) => (f.clone(), *l, format!("fun {name}")),
|
|
628
626
|
None => {
|
|
@@ -1002,7 +1000,7 @@ mod tests {
|
|
|
1002
1000
|
b64.iter().any(|l| l == "encode") && b64.iter().any(|l| l == "decode"),
|
|
1003
1001
|
"{b64:?}"
|
|
1004
1002
|
);
|
|
1005
|
-
let merged = plum_core::
|
|
1003
|
+
let merged = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("loads");
|
|
1006
1004
|
let top: Vec<String> = completion::topLevelItems(&merged)
|
|
1007
1005
|
.into_iter()
|
|
1008
1006
|
.map(|i| i.label)
|
|
@@ -1152,6 +1150,22 @@ mod tests {
|
|
|
1152
1150
|
assert!(n > 20, "found only {n} README snippets");
|
|
1153
1151
|
}
|
|
1154
1152
|
|
|
1153
|
+
/// A type error is published at the position its `Diagnostic` reports (1-based there,
|
|
1154
|
+
/// 0-based here), not on a line parsed out of the message.
|
|
1155
|
+
#[test]
|
|
1156
|
+
fn typeDiagnosticUsesTheDiagnosticPosition() {
|
|
1157
|
+
let dir = std::env::temp_dir().join(format!("plum_lsp_pos_{}", std::process::id()));
|
|
1158
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
1159
|
+
let path = dir.join("main.plum");
|
|
1160
|
+
let text = "import std/Int\n\nfun main() -> Int =\n nope\n";
|
|
1161
|
+
std::fs::write(&path, text).unwrap();
|
|
1162
|
+
let diags = typeDiagnosticsFor(&path, &repoRoot(), text).expect("loads");
|
|
1163
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
1164
|
+
assert_eq!(diags.len(), 1, "{diags:?}");
|
|
1165
|
+
assert_eq!(diags[0].range.start, Position::new(3, 2), "{:?}", diags[0].range);
|
|
1166
|
+
assert!(diags[0].message.contains("nope"), "{}", diags[0].message);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1155
1169
|
#[test]
|
|
1156
1170
|
fn syntaxDiagnosticUnderlinesTheTokenNotTheFunction() {
|
|
1157
1171
|
let src = "import std/Float\n\nexport fun update(w: Float, dt: Float) =\n ()\n\nfun ok() =\n _ := 1.0\n";
|
plum-test-support/Cargo.toml
CHANGED
|
@@ -5,7 +5,7 @@ edition = "2021"
|
|
|
5
5
|
|
|
6
6
|
[dependencies]
|
|
7
7
|
plum-core = { path = "../plum-core" }
|
|
8
|
-
plum-
|
|
8
|
+
plum-driver = { path = "../plum-driver" }
|
|
9
9
|
plum-wasm-codegen = { path = "../plum-wasm-codegen" }
|
|
10
10
|
tree-sitter = "0.26"
|
|
11
11
|
tree-sitter-plum = { path = "../plum-tooling/tree-sitter-plum" }
|
plum-test-support/src/lib.rs
CHANGED
|
@@ -60,16 +60,22 @@ pub fn load(name: &str, src: &str) -> Result<Source, String> {
|
|
|
60
60
|
std::fs::create_dir_all(&dir).unwrap();
|
|
61
61
|
let path = dir.join("main.plum");
|
|
62
62
|
std::fs::write(&path, src).unwrap();
|
|
63
|
-
let r = plum_core::
|
|
63
|
+
let r = plum_core::loader::loadAndMergeDiag(&path, &repoRoot());
|
|
64
64
|
let _ = std::fs::remove_dir_all(&dir);
|
|
65
|
-
r.map_err(|
|
|
65
|
+
r.map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n"))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Compiles a loaded `source` with the default options; the errors are one text.
|
|
69
|
+
pub fn compileSource(source: &Source) -> Result<Vec<u8>, String> {
|
|
70
|
+
plum_wasm_codegen::compile(source, &plum_wasm_codegen::CompileOptions::default())
|
|
71
|
+
.map(|o| o.wasm)
|
|
72
|
+
.map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n"))
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/// load + compileSource + validate; panics on any error.
|
|
69
76
|
pub fn compile(name: &str, src: &str) -> Vec<u8> {
|
|
70
77
|
let source = load(name, src).unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
71
|
-
let bytes = plum_wasm_codegen::compileSource(&source)
|
|
72
|
-
|
|
78
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
73
79
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
74
80
|
bytes
|
|
75
81
|
}
|
|
@@ -125,11 +131,11 @@ pub fn runStr(name: &str, src: &str) -> String {
|
|
|
125
131
|
String::from_utf8_lossy(&buf[start..start + len]).into_owned()
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
/// load +
|
|
134
|
+
/// load + `plum_driver::check`; the checker's messages (a load error is returned as one string).
|
|
129
135
|
pub fn checkErrors(name: &str, src: &str) -> Vec<String> {
|
|
130
136
|
match load(name, src) {
|
|
131
137
|
Err(e) => vec![e],
|
|
132
|
-
Ok(source) => match
|
|
138
|
+
Ok(source) => match plum_driver::check(&source, &plum_driver::PassOptions::default()) {
|
|
133
139
|
Ok(_) => vec![],
|
|
134
140
|
Err(errs) => errs.iter().map(|e| e.message.clone()).collect(),
|
|
135
141
|
},
|
plum-wasm-codegen/Cargo.toml
CHANGED
|
@@ -7,6 +7,7 @@ edition = "2021"
|
|
|
7
7
|
wasm-encoder = "0.220"
|
|
8
8
|
plum-core = { path = "../plum-core" }
|
|
9
9
|
plum-checker = { path = "../plum-checker" }
|
|
10
|
+
plum-driver = { path = "../plum-driver" }
|
|
10
11
|
|
|
11
12
|
[dev-dependencies]
|
|
12
13
|
plum-test-support = { path = "../plum-test-support" }
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
|
|
6
6
|
use plum_checker::{EnumParams, EnumVariantInfo, EnumVariants, MethodEnv};
|
|
7
7
|
use plum_core::ast;
|
|
8
|
+
use plum_core::diag::{DiagKind, Diagnostic};
|
|
8
9
|
use std::cell::{Cell, RefCell};
|
|
9
10
|
use std::collections::{HashMap, HashSet};
|
|
10
11
|
use wasm_encoder::*;
|
|
@@ -1471,43 +1472,11 @@ fn inferFallbackType(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
|
|
|
1471
1472
|
plum_checker::inferExpr(expr, &env, &cctx).unwrap_or(PlumType::TInt)
|
|
1472
1473
|
}
|
|
1473
1474
|
|
|
1474
|
-
/// Type-checks `source`
|
|
1475
|
+
/// Type-checks `source` through the pass list (`plum_driver::check`), returning the
|
|
1475
|
-
///
|
|
1476
|
+
/// per-expression types codegen needs.
|
|
1476
|
-
pub fn typeCheck(source: &ast::Source) -> Result<plum_checker::TypedInfo, String> {
|
|
1477
|
-
typeCheckIn(source, "")
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/// Like `typeCheck`, naming `file` in positioned errors. Positions refer to the
|
|
1481
|
-
///
|
|
1477
|
+
/// An internal codegen failure as a diagnostic.
|
|
1482
|
-
pub fn typeCheckIn(source: &ast::Source, file: &str) -> Result<plum_checker::TypedInfo, String> {
|
|
1483
|
-
typeCheckWith(source, file, plum_checker::FusionOptions::default())
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
fn typeCheckWith(
|
|
1487
|
-
source: &ast::Source,
|
|
1488
|
-
file: &str,
|
|
1489
|
-
fusion: plum_checker::FusionOptions,
|
|
1490
|
-
) ->
|
|
1478
|
+
fn codegenDiag(message: String) -> Vec<Diagnostic> {
|
|
1491
|
-
plum_checker::typedSourceWith(source, fusion).map_err(|errs| {
|
|
1492
|
-
errs.iter()
|
|
1493
|
-
.map(|e| {
|
|
1494
|
-
if e.line > 0 && !file.is_empty() {
|
|
1495
|
-
format!(
|
|
1496
|
-
"type error in '{}' at line {}, column {}: {}",
|
|
1497
|
-
file, e.line, e.column, e.message
|
|
1498
|
-
)
|
|
1499
|
-
} else if e.line > 0 {
|
|
1500
|
-
format!(
|
|
1501
|
-
"type error at line {}, column {}: {}",
|
|
1502
|
-
e.line, e.column, e.message
|
|
1503
|
-
)
|
|
1504
|
-
} else {
|
|
1505
|
-
|
|
1479
|
+
vec![Diagnostic::bare(DiagKind::Codegen, message)]
|
|
1506
|
-
}
|
|
1507
|
-
})
|
|
1508
|
-
.collect::<Vec<_>>()
|
|
1509
|
-
.join("\n")
|
|
1510
|
-
})
|
|
1511
1480
|
}
|
|
1512
1481
|
|
|
1513
1482
|
/// What `compile` does besides compiling.
|
|
@@ -1516,12 +1485,13 @@ pub struct CompileOptions {
|
|
|
1516
1485
|
/// Lower `test` blocks into exported `"test#<name>"` fns (`plum test`); otherwise
|
|
1517
1486
|
/// they are stripped.
|
|
1518
1487
|
pub tests: bool,
|
|
1488
|
+
/// Which rewrite passes run (`--no-fusion`, `--explain-fusion`).
|
|
1519
|
-
pub
|
|
1489
|
+
pub passes: plum_driver::PassOptions,
|
|
1520
1490
|
}
|
|
1521
1491
|
|
|
1522
1492
|
impl Default for CompileOptions {
|
|
1523
1493
|
fn default() -> Self {
|
|
1524
|
-
CompileOptions { tests: false,
|
|
1494
|
+
CompileOptions { tests: false, passes: plum_driver::PassOptions::default() }
|
|
1525
1495
|
}
|
|
1526
1496
|
}
|
|
1527
1497
|
|
|
@@ -1535,23 +1505,23 @@ pub struct Output {
|
|
|
1535
1505
|
pub test_names: Vec<String>,
|
|
1536
1506
|
}
|
|
1537
1507
|
|
|
1538
|
-
/// The one compile entry: type-checks `source` (
|
|
1508
|
+
/// The one compile entry: type-checks `source` (passes per `opts.passes`) and compiles it.
|
|
1539
1509
|
/// With `opts.tests`, `test` blocks are first lowered to ordinary fns (see
|
|
1540
1510
|
/// `plum_core::tests_lower`) and exported.
|
|
1541
|
-
pub fn compile(source: &ast::Source, opts: &CompileOptions) -> Result<Output,
|
|
1511
|
+
pub fn compile(source: &ast::Source, opts: &CompileOptions) -> Result<Output, Vec<Diagnostic>> {
|
|
1542
|
-
if opts.
|
|
1512
|
+
if opts.passes.explain {
|
|
1543
1513
|
for line in explainLayouts(source) {
|
|
1544
1514
|
eprintln!("{line}");
|
|
1545
1515
|
}
|
|
1546
1516
|
}
|
|
1547
1517
|
if opts.tests {
|
|
1548
1518
|
let prog = plum_core::tests_lower::lowerTests(source);
|
|
1549
|
-
let typed =
|
|
1519
|
+
let typed = plum_driver::check(&prog.source, &opts.passes)?;
|
|
1550
|
-
let (wasm, fn_keys) = compileSourceInner(&prog.source, &prog.extraExports, typed)?;
|
|
1520
|
+
let (wasm, fn_keys) = compileSourceInner(&prog.source, &prog.extraExports, typed).map_err(codegenDiag)?;
|
|
1551
1521
|
Ok(Output { wasm, fn_keys, test_names: prog.names })
|
|
1552
1522
|
} else {
|
|
1553
|
-
let typed =
|
|
1523
|
+
let typed = plum_driver::check(source, &opts.passes)?;
|
|
1554
|
-
let (wasm, fn_keys) = compileSourceInner(source, &[], typed)?;
|
|
1524
|
+
let (wasm, fn_keys) = compileSourceInner(source, &[], typed).map_err(codegenDiag)?;
|
|
1555
1525
|
Ok(Output { wasm, fn_keys, test_names: Vec::new() })
|
|
1556
1526
|
}
|
|
1557
1527
|
}
|
|
@@ -1571,10 +1541,6 @@ pub fn explainLayouts(source: &ast::Source) -> Vec<String> {
|
|
|
1571
1541
|
.collect()
|
|
1572
1542
|
}
|
|
1573
1543
|
|
|
1574
|
-
pub fn compileSource(source: &ast::Source) -> Result<Vec<u8>, String> {
|
|
1575
|
-
compile(source, &CompileOptions::default()).map(|o| o.wasm)
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
1544
|
pub use plum_core::tests_lower::{TestProgram, ASSERT_REPORT_MARK};
|
|
1579
1545
|
|
|
1580
1546
|
#[cfg(test)]
|
|
@@ -1587,15 +1553,14 @@ mod compileEntryTests {
|
|
|
1587
1553
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1588
1554
|
let path = dir.join("main.plum");
|
|
1589
1555
|
std::fs::write(&path, SRC).unwrap();
|
|
1590
|
-
plum_core::
|
|
1556
|
+
plum_core::loadAndMergeDiag(&path, &plum_test_support::repoRoot()).expect("load")
|
|
1591
1557
|
}
|
|
1592
1558
|
|
|
1593
1559
|
#[test]
|
|
1594
|
-
fn
|
|
1560
|
+
fn defaultOptionsCompileWithoutTests() {
|
|
1595
1561
|
let src = load();
|
|
1596
1562
|
let out = compile(&src, &CompileOptions::default()).expect("compiles");
|
|
1597
1563
|
assert!(out.test_names.is_empty());
|
|
1598
|
-
assert_eq!(out.wasm, compileSource(&src).expect("compiles"));
|
|
1599
1564
|
}
|
|
1600
1565
|
|
|
1601
1566
|
#[test]
|
plum-wasm-codegen/tests/wasm_codegen/codegen_tests.rs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
|
+
use plum_test_support::compileSource;
|
|
2
|
-
use plum_wasm_codegen::{compile,
|
|
3
|
+
use plum_wasm_codegen::{compile, CompileOptions};
|
|
3
4
|
|
|
4
5
|
fn compileTestSource(source: &plum_core::ast::Source) -> Result<(Vec<u8>, Vec<String>), String> {
|
|
5
|
-
compile(source, &CompileOptions { tests: true, ..CompileOptions::default() })
|
|
6
|
+
compile(source, &CompileOptions { tests: true, ..CompileOptions::default() })
|
|
7
|
+
.map(|o| (o.wasm, o.test_names))
|
|
8
|
+
.map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n"))
|
|
6
9
|
}
|
|
7
10
|
use wasm_encoder::Encode;
|
|
8
11
|
|
|
@@ -81,7 +84,7 @@ test \"hard assert traps\"
|
|
|
81
84
|
std::fs::create_dir_all(&dir).unwrap();
|
|
82
85
|
let path = dir.join("main.plum");
|
|
83
86
|
std::fs::write(&path, src).unwrap();
|
|
84
|
-
let source = plum_core::
|
|
87
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
85
88
|
let (bytes, names) = compileTestSource(&source).expect("test source should compile");
|
|
86
89
|
let result = wasmparser::validate(&bytes);
|
|
87
90
|
assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
|
|
@@ -169,7 +172,7 @@ test \"fb int\"
|
|
|
169
172
|
std::fs::create_dir_all(&dir).unwrap();
|
|
170
173
|
let path = dir.join("main.plum");
|
|
171
174
|
std::fs::write(&path, src).unwrap();
|
|
172
|
-
let source = plum_core::
|
|
175
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
173
176
|
let (bytes, _names) = compileTestSource(&source).expect("test source should compile");
|
|
174
177
|
let engine = gcEngine();
|
|
175
178
|
let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable");
|
|
@@ -1049,7 +1052,7 @@ fn runMerged<R: wasmtime::WasmResults>(name: &str, src: &str) -> R {
|
|
|
1049
1052
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1050
1053
|
let path = dir.join("main.plum");
|
|
1051
1054
|
std::fs::write(&path, src).unwrap();
|
|
1052
|
-
let source = plum_core::
|
|
1055
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
1053
1056
|
plum_checker::checkSource(&source).unwrap_or_else(|e| {
|
|
1054
1057
|
panic!(
|
|
1055
1058
|
"check errors: {:?}",
|
|
@@ -1083,7 +1086,7 @@ fn compiledKeys(name: &str, src: &str) -> (Vec<u8>, Vec<String>) {
|
|
|
1083
1086
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1084
1087
|
let path = dir.join("main.plum");
|
|
1085
1088
|
std::fs::write(&path, src).unwrap();
|
|
1086
|
-
let source = plum_core::
|
|
1089
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
1087
1090
|
let o = compile(&source, &CompileOptions::default()).expect("compile");
|
|
1088
1091
|
let r = (o.wasm, o.fn_keys);
|
|
1089
1092
|
let _ = std::fs::remove_dir_all(&dir);
|
|
@@ -1537,7 +1540,7 @@ fun main() -> Int =
|
|
|
1537
1540
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1538
1541
|
let path = dir.join("main.plum");
|
|
1539
1542
|
std::fs::write(&path, src).unwrap();
|
|
1540
|
-
let source = plum_core::
|
|
1543
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
1541
1544
|
// Same as any other method of a std type that was not imported: rejected by the checker.
|
|
1542
1545
|
assert!(
|
|
1543
1546
|
plum_checker::checkSource(&source).is_err(),
|
|
@@ -1911,7 +1914,7 @@ fn checkErrorsMerged(name: &str, src: &str) -> String {
|
|
|
1911
1914
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1912
1915
|
let path = dir.join("main.plum");
|
|
1913
1916
|
std::fs::write(&path, src).unwrap();
|
|
1914
|
-
let source = plum_core::
|
|
1917
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
1915
1918
|
let _ = std::fs::remove_dir_all(&dir);
|
|
1916
1919
|
match plum_checker::checkSource(&source) {
|
|
1917
1920
|
Ok(()) => String::new(),
|
|
@@ -1963,7 +1966,7 @@ fn runWithCrash(name: &str, src: &str) -> (Result<i64, String>, Vec<String>) {
|
|
|
1963
1966
|
std::fs::create_dir_all(&dir).unwrap();
|
|
1964
1967
|
let path = dir.join("main.plum");
|
|
1965
1968
|
std::fs::write(&path, src).unwrap();
|
|
1966
|
-
let source = plum_core::
|
|
1969
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
1967
1970
|
plum_checker::checkSource(&source).unwrap_or_else(|e| {
|
|
1968
1971
|
panic!(
|
|
1969
1972
|
"check errors: {:?}",
|
|
@@ -2229,7 +2232,7 @@ fn strIsAViewOfAPackedByteArray() {
|
|
|
2229
2232
|
std::fs::create_dir_all(&dir).unwrap();
|
|
2230
2233
|
let path = dir.join("main.plum");
|
|
2231
2234
|
std::fs::write(&path, src).unwrap();
|
|
2232
|
-
let source = plum_core::
|
|
2235
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
2233
2236
|
let bytes = compileSource(&source).expect("compile failed");
|
|
2234
2237
|
let _ = std::fs::remove_dir_all(&dir);
|
|
2235
2238
|
let mut byte_arrays = std::collections::HashSet::new();
|
|
@@ -2305,7 +2308,7 @@ fun main() -> Float =
|
|
|
2305
2308
|
std::fs::create_dir_all(&dir).unwrap();
|
|
2306
2309
|
let path = dir.join("main.plum");
|
|
2307
2310
|
std::fs::write(&path, src).unwrap();
|
|
2308
|
-
let source = plum_core::
|
|
2311
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
2309
2312
|
plum_checker::checkSource(&source).expect("check");
|
|
2310
2313
|
let bytes = compileSource(&source).expect("compile failed");
|
|
2311
2314
|
let _ = std::fs::remove_dir_all(&dir);
|
|
@@ -2386,7 +2389,7 @@ fn instantiateExports(name: &str, src: &str) -> (wasmtime::Store<()>, wasmtime::
|
|
|
2386
2389
|
let path = dir.join("main.plum");
|
|
2387
2390
|
let full = format!("import std/Int\nimport std/Float\nimport std/Bool\nimport std/Str\n{src}");
|
|
2388
2391
|
std::fs::write(&path, full).unwrap();
|
|
2389
|
-
let source = plum_core::
|
|
2392
|
+
let source = plum_core::loadAndMergeDiag(&path, &exportRepoRoot());
|
|
2390
2393
|
let _ = std::fs::remove_dir_all(&dir);
|
|
2391
2394
|
let source = source.expect("load");
|
|
2392
2395
|
let bytes = compileSource(&source).expect("compile failed");
|
|
@@ -2713,7 +2716,7 @@ fun main() -> Int =
|
|
|
2713
2716
|
std::fs::create_dir_all(&dir).unwrap();
|
|
2714
2717
|
let path = dir.join("main.plum");
|
|
2715
2718
|
std::fs::write(&path, src).unwrap();
|
|
2716
|
-
let source = plum_core::
|
|
2719
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("load");
|
|
2717
2720
|
let bytes = compileSource(&source).expect("compile");
|
|
2718
2721
|
let engine = gcEngine();
|
|
2719
2722
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
plum-wasm-codegen/tests/wasm_codegen/e2e_mem/reuse.rs
CHANGED
|
@@ -9,7 +9,7 @@ fn load(name: &str, src: &str, reuse: bool) -> Result<plum_core::ast::Source, St
|
|
|
9
9
|
|
|
10
10
|
fn runInt(name: &str, src: &str, reuse: bool) -> i64 {
|
|
11
11
|
let source = load(name, src, reuse).expect("load");
|
|
12
|
-
let bytes =
|
|
12
|
+
let bytes = plum_test_support::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
13
13
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
14
14
|
let engine = engine();
|
|
15
15
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
@@ -36,12 +36,12 @@ fn structNewCount(name: &str, src: &str, reuse: bool) -> usize {
|
|
|
36
36
|
fn structNewCountWith(name: &str, src: &str, reuse: bool, fusion: bool) -> usize {
|
|
37
37
|
let source = load(name, src, reuse).expect("load");
|
|
38
38
|
let opts = plum_wasm_codegen::CompileOptions {
|
|
39
|
-
|
|
39
|
+
passes: plum_driver::PassOptions { fusion: fusion, explain: false, ..plum_driver::PassOptions::default() },
|
|
40
40
|
..Default::default()
|
|
41
41
|
};
|
|
42
42
|
let bytes = plum_wasm_codegen::compile(&source, &opts)
|
|
43
43
|
.map(|o| o.wasm)
|
|
44
|
-
.unwrap_or_else(|e| panic!("compile failed: {
|
|
44
|
+
.unwrap_or_else(|e| panic!("compile failed: {}", e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")));
|
|
45
45
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
46
46
|
let mut n = 0;
|
|
47
47
|
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
|
@@ -97,7 +97,7 @@ fn bump_reuses_cells() {
|
|
|
97
97
|
fn pipeline_marks_bump_reuse() {
|
|
98
98
|
let p = format!("{PRE}{INC}{BUMP}");
|
|
99
99
|
let source = load("bump_pipeline", &p, false).expect("load");
|
|
100
|
-
let info =
|
|
100
|
+
let info = plum_driver::check(&source, &plum_driver::PassOptions::default()).expect("checks");
|
|
101
101
|
let low = info.lowered.as_ref().expect("lowered");
|
|
102
102
|
let bump = low
|
|
103
103
|
.items
|
plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_inplace.rs
CHANGED
|
@@ -4,7 +4,7 @@ use plum_test_support::*;
|
|
|
4
4
|
/// fn `name` as unique, i.e. proved written in place by uniqueness alone.
|
|
5
5
|
fn spreadIsUnique(name: &str, fnName: &str, src: &str) -> bool {
|
|
6
6
|
let source = load(name, src).expect("load");
|
|
7
|
-
let info =
|
|
7
|
+
let info = plum_driver::check(&source, &plum_driver::PassOptions::default()).expect("checks");
|
|
8
8
|
let low = info.lowered.as_ref().expect("lowered");
|
|
9
9
|
low.items
|
|
10
10
|
.iter()
|
plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_spread.rs
CHANGED
|
@@ -19,7 +19,7 @@ fn load(name: &str, src: &str, unique: bool) -> Result<plum_core::ast::Source, S
|
|
|
19
19
|
|
|
20
20
|
fn runInt(name: &str, src: &str, unique: bool) -> i64 {
|
|
21
21
|
let source = load(name, src, unique).expect("load");
|
|
22
|
-
let bytes =
|
|
22
|
+
let bytes = plum_test_support::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
23
23
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
24
24
|
let engine = engine();
|
|
25
25
|
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
@@ -38,7 +38,7 @@ fn runInt(name: &str, src: &str, unique: bool) -> i64 {
|
|
|
38
38
|
/// Number of `struct.new` instructions in the whole module.
|
|
39
39
|
fn structNewCount(name: &str, src: &str, unique: bool) -> usize {
|
|
40
40
|
let source = load(name, src, unique).expect("load");
|
|
41
|
-
let bytes =
|
|
41
|
+
let bytes = plum_test_support::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
42
42
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
43
43
|
let mut n = 0;
|
|
44
44
|
for payload in wasmparser::Parser::new(0).parse_all(&bytes) {
|
plum-wasm-codegen/tests/wasm_codegen/e2e_records/enum_init.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use plum_test_support::{load, runInt};
|
|
2
|
-
use
|
|
2
|
+
use plum_test_support::compileSource;
|
|
3
3
|
const LINK: &str = "enum Link[T] =
|
|
4
4
|
| Nil
|
|
5
5
|
| Cons(head: T, tail: Link[T])
|
plum-wasm-codegen/tests/wasm_codegen/e2e_types/subtyping.rs
CHANGED
|
@@ -10,11 +10,11 @@ fn prog(body: &str) -> String {
|
|
|
10
10
|
fn runWith(name: &str, src: &str, fusion: bool) -> i64 {
|
|
11
11
|
let source = load(name, src).unwrap_or_else(|e| panic!("load failed: {e}"));
|
|
12
12
|
let opts = plum_wasm_codegen::CompileOptions {
|
|
13
|
-
|
|
13
|
+
passes: plum_driver::PassOptions { fusion: fusion, explain: false, ..plum_driver::PassOptions::default() },
|
|
14
14
|
..plum_wasm_codegen::CompileOptions::default()
|
|
15
15
|
};
|
|
16
16
|
let bytes = plum_wasm_codegen::compile(&source, &opts)
|
|
17
|
-
.unwrap_or_else(|e| panic!("compile failed: {
|
|
17
|
+
.unwrap_or_else(|e| panic!("compile failed: {}", e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")))
|
|
18
18
|
.wasm;
|
|
19
19
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
20
20
|
let engine = engine();
|
plum-wasm-codegen/tests/wasm_codegen/examples_test.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#![allow(non_snake_case)]
|
|
2
2
|
use plum_test_support::{engine, repoRoot};
|
|
3
|
-
use
|
|
3
|
+
use plum_test_support::compileSource;
|
|
4
4
|
|
|
5
5
|
fn examplesDir() -> std::path::PathBuf {
|
|
6
6
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../plum-examples")
|
|
@@ -13,8 +13,8 @@ fn examplesDir() -> std::path::PathBuf {
|
|
|
13
13
|
/// single-file parse would leave those names undeclared.
|
|
14
14
|
fn parseFile(name: &str) -> plum_core::ast::Source {
|
|
15
15
|
let path = examplesDir().join(name);
|
|
16
|
-
plum_core::
|
|
16
|
+
plum_core::loadAndMergeDiag(&path, &repoRoot())
|
|
17
|
-
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e))
|
|
17
|
+
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
fn assertCompiles(name: &str) -> Vec<u8> {
|
|
@@ -175,7 +175,7 @@ fn const_table_1000_entries() {
|
|
|
175
175
|
std::fs::create_dir_all(&dir).unwrap();
|
|
176
176
|
let path = dir.join("t.plum");
|
|
177
177
|
std::fs::write(&path, src).unwrap();
|
|
178
|
-
let source = plum_core::
|
|
178
|
+
let source = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("1000-const file should load");
|
|
179
179
|
let bytes = compileSource(&source).expect("1000-const file should compile");
|
|
180
180
|
assert!(wasmparser::validate(&bytes).is_ok());
|
|
181
181
|
std::fs::remove_dir_all(&dir).ok();
|
plum-wasm-codegen/tests/wasm_codegen/fusion_alloc_test.rs
CHANGED
|
@@ -6,19 +6,19 @@ use std::collections::HashMap;
|
|
|
6
6
|
use std::collections::HashSet;
|
|
7
7
|
|
|
8
8
|
fn loadFile(path: &std::path::Path) -> plum_core::ast::Source {
|
|
9
|
-
plum_core::
|
|
9
|
+
plum_core::loadAndMergeDiag(path, &repoRoot()).unwrap_or_else(|e| panic!("load: {e:?}"))
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
fn load(name: &str, src: &str) -> plum_core::ast::Source {
|
|
13
|
-
plum_test_support::load(name, src).unwrap_or_else(|e| panic!("load: {e}"))
|
|
13
|
+
plum_test_support::load(name, src).unwrap_or_else(|e| panic!("load: {e:?}"))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
fn compile(source: &plum_core::ast::Source, fusion: bool) -> Vec<u8> {
|
|
17
17
|
let opts = plum_wasm_codegen::CompileOptions {
|
|
18
|
-
|
|
18
|
+
passes: plum_driver::PassOptions { fusion: fusion, explain: false, ..plum_driver::PassOptions::default() },
|
|
19
19
|
..Default::default()
|
|
20
20
|
};
|
|
21
|
-
let bytes = plum_wasm_codegen::compile(source, &opts).map(|o| o.wasm).unwrap_or_else(|e| panic!("compile failed: {
|
|
21
|
+
let bytes = plum_wasm_codegen::compile(source, &opts).map(|o| o.wasm).unwrap_or_else(|e| panic!("compile failed: {}", e.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("\n")));
|
|
22
22
|
wasmparser::validate(&bytes).expect("valid wasm");
|
|
23
23
|
bytes
|
|
24
24
|
}
|
|
@@ -120,16 +120,16 @@ const PRE: &str = "import std/Str\nimport std/Int\nimport std/List\nimport std/S
|
|
|
120
120
|
/// The fusion notes for `source` (what `--explain-fusion` prints): the stage is run
|
|
121
121
|
/// by hand on the checker's lowered program, built without fusion.
|
|
122
122
|
fn notes(source: &plum_core::ast::Source) -> Vec<String> {
|
|
123
|
-
let info =
|
|
123
|
+
let info = plum_driver::check(
|
|
124
124
|
source,
|
|
125
|
-
|
|
125
|
+
&plum_driver::PassOptions { fusion: false, explain: false, ..plum_driver::PassOptions::default() },
|
|
126
126
|
)
|
|
127
127
|
.unwrap_or_else(|e| panic!("check: {e:?}"));
|
|
128
128
|
let mut copy = match info.lowered.as_ref() {
|
|
129
129
|
Some(l) => (**l).clone(),
|
|
130
130
|
None => source.clone(),
|
|
131
131
|
};
|
|
132
|
-
|
|
132
|
+
plum_driver::fusionNotes(&mut copy)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/// `struct.new` descriptions of every function reachable from `root` through direct calls,
|