plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-runtime/src/main.rs
| a271f34 | 1 | // Functions are named camelCase across this project (matching plum-cli, which |
| a271f34 | 2 | // mirrors this file's host-import logic), not Rust's idiomatic snake_case. |
| a271f34 | 3 | #![allow(non_snake_case)] |
| a271f34 | 4 | |
| a271f34 | 5 | // Embeds a Plum-compiled wasm module (path supplied via the PLUM_WASM_PATH |
| a271f34 | 6 | // env var at build time by `plum build`) and runs its `main` export under |
| a271f34 | 7 | // wasmtime, so the result is a single native executable with no runtime |
| a271f34 | 8 | // dependency on a `wasmtime` install or a separate .wasm file. |
| a271f34 | 9 | const WASM_BYTES: &[u8] = include_bytes!(env!("PLUM_WASM_PATH")); |
| a271f34 | 10 | |
| a271f34 | 11 | /// Builds the `Extern`s that satisfy a compiled Plum module's host imports (today, |
| a271f34 | 12 | /// only `plum::printLn`) in declaration order, matching each one's exact `FuncType` |
| a271f34 | 13 | /// (its `Str` param is a concrete wasm-gc array type, so the host function must be |
| a271f34 | 14 | /// built from the module's own reported type rather than a generic `Rooted<ArrayRef>` |
| a271f34 | 15 | /// wrapper, which would carry the wrong, unrelated top-array type and fail to |
| a271f34 | 16 | /// instantiate). |
| a271f34 | 17 | fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Vec<wasmtime::Extern> { |
| a271f34 | 18 | module.imports().map(|imp| { |
| a271f34 | 19 | match (imp.module(), imp.name()) { |
| a271f34 | 20 | ("plum", "printLn") => { |
| a271f34 | 21 | let func_ty = imp.ty().func().cloned().expect("plum::printLn import should be a function"); |
| a271f34 | 22 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, _results| { |
| a271f34 | 23 | let wasmtime::Val::AnyRef(Some(s)) = ¶ms[0] else { |
| a271f34 | 24 | return Err(wasmtime::Error::msg("printLn expects a Str argument")); |
| a271f34 | 25 | }; |
| 73e4ff1 | 26 | let arr = unwrapStrToArray(&mut caller, s)?; |
| a271f34 | 27 | let len = arr.len(&caller)?; |
| a271f34 | 28 | let mut buf = vec![0u8; len as usize]; |
| a271f34 | 29 | arr.copy_to_i8_slice(&mut caller, &mut buf)?; |
| a271f34 | 30 | println!("{}", String::from_utf8_lossy(&buf)); |
| a271f34 | 31 | Ok(()) |
| a271f34 | 32 | })) |
| a271f34 | 33 | } |
| a271f34 | 34 | ("plum", "rawRandomInt") => { |
| a271f34 | 35 | let func_ty = imp.ty().func().cloned().expect("plum::rawRandomInt import should be a function"); |
| a271f34 | 36 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| { |
| a271f34 | 37 | results[0] = wasmtime::Val::I64(randomI64()); |
| a271f34 | 38 | Ok(()) |
| a271f34 | 39 | })) |
| a271f34 | 40 | } |
| a271f34 | 41 | ("plum", "rawNowMillis") => { |
| a271f34 | 42 | let func_ty = imp.ty().func().cloned().expect("plum::rawNowMillis import should be a function"); |
| a271f34 | 43 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |_caller, _params, results| { |
| a271f34 | 44 | results[0] = wasmtime::Val::I64(nowMillis()); |
| a271f34 | 45 | Ok(()) |
| a271f34 | 46 | })) |
| a271f34 | 47 | } |
| 2ec05c8 | 48 | ("plum", "rawReadFile") => { |
| 2ec05c8 | 49 | let func_ty = imp.ty().func().cloned().expect("plum::rawReadFile import should be a function"); |
| 2ec05c8 | 50 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| { |
| 2ec05c8 | 51 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 52 | let bytes = std::fs::read(&path).unwrap_or_default(); |
| 2ec05c8 | 53 | results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, &bytes)?)); |
| 2ec05c8 | 54 | Ok(()) |
| 2ec05c8 | 55 | })) |
| 2ec05c8 | 56 | } |
| 2ec05c8 | 57 | ("plum", "rawWriteFile") => { |
| 2ec05c8 | 58 | let func_ty = imp.ty().func().cloned().expect("plum::rawWriteFile import should be a function"); |
| 2ec05c8 | 59 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 60 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 61 | let data = readStrArg(&mut caller, ¶ms[1])?; |
| 2ec05c8 | 62 | results[0] = wasmtime::Val::I64(std::fs::write(&path, &data).is_ok() as i64); |
| 2ec05c8 | 63 | Ok(()) |
| 2ec05c8 | 64 | })) |
| 2ec05c8 | 65 | } |
| 2ec05c8 | 66 | ("plum", "rawExists") => { |
| 2ec05c8 | 67 | let func_ty = imp.ty().func().cloned().expect("plum::rawExists import should be a function"); |
| 2ec05c8 | 68 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 69 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 70 | // See the identical `rawExists` in plum-cli for why this uses |
| 2ec05c8 | 71 | // `symlink_metadata` rather than `Path::exists`. |
| 2ec05c8 | 72 | results[0] = wasmtime::Val::I64(std::fs::symlink_metadata(&path).is_ok() as i64); |
| 2ec05c8 | 73 | Ok(()) |
| 2ec05c8 | 74 | })) |
| 2ec05c8 | 75 | } |
| 2ec05c8 | 76 | ("plum", "rawMkdir") => { |
| 2ec05c8 | 77 | let func_ty = imp.ty().func().cloned().expect("plum::rawMkdir import should be a function"); |
| 2ec05c8 | 78 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 79 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 80 | results[0] = wasmtime::Val::I64(std::fs::create_dir_all(&path).is_ok() as i64); |
| 2ec05c8 | 81 | Ok(()) |
| 2ec05c8 | 82 | })) |
| 2ec05c8 | 83 | } |
| 2ec05c8 | 84 | ("plum", "rawRemove") => { |
| 2ec05c8 | 85 | let func_ty = imp.ty().func().cloned().expect("plum::rawRemove import should be a function"); |
| 2ec05c8 | 86 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 87 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 88 | results[0] = wasmtime::Val::I64(std::fs::remove_file(&path).is_ok() as i64); |
| 2ec05c8 | 89 | Ok(()) |
| 2ec05c8 | 90 | })) |
| 2ec05c8 | 91 | } |
| 2ec05c8 | 92 | ("plum", "rawRemoveDir") => { |
| 2ec05c8 | 93 | let func_ty = imp.ty().func().cloned().expect("plum::rawRemoveDir import should be a function"); |
| 2ec05c8 | 94 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 95 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 96 | results[0] = wasmtime::Val::I64(std::fs::remove_dir(&path).is_ok() as i64); |
| 2ec05c8 | 97 | Ok(()) |
| 2ec05c8 | 98 | })) |
| 2ec05c8 | 99 | } |
| 2ec05c8 | 100 | ("plum", "rawRename") => { |
| 2ec05c8 | 101 | let func_ty = imp.ty().func().cloned().expect("plum::rawRename import should be a function"); |
| 2ec05c8 | 102 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 103 | let old_path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 104 | let new_path = readStrArg(&mut caller, ¶ms[1])?; |
| 2ec05c8 | 105 | results[0] = wasmtime::Val::I64(std::fs::rename(&old_path, &new_path).is_ok() as i64); |
| 2ec05c8 | 106 | Ok(()) |
| 2ec05c8 | 107 | })) |
| 2ec05c8 | 108 | } |
| 2ec05c8 | 109 | ("plum", "rawTruncate") => { |
| 2ec05c8 | 110 | let func_ty = imp.ty().func().cloned().expect("plum::rawTruncate import should be a function"); |
| 2ec05c8 | 111 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 112 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 113 | let wasmtime::Val::I64(len) = params[1] else { |
| 2ec05c8 | 114 | return Err(wasmtime::Error::msg("rawTruncate expects an Int length argument")); |
| 2ec05c8 | 115 | }; |
| 2ec05c8 | 116 | let ok = std::fs::OpenOptions::new().write(true).open(&path) |
| 2ec05c8 | 117 | .and_then(|f| f.set_len(len.max(0) as u64)).is_ok(); |
| 2ec05c8 | 118 | results[0] = wasmtime::Val::I64(ok as i64); |
| 2ec05c8 | 119 | Ok(()) |
| 2ec05c8 | 120 | })) |
| 2ec05c8 | 121 | } |
| 2ec05c8 | 122 | ("plum", "rawLink") => { |
| 2ec05c8 | 123 | let func_ty = imp.ty().func().cloned().expect("plum::rawLink import should be a function"); |
| 2ec05c8 | 124 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 125 | let existing_path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 126 | let new_path = readStrArg(&mut caller, ¶ms[1])?; |
| 2ec05c8 | 127 | results[0] = wasmtime::Val::I64(std::fs::hard_link(&existing_path, &new_path).is_ok() as i64); |
| 2ec05c8 | 128 | Ok(()) |
| 2ec05c8 | 129 | })) |
| 2ec05c8 | 130 | } |
| 2ec05c8 | 131 | ("plum", "rawSymlink") => { |
| 2ec05c8 | 132 | let func_ty = imp.ty().func().cloned().expect("plum::rawSymlink import should be a function"); |
| 2ec05c8 | 133 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 134 | let target = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 135 | let path = readStrArg(&mut caller, ¶ms[1])?; |
| 2ec05c8 | 136 | #[cfg(unix)] |
| 2ec05c8 | 137 | let ok = std::os::unix::fs::symlink(&target, &path).is_ok(); |
| 2ec05c8 | 138 | #[cfg(not(unix))] |
| 2ec05c8 | 139 | let ok = false; |
| 2ec05c8 | 140 | results[0] = wasmtime::Val::I64(ok as i64); |
| 2ec05c8 | 141 | Ok(()) |
| 2ec05c8 | 142 | })) |
| 2ec05c8 | 143 | } |
| 2ec05c8 | 144 | ("plum", "rawReadlink") => { |
| 2ec05c8 | 145 | let func_ty = imp.ty().func().cloned().expect("plum::rawReadlink import should be a function"); |
| 2ec05c8 | 146 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| { |
| 2ec05c8 | 147 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 148 | let target = std::fs::read_link(&path).ok() |
| 2ec05c8 | 149 | .map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); |
| 2ec05c8 | 150 | results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, target.as_bytes())?)); |
| 2ec05c8 | 151 | Ok(()) |
| 2ec05c8 | 152 | })) |
| 2ec05c8 | 153 | } |
| 2ec05c8 | 154 | ("plum", "rawRealpath") => { |
| 2ec05c8 | 155 | let func_ty = imp.ty().func().cloned().expect("plum::rawRealpath import should be a function"); |
| 2ec05c8 | 156 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| { |
| 2ec05c8 | 157 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 158 | let resolved = std::fs::canonicalize(&path).ok() |
| 2ec05c8 | 159 | .map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); |
| 2ec05c8 | 160 | results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, resolved.as_bytes())?)); |
| 2ec05c8 | 161 | Ok(()) |
| 2ec05c8 | 162 | })) |
| 2ec05c8 | 163 | } |
| 2ec05c8 | 164 | ("plum", "rawChmod") => { |
| 2ec05c8 | 165 | let func_ty = imp.ty().func().cloned().expect("plum::rawChmod import should be a function"); |
| 2ec05c8 | 166 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty, |mut caller, params, results| { |
| 2ec05c8 | 167 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 168 | let wasmtime::Val::I64(mode) = params[1] else { |
| 2ec05c8 | 169 | return Err(wasmtime::Error::msg("rawChmod expects an Int mode argument")); |
| 2ec05c8 | 170 | }; |
| 2ec05c8 | 171 | #[cfg(unix)] |
| 2ec05c8 | 172 | let ok = { |
| 2ec05c8 | 173 | use std::os::unix::fs::PermissionsExt; |
| 2ec05c8 | 174 | std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode as u32)).is_ok() |
| 2ec05c8 | 175 | }; |
| 2ec05c8 | 176 | #[cfg(not(unix))] |
| 2ec05c8 | 177 | let ok = false; |
| 2ec05c8 | 178 | results[0] = wasmtime::Val::I64(ok as i64); |
| 2ec05c8 | 179 | Ok(()) |
| 2ec05c8 | 180 | })) |
| 2ec05c8 | 181 | } |
| 2ec05c8 | 182 | ("plum", "rawReadDir") => { |
| 2ec05c8 | 183 | let func_ty = imp.ty().func().cloned().expect("plum::rawReadDir import should be a function"); |
| 2ec05c8 | 184 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| { |
| 2ec05c8 | 185 | let path = readStrArg(&mut caller, ¶ms[0])?; |
| 2ec05c8 | 186 | let joined = std::fs::read_dir(&path).ok().map(|entries| { |
| 2ec05c8 | 187 | entries.filter_map(|e| e.ok()) |
| 2ec05c8 | 188 | .map(|e| e.file_name().to_string_lossy().into_owned()) |
| 2ec05c8 | 189 | .collect::<Vec<_>>() |
| 2ec05c8 | 190 | .join("\n") |
| 2ec05c8 | 191 | }).unwrap_or_default(); |
| 2ec05c8 | 192 | results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, joined.as_bytes())?)); |
| 2ec05c8 | 193 | Ok(()) |
| 2ec05c8 | 194 | })) |
| 2ec05c8 | 195 | } |
| bf629a2 | 196 | ("plum", "rawHttpRequest") => { |
| bf629a2 | 197 | let func_ty = imp.ty().func().cloned().expect("plum::rawHttpRequest import should be a function"); |
| bf629a2 | 198 | wasmtime::Extern::Func(wasmtime::Func::new(&mut *store, func_ty.clone(), move |mut caller, params, results| { |
| bf629a2 | 199 | let method = readStrArg(&mut caller, ¶ms[0])?; |
| bf629a2 | 200 | let url = readStrArg(&mut caller, ¶ms[1])?; |
| bf629a2 | 201 | let headers = readStrArg(&mut caller, ¶ms[2])?; |
| bf629a2 | 202 | let body = readStrArg(&mut caller, ¶ms[3])?; |
| bf629a2 | 203 | let raw = performHttpRequest(&method, &url, &headers, &body); |
| bf629a2 | 204 | results[0] = wasmtime::Val::AnyRef(Some(makeStrResult(&mut caller, &func_ty, raw.as_bytes())?)); |
| bf629a2 | 205 | Ok(()) |
| bf629a2 | 206 | })) |
| bf629a2 | 207 | } |
| a271f34 | 208 | (m, n) => panic!("unsupported host import {m}::{n}"), |
| a271f34 | 209 | } |
| a271f34 | 210 | }).collect() |
| a271f34 | 211 | } |
| a271f34 | 212 | |
| bf629a2 | 213 | /// See the identical helper in `plum-cli/src/main.rs` — this crate is built |
| bf629a2 | 214 | /// fresh per `plum build` invocation, not linked against `plum-cli`, so the |
| bf629a2 | 215 | /// logic is duplicated rather than shared. |
| bf629a2 | 216 | fn performHttpRequest(method: &str, url: &str, headers: &str, body: &str) -> String { |
| bf629a2 | 217 | let agent = ureq::Agent::new(); |
| bf629a2 | 218 | let mut req = agent.request(method, url); |
| bf629a2 | 219 | for line in headers.split('\n') { |
| bf629a2 | 220 | if let Some((k, v)) = line.split_once(": ") { |
| bf629a2 | 221 | req = req.set(k, v); |
| bf629a2 | 222 | } |
| bf629a2 | 223 | } |
| bf629a2 | 224 | let result = if body.is_empty() { req.call() } else { req.send_string(body) }; |
| bf629a2 | 225 | match result { |
| bf629a2 | 226 | Ok(resp) | Err(ureq::Error::Status(_, resp)) => { |
| bf629a2 | 227 | let status = resp.status(); |
| bf629a2 | 228 | let mut header_lines = String::new(); |
| bf629a2 | 229 | for name in resp.headers_names() { |
| bf629a2 | 230 | if let Some(v) = resp.header(&name) { |
| bf629a2 | 231 | if !header_lines.is_empty() { |
| bf629a2 | 232 | header_lines.push('\n'); |
| bf629a2 | 233 | } |
| bf629a2 | 234 | header_lines.push_str(&format!("{name}: {v}")); |
| bf629a2 | 235 | } |
| bf629a2 | 236 | } |
| bf629a2 | 237 | let body_text = resp.into_string().unwrap_or_default(); |
| bf629a2 | 238 | format!("1\x01{status}\x01{header_lines}\x00{body_text}") |
| bf629a2 | 239 | } |
| bf629a2 | 240 | Err(e) => format!("0\x01\x01{e}\x00"), |
| bf629a2 | 241 | } |
| bf629a2 | 242 | } |
| bf629a2 | 243 | |
| a271f34 | 244 | /// See the identical helper in `plum-cli/src/main.rs` — this crate is built |
| a271f34 | 245 | /// fresh per `plum build` invocation, not linked against `plum-cli`, so the |
| a271f34 | 246 | /// logic is duplicated rather than shared. |
| a271f34 | 247 | fn randomI64() -> i64 { |
| a271f34 | 248 | let nanos = std::time::SystemTime::now() |
| a271f34 | 249 | .duration_since(std::time::UNIX_EPOCH) |
| a271f34 | 250 | .map(|d| d.as_nanos() as u64) |
| a271f34 | 251 | .unwrap_or(0); |
| a271f34 | 252 | let mut x = nanos ^ 0x9E3779B97F4A7C15; |
| a271f34 | 253 | x ^= x << 13; |
| a271f34 | 254 | x ^= x >> 7; |
| a271f34 | 255 | x ^= x << 17; |
| a271f34 | 256 | x as i64 |
| a271f34 | 257 | } |
| a271f34 | 258 | |
| 2ec05c8 | 259 | /// See the identical helpers in `plum-cli/src/main.rs` — this crate is built |
| 2ec05c8 | 260 | /// fresh per `plum build` invocation, not linked against `plum-cli`, so the |
| 2ec05c8 | 261 | /// logic is duplicated rather than shared. |
| 73e4ff1 | 262 | /// |
| 73e4ff1 | 263 | /// `Str` is a real two-level wasm-gc struct — `Str{data: Buffer{data: []Byte, |
| 73e4ff1 | 264 | /// len: Int}}` (see `libs/std/str.plum`'s header comment) — not a raw |
| 73e4ff1 | 265 | /// `array<i8>` directly. Unwraps a `Str` `AnyRef` down to that raw array. |
| 73e4ff1 | 266 | fn unwrapStrToArray( |
| 73e4ff1 | 267 | mut store: impl wasmtime::AsContextMut, |
| 73e4ff1 | 268 | s: &wasmtime::Rooted<wasmtime::AnyRef>, |
| 73e4ff1 | 269 | ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::ArrayRef>> { |
| 73e4ff1 | 270 | let str_struct = s.unwrap_struct(&store)?; |
| 73e4ff1 | 271 | let wasmtime::Val::AnyRef(Some(buffer_ref)) = str_struct.field(&mut store, 0)? else { |
| 73e4ff1 | 272 | return Err(wasmtime::Error::msg("Str.data is not a Buffer reference")); |
| 73e4ff1 | 273 | }; |
| 73e4ff1 | 274 | let buffer_struct = buffer_ref.unwrap_struct(&store)?; |
| 73e4ff1 | 275 | let wasmtime::Val::AnyRef(Some(bytes_ref)) = buffer_struct.field(&mut store, 0)? else { |
| 73e4ff1 | 276 | return Err(wasmtime::Error::msg("Buffer.data is not a []Byte reference")); |
| 73e4ff1 | 277 | }; |
| 73e4ff1 | 278 | bytes_ref.unwrap_array(&store) |
| 73e4ff1 | 279 | } |
| 73e4ff1 | 280 | |
| 2ec05c8 | 281 | fn readStrArg(caller: &mut wasmtime::Caller<'_, ()>, val: &wasmtime::Val) -> wasmtime::Result<String> { |
| 2ec05c8 | 282 | let wasmtime::Val::AnyRef(Some(s)) = val else { |
| 2ec05c8 | 283 | return Err(wasmtime::Error::msg("expected a Str argument")); |
| 2ec05c8 | 284 | }; |
| 73e4ff1 | 285 | let arr = unwrapStrToArray(&mut *caller, s)?; |
| 2ec05c8 | 286 | let len = arr.len(&caller)?; |
| 2ec05c8 | 287 | let mut buf = vec![0u8; len as usize]; |
| 2ec05c8 | 288 | arr.copy_to_i8_slice(caller, &mut buf)?; |
| 2ec05c8 | 289 | Ok(String::from_utf8_lossy(&buf).into_owned()) |
| 2ec05c8 | 290 | } |
| 2ec05c8 | 291 | |
| 73e4ff1 | 292 | /// Builds a new `Str` value from raw bytes — allocates the raw `array<i8>` |
| 73e4ff1 | 293 | /// (needs the import's own reported `FuncType` to find the concrete nested |
| 73e4ff1 | 294 | /// array type — a generic `ArrayRef` type would be the wrong, unrelated top |
| 73e4ff1 | 295 | /// array type), then wraps it in a fresh `Buffer` struct and a `Str` struct |
| 73e4ff1 | 296 | /// around that, mirroring `str.plum`'s real shape. |
| 2ec05c8 | 297 | fn makeStrResult( |
| 2ec05c8 | 298 | mut caller: impl wasmtime::AsContextMut, |
| 2ec05c8 | 299 | func_ty: &wasmtime::FuncType, |
| 2ec05c8 | 300 | bytes: &[u8], |
| 2ec05c8 | 301 | ) -> wasmtime::Result<wasmtime::Rooted<wasmtime::AnyRef>> { |
| 2ec05c8 | 302 | let result_ty = func_ty.results().next() |
| 2ec05c8 | 303 | .ok_or_else(|| wasmtime::Error::msg("expected a Str-returning function"))?; |
| 2ec05c8 | 304 | let wasmtime::ValType::Ref(ref_ty) = result_ty else { |
| 2ec05c8 | 305 | return Err(wasmtime::Error::msg("expected a Str (ref) return type")); |
| 2ec05c8 | 306 | }; |
| 73e4ff1 | 307 | let str_ty = ref_ty.heap_type().as_concrete_struct() |
| 73e4ff1 | 308 | .ok_or_else(|| wasmtime::Error::msg("expected a concrete struct return type"))? |
| 2ec05c8 | 309 | .clone(); |
| 73e4ff1 | 310 | let buffer_ty = str_ty.field(0) |
| 73e4ff1 | 311 | .and_then(|f| match f.element_type() { |
| 73e4ff1 | 312 | wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_struct().cloned(), |
| 73e4ff1 | 313 | _ => None, |
| 73e4ff1 | 314 | }) |
| 73e4ff1 | 315 | .ok_or_else(|| wasmtime::Error::msg("expected Str.data to be a concrete Buffer struct type"))?; |
| 73e4ff1 | 316 | let array_ty = buffer_ty.field(0) |
| 73e4ff1 | 317 | .and_then(|f| match f.element_type() { |
| 73e4ff1 | 318 | wasmtime::StorageType::ValType(wasmtime::ValType::Ref(rt)) => rt.heap_type().as_concrete_array().cloned(), |
| 73e4ff1 | 319 | _ => None, |
| 73e4ff1 | 320 | }) |
| 73e4ff1 | 321 | .ok_or_else(|| wasmtime::Error::msg("expected Buffer.data to be a concrete []Byte array type"))?; |
| 73e4ff1 | 322 | |
| 73e4ff1 | 323 | let array_pre = wasmtime::ArrayRefPre::new(&mut caller, array_ty); |
| 73e4ff1 | 324 | let arr = wasmtime::ArrayRef::new_from_i8_slice(&mut caller, &array_pre, bytes)?; |
| 73e4ff1 | 325 | |
| 73e4ff1 | 326 | let buffer_pre = wasmtime::StructRefPre::new(&mut caller, buffer_ty); |
| 73e4ff1 | 327 | let buffer = wasmtime::StructRef::new( |
| 73e4ff1 | 328 | &mut caller, |
| 73e4ff1 | 329 | &buffer_pre, |
| 73e4ff1 | 330 | &[wasmtime::Val::AnyRef(Some(arr.to_anyref())), wasmtime::Val::I64(bytes.len() as i64)], |
| 73e4ff1 | 331 | )?; |
| 73e4ff1 | 332 | |
| 73e4ff1 | 333 | let str_pre = wasmtime::StructRefPre::new(&mut caller, str_ty); |
| 73e4ff1 | 334 | let str_val = wasmtime::StructRef::new(&mut caller, &str_pre, &[wasmtime::Val::AnyRef(Some(buffer.to_anyref()))])?; |
| 73e4ff1 | 335 | Ok(str_val.to_anyref()) |
| 2ec05c8 | 336 | } |
| 2ec05c8 | 337 | |
| a271f34 | 338 | fn nowMillis() -> i64 { |
| a271f34 | 339 | std::time::SystemTime::now() |
| a271f34 | 340 | .duration_since(std::time::UNIX_EPOCH) |
| a271f34 | 341 | .map(|d| d.as_millis() as i64) |
| a271f34 | 342 | .unwrap_or(0) |
| a271f34 | 343 | } |
| a271f34 | 344 | |
| a271f34 | 345 | fn main() { |
| a271f34 | 346 | // GC + typed function references are required to load modules containing |
| a271f34 | 347 | // Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md). |
| a271f34 | 348 | let mut config = wasmtime::Config::new(); |
| a271f34 | 349 | config.wasm_gc(true); |
| a271f34 | 350 | config.wasm_function_references(true); |
| a271f34 | 351 | let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct"); |
| a271f34 | 352 | |
| a271f34 | 353 | let module = wasmtime::Module::new(&engine, WASM_BYTES).expect("embedded wasm should be valid"); |
| a271f34 | 354 | let mut store = wasmtime::Store::new(&engine, ()); |
| a271f34 | 355 | let imports = hostImports(&mut store, &module); |
| a271f34 | 356 | let instance = wasmtime::Instance::new(&mut store, &module, &imports).expect("module should instantiate"); |
| a271f34 | 357 | |
| a271f34 | 358 | // `main` returns `i64` unless it's declared `Unit` (no return value), in |
| a271f34 | 359 | // which case codegen gives it an empty wasm result type instead. |
| a271f34 | 360 | if let Ok(main) = instance.get_typed_func::<(), i64>(&mut store, "main") { |
| a271f34 | 361 | match main.call(&mut store, ()) { |
| a271f34 | 362 | Ok(result) => println!("{result}"), |
| a271f34 | 363 | Err(trap) => { |
| a271f34 | 364 | eprintln!("error: {trap}"); |
| a271f34 | 365 | std::process::exit(1); |
| a271f34 | 366 | } |
| a271f34 | 367 | } |
| a271f34 | 368 | return; |
| a271f34 | 369 | } |
| a271f34 | 370 | |
| a271f34 | 371 | let main = instance |
| a271f34 | 372 | .get_typed_func::<(), ()>(&mut store, "main") |
| a271f34 | 373 | .expect("module has no `main` export with signature () -> i64 or () -> ()"); |
| a271f34 | 374 | if let Err(trap) = main.call(&mut store, ()) { |
| a271f34 | 375 | eprintln!("error: {trap}"); |
| a271f34 | 376 | std::process::exit(1); |
| a271f34 | 377 | } |
| a271f34 | 378 | } |