use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use super::definition_buffer::{BufferedInput, DefinitionBuffer, Replacement};
use super::definition_rounds::{AcceptedDefinition, DefinitionOutcome, DefinitionRounds};
use super::error_location::{InputLocator, ModuleLoader};
use super::eval::{EvalCommand, EvaluationSettings, FormsText, ModuleText};
use super::evaluation_output::PrintedEvaluation;
use super::exit_status::ExitStatus;
use super::interruption::Interruption;
use super::library_walk::FetchingStore;
use super::located_project::LocatedProject;
use super::name_records::NameRecords;
use super::program_file::ProgramFile;
use super::prompt_diagnostics::{PromptAnnotations, PromptDiagnostics};
use super::prompt_lines::{EnteredText, PromptLines};
use super::prompt_module::PromptModule;
use super::standard_error::StandardError;
use super::terminal::Terminal;
use crate::build::BuildProfile;
use crate::diagnostic::Diagnostics;
use crate::expand::ClassifiedForm;
use crate::source::{SourceId, Span};
pub struct ReplCommand {
root: Option<PathBuf>,
profile: BuildProfile,
}
struct PromptSession {
module: PromptModule,
profile: BuildProfile,
buffer: DefinitionBuffer,
annotations: Rc<RefCell<PromptAnnotations>>,
loaders: Vec<ModuleLoader>,
entered: usize,
interruption: Interruption,
}
struct EnteredInput {
name: String,
text: String,
source: SourceId,
}
enum ClassifiedInput {
Expression,
Definition(BufferedInput, Replacement),
}
impl ReplCommand {
const COMMAND: &str = "repl";
pub fn new(root: Option<PathBuf>, profile: BuildProfile) -> Self {
Self { root, profile }
}
pub fn run(self, terminal: &mut Terminal) -> ExitStatus {
let file = match self.prompt_file(terminal) {
Ok(file) => file,
Err(status) => return status,
};
let mut lines = match PromptLines::standard_input(terminal.diagnostic_output()) {
Ok(lines) => lines,
Err(error) => return ExitStatus::report_unreadable_input(&error),
};
let interruption = lines
.catch_interrupts()
.unwrap_or_else(|error| Self::note_uncaught_interrupts(terminal, &error));
terminal.stop_before_rustc_on(interruption.clone());
let session = PromptSession::new(terminal, file, self.profile, interruption);
Self::answer(&mut lines, session, terminal)
}
fn answer(
lines: &mut PromptLines,
mut session: PromptSession,
terminal: &mut Terminal,
) -> ExitStatus {
loop {
match lines.next_text() {
Ok(Some(EnteredText::Complete(text))) => session.enter(terminal, text),
Ok(Some(EnteredText::Unfinished(text))) => {
session.report_unfinished(terminal, text);
return ExitStatus::Success;
}
Ok(None) => return ExitStatus::Success,
Err(error) => return ExitStatus::report_unreadable_input(&error),
}
}
}
fn prompt_file(&self, terminal: &Terminal) -> Result<ProgramFile, ExitStatus> {
let working_directory = Path::new(Terminal::WORKING_DIRECTORY);
let project =
LocatedProject::search_from(working_directory, terminal, FetchingStore::default())?;
if let Some(project) = &project {
project.refuse_crates(Self::COMMAND, terminal)?;
}
let root = match (&self.root, &project) {
(Some(root), _) => root.clone(),
(None, Some(project)) => project.source_root(),
(None, None) => working_directory.to_path_buf(),
};
let file = ProgramFile::within(PathBuf::from(PromptModule::PATH), root);
Ok(LocatedProject::lend_libraries(project.as_ref(), file))
}
fn note_uncaught_interrupts(terminal: &Terminal, error: &ctrlc::Error) -> Interruption {
terminal.print_note(&format!("Ctrl-C will end the session: {error}"));
Interruption::default()
}
}
impl PromptSession {
const INTERRUPTED: &str = "interrupted";
fn new(
terminal: &mut Terminal,
file: ProgramFile,
profile: BuildProfile,
interruption: Interruption,
) -> Self {
let anchor = terminal.sources().add(PromptModule::PATH, "");
let session = Self {
module: PromptModule::new(anchor, file),
profile,
buffer: DefinitionBuffer::default(),
annotations: Rc::default(),
loaders: Vec::new(),
entered: 0,
interruption,
};
session.place_prelude_sources_before_the_inputs(terminal);
session.annotate_diagnostics(terminal);
session
}
fn place_prelude_sources_before_the_inputs(&self, terminal: &mut Terminal) {
terminal
.check_sources(&self.module.of(Vec::new()), |_| false)
.ok();
}
fn annotate_diagnostics(&self, terminal: &mut Terminal) {
let annotated =
PromptDiagnostics::new(terminal.diagnostic_output(), Rc::clone(&self.annotations));
terminal.report_diagnostics_to(Rc::new(annotated));
}
fn enter(&mut self, terminal: &mut Terminal, text: String) {
let mut sources = terminal.sources();
let whole = sources.add(Self::input_name(self.entered + 1), &text);
let Ok(forms) = sources.read(whole) else {
self.entered += 1;
return;
};
for form in forms {
let input = self.next_input(terminal, text[form.span().range()].to_string());
self.interruption.clear();
if self.enter_input(terminal, input).is_interrupted() {
return self.acknowledge_interruption();
}
}
}
fn acknowledge_interruption(&self) {
self.interruption.acknowledge();
StandardError::write_line(Self::INTERRUPTED);
}
fn report_unfinished(&mut self, terminal: &mut Terminal, text: String) {
let input = self.next_input(terminal, text);
terminal.sources().read(input.source).ok();
}
fn next_input(&mut self, terminal: &mut Terminal, text: String) -> EnteredInput {
self.entered += 1;
let name = Self::input_name(self.entered);
let source = terminal.sources().add(&name, &text);
EnteredInput { name, text, source }
}
fn input_name(number: usize) -> String {
format!("<input {number}>")
}
fn enter_input(&mut self, terminal: &mut Terminal, input: EnteredInput) -> ExitStatus {
match self.classify(terminal, &input) {
ClassifiedInput::Expression => self.evaluate(terminal, input),
ClassifiedInput::Definition(definition, replacement) => {
self.define(terminal, definition, &replacement);
ExitStatus::Success
}
}
}
fn classify(&self, terminal: &mut Terminal, input: &EnteredInput) -> ClassifiedInput {
let form = Span::new(input.source, 0..input.text.len());
let mut replacement = Replacement::default();
loop {
let mut sources = self.buffer.sources_kept_by(&replacement);
sources.push(input.source);
let module = self.module.of(sources);
let ClassifiedForm::Definition(names) = terminal.classify(&module, form) else {
return ClassifiedInput::Expression;
};
match self.buffer.widened(&names, &replacement) {
Some(wider) => replacement = wider,
None => {
let buffered = BufferedInput::new(input.name.clone(), form, names);
return ClassifiedInput::Definition(buffered, replacement);
}
}
}
}
fn define(&mut self, terminal: &mut Terminal, input: BufferedInput, replacement: &Replacement) {
let rounds = DefinitionRounds::new(&self.module, &self.buffer, input, replacement);
match rounds.run(terminal) {
DefinitionOutcome::Accepted(accepted) => self.accept(terminal, accepted),
DefinitionOutcome::Rejected { errors, pending } => {
self.reject(terminal, &errors, pending)
}
}
}
fn accept(&mut self, terminal: &Terminal, accepted: AcceptedDefinition) {
self.annotations
.borrow_mut()
.commit(accepted.records, accepted.buffer.bound_names());
self.buffer = accepted.buffer;
self.loaders = accepted.loaders;
terminal.print_diagnostics(&accepted.report);
}
fn reject(&self, terminal: &Terminal, errors: &Diagnostics, pending: NameRecords) {
self.annotations.borrow_mut().hold_pending(pending);
terminal.report(errors);
self.annotations.borrow_mut().drop_pending();
}
fn evaluate(&self, terminal: &mut Terminal, input: EnteredInput) -> ExitStatus {
let module = self.module.of(self.buffer.sources());
self.locate_expression_errors(&input.name);
let forms = FormsText::named(input.name, input.text, None);
let status = EvalCommand::new(
ModuleText::Loaded(module.file.path().to_path_buf(), module.sources),
Some(self.module.root().to_path_buf()),
forms,
EvaluationSettings {
profile: self.profile,
output: Box::new(PrintedEvaluation::for_standard_output()),
},
)
.run(terminal);
self.annotations.borrow_mut().forget_expression();
Self::note_failed_program(terminal, status);
status
}
fn locate_expression_errors(&self, expression: &str) {
let locator = InputLocator::new(
self.buffer.sources(),
expression.to_string(),
self.loaders.clone(),
);
self.annotations.borrow_mut().locate_expression(locator);
}
fn note_failed_program(terminal: &Terminal, status: ExitStatus) {
let code = match status {
ExitStatus::Program(code @ 1..) => code,
ExitStatus::KilledBy(signal) if !signal.is_interrupt() => signal.shell_code(),
_ => return,
};
terminal.print_note(&format!("the program exited with code {code}"));
}
}