1#![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 #[serde(default)]
67 budget: Option<usize>,
68 #[serde(default)]
70 options: Vec<String>,
71 #[serde(default)]
73 instructions: bool,
74 #[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 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 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
146fn 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 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}