plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-runtime/src/main.rs
// Functions are named camelCase across this project (matching plum-cli, which
// mirrors this file's host-import logic), not Rust's idiomatic snake_case.
#![allow(non_snake_case)]
// Embeds a Plum-compiled wasm module (path supplied via the PLUM_WASM_PATH
// env var at build time by `plum build`) and runs its `main` export under
// wasmtime, so the result is a single native executable with no runtime
// dependency on a `wasmtime` install or a separate .wasm file.
const WASM_BYTES: &[u8] = include_bytes!(env!("PLUM_WASM_PATH"));
/// 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) -> Vec<wasmtime::Extern> {
module.imports().map(|imp| {
match (imp.module(), imp.name()) {
("plum", "printLn") => {
let func_ty = imp.ty().func().cloned().expect("plum::printLn import should be a function");
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 = unwrapStrToArray(&mut caller, s)?;
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(())
}))
}
("plum", "rawRandomInt") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawRandomInt import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
results[0] = wasmtime::Val::I64(randomI64());
Ok(())
}))
}
("plum", "rawNowMillis") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawNowMillis import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| {
results[0] = wasmtime::Val::I64(nowMillis());
Ok(())
}))
}
("plum", "rawReadFile") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawReadFile import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let bytes = std::fs::read(&path).unwrap_or_default();
results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, &bytes)?));
Ok(())
}))
}
("plum", "rawWriteFile") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawWriteFile import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let data = readStrArg(&mut caller, ¶ms[1])?;
results[0] = wasmtime::Val::I64(std::fs::write(&path, &data).is_ok() as i64);
Ok(())
}))
}
("plum", "rawExists") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawExists import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
// See the identical `rawExists` in plum-cli for why this uses
// `symlink_metadata` rather than `Path::exists`.
results[0] = wasmtime::Val::I64(std::fs::symlink_metadata(&path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawMkdir") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawMkdir import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
results[0] = wasmtime::Val::I64(std::fs::create_dir_all(&path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawRemove") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawRemove import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
results[0] = wasmtime::Val::I64(std::fs::remove_file(&path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawRemoveDir") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawRemoveDir import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
results[0] = wasmtime::Val::I64(std::fs::remove_dir(&path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawRename") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawRename import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let old_path = readStrArg(&mut caller, ¶ms[0])?;
let new_path = readStrArg(&mut caller, ¶ms[1])?;
results[0] = wasmtime::Val::I64(std::fs::rename(&old_path, &new_path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawTruncate") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawTruncate import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let wasmtime::Val::I64(len) = params[1] else {
return Err(wasmtime::Error::msg("rawTruncate expects an Int length argument"));
};
let ok = std::fs::OpenOptions::new().write(true).open(&path)
.and_then(|f| f.set_len(len.max(0) as u64)).is_ok();
results[0] = wasmtime::Val::I64(ok as i64);
Ok(())
}))
}
("plum", "rawLink") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawLink import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let existing_path = readStrArg(&mut caller, ¶ms[0])?;
let new_path = readStrArg(&mut caller, ¶ms[1])?;
results[0] = wasmtime::Val::I64(std::fs::hard_link(&existing_path, &new_path).is_ok() as i64);
Ok(())
}))
}
("plum", "rawSymlink") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawSymlink import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let target = readStrArg(&mut caller, ¶ms[0])?;
let path = readStrArg(&mut caller, ¶ms[1])?;
#[cfg(unix)]
let ok = std::os::unix::fs::symlink(&target, &path).is_ok();
#[cfg(not(unix))]
let ok = false;
results[0] = wasmtime::Val::I64(ok as i64);
Ok(())
}))
}
("plum", "rawReadlink") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawReadlink import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let target = std::fs::read_link(&path).ok()
.map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, target.as_bytes())?));
Ok(())
}))
}
("plum", "rawRealpath") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawRealpath import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let resolved = std::fs::canonicalize(&path).ok()
.map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, resolved.as_bytes())?));
Ok(())
}))
}
("plum", "rawChmod") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawChmod import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let wasmtime::Val::I64(mode) = params[1] else {
return Err(wasmtime::Error::msg("rawChmod expects an Int mode argument"));
};
#[cfg(unix)]
let ok = {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode as u32)).is_ok()
};
#[cfg(not(unix))]
let ok = false;
results[0] = wasmtime::Val::I64(ok as i64);
Ok(())
}))
}
("plum", "rawReadDir") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawReadDir import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
let path = readStrArg(&mut caller, ¶ms[0])?;
let joined = std::fs::read_dir(&path).ok().map(|entries| {
entries.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("\n")
}).unwrap_or_default();
results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, joined.as_bytes())?));
Ok(())
}))
}
("plum", "rawHttpRequest") => {
let func_ty = imp.ty().func().cloned().expect("plum::rawHttpRequest import should be a function");
wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| {
let method = readStrArg(&mut caller, ¶ms[0])?;
let url = readStrArg(&mut caller, ¶ms[1])?;
let headers = readStrArg(&mut caller, ¶ms[2])?;
let body = readStrArg(&mut caller, ¶ms[3])?;
let raw = performHttpRequest(&method, &url, &headers, &body);
results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, raw.as_bytes())?));
Ok(())
}))
}
(m, n) => panic!("unsupported host import {m}::{n}"),
}
}).collect()
}
/// See the identical helper in `plum-cli/src/main.rs` — this crate is built
/// fresh per `plum build` invocation, not linked against `plum-cli`, so the
/// logic is duplicated rather than shared.
fn performHttpRequest(method: &str, url: &str, headers: &str, body: &str) -> String {
let agent = ureq::Agent::new();
let mut req = agent.request(method, url);
for line in headers.split('\n') {
if let Some((k, v)) = line.split_once(": ") {
req = req.set(k, v);
}
}
let result = if body.is_empty() { req.call() } else { req.send_string(body) };
match result {
Ok(resp) | Err(ureq::Error::Status(_, resp)) => {
let status = resp.status();
let mut header_lines = String::new();
for name in resp.headers_names() {
if let Some(v) = resp.header(&name) {
if !header_lines.is_empty() {
header_lines.push('\n');
}
header_lines.push_str(&format!("{name}: {v}"));
}
}
let body_text = resp.into_string().unwrap_or_default();
format!("1\x01{status}\x01{header_lines}\x00{body_text}")
}
Err(e) => format!("0\x01\x01{e}\x00"),
}
}
/// See the identical helper in `plum-cli/src/main.rs` — this crate is built
/// fresh per `plum build` invocation, not linked against `plum-cli`, so the
/// logic is duplicated rather than shared.
fn randomI64() -> i64 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let mut x = nanos ^ 0x9E3779B97F4A7C15;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x as i64
}
/// See the identical helpers in `plum-cli/src/main.rs` — this crate is built
/// fresh per `plum build` invocation, not linked against `plum-cli`, so the
/// logic is duplicated rather than shared.
///
/// `Str` is a real two-level wasm-gc struct — `Str{data: Buffer{data: []Byte,
/// len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw
/// `array<i8>` directly. Unwraps a `Str` `AnyRef` down to that raw array.
fn unwrapStrToArray(
mut store: impl wasmtime::AsContextMut,
s: &wasmtime::Rooted<wasmtime::AnyRef>,
) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> {
let str_struct = s.unwrap_struct(&store)?;
let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else {
return Err(wasmtime::Error::msg("Str.data is not a Buffer reference"));
};
let buffer_struct = buffer_ref.unwrap_struct(&store)?;
let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else {
return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference"));
};
bytes_ref.unwrap_array(&store)
}
fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> wasmtime::Result<String> {
let wasmtime::Val::AnyRef(Some(s)) = val else {
return Err(wasmtime::Error::msg("expected a Str argument"));
};
let arr = unwrapStrToArray(&mut *caller, s)?;
let len = arr.len(&caller)?;
let mut buf = vec![0u8; len as usize];
arr.copy_to_i8_slice(caller, &mut buf)?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
/// Builds a new `Str` value from raw bytes — allocates the raw `array<i8>`
/// (needs the import's own reported `FuncType` to find the concrete nested
/// array type — a generic `ArrayRef` type would be the wrong, unrelated top
/// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct
/// around that, mirroring `str.plum`'s real shape.
fn makeStrResult(
mut caller: impl wasmtime::AsContextMut,
func_ty: &wasmtime::FuncType,
bytes: &[u8],
) -> wasmtime::Result<wasmtime::Rooted<wasmtime::AnyRef>> {
let result_ty = func_ty.results().next()
.ok_or_else(|| wasmtime::Error::msg("expected a Str-returning function"))?;
let wasmtime::ValType::Ref(ref_ty) = result_ty else {
return Err(wasmtime::Error::msg("expected a Str (ref) return type"));
};
let str_ty = ref_ty.heap_type().as_concrete_struct()
.ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))?
.clone();
let buffer_ty = str_ty.field(0)
.and_then(|f| match f.element_type() {
wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(),
_ => None,
})
.ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?;
let array_ty = buffer_ty.field(0)
.and_then(|f| match f.element_type() {
wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(),
_ => None,
})
.ok_or_else(|| wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type"))?;
let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty);
let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?;
let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty);
let buffer = wasmtime::StructRef::new(
&mut caller,
&buffer_pre,
&[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)],
)?;
let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty);
let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?;
Ok(str_val.to_anyref())
}
fn nowMillis() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
fn main() {
// 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).expect("engine with GC config should construct");
let module = wasmtime::Module::new(&engine, WASM_BYTES).expect("embedded wasm should be valid");
let mut store = wasmtime::Store::new(&engine, ());
let imports = hostImports(&mut store, &module);
let instance = wasmtime::Instance::new(&mut store, &module, &imports).expect("module should instantiate");
// `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}");
std::process::exit(1);
}
}
return;
}
let main = instance
.get_typed_func::<(), ()>(&mut store, "main")
.expect("module has no `main` export with signature () -> i64 or () -> ()");
if let Err(trap) = main.call(&mut store, ()) {
eprintln!("error: {trap}");
std::process::exit(1);
}
}