char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somadd progress to module imports from registries7b01146

main
25.8 KiB704 linesraw
1use std::future::Future;
2use std::io::{self, BufRead, IsTerminal, Write};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::mpsc::{self, Receiver};
6use std::task::Poll;
7use std::thread;
8use std::time::Duration;
9
10use avril_core::{Error, Runtime};
11use futures::StreamExt;
12use futures::channel::mpsc::{self as async_mpsc, UnboundedReceiver, UnboundedSender};
13use futures::future::poll_fn;
14use oxc::allocator::Allocator;
15use oxc::ast::ast::{Expression, ImportAttributeKey, ImportDeclarationSpecifier, Statement};
16use oxc::parser::Parser;
17use oxc::span::{GetSpan, SourceType};
18use reedline::Signal;
19use serde_json::{Value, json};
20use url::Url;
21
22mod line_editor;
23
24enum Input {
25    Line(String),
26    Eof,
27    Cancel,
28    Interrupt,
29    Error(String),
30}
31
32enum Activity {
33    Input(Option<Input>),
34    RuntimeError(Error),
35    StalledEvaluation,
36    Background,
37}
38
39struct InterruptState {
40    active: Arc<AtomicBool>,
41    interrupted: Arc<AtomicBool>,
42    handle: avril_core::v8::IsolateHandle,
43}
44
45pub(crate) fn run(
46    runtime: &mut Runtime,
47    referrer: &str,
48    stdio: &avril_ext_stdio::Stdio,
49    host: &tokio::runtime::Handle,
50    reporter: Option<&crate::progress::Reporter>,
51) -> Result<(), Error> {
52    let source_url = Url::from_file_path(referrer)
53        .map_err(|_| Error::JavaScript(format!("Invalid REPL referrer: {referrer}")))?
54        .to_string();
55    runtime.enable_repl()?;
56
57    let active = Arc::new(AtomicBool::new(false));
58    let interrupted = stdio.interrupt_flag();
59    let handle = runtime.isolate_handle();
60    let (input_sender, input_receiver) = async_mpsc::unbounded();
61    let (request, requests) = mpsc::channel::<()>();
62    let terminal = io::stdin().is_terminal() && io::stderr().is_terminal();
63    let input_thread_sender = input_sender.clone();
64    let input_thread = thread::Builder::new()
65        .name("avril:repl-input".into())
66        .spawn(move || read_input(terminal, requests, input_thread_sender))
67        .map_err(|error| Error::JavaScript(error.to_string()));
68
69    let signal_active = Arc::clone(&active);
70    let signal_interrupted = Arc::clone(&interrupted);
71    let signal_handle = handle.clone();
72    let signal_sender = input_sender;
73    let result = match input_thread {
74        Err(error) => Err(error),
75        Ok(_input_thread) => {
76            // Route Ctrl-C through the shared chaining registry instead of
77            // ctrlc's overwriting handler, so REPL listeners observe the
78            // interrupt rather than being clobbered by it.
79            let on_interrupt = move || {
80                signal_interrupted.store(true, Ordering::SeqCst);
81                let _ = signal_sender.unbounded_send(Input::Interrupt);
82                if signal_active.load(Ordering::SeqCst) {
83                    // A blocking runSync cannot observe V8 termination, so
84                    // stop exactly the subprocess it is waiting on. Other
85                    // session work is left alone.
86                    avril_ext_process::Tree::global().stop_sync_processes();
87                    signal_handle.terminate_execution();
88                }
89            };
90            #[cfg(unix)]
91            avril_ext_process::Tree::global()
92                .claim_interrupt(host, Arc::new(on_interrupt))
93                .map_err(|error| Error::JavaScript(error.to_string()))?;
94            #[cfg(not(unix))]
95            ctrlc::set_handler(on_interrupt)
96                .map_err(|error| Error::JavaScript(error.to_string()))?;
97            run_session(Repl {
98                runtime,
99                host,
100                source_url: &source_url,
101                request,
102                input: input_receiver,
103                interrupts: InterruptState { active, interrupted, handle },
104                stdio,
105                terminal,
106                reporter,
107            })
108        }
109    };
110    let disabled = runtime.disable_repl();
111    result?;
112    disabled
113}
114
115/// One interactive session's runtime, input plumbing, and terminal state.
116struct Repl<'a> {
117    runtime: &'a mut Runtime,
118    host: &'a tokio::runtime::Handle,
119    source_url: &'a str,
120    request: mpsc::Sender<()>,
121    input: UnboundedReceiver<Input>,
122    interrupts: InterruptState,
123    stdio: &'a avril_ext_stdio::Stdio,
124    terminal: bool,
125    reporter: Option<&'a crate::progress::Reporter>,
126}
127
128fn run_session(
129    Repl {
130        runtime,
131        host,
132        source_url,
133        request,
134        input,
135        interrupts,
136        stdio,
137        terminal,
138        reporter,
139    }: Repl<'_>,
140) -> Result<(), Error> {
141    let mut input = input;
142    runtime.execute_script::<()>("<repl>", "globalThis._ = undefined")?;
143    let InterruptState { active, interrupted, handle } = &interrupts;
144    let mut source = String::new();
145    let mut input_pending = false;
146    'session: loop {
147        if !input_pending {
148            request
149                .send(())
150                .map_err(|error| Error::JavaScript(error.to_string()))?;
151            input_pending = true;
152        }
153        let line = loop {
154            match host.block_on(next_activity(runtime, &mut input, active, None, terminal)) {
155                Activity::Input(Some(Input::Line(line))) => {
156                    input_pending = false;
157                    if interrupted.swap(false, Ordering::SeqCst) {
158                        handle.cancel_terminate_execution();
159                        source.clear();
160                        continue 'session;
161                    }
162                    break Some(line);
163                }
164                Activity::Input(Some(Input::Eof)) | Activity::Input(None) => break None,
165                Activity::Input(Some(Input::Cancel)) => {
166                    interrupted.store(false, Ordering::SeqCst);
167                    handle.cancel_terminate_execution();
168                    source.clear();
169                    input_pending = false;
170                    continue 'session;
171                }
172                Activity::Input(Some(Input::Interrupt)) => {
173                    interrupted.store(false, Ordering::SeqCst);
174                    handle.cancel_terminate_execution();
175                    source.clear();
176                    continue 'session;
177                }
178                Activity::Input(Some(Input::Error(error))) => {
179                    return Err(Error::JavaScript(error));
180                }
181                Activity::RuntimeError(error) => {
182                    if interrupted.swap(false, Ordering::SeqCst) {
183                        handle.cancel_terminate_execution();
184                        eprintln!("Evaluation interrupted");
185                        source.clear();
186                    } else {
187                        eprintln!("{error}");
188                    }
189                }
190                Activity::Background | Activity::StalledEvaluation => {}
191            }
192        };
193        let Some(line) = line else {
194            break;
195        };
196        if source.is_empty() {
197            match line.trim() {
198                ".exit" | ".quit" => break,
199                ".help" => {
200                    println!(
201                        "Enter JavaScript; incomplete expressions continue on the next line.\n.help  Show this help\n.exit  Exit the REPL"
202                    );
203                    continue;
204                }
205                _ => {}
206            }
207        }
208        if line.trim().is_empty() && source.is_empty() {
209            continue;
210        }
211        source.push_str(&line);
212        source.push('\n');
213        if is_incomplete(&source) {
214            continue;
215        }
216
217        let expression = prepare_imports(&prepare_expression(&source));
218        source.clear();
219        let shown = Session {
220            runtime: &mut *runtime,
221            host,
222            input: &mut input,
223            interrupts: &interrupts,
224            stdio,
225            terminal,
226            reporter,
227        }
228        .show(&expression, source_url);
229        runtime.release_repl_objects()?;
230        if shown? == Waited::Eof {
231            break;
232        }
233    }
234    Ok(())
235}
236
237// Results go through avril:std's inspect so they match console.log and honor
238// custom inspectors; the inspector can only reach it from the guest realm.
239const INSPECT: &str = r#"async function (value, colors, remember) {
240    if (remember) this._ = value;
241    const { inspect } = await import("avril:std");
242    return inspect(value, { colors });
243}"#;
244
245#[derive(PartialEq)]
246enum Waited {
247    Response(String),
248    Abandoned,
249    Eof,
250}
251
252struct Session<'a> {
253    runtime: &'a mut Runtime,
254    host: &'a tokio::runtime::Handle,
255    input: &'a mut UnboundedReceiver<Input>,
256    interrupts: &'a InterruptState,
257    stdio: &'a avril_ext_stdio::Stdio,
258    terminal: bool,
259    reporter: Option<&'a crate::progress::Reporter>,
260}
261
262struct ProgressScope<'a>(&'a crate::progress::Reporter);
263
264impl Drop for ProgressScope<'_> {
265    fn drop(&mut self) {
266        self.0.scope(false);
267    }
268}
269
270impl Session<'_> {
271    fn show(&mut self, expression: &str, source_url: &str) -> Result<Waited, Error> {
272        let response = match self.run(|runtime| runtime.evaluate_repl(expression, source_url))? {
273            Waited::Response(response) => response,
274            waited => return Ok(waited),
275        };
276        let (value, stderr) = match completion(&response) {
277            Ok(completion) => completion,
278            Err(message) => {
279                eprintln!("{message}");
280                return Ok(Waited::Abandoned);
281            }
282        };
283        let colors = json!({ "value": avril_ext_console::colors(stderr) });
284        let remember = json!({ "value": !stderr });
285        let response = match self
286            .run(|runtime| runtime.call_repl_function(INSPECT, &[value, colors, remember]))?
287        {
288            Waited::Response(response) => response,
289            waited => return Ok(waited),
290        };
291        let text = inspected(&response);
292        if stderr {
293            eprintln!("{text}");
294        } else {
295            println!("{text}");
296        }
297        io::stdout()
298            .flush()
299            .map_err(|error| Error::JavaScript(error.to_string()))?;
300        Ok(Waited::Abandoned)
301    }
302
303    /// Dispatch an inspector command and drive the event loop until it answers,
304    /// unless the user interrupts it or input ends first.
305    fn run(
306        &mut self,
307        dispatch: impl FnOnce(&mut Runtime) -> Result<i32, Error>,
308    ) -> Result<Waited, Error> {
309        let progress_scope = self.reporter.map(|reporter| {
310            reporter.scope(true);
311            ProgressScope(reporter)
312        });
313        let InterruptState { active, interrupted, handle } = self.interrupts;
314        active.store(true, Ordering::SeqCst);
315        if interrupted.swap(false, Ordering::SeqCst) {
316            active.store(false, Ordering::SeqCst);
317            handle.cancel_terminate_execution();
318            return Ok(Waited::Abandoned);
319        }
320        let dispatched = dispatch(self.runtime);
321        active.store(false, Ordering::SeqCst);
322        let id = match dispatched {
323            Ok(id) => id,
324            Err(_) if interrupted.swap(false, Ordering::SeqCst) => {
325                handle.cancel_terminate_execution();
326                drop(progress_scope);
327                eprintln!("Evaluation interrupted");
328                return Ok(Waited::Abandoned);
329            }
330            Err(error) => return Err(error),
331        };
332        loop {
333            if let Some(response) = self.runtime.take_repl_response(id) {
334                self.stdio
335                    .flush_interruptible(|| interrupted.load(Ordering::SeqCst))
336                    .map_err(|error| Error::JavaScript(error.to_string()))?;
337                if interrupted.swap(false, Ordering::SeqCst) {
338                    handle.cancel_terminate_execution();
339                    drop(progress_scope);
340                    eprintln!("Evaluation interrupted");
341                    return Ok(Waited::Abandoned);
342                }
343                return Ok(Waited::Response(response));
344            }
345            match self.host.block_on(next_activity(
346                self.runtime,
347                self.input,
348                active,
349                Some(id),
350                self.terminal,
351            )) {
352                Activity::Input(Some(Input::Interrupt | Input::Cancel)) => {
353                    if interrupted.swap(false, Ordering::SeqCst) {
354                        self.runtime.terminate_repl_execution()?;
355                        self.abandon(id);
356                        drop(progress_scope);
357                        eprintln!("Evaluation interrupted");
358                        return Ok(Waited::Abandoned);
359                    }
360                }
361                Activity::RuntimeError(error) => {
362                    if interrupted.swap(false, Ordering::SeqCst) {
363                        self.abandon(id);
364                        drop(progress_scope);
365                        eprintln!("Evaluation interrupted");
366                        return Ok(Waited::Abandoned);
367                    }
368                    if let Some(reporter) = self.reporter {
369                        reporter.flush();
370                    }
371                    eprintln!("{error}");
372                }
373                Activity::Input(Some(Input::Error(error))) => {
374                    return Err(Error::JavaScript(error));
375                }
376                Activity::Input(Some(Input::Eof)) | Activity::Input(None) => {
377                    self.runtime.terminate_repl_execution()?;
378                    self.abandon(id);
379                    return Ok(Waited::Eof);
380                }
381                Activity::StalledEvaluation => {
382                    self.runtime.terminate_repl_execution()?;
383                    self.abandon(id);
384                    drop(progress_scope);
385                    eprintln!("REPL evaluation is still pending with no runnable host work");
386                    return Ok(Waited::Abandoned);
387                }
388                Activity::Input(Some(Input::Line(_))) | Activity::Background => {}
389            }
390        }
391    }
392
393    fn abandon(&mut self, id: i32) {
394        self.interrupts.handle.cancel_terminate_execution();
395        let _ = self.runtime.take_repl_response(id);
396    }
397}
398
399fn next_activity<'a>(
400    runtime: &'a mut Runtime,
401    input: &'a mut UnboundedReceiver<Input>,
402    active: &'a AtomicBool,
403    evaluation: Option<i32>,
404    terminal: bool,
405) -> impl Future<Output = Activity> + 'a {
406    let mut background_delay = None;
407    poll_fn(move |context| {
408        if evaluation.is_some_and(|id| runtime.has_repl_response(id)) {
409            return Poll::Ready(Activity::Background);
410        }
411        if let Poll::Ready(input) = input.poll_next_unpin(context) {
412            return Poll::Ready(Activity::Input(input));
413        }
414        let background_pending = runtime.has_pending_background_tasks();
415        active.store(true, Ordering::SeqCst);
416        let result = runtime.poll_event_loop(context);
417        active.store(false, Ordering::SeqCst);
418        match result {
419            Err(error) => return Poll::Ready(Activity::RuntimeError(error)),
420            Ok(true) => return Poll::Ready(Activity::Background),
421            Ok(false) => {}
422        }
423        // The checkpoint can finish an inspector evaluation without a native completion.
424        if evaluation.is_some_and(|id| runtime.has_repl_response(id)) {
425            return Poll::Ready(Activity::Background);
426        }
427        // The default V8 platform does not notify the host about foreground tasks.
428        if background_pending || runtime.has_pending_background_tasks() {
429            let delay = background_delay
430                .get_or_insert_with(|| Box::pin(tokio::time::sleep(Duration::from_millis(20))));
431            if delay.as_mut().poll(context).is_ready() {
432                return Poll::Ready(Activity::Background);
433            }
434        } else {
435            background_delay = None;
436            if !terminal && evaluation.is_some() && !runtime.has_pending_work() {
437                return Poll::Ready(Activity::StalledEvaluation);
438            }
439        }
440        Poll::Pending
441    })
442}
443
444fn read_input(terminal: bool, requests: Receiver<()>, input: UnboundedSender<Input>) {
445    if terminal {
446        let mut editor = line_editor::create();
447        for () in requests {
448            let line = match editor.read_line(&line_editor::Prompt) {
449                Ok(Signal::Success(line)) => Input::Line(line),
450                Ok(Signal::CtrlC) => Input::Cancel,
451                Ok(Signal::CtrlD) => Input::Eof,
452                Err(error) => Input::Error(error.to_string()),
453            };
454            let eof = matches!(line, Input::Eof | Input::Error(_));
455            if input.unbounded_send(line).is_err() || eof {
456                break;
457            }
458        }
459        return;
460    }
461
462    let stdin = io::stdin();
463    let mut stdin = stdin.lock();
464    for _ in requests {
465        let mut line = String::new();
466        let line = loop {
467            match stdin.read_line(&mut line) {
468                Ok(0) => break Input::Eof,
469                Ok(_) => {
470                    break Input::Line(line.trim_end_matches(['\r', '\n']).to_owned());
471                }
472                Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
473                Err(error) => break Input::Error(error.to_string()),
474            }
475        };
476        let eof = matches!(line, Input::Eof | Input::Error(_));
477        if input.unbounded_send(line).is_err() || eof {
478            break;
479        }
480    }
481}
482
483fn is_incomplete(source: &str) -> bool {
484    let allocator = Allocator::default();
485    let parsed = Parser::new(&allocator, source, SourceType::mjs()).parse();
486    if parsed.diagnostics.is_empty() || prepare_expression(source) != source {
487        return false;
488    }
489    let incomplete = |candidate: &str| {
490        let allocator = Allocator::default();
491        let parsed = Parser::new(&allocator, candidate, SourceType::mjs()).parse();
492        let eof = candidate.len();
493        !parsed.diagnostics.is_empty()
494            && parsed.diagnostics.iter().all(|error| {
495                let message = error.message.to_ascii_lowercase();
496                let at_end = error
497                    .labels
498                    .iter()
499                    .any(|label| label.offset().saturating_add(label.len()) as usize >= eof);
500                if message.contains("unterminated string") {
501                    error.labels.iter().any(|label| {
502                        candidate.as_bytes().get(label.offset() as usize) == Some(&b'`')
503                    }) || candidate.ends_with("\\\n")
504                } else {
505                    at_end && !message.contains("unterminated regular expression")
506                }
507            })
508    };
509    incomplete(source) || incomplete(&format!("({source}"))
510}
511
512fn prepare_expression(source: &str) -> String {
513    let wrapped = format!("({source})");
514    let allocator = Allocator::default();
515    let parsed = Parser::new(&allocator, &wrapped, SourceType::mjs()).parse();
516    if parsed.diagnostics.is_empty()
517        && matches!(
518            parsed.program.body.as_slice(),
519            [Statement::ExpressionStatement(statement)]
520                if matches!(statement.expression.without_parentheses(), Expression::ObjectExpression(_))
521        )
522    {
523        return wrapped;
524    }
525    source.to_owned()
526}
527
528fn prepare_imports(source: &str) -> String {
529    let allocator = Allocator::default();
530    let parsed = Parser::new(&allocator, source, SourceType::mjs()).parse();
531    if !parsed.diagnostics.is_empty() {
532        return source.to_owned();
533    }
534    let mut result = String::new();
535    let mut offset = 0;
536    for statement in &parsed.program.body {
537        let Statement::ImportDeclaration(import) = statement else {
538            continue;
539        };
540        if import.phase.is_some() {
541            return source.to_owned();
542        }
543        let mut properties = Vec::new();
544        let mut namespace = None;
545        for specifier in import.specifiers.iter().flatten() {
546            match specifier {
547                ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
548                    properties.push(format!("\"default\": {}", specifier.local.name));
549                }
550                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
551                    let span = specifier.imported.span();
552                    let key = &source[span.start as usize..span.end as usize];
553                    properties.push(format!("{key}: {}", specifier.local.name));
554                }
555                ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
556                    namespace = Some(specifier.local.name.as_str());
557                }
558            }
559        }
560        let mut options = String::new();
561        if let Some(clause) = &import.with_clause {
562            let attributes = clause
563                .with_entries
564                .iter()
565                .map(|attribute| {
566                    let key = match &attribute.key {
567                        ImportAttributeKey::Identifier(identifier) => {
568                            serde_json::to_string(identifier.name.as_str()).unwrap()
569                        }
570                        ImportAttributeKey::StringLiteral(literal) => source
571                            [literal.span.start as usize..literal.span.end as usize]
572                            .to_owned(),
573                    };
574                    let value = &source
575                        [attribute.value.span.start as usize..attribute.value.span.end as usize];
576                    format!("[{key}]: {value}")
577                })
578                .collect::<Vec<_>>()
579                .join(", ");
580            options = format!(", {{ with: {{ {attributes} }} }}");
581        }
582        let specifier = &source[import.source.span.start as usize..import.source.span.end as usize];
583        let initializer = format!("await import({specifier}{options})");
584        let mut declarations = Vec::new();
585        if let Some(namespace) = namespace {
586            declarations.push(format!("{namespace} = {initializer}"));
587        }
588        if !properties.is_empty() || namespace.is_none() {
589            declarations.push(format!(
590                "{{ {} }} = {}",
591                properties.join(", "),
592                namespace.unwrap_or(&initializer)
593            ));
594        }
595        let start = import.span.start as usize;
596        let end = import.span.end as usize;
597        result.push_str(&source[offset..start]);
598        result.push_str(&format!("const {};", declarations.join(", ")));
599        // Keep diagnostics for subsequent statements on their original lines.
600        result.extend(
601            source[start..end]
602                .chars()
603                .filter(|ch| matches!(ch, '\r' | '\n' | '\u{2028}' | '\u{2029}')),
604        );
605        offset = end;
606    }
607    result.push_str(&source[offset..]);
608    result
609}
610
611/// An evaluation's completion or thrown value as an inspector `CallArgument`,
612/// and whether it was thrown.
613fn completion(response: &str) -> Result<(Value, bool), String> {
614    let response = serde_json::from_str::<Value>(response)
615        .map_err(|_| "Invalid V8 inspector response".to_owned())?;
616    if let Some(error) = response.get("error") {
617        return Err(error["message"]
618            .as_str()
619            .unwrap_or("Inspector error")
620            .to_owned());
621    }
622    let result = &response["result"];
623    if let Some(exception) = result.get("exceptionDetails") {
624        return match exception.get("exception") {
625            Some(thrown) => Ok((call_argument(thrown), true)),
626            None => Err(exception["text"]
627                .as_str()
628                .unwrap_or("JavaScript exception")
629                .to_owned()),
630        };
631    }
632    Ok((call_argument(&result["result"]), false))
633}
634
635fn call_argument(remote: &Value) -> Value {
636    for key in ["objectId", "unserializableValue", "value"] {
637        if let Some(value) = remote.get(key) {
638            return json!({ key: value });
639        }
640    }
641    json!({})
642}
643
644fn inspected(response: &str) -> String {
645    let response = serde_json::from_str::<Value>(response).unwrap_or_default();
646    let result = &response["result"];
647    result["result"]["value"]
648        .as_str()
649        .or_else(|| result["exceptionDetails"]["exception"]["description"].as_str())
650        .or_else(|| response["error"]["message"].as_str())
651        .unwrap_or("Inspection failed")
652        .to_owned()
653}
654
655#[cfg(test)]
656mod tests {
657    use super::{is_incomplete, prepare_expression, prepare_imports};
658
659    #[test]
660    fn multiline_completeness_uses_the_javascript_parser() {
661        assert!(is_incomplete("function greet() {\n"));
662        assert!(is_incomplete("const value = {\n  answer: 42,\n"));
663        assert!(!is_incomplete("const value = 42;\n"));
664        assert!(!is_incomplete("const = 42;\n"));
665        assert!(is_incomplete("const value =\n"));
666        assert!(is_incomplete("{\n  answer: 42,\n"));
667        assert!(is_incomplete("`brace {\n"));
668        assert!(is_incomplete("/* comment\n"));
669        assert!(!is_incomplete("'broken\n"));
670        assert!(!is_incomplete("/broken\n"));
671        assert!(is_incomplete("import {\n  inspect as show,\n"));
672        assert!(is_incomplete("import { inspect } from\n"));
673        assert!(!is_incomplete("import { inspect } from 'avril:std'\n"));
674    }
675
676    #[test]
677    fn imports_preserve_all_javascript_line_terminators() {
678        for newline in ["\n", "\r", "\r\n", "\u{2028}", "\u{2029}"] {
679            let source = format!(
680                "import {{{newline}inspect{newline}}} from 'avril:std';{newline}throw Error('original line');"
681            );
682            let prepared = prepare_imports(&source);
683            let original_line = source
684                .split(newline)
685                .position(|line| line.contains("throw Error"))
686                .unwrap();
687            let prepared_line = prepared
688                .split(newline)
689                .position(|line| line.contains("throw Error"))
690                .unwrap();
691            assert_eq!(prepared_line, original_line, "{newline:?}");
692        }
693    }
694
695    #[test]
696    fn object_literals_are_evaluated_as_expressions() {
697        assert_eq!(prepare_expression("{ answer: 42 }\n"), "({ answer: 42 }\n)");
698        assert_eq!(
699            prepare_expression("/* comment */ { answer: 42 }\n"),
700            "(/* comment */ { answer: 42 }\n)"
701        );
702        assert_eq!(prepare_expression("if (true) {}\n"), "if (true) {}\n");
703    }
704}