use irust_repl::{DEFAULT_EVALUATOR, EvalConfig, EvalResult, Repl};
use serde::{Deserialize, Serialize};
use serde_json::Deserializer;
use std::{
io::{self, Read},
sync::OnceLock,
};
mod log;
use log::init_log;
#[derive(Serialize, Deserialize, Debug)]
enum Message {
Execute { code: String },
Complete { code: String, cursor_pos: usize },
}
#[derive(Serialize, Deserialize)]
enum Action {
Eval { value: String, mime_type: MimeType },
Insert,
AddDependencyStream { output_chunk: String },
AddDependencyEnd,
}
#[derive(Debug, Serialize, Deserialize)]
enum MimeType {
#[serde(rename = "text/plain")]
PlainText,
#[serde(rename = "text/html")]
Html,
#[serde(rename = "image/png")]
Png,
#[serde(rename = "image/jpeg")]
Jpeg,
}
impl MimeType {
fn from_str(mime_type: &str) -> Self {
match mime_type {
"text/plain" => Self::PlainText,
"text/html" => Self::Html,
"image/png" => Self::Png,
"image/jpeg" => Self::Jpeg,
_ => Self::PlainText,
}
}
}
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
init_log(
std::env::temp_dir().to_path_buf().join("irust-kernel.log"),
"IRUST_KERNEL_DEBUG",
);
let stdin = io::stdin();
let reader = stdin.lock();
let deserializer = Deserializer::from_reader(reader).into_iter::<Message>();
let mut repl = Repl::default();
log!("Starting REPL");
for json in deserializer {
let result = (|| -> Result<()> {
log!("Received message: {:?}", json);
let message = json?;
match message {
Message::Execute { code } => execute(&mut repl, code),
Message::Complete { code, cursor_pos } => complete(&mut repl, code, cursor_pos),
}
})();
if result.is_err() {
eprintln!("An error occurred: {result:?}");
println!("{{}}"); }
}
Ok(())
}
fn complete(_repl: &mut Repl, _code: String, _cursor_poss: usize) -> Result<()> {
Ok(())
}
fn execute(repl: &mut Repl, code: String) -> Result<()> {
let mut code = code.trim();
if code.starts_with("//") && code.contains("!irust") {
code = code
.split_once("!irust")
.map(|x| x.1)
.expect("checked")
.trim();
}
if code.ends_with(';') || is_a_statement(code) {
let EvalResult { output, status } = repl.eval_check(code.to_owned())?;
if !status.success() {
let output = serde_json::to_string(&Action::Eval {
value: format_err(&output, false, &repl.cargo.name),
mime_type: MimeType::PlainText,
})?;
println!("{output}");
return Ok(());
}
repl.insert(code);
let output = serde_json::to_string(&Action::Insert)?;
println!("{output}");
} else if code.starts_with(":add") {
let cargo_add_arg = code
.strip_prefix(":add")
.expect("checked")
.split_whitespace()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
{
let mut process = repl.cargo.cargo_add(&cargo_add_arg)?;
let mut stderr = process.stderr.take().expect("piped");
let mut buf = [0; 512];
log!("Adding dependencies");
loop {
let n = stderr.read(&mut buf)?;
log!("Read {n} bytes");
if n == 0 {
break;
}
let output = serde_json::to_string(&Action::AddDependencyStream {
output_chunk: String::from_utf8_lossy(&buf[..n]).to_string(),
})?;
println!("{output}");
}
}
log!("Dependencies added");
repl.cargo.cargo_build(repl.toolchain())?;
let output = serde_json::to_string(&Action::AddDependencyEnd)?;
println!("{output}");
return Ok(());
} else {
let EvalResult {
output: value,
status,
} = repl.eval_with_configuration(EvalConfig {
input: code,
interactive_function: None,
color: true,
evaluator: &*DEFAULT_EVALUATOR,
compile_mode: irust_repl::CompileMode::Debug,
})?;
if !status.success() {
let output = serde_json::to_string(&Action::Eval {
value: format_err(&value, false, &repl.cargo.name),
mime_type: MimeType::PlainText,
})?;
println!("{output}");
return Ok(());
}
if value.starts_with("EVCXR_BEGIN_CONTENT") {
let data = value.strip_prefix("EVCXR_BEGIN_CONTENT").expect("checked");
let data = &data[..data.find("EVCXR_END_CONTENT").ok_or("malformed content")?];
let mut data = data.chars();
let mime_type = data
.by_ref()
.skip_while(|c| c.is_whitespace())
.take_while(|c| !c.is_whitespace())
.collect::<String>();
let output = serde_json::to_string(&Action::Eval {
value: data.collect(),
mime_type: MimeType::from_str(&mime_type),
})?;
println!("{output}");
return Ok(());
}
let output = serde_json::to_string(&Action::Eval {
value,
mime_type: MimeType::PlainText,
})?;
println!("{output}");
}
Ok(())
}
pub fn is_a_statement(buffer_trimmed: &str) -> bool {
match buffer_trimmed
.split_whitespace()
.collect::<Vec<_>>()
.as_slice()
{
[_, "fn", ..]
| ["fn", ..]
| ["enum", ..]
| ["struct", ..]
| ["trait", ..]
| ["impl", ..]
| ["pub", ..]
| ["extern", ..]
| ["macro", ..] => true,
["macro_rules!", ..] => true,
[tag, ..] if tag.starts_with('#') => true,
_ => false,
}
}
static NO_COLOR: OnceLock<bool> = OnceLock::new();
fn no_color() -> bool {
*NO_COLOR.get_or_init(|| std::env::var("NO_COLOR").is_ok())
}
pub fn format_err<'a>(original_output: &'a str, show_warnings: bool, repl_name: &str) -> String {
const BEFORE_2021_END_TAG: &str = ": aborting due to ";
const ERROR_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[38;5;9merror";
const WARNING_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[33mwarning";
const ERROR_TAG_NO_COLOR: &str = "error[";
const WARNING_TAG_NO_COLOR: &str = "warning: ";
let go_to_start = |output: &'a str| -> Vec<&'a str> {
if show_warnings {
output
.lines()
.skip_while(|line| !line.contains(&format!("{repl_name} v0.1.0")))
.skip(1)
.collect()
} else {
output
.lines()
.skip_while(|line| {
if no_color() {
!line.starts_with(ERROR_TAG_NO_COLOR)
} else {
!line.starts_with(ERROR_TAG)
}
})
.collect()
}
};
let go_to_end = |output: Box<dyn Iterator<Item = &str>>| -> String {
if show_warnings {
output
} else {
Box::new(output.take_while(|line| {
if no_color() {
!line.starts_with(WARNING_TAG_NO_COLOR)
} else {
!line.starts_with(WARNING_TAG)
}
}))
}
.collect::<Vec<_>>()
.join("\n")
};
let handle_error = |output: &'a str| {
go_to_start(output)
.into_iter()
.take_while(|line| !line.contains(BEFORE_2021_END_TAG))
};
let handle_error_2021 = |output: &'a str| {
go_to_start(output)
.into_iter()
.rev()
.skip_while(|line| !line.is_empty())
.collect::<Vec<_>>()
.into_iter()
.rev()
};
let output: Box<dyn Iterator<Item = &str>> = if original_output.contains(BEFORE_2021_END_TAG) {
Box::new(handle_error(original_output))
} else {
Box::new(handle_error_2021(original_output))
};
let formatted_error = go_to_end(output);
if !formatted_error.is_empty() {
formatted_error
} else {
format!(
"IRust: failed to format the error output.\nThis is a bug in IRust.\nFeel free to open a bug-report at https://github.com/sigmaSd/IRust/issues/new with the next text:\n\noriginal_output:\n{original_output}"
)
}
}