plum

#treesitter#compiler#wasm

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)) = &params[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, &params[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, &params[0])?;
                    let data = readStrArg(&mut caller, &params[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, &params[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, &params[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, &params[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, &params[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, &params[0])?;
                    let new_path = readStrArg(&mut caller, &params[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, &params[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, &params[0])?;
                    let new_path = readStrArg(&mut caller, &params[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, &params[0])?;
                    let path = readStrArg(&mut caller, &params[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, &params[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, &params[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, &params[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, &params[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, &params[0])?;
                    let url = readStrArg(&mut caller, &params[1])?;
                    let headers = readStrArg(&mut caller, &params[2])?;
                    let body = readStrArg(&mut caller, &params[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);
    }
}