use std::future::Future;
use std::io::{self, BufRead, IsTerminal, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver};
use std::task::Poll;
use std::thread;
use std::time::Duration;
use avril_core::{Error, Runtime};
use futures::StreamExt;
use futures::channel::mpsc::{self as async_mpsc, UnboundedReceiver, UnboundedSender};
use futures::future::poll_fn;
use oxc::allocator::Allocator;
use oxc::ast::ast::{Expression, ImportAttributeKey, ImportDeclarationSpecifier, Statement};
use oxc::parser::Parser;
use oxc::span::{GetSpan, SourceType};
use reedline::Signal;
use serde_json::{Value, json};
use url::Url;
mod line_editor;
enum Input {
Line(String),
Eof,
Cancel,
Interrupt,
Error(String),
}
enum Activity {
Input(Option),
RuntimeError(Error),
StalledEvaluation,
Background,
}
struct InterruptState {
active: Arc,
interrupted: Arc,
handle: avril_core::v8::IsolateHandle,
}
pub(crate) fn run(
runtime: &mut Runtime,
referrer: &str,
stdio: &avril_ext_stdio::Stdio,
host: &tokio::runtime::Handle,
reporter: Option<&crate::progress::Reporter>,
) -> Result<(), Error> {
let source_url = Url::from_file_path(referrer)
.map_err(|_| Error::JavaScript(format!("Invalid REPL referrer: {referrer}")))?
.to_string();
runtime.enable_repl()?;
let active = Arc::new(AtomicBool::new(false));
let interrupted = stdio.interrupt_flag();
let handle = runtime.isolate_handle();
let (input_sender, input_receiver) = async_mpsc::unbounded();
let (request, requests) = mpsc::channel::<()>();
let terminal = io::stdin().is_terminal() && io::stderr().is_terminal();
let input_thread_sender = input_sender.clone();
let input_thread = thread::Builder::new()
.name("avril:repl-input".into())
.spawn(move || read_input(terminal, requests, input_thread_sender))
.map_err(|error| Error::JavaScript(error.to_string()));
let signal_active = Arc::clone(&active);
let signal_interrupted = Arc::clone(&interrupted);
let signal_handle = handle.clone();
let signal_sender = input_sender;
let result = match input_thread {
Err(error) => Err(error),
Ok(_input_thread) => {
// Route Ctrl-C through the shared chaining registry instead of
// ctrlc's overwriting handler, so REPL listeners observe the
// interrupt rather than being clobbered by it.
let on_interrupt = move || {
signal_interrupted.store(true, Ordering::SeqCst);
let _ = signal_sender.unbounded_send(Input::Interrupt);
if signal_active.load(Ordering::SeqCst) {
// A blocking runSync cannot observe V8 termination, so
// stop exactly the subprocess it is waiting on. Other
// session work is left alone.
avril_ext_process::Tree::global().stop_sync_processes();
signal_handle.terminate_execution();
}
};
#[cfg(unix)]
avril_ext_process::Tree::global()
.claim_interrupt(host, Arc::new(on_interrupt))
.map_err(|error| Error::JavaScript(error.to_string()))?;
#[cfg(not(unix))]
ctrlc::set_handler(on_interrupt)
.map_err(|error| Error::JavaScript(error.to_string()))?;
run_session(Repl {
runtime,
host,
source_url: &source_url,
request,
input: input_receiver,
interrupts: InterruptState { active, interrupted, handle },
stdio,
terminal,
reporter,
})
}
};
let disabled = runtime.disable_repl();
result?;
disabled
}
/// One interactive session's runtime, input plumbing, and terminal state.
struct Repl<'a> {
runtime: &'a mut Runtime,
host: &'a tokio::runtime::Handle,
source_url: &'a str,
request: mpsc::Sender<()>,
input: UnboundedReceiver,
interrupts: InterruptState,
stdio: &'a avril_ext_stdio::Stdio,
terminal: bool,
reporter: Option<&'a crate::progress::Reporter>,
}
fn run_session(
Repl {
runtime,
host,
source_url,
request,
input,
interrupts,
stdio,
terminal,
reporter,
}: Repl<'_>,
) -> Result<(), Error> {
let mut input = input;
runtime.execute_script::<()>("", "globalThis._ = undefined")?;
let InterruptState { active, interrupted, handle } = &interrupts;
let mut source = String::new();
let mut input_pending = false;
'session: loop {
if !input_pending {
request
.send(())
.map_err(|error| Error::JavaScript(error.to_string()))?;
input_pending = true;
}
let line = loop {
match host.block_on(next_activity(runtime, &mut input, active, None, terminal)) {
Activity::Input(Some(Input::Line(line))) => {
input_pending = false;
if interrupted.swap(false, Ordering::SeqCst) {
handle.cancel_terminate_execution();
source.clear();
continue 'session;
}
break Some(line);
}
Activity::Input(Some(Input::Eof)) | Activity::Input(None) => break None,
Activity::Input(Some(Input::Cancel)) => {
interrupted.store(false, Ordering::SeqCst);
handle.cancel_terminate_execution();
source.clear();
input_pending = false;
continue 'session;
}
Activity::Input(Some(Input::Interrupt)) => {
interrupted.store(false, Ordering::SeqCst);
handle.cancel_terminate_execution();
source.clear();
continue 'session;
}
Activity::Input(Some(Input::Error(error))) => {
return Err(Error::JavaScript(error));
}
Activity::RuntimeError(error) => {
if interrupted.swap(false, Ordering::SeqCst) {
handle.cancel_terminate_execution();
eprintln!("Evaluation interrupted");
source.clear();
} else {
eprintln!("{error}");
}
}
Activity::Background | Activity::StalledEvaluation => {}
}
};
let Some(line) = line else {
break;
};
if source.is_empty() {
match line.trim() {
".exit" | ".quit" => break,
".help" => {
println!(
"Enter JavaScript; incomplete expressions continue on the next line.\n.help Show this help\n.exit Exit the REPL"
);
continue;
}
_ => {}
}
}
if line.trim().is_empty() && source.is_empty() {
continue;
}
source.push_str(&line);
source.push('\n');
if is_incomplete(&source) {
continue;
}
let expression = prepare_imports(&prepare_expression(&source));
source.clear();
let shown = Session {
runtime: &mut *runtime,
host,
input: &mut input,
interrupts: &interrupts,
stdio,
terminal,
reporter,
}
.show(&expression, source_url);
runtime.release_repl_objects()?;
if shown? == Waited::Eof {
break;
}
}
Ok(())
}
// Results go through avril:std's inspect so they match console.log and honor
// custom inspectors; the inspector can only reach it from the guest realm.
const INSPECT: &str = r#"async function (value, colors, remember) {
if (remember) this._ = value;
const { inspect } = await import("avril:std");
return inspect(value, { colors });
}"#;
#[derive(PartialEq)]
enum Waited {
Response(String),
Abandoned,
Eof,
}
struct Session<'a> {
runtime: &'a mut Runtime,
host: &'a tokio::runtime::Handle,
input: &'a mut UnboundedReceiver,
interrupts: &'a InterruptState,
stdio: &'a avril_ext_stdio::Stdio,
terminal: bool,
reporter: Option<&'a crate::progress::Reporter>,
}
struct ProgressScope<'a>(&'a crate::progress::Reporter);
impl Drop for ProgressScope<'_> {
fn drop(&mut self) {
self.0.scope(false);
}
}
impl Session<'_> {
fn show(&mut self, expression: &str, source_url: &str) -> Result {
let response = match self.run(|runtime| runtime.evaluate_repl(expression, source_url))? {
Waited::Response(response) => response,
waited => return Ok(waited),
};
let (value, stderr) = match completion(&response) {
Ok(completion) => completion,
Err(message) => {
eprintln!("{message}");
return Ok(Waited::Abandoned);
}
};
let colors = json!({ "value": avril_ext_console::colors(stderr) });
let remember = json!({ "value": !stderr });
let response = match self
.run(|runtime| runtime.call_repl_function(INSPECT, &[value, colors, remember]))?
{
Waited::Response(response) => response,
waited => return Ok(waited),
};
let text = inspected(&response);
if stderr {
eprintln!("{text}");
} else {
println!("{text}");
}
io::stdout()
.flush()
.map_err(|error| Error::JavaScript(error.to_string()))?;
Ok(Waited::Abandoned)
}
/// Dispatch an inspector command and drive the event loop until it answers,
/// unless the user interrupts it or input ends first.
fn run(
&mut self,
dispatch: impl FnOnce(&mut Runtime) -> Result,
) -> Result {
let progress_scope = self.reporter.map(|reporter| {
reporter.scope(true);
ProgressScope(reporter)
});
let InterruptState { active, interrupted, handle } = self.interrupts;
active.store(true, Ordering::SeqCst);
if interrupted.swap(false, Ordering::SeqCst) {
active.store(false, Ordering::SeqCst);
handle.cancel_terminate_execution();
return Ok(Waited::Abandoned);
}
let dispatched = dispatch(self.runtime);
active.store(false, Ordering::SeqCst);
let id = match dispatched {
Ok(id) => id,
Err(_) if interrupted.swap(false, Ordering::SeqCst) => {
handle.cancel_terminate_execution();
drop(progress_scope);
eprintln!("Evaluation interrupted");
return Ok(Waited::Abandoned);
}
Err(error) => return Err(error),
};
loop {
if let Some(response) = self.runtime.take_repl_response(id) {
self.stdio
.flush_interruptible(|| interrupted.load(Ordering::SeqCst))
.map_err(|error| Error::JavaScript(error.to_string()))?;
if interrupted.swap(false, Ordering::SeqCst) {
handle.cancel_terminate_execution();
drop(progress_scope);
eprintln!("Evaluation interrupted");
return Ok(Waited::Abandoned);
}
return Ok(Waited::Response(response));
}
match self.host.block_on(next_activity(
self.runtime,
self.input,
active,
Some(id),
self.terminal,
)) {
Activity::Input(Some(Input::Interrupt | Input::Cancel)) => {
if interrupted.swap(false, Ordering::SeqCst) {
self.runtime.terminate_repl_execution()?;
self.abandon(id);
drop(progress_scope);
eprintln!("Evaluation interrupted");
return Ok(Waited::Abandoned);
}
}
Activity::RuntimeError(error) => {
if interrupted.swap(false, Ordering::SeqCst) {
self.abandon(id);
drop(progress_scope);
eprintln!("Evaluation interrupted");
return Ok(Waited::Abandoned);
}
if let Some(reporter) = self.reporter {
reporter.flush();
}
eprintln!("{error}");
}
Activity::Input(Some(Input::Error(error))) => {
return Err(Error::JavaScript(error));
}
Activity::Input(Some(Input::Eof)) | Activity::Input(None) => {
self.runtime.terminate_repl_execution()?;
self.abandon(id);
return Ok(Waited::Eof);
}
Activity::StalledEvaluation => {
self.runtime.terminate_repl_execution()?;
self.abandon(id);
drop(progress_scope);
eprintln!("REPL evaluation is still pending with no runnable host work");
return Ok(Waited::Abandoned);
}
Activity::Input(Some(Input::Line(_))) | Activity::Background => {}
}
}
}
fn abandon(&mut self, id: i32) {
self.interrupts.handle.cancel_terminate_execution();
let _ = self.runtime.take_repl_response(id);
}
}
fn next_activity<'a>(
runtime: &'a mut Runtime,
input: &'a mut UnboundedReceiver,
active: &'a AtomicBool,
evaluation: Option,
terminal: bool,
) -> impl Future