plum

#treesitter#compiler#wasm

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

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


b5f5be1Peter John 2026-09-08T15:03:28+05:30
feat(cli): fix stale Helix grammar path and embed a precompiled grammar .so
Files changed (3) hide show
  1. plum-cli/Cargo.toml +1 -0
  2. plum-cli/build.rs +58 -0
  3. plum-cli/src/editor.rs +91 -3
plum-cli/Cargo.toml CHANGED
@@ -2,6 +2,7 @@
2
2
  name = "plum-cli"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
+ build = "build.rs"
5
6
 
6
7
  [[bin]]
7
8
  name = "plum"
plum-cli/build.rs ADDED
@@ -0,0 +1,58 @@
1
+ //! Precompiles the `tree-sitter-plum` grammar into a native shared library at
2
+ //! `plum`-build time (using whatever C compiler is already on the machine —
3
+ //! the same one Cargo itself needs to build `tree-sitter-plum`'s own
4
+ //! `scanner.c`/`parser.c` for in-process parsing), so `plum editor helix` can
5
+ //! just copy the finished `.so` into Helix's `runtime/grammars/` directory
6
+ //! instead of shelling out to `hx --grammar build` (which requires `hx` on
7
+ //! PATH, a working C compiler on the *installing* machine again, and a
8
+ //! `languages.toml` grammar `source.path` that still resolves).
9
+ //!
10
+ //! If the compiler invocation fails (no C compiler found), this falls back to
11
+ //! writing an empty placeholder rather than failing the whole `plum-cli`
12
+ //! build — `editor::installHelix` detects the empty payload and falls back to
13
+ //! the old `hx --grammar build` codepath at install time instead.
14
+
15
+ use std::env;
16
+ use std::path::PathBuf;
17
+ use std::process::Command;
18
+
19
+ fn main() {
20
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
21
+ let src_dir = manifest_dir.join("../plum-tooling/tree-sitter-plum/src");
22
+ let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
23
+ let so_path = out_dir.join("plum-grammar.so");
24
+
25
+ println!("cargo:rerun-if-changed={}", src_dir.join("parser.c").display());
26
+ println!("cargo:rerun-if-changed={}", src_dir.join("scanner.c").display());
27
+
28
+ let cc = env::var("CC").unwrap_or_else(|_| "cc".to_string());
29
+ let status = Command::new(&cc)
30
+ .arg("-shared")
31
+ .arg("-fPIC")
32
+ .arg("-O2")
33
+ .arg("-I")
34
+ .arg(&src_dir)
35
+ .arg("-o")
36
+ .arg(&so_path)
37
+ .arg(src_dir.join("parser.c"))
38
+ .arg(src_dir.join("scanner.c"))
39
+ .status();
40
+
41
+ match status {
42
+ Ok(s) if s.success() => {
43
+ println!("cargo:rustc-env=PLUM_GRAMMAR_SO={}", so_path.display());
44
+ }
45
+ other => {
46
+ std::fs::write(&so_path, []).expect("failed to write placeholder grammar .so");
47
+ println!("cargo:rustc-env=PLUM_GRAMMAR_SO={}", so_path.display());
48
+ match other {
49
+ Ok(s) => println!(
50
+ "cargo:warning=`{cc}` exited with {s} while precompiling the Plum tree-sitter grammar; `plum editor helix` will fall back to `hx --grammar build`"
51
+ ),
52
+ Err(e) => println!(
53
+ "cargo:warning=could not run `{cc}` to precompile the Plum tree-sitter grammar ({e}); `plum editor helix` will fall back to `hx --grammar build`"
54
+ ),
55
+ }
56
+ }
57
+ }
58
+ }
plum-cli/src/editor.rs CHANGED
@@ -44,6 +44,12 @@ const HELIX_TAGS: &str =
44
44
  const HELIX_TEXTOBJECTS: &str =
45
45
  include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm");
46
46
 
47
+ /// Precompiled by `build.rs` from the same `parser.c`/`scanner.c` that back
48
+ /// in-process parsing, on whatever machine builds this `plum` binary. Empty
49
+ /// when `build.rs` couldn't find a C compiler — see `buildGrammar` for the
50
+ /// fallback used in that case.
51
+ const HELIX_GRAMMAR_SO: &[u8] = include_bytes!(env!("PLUM_GRAMMAR_SO"));
52
+
47
53
  /// The absolute path to the `tree-sitter-plum` grammar crate on the machine that
48
54
  /// *built* this `plum` binary. Baked in at compile time so a contributor running
49
55
  /// `cargo run -p plum-cli -- editor helix` from their own checkout gets a
@@ -109,8 +115,20 @@ fn installHelix() -> Result<()> {
109
115
  // into a shared library under runtime/grammars/. `hx --health <lang>`
110
116
  // reports the tree-sitter parser as present just because the config is
111
117
  // valid, even when that .so has never been built, which makes a missing
112
- // build step easy to miss. Build it now instead of just telling the user to.
118
+ // build step easy to miss. We already have one precompiled (by our own
119
+ // `build.rs`, on this same machine) embedded in the binary, so just drop
120
+ // it in directly instead of shelling out to `hx --grammar build`.
121
+ if HELIX_GRAMMAR_SO.is_empty() {
113
- buildGrammar();
122
+ buildGrammar();
123
+ } else {
124
+ let grammars_dir = config_dir.join("runtime").join("grammars");
125
+ fs::create_dir_all(&grammars_dir)
126
+ .with_context(|| format!("failed to create {}", grammars_dir.display()))?;
127
+ let so_path = grammars_dir.join("plum.so");
128
+ fs::write(&so_path, HELIX_GRAMMAR_SO)
129
+ .with_context(|| format!("failed to write {}", so_path.display()))?;
130
+ println!("installed precompiled Plum tree-sitter grammar to {}", so_path.display());
131
+ }
114
132
  Ok(())
115
133
  }
116
134
 
@@ -169,7 +187,41 @@ fn mergeLanguagesToml(path: &Path) -> Result<()> {
169
187
  .unwrap_or(false);
170
188
 
171
189
  if already_present {
190
+ let current_path = treeSitterPlumDir();
191
+ let stale_path = doc
192
+ .get_mut("grammar")
193
+ .and_then(|v| v.as_array_of_tables_mut())
194
+ .and_then(|arr| arr.iter_mut().find(|t| t.get("name").and_then(|n| n.as_str()) == Some("plum")))
195
+ .and_then(|grammar| {
196
+ let existing_path = grammar
197
+ .get("source")
198
+ .and_then(|s| s.as_inline_table())
199
+ .and_then(|s| s.get("path"))
200
+ .and_then(|p| p.as_str())
201
+ .map(|s| s.to_string());
202
+ if existing_path.as_deref() != Some(current_path.as_str()) {
203
+ let mut source = toml_edit::InlineTable::new();
204
+ source.insert("path", current_path.clone().into());
205
+ grammar["source"] = toml_edit::Item::Value(toml_edit::Value::InlineTable(source));
206
+ existing_path
207
+ } else {
208
+ None
209
+ }
210
+ });
211
+
212
+ match stale_path {
213
+ Some(old) => {
214
+ fs::write(path, doc.to_string())
215
+ .with_context(|| format!("failed to write {}", path.display()))?;
216
+ println!(
217
+ "{} already has a `plum` language entry; updated its stale grammar path ({old} -> {current_path})",
218
+ path.display()
219
+ );
220
+ }
221
+ None => {
172
- println!("{} already has a `plum` language entry, leaving it as-is", path.display());
222
+ println!("{} already has a `plum` language entry, leaving it as-is", path.display());
223
+ }
224
+ }
173
225
  return Ok(());
174
226
  }
175
227
 
@@ -265,4 +317,40 @@ mod tests {
265
317
 
266
318
  std::fs::remove_dir_all(&dir).ok();
267
319
  }
320
+
321
+ #[test]
322
+ fn mergeLanguagesTomlFixesStaleGrammarPath() {
323
+ let dir = std::env::temp_dir().join(format!("plum-editor-test-stale-{}", std::process::id()));
324
+ std::fs::create_dir_all(&dir).unwrap();
325
+ let path = dir.join("languages.toml");
326
+
327
+ std::fs::write(
328
+ &path,
329
+ r##"[[language]]
330
+ name = "plum"
331
+ scope = "source.plum"
332
+ injection-regex = "plum"
333
+ file-types = ["plum"]
334
+ comment-tokens = "#"
335
+ indent = { tab-width = 2, unit = " " }
336
+
337
+ [[grammar]]
338
+ name = "plum"
339
+ source = { path = "/some/stale/path/tree-sitter-plum" }
340
+ "##,
341
+ )
342
+ .unwrap();
343
+
344
+ mergeLanguagesToml(&path).unwrap();
345
+ let updated = std::fs::read_to_string(&path).unwrap();
346
+ assert!(!updated.contains("/some/stale/path/tree-sitter-plum"));
347
+ assert!(updated.contains(&treeSitterPlumDir()));
348
+
349
+ // running again should now be a stable no-op
350
+ mergeLanguagesToml(&path).unwrap();
351
+ let second = std::fs::read_to_string(&path).unwrap();
352
+ assert_eq!(updated, second, "re-running after the fix should be a no-op");
353
+
354
+ std::fs::remove_dir_all(&dir).ok();
355
+ }
268
356
  }