rune_wasm/
lib.rs

1//! <img alt="rune logo" src="https://raw.githubusercontent.com/rune-rs/rune/main/assets/icon.png" />
2//! <br>
3//! <a href="https://github.com/rune-rs/rune"><img alt="github" src="https://img.shields.io/badge/github-rune--rs/rune-8da0cb?style=for-the-badge&logo=github" height="20"></a>
4//! <a href="https://crates.io/crates/rune-wasm"><img alt="crates.io" src="https://img.shields.io/crates/v/rune-wasm.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20"></a>
5//! <a href="https://docs.rs/rune-wasm"><img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-rune--wasm-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20"></a>
6//! <a href="https://discord.gg/v5AeNkT"><img alt="chat on discord" src="https://img.shields.io/discord/558644981137670144.svg?logo=discord&style=flat-square" height="20"></a>
7//! <br>
8//! Minimum support: Rust <b>1.81+</b>.
9//! <br>
10//! <br>
11//! <a href="https://rune-rs.github.io"><b>Visit the site 🌐</b></a>
12//! &mdash;
13//! <a href="https://rune-rs.github.io/book/"><b>Read the book 📖</b></a>
14//! <br>
15//! <br>
16//!
17//! A WASM module for the Rune Language, an embeddable dynamic programming language for Rust.
18//!
19//! <br>
20//!
21//! ## Usage
22//!
23//! This is part of the [Rune Language].
24//!
25//! [Rune Language]: https://rune-rs.github.io
26
27#![allow(clippy::collapsible_match)]
28#![allow(clippy::single_match)]
29#![allow(clippy::unused_unit)]
30
31use std::fmt;
32use std::sync::Arc;
33
34use anyhow::{Context as _, Result};
35use gloo_utils::format::JsValueSerdeExt;
36use rune::ast::Spanned;
37use rune::compile::LinkerError;
38use rune::diagnostics::{Diagnostic, FatalDiagnosticKind};
39use rune::modules::capture_io::CaptureIo;
40use rune::runtime::{budget, VmResult};
41use rune::{Context, ContextError, Options};
42use serde::{Deserialize, Serialize};
43use wasm_bindgen::prelude::*;
44
45mod http;
46mod time;
47
48#[derive(Default, Serialize)]
49struct WasmPosition {
50    line: u32,
51    character: u32,
52}
53
54impl From<(usize, usize)> for WasmPosition {
55    fn from((line, col): (usize, usize)) -> Self {
56        Self {
57            line: line as u32,
58            character: col as u32,
59        }
60    }
61}
62
63#[derive(Deserialize)]
64struct Config {
65    /// Budget.
66    #[serde(default)]
67    budget: Option<usize>,
68    /// Compiler options.
69    #[serde(default)]
70    options: Vec<String>,
71    /// Show instructions.
72    #[serde(default)]
73    instructions: bool,
74    /// Suppress text warnings.
75    #[serde(default)]
76    suppress_text_warnings: bool,
77}
78
79#[derive(Serialize)]
80enum WasmDiagnosticKind {
81    #[serde(rename = "error")]
82    Error,
83    #[serde(rename = "warning")]
84    Warning,
85}
86
87#[derive(Serialize)]
88struct WasmDiagnostic {
89    kind: WasmDiagnosticKind,
90    start: WasmPosition,
91    end: WasmPosition,
92    message: String,
93}
94
95#[derive(Serialize)]
96pub struct WasmCompileResult {
97    error: Option<String>,
98    diagnostics_output: Option<String>,
99    diagnostics: Vec<WasmDiagnostic>,
100    result: Option<String>,
101    output: Option<String>,
102    instructions: Option<String>,
103}
104
105impl WasmCompileResult {
106    /// Construct output from compile result.
107    fn output(
108        io: &CaptureIo,
109        result: String,
110        diagnostics_output: Option<String>,
111        diagnostics: Vec<WasmDiagnostic>,
112        instructions: Option<String>,
113    ) -> Self {
114        Self {
115            error: None,
116            diagnostics_output,
117            diagnostics,
118            result: Some(result),
119            output: io.drain_utf8().ok().map(|s| s.into_std()),
120            instructions,
121        }
122    }
123
124    /// Construct a result from an error.
125    fn from_error<E>(
126        io: &CaptureIo,
127        error: E,
128        diagnostics_output: Option<String>,
129        diagnostics: Vec<WasmDiagnostic>,
130        instructions: Option<String>,
131    ) -> Self
132    where
133        E: fmt::Display,
134    {
135        Self {
136            error: Some(error.to_string()),
137            diagnostics_output,
138            diagnostics,
139            result: None,
140            output: io.drain_utf8().ok().map(|s| s.into_std()),
141            instructions,
142        }
143    }
144}
145
146/// Setup a wasm-compatible context.
147fn setup_context(io: &CaptureIo) -> Result<Context, ContextError> {
148    let mut context = Context::with_config(false)?;
149
150    context.install(rune::modules::capture_io::module(io)?)?;
151    context.install(time::module()?)?;
152    context.install(http::module()?)?;
153    context.install(rune_modules::json::module(false)?)?;
154    context.install(rune_modules::toml::module(false)?)?;
155    context.install(rune_modules::toml::ser::module(false)?)?;
156    context.install(rune_modules::toml::de::module(false)?)?;
157    context.install(rune_modules::rand::module(false)?)?;
158
159    Ok(context)
160}
161
162async fn inner_compile(
163    input: String,
164    config: JsValue,
165    io: &CaptureIo,
166) -> Result<WasmCompileResult> {
167    let instructions = None;
168
169    let config: Config = JsValueSerdeExt::into_serde(&config)?;
170    let budget = config.budget.unwrap_or(1_000_000);
171
172    let source = rune::Source::new("entry", input)?;
173    let mut sources = rune::Sources::new();
174    sources.insert(source)?;
175
176    let context = setup_context(io)?;
177
178    let mut options = Options::from_default_env()?;
179
180    for option in &config.options {
181        options.parse_option(option)?;
182    }
183
184    let mut d = rune::Diagnostics::new();
185    let mut diagnostics = Vec::new();
186
187    let result = rune::prepare(&mut sources)
188        .with_context(&context)
189        .with_diagnostics(&mut d)
190        .with_options(&options)
191        .build();
192
193    for diagnostic in d.diagnostics() {
194        match diagnostic {
195            Diagnostic::Fatal(error) => {
196                if let Some(source) = sources.get(error.source_id()) {
197                    match error.kind() {
198                        FatalDiagnosticKind::CompileError(error) => {
199                            let span = error.span();
200
201                            let start = WasmPosition::from(
202                                source.pos_to_utf8_linecol(span.start.into_usize()),
203                            );
204                            let end = WasmPosition::from(
205                                source.pos_to_utf8_linecol(span.end.into_usize()),
206                            );
207
208                            diagnostics.push(WasmDiagnostic {
209                                kind: WasmDiagnosticKind::Error,
210                                start,
211                                end,
212                                message: error.to_string(),
213                            });
214                        }
215                        FatalDiagnosticKind::LinkError(error) => match error {
216                            LinkerError::MissingFunction { hash, spans } => {
217                                for (span, _) in spans {
218                                    let start = WasmPosition::from(
219                                        source.pos_to_utf8_linecol(span.start.into_usize()),
220                                    );
221                                    let end = WasmPosition::from(
222                                        source.pos_to_utf8_linecol(span.end.into_usize()),
223                                    );
224
225                                    diagnostics.push(WasmDiagnostic {
226                                        kind: WasmDiagnosticKind::Error,
227                                        start,
228                                        end,
229                                        message: format!("missing function (hash: {})", hash),
230                                    });
231                                }
232                            }
233                            _ => {}
234                        },
235                        _ => {}
236                    }
237                }
238            }
239            Diagnostic::Warning(warning) => {
240                let span = warning.span();
241
242                if let Some(source) = sources.get(warning.source_id()) {
243                    let start =
244                        WasmPosition::from(source.pos_to_utf8_linecol(span.start.into_usize()));
245                    let end = WasmPosition::from(source.pos_to_utf8_linecol(span.end.into_usize()));
246
247                    diagnostics.push(WasmDiagnostic {
248                        kind: WasmDiagnosticKind::Warning,
249                        start,
250                        end,
251                        message: warning.to_string(),
252                    });
253                }
254            }
255            _ => {}
256        }
257    }
258
259    let mut writer = rune::termcolor::Buffer::no_color();
260
261    if !config.suppress_text_warnings {
262        d.emit(&mut writer, &sources)
263            .context("Emitting to buffer should never fail")?;
264    }
265
266    let unit = match result {
267        Ok(unit) => Arc::new(unit),
268        Err(error) => {
269            return Ok(WasmCompileResult::from_error(
270                io,
271                error,
272                diagnostics_output(writer),
273                diagnostics,
274                instructions,
275            ));
276        }
277    };
278
279    let instructions = if config.instructions {
280        let mut out = rune::termcolor::Buffer::no_color();
281        unit.emit_instructions(&mut out, &sources, false)
282            .expect("dumping to string shouldn't fail");
283        Some(diagnostics_output(out).context("Converting instructions to UTF-8")?)
284    } else {
285        None
286    };
287
288    let mut vm = rune::Vm::new(Arc::new(context.runtime()?), unit);
289
290    let mut execution = match vm.execute(["main"], ()) {
291        Ok(execution) => execution,
292        Err(error) => {
293            error
294                .emit(&mut writer, &sources)
295                .context("Emitting to buffer should never fail")?;
296
297            return Ok(WasmCompileResult::from_error(
298                io,
299                error,
300                diagnostics_output(writer),
301                diagnostics,
302                instructions,
303            ));
304        }
305    };
306
307    let future = budget::with(budget, execution.async_complete());
308
309    let output = match future.await {
310        VmResult::Ok(output) => output,
311        VmResult::Err(error) => {
312            let vm = execution.vm();
313
314            let (unit, ip) = match error.first_location() {
315                Some(loc) => (&loc.unit, loc.ip),
316                None => (vm.unit(), vm.last_ip()),
317            };
318
319            // NB: emit diagnostics if debug info is available.
320            if let Some(debug) = unit.debug_info() {
321                if let Some(inst) = debug.instruction_at(ip) {
322                    if let Some(source) = sources.get(inst.source_id) {
323                        let start = WasmPosition::from(
324                            source.pos_to_utf8_linecol(inst.span.start.into_usize()),
325                        );
326
327                        let end = WasmPosition::from(
328                            source.pos_to_utf8_linecol(inst.span.end.into_usize()),
329                        );
330
331                        diagnostics.push(WasmDiagnostic {
332                            kind: WasmDiagnosticKind::Error,
333                            start,
334                            end,
335                            message: error.to_string(),
336                        });
337                    }
338                }
339            }
340
341            error
342                .emit(&mut writer, &sources)
343                .context("Emitting to buffer should never fail")?;
344
345            return Ok(WasmCompileResult::from_error(
346                io,
347                error,
348                diagnostics_output(writer),
349                diagnostics,
350                instructions,
351            ));
352        }
353    };
354
355    let result = vm.with(|| format!("{output:?}"));
356
357    Ok(WasmCompileResult::output(
358        io,
359        result,
360        diagnostics_output(writer),
361        diagnostics,
362        instructions,
363    ))
364}
365
366fn diagnostics_output(writer: rune::termcolor::Buffer) -> Option<String> {
367    let mut string = String::from_utf8(writer.into_inner()).ok()?;
368    let new_len = string.trim_end().len();
369    string.truncate(new_len);
370    Some(string)
371}
372
373#[wasm_bindgen]
374pub async fn compile(input: String, config: JsValue) -> JsValue {
375    let io = CaptureIo::new();
376
377    let result = match inner_compile(input, config, &io).await {
378        Ok(result) => result,
379        Err(error) => WasmCompileResult::from_error(&io, error, None, Vec::new(), None),
380    };
381
382    <JsValue as JsValueSerdeExt>::from_serde(&result).unwrap()
383}