plum

#treesitter#compiler#wasm

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

Files changed (62) hide show
  1. Cargo.toml +1 -1
  2. docs/backlog.md +1 -1
  3. docs/plans/2026-11-05-compiler-cleanup-phase3b.md +106 -0
  4. docs/plans/2026-11-05-compiler-cleanup-phase3b.results.md +11 -0
  5. docs/plans/2026-11-05-explicit-fn-generics.progress.md +5 -0
  6. plum-checker/Cargo.toml +1 -0
  7. plum-checker/src/lib.rs +47 -231
  8. plum-checker/src/state.rs +35 -4
  9. plum-checker/src/types.rs +19 -32
  10. plum-checker/tests/checker/checker_tests.rs +9 -4
  11. plum-checker/tests/checker/closure_hint_test.rs +2 -2
  12. plum-checker/tests/checker/diagnostic_tests.rs +34 -0
  13. plum-checker/tests/checker/explicit_generics_tests.rs +1 -1
  14. plum-checker/tests/checker/features/io_capability.rs +1 -1
  15. plum-checker/tests/checker/features/mutability.rs +1 -1
  16. plum-checker/tests/checker/features/pattern_casing.rs +1 -1
  17. plum-checker/tests/checker/features/variant_pattern.rs +1 -1
  18. plum-checker/tests/checker/fusion_pipeline_test.rs +12 -18
  19. plum-checker/tests/checker/inline_variants_tests.rs +1 -1
  20. plum-checker/tests/checker/main.rs +1 -0
  21. plum-checker/tests/checker/question_tests.rs +2 -2
  22. plum-checker/tests/checker/subtyping_tests.rs +4 -4
  23. plum-cli/Cargo.toml +1 -0
  24. plum-cli/src/game.rs +1 -1
  25. plum-cli/src/main.rs +39 -28
  26. plum-cli/tests/cli/compile_tests.rs +16 -0
  27. plum-cli/tests/cli/crash_test.rs +1 -1
  28. plum-core/Cargo.toml +1 -0
  29. plum-core/src/diag.rs +50 -0
  30. plum-core/src/drive.rs +28 -9
  31. plum-core/src/lib.rs +4 -1
  32. plum-core/src/loader.rs +71 -48
  33. plum-core/src/lower.rs +15 -9
  34. plum-core/src/parser.rs +46 -24
  35. plum-core/tests/core/closure_escape_test.rs +2 -2
  36. plum-core/tests/core/closure_spec_test.rs +2 -2
  37. plum-core/tests/core/diag_test.rs +37 -0
  38. plum-core/tests/core/inline_variants_test.rs +21 -8
  39. plum-core/tests/core/layout_test.rs +2 -2
  40. plum-core/tests/core/loader_test.rs +43 -40
  41. plum-core/tests/core/lower_test.rs +4 -4
  42. plum-core/tests/core/main.rs +1 -0
  43. plum-core/tests/core/session_test.rs +2 -2
  44. plum-core/tests/core/trmc_test.rs +6 -4
  45. plum-driver/Cargo.toml +11 -0
  46. plum-driver/src/lib.rs +187 -0
  47. plum-driver/tests/driver_tests.rs +75 -0
  48. plum-game/tests/host_test.rs +1 -1
  49. plum-lsp/Cargo.toml +1 -0
  50. plum-lsp/src/main.rs +40 -26
  51. plum-test-support/Cargo.toml +1 -1
  52. plum-test-support/src/lib.rs +12 -6
  53. plum-wasm-codegen/Cargo.toml +1 -0
  54. plum-wasm-codegen/src/lib.rs +18 -53
  55. plum-wasm-codegen/tests/wasm_codegen/codegen_tests.rs +16 -13
  56. plum-wasm-codegen/tests/wasm_codegen/e2e_mem/reuse.rs +4 -4
  57. plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_inplace.rs +1 -1
  58. plum-wasm-codegen/tests/wasm_codegen/e2e_mem/unique_spread.rs +2 -2
  59. plum-wasm-codegen/tests/wasm_codegen/e2e_records/enum_init.rs +1 -1
  60. plum-wasm-codegen/tests/wasm_codegen/e2e_types/subtyping.rs +2 -2
  61. plum-wasm-codegen/tests/wasm_codegen/examples_test.rs +4 -4
  62. 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 (running) → cleanup phase 3b (pass list, positioned errors) → Io primitive + extern methods → move Rust crates under `compiler/` → cleanup phase 4 (node ids, typed codegen) → 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.
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 `typedSource` can resolve recorded types at the end.
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
- .map(|s| *s.body.clone())
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
- .ok_or_else(|| format!("undefined name '{}'", name))
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(&copy, &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 &notes {
982
- eprintln!("fusion: {n}");
983
- }
984
- }
985
- if !notes.is_empty() {
986
- adoptPre(&copy, &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(&copy);
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| isResolutionError(&e.message));
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::at(e.line, e.column, full));
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
- /// Errors a generic body keeps even when uninstantiated (name resolution and
1787
+ /// (A generic body keeps its `DiagKind::Resolution` errors even when uninstantiated; every
1911
- /// declaration structure); everything else is a type error found per instantiation.
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::at(
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::at(
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::at(
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" => Err(
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 `typedSource`).
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 typedSourceIsRepeatable() {
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::typedSource(&src).expect("checks");
130
+ let (r1, first) = crate::checkProgram(&src);
102
- let second = crate::typedSource(&src).expect("checks");
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
- #[derive(Debug, Clone)]
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 struct CheckError {
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
- /// `ast::Test`'s own `line`/`column`), not the exact sub-expression.
96
+ /// A type error at `line:column` of the declaration being checked.
97
+ fn at(line: usize, column: usize, message: String) -> Self;
94
- /// `(0, 0)` when unknown (e.g. a cross-cutting error with no single
98
+ /// A name-resolution or declaration-structure error (kept even in an uninstantiated
95
- /// declaration to blame, like a duplicate top-level name).
99
+ /// generic body).
96
- pub line: usize,
97
- pub column: usize,
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
- pub fn new(message: String) -> Self {
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
- pub fn at(line: usize, column: usize, message: String) -> Self {
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
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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, typedSource, unify, CheckCtx};
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 = plum_checker::typedSource(&parseClean(&src)).unwrap_or_else(|e| panic!("{:?}", e));
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::loadAndMerge(&path, &root) {
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 plum_checker::{typedSourceWith, FusionOptions};
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 = typedSourceWith(&parse(src), FusionOptions { enabled: false, explain: false }).expect("checks");
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::frontEnd(&mut source).map_err(|e| format!("lower: {e}"))?;
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::frontEnd(&mut source).expect("front end");
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::frontEnd(&mut source).expect("front end");
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::frontEnd(&mut source).expect("front end");
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::frontEnd(&mut source).expect("front end");
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 `typedSource`: switches, explain notes, re-check fallback.
2
+ //! The supercompiler stage of `plum_driver::check`: switches, explain notes, re-check fallback.
3
- use plum_checker::{checkSource, fusionRewrite, recheckKeep, typedSource};
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
- fusionRewrite(&mut copy);
61
+ fusionNotes(&mut copy);
55
62
  assert!(checkSource(&copy).is_ok());
56
63
  // an already-fused program yields no further notes
57
- assert!(fusionRewrite(&mut copy).is_empty());
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 = fusionRewrite(&mut copy);
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::frontEnd(&mut source).expect("front end");
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::frontEnd(&mut source).expect("front end");
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::frontEnd(&mut source).is_err() || checkSource(&source).is_err()
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::frontEnd(&mut source).map_err(|e| format!("lower: {e}"))?;
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::frontEnd(&mut source).unwrap();
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::frontEnd(&mut source).unwrap();
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::frontEnd(&mut source).unwrap();
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: plum_checker::FusionOptions,
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) -> plum_checker::FusionOptions {
142
+ fn fusionFlags(explain: bool, no_fusion: bool) -> plum_driver::PassOptions {
142
- plum_checker::FusionOptions { enabled: !no_fusion, explain }
143
+ plum_driver::PassOptions { fusion: !no_fusion, explain, ..plum_driver::PassOptions::default() }
143
144
  }
144
145
 
145
- /// `compile` reports positioned type errors without a file name; name `file` in them
146
+ /// One diagnostic as `file:line:col: <kind>: message`. `entry` names the file when the
146
- /// (the entry file, since positions refer to the merged source).
147
+ /// diagnostic carries none (positions then refer to the merged entry source).
147
- fn nameFileInErrors(msg: &str, file: &std::path::Path) -> String {
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
- msg.replace("type error at line", &format!("type error in '{}' at line", file.display()))
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: plum_checker::FusionOptions,
246
+ fusion: plum_driver::PassOptions,
229
247
  ) -> Result<Vec<u8>> {
230
- let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
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 }).map(|o| o.wasm).map_err(|e| {
250
+ plum_wasm_codegen::compile(&ast, &plum_wasm_codegen::CompileOptions { tests: false, passes: fusion })
251
+ .map(|o| o.wasm)
233
- if e.starts_with("type error") {
252
+ .map_err(|ds| {
234
- eprintln!("{}", nameFileInErrors(&e, file));
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: plum_checker::FusionOptions,
261
+ fusion: plum_driver::PassOptions,
245
262
  ) -> Result<(Vec<u8>, Vec<String>)> {
246
- let ast = plum_core::loadAndMerge(file, lib_path).map_err(|e| anyhow::anyhow!("{e}"))?;
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(|e| {
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, plum_checker::FusionOptions::default())?;
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: plum_checker::FusionOptions,
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: plum_checker::FusionOptions,
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: plum_checker::FusionOptions,
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: plum_checker::FusionOptions,
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' at line 4, column"), "output: {all}");
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(why) if why.starts_with("consumer ") => {
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, String> {
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(format!("consumer {} is neither a single match on a parameter nor a one-expression wrapper", c.key));
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".to_string()) };
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
- loadAndMerge, loadAndMergeWithOverrides, syntaxErrors, SyntaxError, SyntaxErrorKind,
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 loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
19
+ pub fn loadAndMergeDiag(entry: &Path, lib_path: &Path) -> Result<Source, Vec<Diagnostic>> {
19
- loadAndMergeWithOverrides(entry, lib_path, &HashMap::new())
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 loadAndMergeWithOverrides(
42
+ pub fn loadAndMergeWithOverridesDiag(
31
43
  entry: &Path,
32
44
  lib_path: &Path,
33
45
  overrides: &HashMap<PathBuf, String>,
34
- ) -> Result<Source, String> {
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::frontEnd(&mut merged)?;
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>) -> Result<(), String> {
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(format!(
105
+ return Err(vec![Diagnostic::new(
106
+ DiagKind::Resolution,
107
+ f.display().to_string(),
108
+ 0,
109
+ 0,
94
- "{}: primitive {} extends a declared type and is only allowed in std",
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) -> Result<PathBuf, String> {
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) -> Result<PathBuf, String> {
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
- ) -> Result<(), String> {
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) -> Result<(), String> {
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>) -> Result<Option<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) -> Result<(), String> {
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>) -> Result<String, 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) -> Result<Source, String> {
348
+ fn parseText(path: &Path, text: &str) -> Loaded_<Source> {
331
- let tree = treeOf(text).map_err(|e| format!("{} for '{}'", e, path.display()))?;
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(format!(
359
+ return Err(vec![Diagnostic::new(
342
- "parse error in '{}' at line {}, column {}: {}",
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
- .checkTopLevelOrder()
375
+ source.checkTopLevelOrder().map_err(|e| {
354
- .map_err(|e| format!("{} in '{}'", e, path.display()))?;
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
- ) -> Result<(), String> {
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::perFile(&mut hoisted)?;
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) -> Result<(), String> {
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(format!(
749
+ return Err(vec![Diagnostic::new(
727
- "{}:{}:{}: primitive {} declares a built-in type and is only allowed in std",
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 perFile(source: &mut Source) -> Result<(), String> {
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(legacy.join("\n"));
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(inlineErrors.join("\n"));
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 joined with "\n".
25
+ /// errors.
21
- pub fn frontEnd(source: &mut Source) -> Result<(), String> {
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(embedErrors.join("\n"));
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(ctorErrors.join("\n"));
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(typeErrors.join("\n"));
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(memberErrors.join("\n"));
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::perFile(&mut source);
89
+ let _ = crate::lower::perFileDiag(&mut source);
68
- let _ = crate::lower::frontEnd(&mut source);
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0)),
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0)),
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0))
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
- .unwrap_or(Expr::Int(0));
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).unwrap_or(Expr::Int(0));
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).unwrap_or(Expr::Int(0)),
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
- .unwrap_or(Expr::Int(0));
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
- .unwrap_or(Expr::Int(0));
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::loadAndMerge(&path, &root);
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 plum_wasm_codegen::compileSource;
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 = plum_checker::typedSource(&source).unwrap_or_else(|e| panic!("check: {e:?}"));
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!(plum_core::lower::perFile(&mut s), Ok(()));
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
- plum_core::lower::perFile(&mut s),
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!(plum_core::lower::perFile(&mut s).unwrap_err().contains("already declared"));
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!(plum_core::lower::perFile(&mut s), Ok(()));
110
+ assert_eq!(perFile(&mut s), Ok(()));
111
111
  assert_eq!(
112
- plum_core::lower::frontEnd(&mut s),
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!(plum_core::lower::frontEnd(&mut s).unwrap_err().contains("lists type Int as a member"));
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!(plum_core::lower::frontEnd(&mut s), Ok(()));
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::loadAndMerge(&entry, &lib)
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::loadAndMerge(f, root) {
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::loadAndMerge;
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 = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
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 = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
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 = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
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 = loadAndMerge(&entry, &lib_path).expect("import cycle should resolve, not error");
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 = loadAndMerge(&entry, &lib_path);
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 = loadAndMerge(&entry, &lib_path);
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 = loadAndMerge(&entry, &lib_path)
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 err = loadAndMerge(&entry, dir.path()).unwrap_err();
271
+ let errs = loadAndMergeDiag(&entry, dir.path()).unwrap_err();
267
- assert!(err.contains("parse error"), "{src}: {err}");
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
- loadAndMerge(&entry, dir.path()).unwrap_err()
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("at line 4, column 3: unexpected '()'"),
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("at line 2, column 12: unexpected '*'"),
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("at line 2, column 8: unclosed '('"), "{err}");
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("at line 3, column 3: unexpected 'y'"), "{err}");
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("at line 4, column 4: unclosed '('"), "{err}");
320
+ assert!(err.contains(":4:4: unclosed '('"), "{err}");
316
321
  let err = parseErrorOf("fun f() =\n x := [1, 2\n");
317
- assert!(err.contains("at line 2, column 8: unclosed '['"), "{err}");
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("at line 2, column 9: unclosed '('"), "{err}");
324
+ assert!(err.contains(":2:9: unclosed '('"), "{err}");
320
325
  let err = parseErrorOf("fun f() =\n x := { 1 + 2\n");
321
- assert!(err.contains("at line 2, column 8: unclosed '{'"), "{err}");
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("at line 2, column 11: unexpected end of file"),
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("at line 2, column 11: unclosed '('"), "{err}");
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("at line 4, column 7: unexpected end of file"),
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 = loadAndMerge(&entry, &lib_path).unwrap_err();
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 = loadAndMerge(&entry, &lib_path).unwrap();
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 = loadAndMerge(&entry, dir.path()).unwrap_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!(loadAndMerge(&ok, dir.path()).is_ok());
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!(loadAndMerge(&byte, dir.path()).is_ok());
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 = loadAndMerge(&entry, &lib_path).unwrap();
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 = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
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 = loadAndMerge(&entry, &lib_path).unwrap();
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 = loadAndMerge(&entry, &lib_path).unwrap_err();
510
+ let err = loadAndMergeDiag(&entry, &lib_path).unwrap_err();
506
- assert_eq!(
511
+ assert_eq!(err.len(), 1, "{err:?}");
507
- err,
508
- "enum User has a single variant named User — write struct User(...)"
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 = loadAndMerge(&entry, &lib_path).expect("nested std import loads");
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!(loadAndMerge(&missing, &lib_path).is_err());
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 = loadAndMerge(&entry, dir.path()).expect("loadAndMerge failed");
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::frontEnd;
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!(frontEnd(&mut s), Ok(()));
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 = frontEnd(&mut s).unwrap_err();
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::frontEnd;
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
- frontEnd(&mut s).unwrap();
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 plum_checker::FusionOptions;
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 { fusion: FusionOptions { enabled: fusion, explain: false }, ..Default::default() };
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).map(|o| o.wasm)
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
- plum_checker::typedSourceWith(source, FusionOptions::default())
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 &notes {
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::loadAndMerge(src_path, &root()).expect("load");
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 `plum_checker::checkSource`. `CheckError` carries a
6
+ // 2. Type errors — from `plum_driver::check`. A `plum_core::Diagnostic` carries a
7
- // `line`/`column` pointing at the enclosing `fn`/`enum`/`test`
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
- // covers that declaration's line rather than the whole document.
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::loadAndMergeWithOverrides`, passing the
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 `loadAndMergeWithOverrides`) rather than
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::loadAndMergeWithOverrides(&path, &self.libPath, &overrides).ok()
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::loadAndMergeWithOverrides(path, libPath, &overrides).ok()?;
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 plum_checker::checkSource(&ast) {
312
+ match plum_driver::check(&ast, &opts) {
314
- Ok(()) => Some(Vec::new()),
313
+ Ok(_) => Some(Vec::new()),
315
- Err(errors) => Some(
314
+ Err(diags) => Some(
316
- errors
315
+ diags
317
316
  .into_iter()
318
- .map(|e| {
317
+ .map(|d| {
319
- // `CheckError`'s position is 1-based and points at the
318
+ // `plum_core::Diagnostic` positions are 1-based (0 = unknown); LSP's
320
- // enclosing declaration, not a sub-expression — clamp
321
- // to the document and cover the whole line so an
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 itself is short/blank.
320
+ // its line, clamped to the document.
324
- let line = e.line.saturating_sub(1).min(lastLine as usize) as u32;
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, 0),
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: e.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::loadAndMergeWithOverrides(path, libPath, &overrides) else {
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::loadAndMergeWithOverrides(path, libPath, &overrides).ok()?;
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::loadAndMergeWithOverrides(path, libPath, &overrides).ok()?;
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::loadAndMerge(&path, &repoRoot()).expect("loads");
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-checker = { path = "../plum-checker" }
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::loadAndMerge(&path, &repoRoot());
63
+ let r = plum_core::loader::loadAndMergeDiag(&path, &repoRoot());
64
64
  let _ = std::fs::remove_dir_all(&dir);
65
- r.map_err(|e| format!("{e}"))
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
- .unwrap_or_else(|e| panic!("compile failed: {e}"));
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 + typedSource; the checker's messages (a load error is returned as one string).
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 plum_checker::typedSource(&source) {
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` once, returning the per-expression types codegen needs
1475
+ /// Type-checks `source` through the pass list (`plum_driver::check`), returning the
1475
- /// (`plum_checker::typedSource`) or the joined `type error: ...` lines.
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
- /// merged source, so imported-file errors are attributed to the entry file.
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
- ) -> Result<plum_checker::TypedInfo, String> {
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
- format!("type error: {}", e.message)
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 fusion: plum_checker::FusionOptions,
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, fusion: plum_checker::FusionOptions::default() }
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` (fusion per `opts.fusion`) and compiles it.
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, String> {
1511
+ pub fn compile(source: &ast::Source, opts: &CompileOptions) -> Result<Output, Vec<Diagnostic>> {
1542
- if opts.fusion.explain {
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 = typeCheckWith(&prog.source, "", opts.fusion)?;
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 = typeCheckWith(source, "", opts.fusion)?;
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::loadAndMerge(&path, &plum_test_support::repoRoot()).expect("load")
1556
+ plum_core::loadAndMergeDiag(&path, &plum_test_support::repoRoot()).expect("load")
1591
1557
  }
1592
1558
 
1593
1559
  #[test]
1594
- fn defaultMatchesCompileSource() {
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, compileSource, CompileOptions};
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() }).map(|o| (o.wasm, o.test_names))
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &repoRoot()).expect("load");
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::loadAndMerge(&path, &exportRepoRoot());
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::loadAndMerge(&path, &repoRoot()).expect("load");
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 = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
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
- fusion: plum_checker::FusionOptions { enabled: fusion, explain: false },
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: {e}"));
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 = plum_checker::typedSource(&source).expect("checks");
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 = plum_checker::typedSource(&source).expect("checks");
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 = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
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 = plum_wasm_codegen::compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
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 plum_wasm_codegen::compileSource;
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
- fusion: plum_checker::FusionOptions { enabled: fusion, explain: false },
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: {e}"))
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 plum_wasm_codegen::compileSource;
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::loadAndMerge(&path, &repoRoot())
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::loadAndMerge(&path, &repoRoot()).expect("1000-const file should load");
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::loadAndMerge(path, &repoRoot()).unwrap_or_else(|e| panic!("load: {e}"))
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
- fusion: plum_checker::FusionOptions { enabled: fusion, explain: false },
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: {e}"));
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 = plum_checker::typedSourceWith(
123
+ let info = plum_driver::check(
124
124
  source,
125
- plum_checker::FusionOptions { enabled: false, explain: false },
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
- plum_checker::fusionRewrite(&mut copy)
132
+ plum_driver::fusionNotes(&mut copy)
133
133
  }
134
134
 
135
135
  /// `struct.new` descriptions of every function reachable from `root` through direct calls,