plum

#treesitter#compiler#wasm

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

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


d2781a5Peter John 2026-07-19T16:18:06+05:30
docs: add CLI and formatter design spec
docs/superpowers/specs/2026-07-19-cli-formatter-design.md ADDED
@@ -0,0 +1,146 @@
1
+ # Plum CLI & Formatter Design
2
+
3
+ **Date:** 2026-07-19
4
+ **Status:** Approved
5
+
6
+ ## Overview
7
+
8
+ Add a `plum` CLI binary and a `format` command that formats `.plum` source files in place using Topiary (topiary-core v0.7.3) as the formatting engine. The project is restructured as a Cargo workspace.
9
+
10
+ ---
11
+
12
+ ## Workspace Structure
13
+
14
+ ```
15
+ plum/
16
+ ├── Cargo.toml ← workspace root
17
+ ├── plum-core/
18
+ │ ├── Cargo.toml
19
+ │ └── src/
20
+ │ ├── lib.rs ← public API surface
21
+ │ ├── ast.rs ← moved from current src/ast.rs
22
+ │ ├── parser.rs ← moved from current src/parser.rs
23
+ │ └── formatter.rs ← new: Topiary wrapper
24
+ ├── plum-cli/
25
+ │ ├── Cargo.toml
26
+ │ └── src/
27
+ │ └── main.rs ← clap CLI, file I/O only
28
+ └── tooling/
29
+ └── tree-sitter-plum/
30
+ └── queries/plum/
31
+ └── format.scm ← new: Topiary formatting rules
32
+ ```
33
+
34
+ The current `plum` crate (`src/ast.rs`, `src/parser.rs`, `src/main.rs`) is split: library code moves to `plum-core`, the entry point becomes `plum-cli`.
35
+
36
+ ---
37
+
38
+ ## plum-core
39
+
40
+ ### Public API
41
+
42
+ ```rust
43
+ // src/lib.rs
44
+ pub mod ast;
45
+ pub mod parser;
46
+ pub mod formatter;
47
+
48
+ pub use formatter::{format_source, FormatterError};
49
+ pub use parser::AstParser;
50
+ ```
51
+
52
+ ### formatter.rs
53
+
54
+ ```rust
55
+ pub fn format_source(source: &str) -> Result<String, FormatterError>
56
+ pub fn format_source_with_opts(source: &str, skip_idempotence: bool) -> Result<String, FormatterError>
57
+ ```
58
+
59
+ - Constructs a Topiary `Language` from:
60
+ - `tree_sitter_plum::LANGUAGE` (converted via `.into()` to `topiary_tree_sitter_facade::Language`)
61
+ - `format.scm` embedded at compile time via `include_str!`
62
+ - Calls `topiary_core::formatter_str`
63
+ - Returns the formatted source as a `String`
64
+
65
+ ### Dependencies (plum-core/Cargo.toml)
66
+
67
+ ```toml
68
+ [dependencies]
69
+ tree-sitter = "0.24.5"
70
+ tree-sitter-plum = "0.1.0"
71
+ topiary-core = "0.7.3"
72
+ topiary-tree-sitter-facade = "0.7.3"
73
+ ```
74
+
75
+ ---
76
+
77
+ ## plum-cli
78
+
79
+ ### Commands
80
+
81
+ ```
82
+ plum format <file> Format file in place (silent on success)
83
+ plum format --check <file> Exit 1 if file would change; print message to stderr
84
+ plum format --stdin Read from stdin, write to stdout
85
+ ```
86
+
87
+ ### Behaviour
88
+
89
+ - `format <file>`: reads file, calls `plum_core::format_source`, writes result back only if content changed (avoids touching mtime unnecessarily)
90
+ - `format --check <file>`: same read + format, but instead of writing, compares and exits 1 with a message if different
91
+ - `format --stdin`: reads all of stdin, formats, writes to stdout — useful for editor integrations
92
+ - All errors print to stderr; stdout is reserved for formatted source (`--stdin` mode only)
93
+
94
+ ### Dependencies (plum-cli/Cargo.toml)
95
+
96
+ ```toml
97
+ [dependencies]
98
+ plum-core = { path = "../plum-core" }
99
+ clap = { version = "4", features = ["derive"] }
100
+ anyhow = "1"
101
+ ```
102
+
103
+ ---
104
+
105
+ ## format.scm — Topiary Formatting Rules
106
+
107
+ Located at `tooling/tree-sitter-plum/queries/plum/format.scm`, embedded in `formatter.rs` via `include_str!`.
108
+
109
+ ### Rules
110
+
111
+ | Construct | Rule |
112
+ |---|---|
113
+ | Binary operators (`+`, `-`, `*`, `/`, `%`, `\|`, `&`, `^`, `<<`, `>>`, `..`) | space before and after |
114
+ | Comparison operators (`<`, `<=`, `==`, `!=`, `>=`, `>`, `<>`) | space before and after |
115
+ | Boolean operators (`&&`, `\|\|`) | space before and after |
116
+ | `->` (return type arrow) | space before and after |
117
+ | `=` (in fn/const/assign) | space before and after |
118
+ | `=>` (in match case / pair argument) | space before and after |
119
+ | `,` separator | no space before, one space after |
120
+ | `:` in params and fields | no space before, one space after |
121
+ | `\|` in enum variants | hardline before |
122
+ | Statements in a `body` block | hardline between each |
123
+ | Top-level items (`fn`, `type`, `enum`, `trait`, `const`) | blank line between each |
124
+ | Comments | preserved as-is; blank line allowed before |
125
+ | `(` `)` in argument lists | scoped softline (single-line if fits, multi-line if not) |
126
+
127
+ ### Idempotence
128
+
129
+ `skip_idempotence` defaults to `false` — Topiary runs formatting twice and errors if the result differs. This catches poorly-written query rules during development. The CLI exposes no flag for this; it is a library-level option.
130
+
131
+ ---
132
+
133
+ ## Error Handling
134
+
135
+ - Parse errors in the source: `tolerate_parsing_errors: false` — the formatter refuses to format files with syntax errors, printing the tree-sitter error to stderr.
136
+ - File not found / permission errors: `anyhow` propagates these with context.
137
+ - Formatter errors (bad query, non-idempotent output): printed to stderr, exit code 1.
138
+
139
+ ---
140
+
141
+ ## Out of Scope
142
+
143
+ - `plum check` / `plum build` / other compiler subcommands
144
+ - LSP integration
145
+ - Watch mode
146
+ - Formatting multiple files via glob patterns