plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-cli/src/main.rs
// Functions/methods are named camelCase across this project (matching plum's own
// naming convention), not Rust's idiomatic snake_case — silence the resulting lint.
#![allow(non_snake_case)]
use std::fs;
use std::io::{self, Read};
use std::process;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use plum_core::formatSource;
mod editor;
use editor::EditorType;
#[derive(Parser)]
#[command(name = "plum", about = "The Plum language toolchain")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Format a Plum source file
Format {
/// File to format (omit to use --stdin)
file: Option<std::path::PathBuf>,
/// Check if file is formatted; exit 1 if it would change
#[arg(long)]
check: bool,
/// Read from stdin and write formatted source to stdout
#[arg(long)]
stdin: bool,
},
/// Compile a Plum source file to WASM
Compile {
/// Source file to compile
file: std::path::PathBuf,
/// Output path (default: input with .wasm extension)
#[arg(short, long)]
output: Option<std::path::PathBuf>,
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
#[arg(long, default_value = "libs")]
lib_path: std::path::PathBuf,
},
/// Compile a Plum source file to WASM and immediately run it under wasmtime
Run {
/// Source file to compile and run
file: std::path::PathBuf,
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
#[arg(long, default_value = "libs")]
lib_path: std::path::PathBuf,
},
/// Compile a Plum source file to WASM and embed it into a wasmtime-backed
/// native executable
Build {
/// Source file to compile
file: std::path::PathBuf,
/// Output executable path (default: input file's name, no extension)
#[arg(short, long)]
output: Option<std::path::PathBuf>,
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
#[arg(long, default_value = "libs")]
lib_path: std::path::PathBuf,
},
/// Install Plum syntax highlighting into an editor's configuration
Editor {
/// Editor to install support for
editor: EditorType,
/// Install for VSCode Insiders instead of stable VSCode (ignored for helix)
#[arg(long)]
insiders: bool,
},
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e:#}");
process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Format { file, check, stdin } => cmdFormat(file, check, stdin),
Command::Compile { file, output, lib_path } => cmdCompile(file, output, lib_path),
Command::Run { file, lib_path } => cmdRun(file, lib_path),
Command::Build { file, output, lib_path } => cmdBuild(file, output, lib_path),
Command::Editor { editor, insiders } => editor::install(editor, insiders),
}
}
fn cmdFormat(
file: Option<std::path::PathBuf>,
check: bool,
use_stdin: bool,
) -> Result<()> {
if use_stdin && check {
anyhow::bail!("--check cannot be used with --stdin");
}
if use_stdin {
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.context("failed to read stdin")?;
let formatted = formatSource(&source).map_err(|e| anyhow::anyhow!("{e}"))?;
print!("{formatted}");
return Ok(());
}
let path = file.ok_or_else(|| anyhow::anyhow!("provide a file path or --stdin"))?;
let source =
fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
let formatted = formatSource(&source).map_err(|e| anyhow::anyhow!("{e}"))?;
if check {
if source != formatted {
eprintln!("{}: would reformat", path.display());
process::exit(1);
}
return Ok(());
}
if source != formatted {
fs::write(&path, formatted.as_bytes())
.with_context(|| format!("failed to write {}", path.display()))?;
}
Ok(())
}
fn compileToWasm(file: &std::path::Path, lib_path: &std::path::Path) -> Result<Vec<u8>> {
let ast = plum_core::loadAndMerge(file, lib_path)
.map_err(|e| anyhow::anyhow!("{e}"))?;
// Type check
if let Err(errors) = plum_checker::checkSource(&ast) {
for e in &errors {
eprintln!("type error: {}", e.message);
}
process::exit(1);
}
// Codegen
plum_wasm_codegen::compileSource(&ast).map_err(|e| anyhow::anyhow!("codegen error: {e}"))
}
fn cmdCompile(
file: std::path::PathBuf,
output: Option<std::path::PathBuf>,
lib_path: std::path::PathBuf,
) -> Result<()> {
let wasm_bytes = compileToWasm(&file, &lib_path)?;
let out_path = output.unwrap_or_else(|| file.with_extension("wasm"));
fs::write(&out_path, &wasm_bytes)
.with_context(|| format!("failed to write {}", out_path.display()))?;
eprintln!("compiled {} → {}", file.display(), out_path.display());
Ok(())
}
/// Builds the `Extern`s that satisfy a compiled Plum module's host imports (today,
/// only `plum::printLn`) in declaration order, matching each one's exact `FuncType`
/// (its `Str` param is a concrete wasm-gc array type, so the host function must be
/// built from the module's own reported type rather than a generic `Rooted<ArrayRef>`
/// wrapper, which would carry the wrong, unrelated top-array type and fail to
/// instantiate).
fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Result<Vec<wasmtime::Extern>> {
module.imports().map(|imp| {
match (imp.module(), imp.name()) {
("plum", "printLn") => {
let func_ty = imp.ty().func().cloned()
.ok_or_else(|| anyhow::anyhow!("plum::printLn import must be a function"))?;
Ok(wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, _results| {
let wasmtime::Val::AnyRef(Some(s)) = ¶ms[0] else {
return Err(wasmtime::Error::msg("printLn expects a Str argument"));
};
let arr = s.unwrap_array(&caller)?;
let len = arr.len(&caller)?;
let mut buf = vec![0u8; len as usize];
arr.copy_to_i8_slice(&mut caller, &mut buf)?;
println!("{}", String::from_utf8_lossy(&buf));
Ok(())
})))
}
(m, n) => anyhow::bail!("unsupported host import {m}::{n}"),
}
}).collect()
}
fn cmdRun(file: std::path::PathBuf, lib_path: std::path::PathBuf) -> Result<()> {
let wasm_bytes = compileToWasm(&file, &lib_path)?;
// GC + typed function references are required to load modules containing
// Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
let mut config = wasmtime::Config::new();
config.wasm_gc(true);
config.wasm_function_references(true);
let engine = wasmtime::Engine::new(&config)
.map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
let module = wasmtime::Module::new(&engine, &wasm_bytes)
.map_err(|e| anyhow::anyhow!("compiled wasm failed to load: {e}"))?;
let mut store = wasmtime::Store::new(&engine, ());
let imports = hostImports(&mut store, &module)?;
let instance = wasmtime::Instance::new(&mut store, &module, &imports)
.map_err(|e| anyhow::anyhow!("module failed to instantiate: {e}"))?;
// `main` returns `i64` unless it's declared `Unit` (no return value), in
// which case codegen gives it an empty wasm result type instead.
if let Ok(main) = instance.get_typed_func::<(), i64>(&mut store, "main") {
match main.call(&mut store, ()) {
Ok(result) => println!("{result}"),
Err(trap) => {
eprintln!("error: {trap}");
process::exit(1);
}
}
return Ok(());
}
let main = instance
.get_typed_func::<(), ()>(&mut store, "main")
.map_err(|_| anyhow::anyhow!("module has no `main` export with signature () -> i64 or () -> ()"))?;
if let Err(trap) = main.call(&mut store, ()) {
eprintln!("error: {trap}");
process::exit(1);
}
Ok(())
}
/// Path to the `plum-runtime` crate, resolved relative to `plum-cli`'s own
/// manifest directory (baked in at compile time) rather than the current
/// working directory, so `plum build` works no matter where it's invoked from.
fn runtimeCrateDir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("plum-cli should live inside the workspace root")
.join("plum-runtime")
}
fn cmdBuild(
file: std::path::PathBuf,
output: Option<std::path::PathBuf>,
lib_path: std::path::PathBuf,
) -> Result<()> {
let wasm_bytes = compileToWasm(&file, &lib_path)?;
let stem = file
.file_stem()
.ok_or_else(|| anyhow::anyhow!("invalid source file path {}", file.display()))?;
let wasm_path = std::env::temp_dir().join(format!(
"plum-build-{}-{}.wasm",
stem.to_string_lossy(),
process::id()
));
fs::write(&wasm_path, &wasm_bytes)
.with_context(|| format!("failed to write {}", wasm_path.display()))?;
let wasm_abs_path = fs::canonicalize(&wasm_path)
.with_context(|| format!("failed to resolve {}", wasm_path.display()))?;
let runtime_dir = runtimeCrateDir();
eprintln!("building executable (embedding wasm via wasmtime)...");
let status = process::Command::new("cargo")
.args(["build", "--release", "--manifest-path"])
.arg(runtime_dir.join("Cargo.toml"))
.env("PLUM_WASM_PATH", &wasm_abs_path)
.status()
.context("failed to run cargo — is it installed and on PATH?")?;
let _ = fs::remove_file(&wasm_path);
if !status.success() {
anyhow::bail!("cargo build failed");
}
let built_bin = runtime_dir.join("target/release/plum-runtime");
let out_path = output.unwrap_or_else(|| std::path::PathBuf::from(stem));
fs::copy(&built_bin, &out_path).with_context(|| {
format!("failed to copy {} to {}", built_bin.display(), out_path.display())
})?;
eprintln!("built {} → {}", file.display(), out_path.display());
Ok(())
}