pub(crate) mod command;
pub(crate) mod editor;
mod highlight;
pub(crate) mod multiline;
pub(crate) mod render;
use std::io::{BufRead, IsTerminal};
use std::path::{Path, PathBuf};
use logicaffeine_compile::ReplSession;
use logicaffeine_language::{CompileOptions, Session};
use crate::commands::logic::{collect_readings, LogicFormat};
use crate::ui;
use command::{parse_meta, MetaCommand, Mode, HELP};
use multiline::{input_state, InputState};
use render::{colorize_fol, render_vars};
#[derive(Debug, PartialEq, Eq)]
enum Action {
Continue,
Quit,
}
pub(crate) struct ReplState {
mode: Mode,
imperative: ReplSession,
logic: Session,
format: LogicFormat,
discourse: bool,
last_sentence: Option<String>,
color: bool,
}
impl ReplState {
fn new(start_in_logic: bool, format: LogicFormat, color: bool) -> Self {
ReplState {
mode: if start_in_logic { Mode::Logic } else { Mode::Imperative },
imperative: ReplSession::new(),
logic: Session::with_format(format.into()),
format,
discourse: true,
last_sentence: None,
color,
}
}
fn handle(&mut self, input: &str) -> Action {
let input = input.trim_end();
if input.trim().is_empty() {
return Action::Continue;
}
if let Some(meta) = parse_meta(input) {
return match meta {
Ok(cmd) => self.dispatch(cmd),
Err(msg) => {
print_error(&msg);
Action::Continue
}
};
}
match self.mode {
Mode::Imperative => self.eval_imperative(input),
Mode::Logic => self.eval_logic(input),
}
Action::Continue
}
fn eval_imperative(&mut self, input: &str) {
let outcome = self.imperative.eval_sync(input);
for line in &outcome.new_lines {
anstream::println!("{line}");
}
if let Some(err) = outcome.error {
print_error(&err);
}
}
fn eval_logic(&mut self, sentence: &str) {
if !self.discourse {
self.logic = Session::with_format(self.format.into());
}
match self.logic.eval(sentence) {
Ok(fol) => {
anstream::println!("{}", colorize_fol(&fol, self.color));
self.last_sentence = Some(sentence.to_string());
}
Err(e) => print_error(&format!("could not parse: {e:?}")),
}
}
fn dispatch(&mut self, cmd: MetaCommand) -> Action {
match cmd {
MetaCommand::Quit => return Action::Quit,
MetaCommand::Help => anstream::println!("{HELP}"),
MetaCommand::ModeShow => anstream::println!(
"{}",
match self.mode {
Mode::Imperative => "imperative",
Mode::Logic => "logic",
}
),
MetaCommand::ModeSet(mode) => self.mode = mode,
MetaCommand::Format(format) => {
self.format = format;
self.logic.set_format(format.into());
}
MetaCommand::Readings => match &self.last_sentence {
Some(sentence) => {
let options =
CompileOptions { format: self.format.into(), pragmatic: false };
let readings = collect_readings(sentence, options);
if readings.is_empty() {
print_error("no readings for the last sentence");
}
for (i, r) in readings.iter().enumerate() {
anstream::println!("{}. {}", i + 1, colorize_fol(r, self.color));
}
}
None => print_error("no sentence yet — say something in logic mode first"),
},
MetaCommand::Discourse(on) => {
self.discourse = on;
if !on {
self.logic = Session::with_format(self.format.into());
}
}
MetaCommand::Reset => match self.mode {
Mode::Imperative => self.imperative.reset(),
Mode::Logic => {
self.logic = Session::with_format(self.format.into());
self.last_sentence = None;
}
},
MetaCommand::Vars => {
anstream::println!("{}", render_vars(&self.imperative.vars()));
}
MetaCommand::Type(name) => {
match self.imperative.vars().iter().find(|(n, _, _)| *n == name) {
Some((_, ty, _)) => anstream::println!("{name} : {ty}"),
None => print_error(&format!("`{name}` is not bound")),
}
}
MetaCommand::Program => anstream::print!("{}", self.imperative.source()),
MetaCommand::Explain(word) => {
anstream::println!("{}", render::render_explain(&word, self.color));
}
MetaCommand::Save(path) => self.save(&path),
MetaCommand::Load(path) => self.load(&path),
}
Action::Continue
}
fn save(&self, path: &Path) {
let content = match self.mode {
Mode::Imperative => self.imperative.source(),
Mode::Logic => self.logic.history(),
};
match std::fs::write(path, content) {
Ok(()) => anstream::println!("Saved {}", path.display()),
Err(e) => print_error(&format!("cannot save {}: {e}", path.display())),
}
}
fn load(&mut self, path: &Path) {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
print_error(&format!("cannot load {}: {e}", path.display()));
return;
}
};
match self.mode {
Mode::Imperative => {
let outcome = self.imperative.load_source(&source);
for line in &outcome.new_lines {
anstream::println!("{line}");
}
if let Some(err) = outcome.error {
print_error(&err);
}
}
Mode::Logic => {
for line in source.lines().map(str::trim).filter(|l| !l.is_empty()) {
self.eval_logic(line);
}
}
}
}
}
fn print_error(msg: &str) {
anstream::eprintln!("{}error:{:#} {msg}", ui::ERROR_STYLE, ui::ERROR_STYLE);
}
pub(crate) fn cmd_repl(
logic: bool,
format: Option<LogicFormat>,
load: Option<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let interactive = std::io::stdin().is_terminal();
let mut state = ReplState::new(logic, format.unwrap_or_default(), interactive);
if let Some(path) = load {
state.load(&path);
}
if interactive {
editor::run_editor(&mut state)
} else {
run_pipe(&mut state)
}
}
fn run_pipe(state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
let stdin = std::io::stdin();
let mut buffer = String::new();
for line in stdin.lock().lines() {
let line = line?;
if buffer.is_empty() && line.trim().is_empty() {
continue;
}
buffer.push_str(&line);
buffer.push('\n');
if input_state(state.mode, &buffer) == InputState::Complete {
let input = std::mem::take(&mut buffer);
if state.handle(&input) == Action::Quit {
return Ok(());
}
}
}
if !buffer.trim().is_empty() {
state.handle(&buffer);
}
Ok(())
}