diff --git a/Cargo.lock b/Cargo.lock index 71c1a1d..53fb020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1120,7 +1120,9 @@ dependencies = [ "reqwest", "serde", "serde_derive", + "serde_json", "serenity", + "strip-ansi-escapes", ] [[package]] @@ -1319,6 +1321,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strip-ansi-escapes" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d63676e2abafa709460982ddc02a3bb586b6d15a49b75c212e06edd3933acee" +dependencies = [ + "vte", +] + [[package]] name = "syn" version = "1.0.24" @@ -1552,6 +1563,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "utf8parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" + [[package]] name = "uwl" version = "0.6.0" @@ -1570,6 +1587,15 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "vte" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f42f536e22f7fcbb407639765c8fd78707a33109301f834a594758bedd6e8cf" +dependencies = [ + "utf8parse", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 67f1650..0298eae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,12 @@ serenity = { version = "0.8.7", features = ["model"] } diesel = { version = "1.4.0", features = ["postgres", "r2d2"] } diesel_migrations = { version = "1.4.0", features = ["postgres"] } reqwest = { version = "0.10", features = ["blocking", "json"] } -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" serde_derive = "1.0" lazy_static = "1.4.0" log = "0.4.0" env_logger = "0.7.1" envy = "0.4" indexmap = "1.6" +strip-ansi-escapes = "0.1.0" # For normalizing godbolt responses \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index c0b08df..e6be5c8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -21,6 +21,40 @@ pub(crate) fn send_reply(args: &Args, message: &str) -> Result<(), Error> { Ok(()) } +/// Send a Discord reply message and truncate the message with a given truncation message if the +/// text is too long. +/// +/// Only `text_body` is truncated. `text_end` will always be appended at the end. This is useful +/// for example for large code blocks. You will want to truncate the code block contents, but the +/// finalizing \`\`\` should always stay - that's what `text_end` is for. +pub(crate) fn reply_potentially_long_text( + args: &Args, + text_body: &str, + text_end: &str, + truncation_msg: &str, +) -> Result<(), Error> { + let msg = if text_body.len() + text_end.len() > 2000 { + // This is how long the text body may be at max to conform to Discord's limit + let available_space = 2000 - text_end.len() - truncation_msg.len(); + + let mut cut_off_point = available_space; + while !text_body.is_char_boundary(cut_off_point) { + cut_off_point -= 1; + } + + format!( + "{}{}{}", + &text_body[..cut_off_point], + text_end, + truncation_msg + ) + } else { + format!("{}{}", text_body, text_end) + }; + + send_reply(args, &msg) +} + fn response_exists(args: &Args) -> Option { let data = args.cx.data.read(); let history = data.get::().unwrap(); diff --git a/src/crates.rs b/src/crates.rs index 5a46843..6cc6888 100644 --- a/src/crates.rs +++ b/src/crates.rs @@ -39,7 +39,7 @@ fn get_crate(args: &Args) -> Result, Error> { .send()? .json::()?; - Ok(crate_list.crates.into_iter().nth(0)) + Ok(crate_list.crates.into_iter().next()) } pub fn search(args: Args) -> Result<(), Error> { diff --git a/src/godbolt.rs b/src/godbolt.rs new file mode 100644 index 0000000..77b5c4a --- /dev/null +++ b/src/godbolt.rs @@ -0,0 +1,105 @@ +use crate::{api, commands::Args}; +pub enum Compilation { + Success { asm: String }, + Error { stderr: String }, +} + +#[derive(Debug, serde::Deserialize)] +struct GodboltOutputSegment { + text: String, +} + +#[derive(Debug, serde::Deserialize)] +struct GodboltOutput(Vec); + +impl GodboltOutput { + pub fn full_with_ansi_codes_stripped(&self) -> Result { + let mut complete_text = String::new(); + for segment in self.0.iter() { + complete_text.push_str(&segment.text); + complete_text.push_str("\n"); + } + Ok(String::from_utf8(strip_ansi_escapes::strip( + complete_text.trim(), + )?)?) + } +} + +#[derive(Debug, serde::Deserialize)] +struct GodboltResponse { + code: u8, + stdout: GodboltOutput, + stderr: GodboltOutput, + asm: GodboltOutput, +} + +pub fn help(args: Args) -> Result<(), crate::Error> { + let message = "Compile Rust code using . Full optimizations are applied unless overriden. +```?godbolt flags={} rustc={} ``\u{200B}`code``\u{200B}` ``` +Optional arguments: + \tflags: flags to pass to rustc invocation. Defaults to \"-Copt-level=3 --edition=2018\". + \trustc: compiler version to invoke. Defaults to `nightly`. Possible values: `nightly`, `beta` or full version like `1.45.2`. + "; + + api::send_reply(&args, &message)?; + Ok(()) +} + +/// Compile a given Rust source code file on Godbolt using the latest nightly compiler with +/// full optimizations (-O3) by default +/// Returns a multiline string with the pretty printed assembly +pub fn compile_rust_source( + http: &reqwest::blocking::Client, + source_code: &str, + flags: &str, + rustc: &str, +) -> Result { + let cv = rustc_to_godbolt(rustc); + let cv = match cv { + Ok(c) => c, + Err(e) => { + return Ok(Compilation::Error { stderr: e }); + } + }; + info!("cv: rustc {}", cv); + + let response: GodboltResponse = http + .execute( + http.post(&format!("https://godbolt.org/api/compiler/{}/compile", cv)) + .query(&[("options", &flags)]) + .header(reqwest::header::ACCEPT, "application/json") + .body(source_code.to_owned()) + .build()?, + )? + .json()?; + + info!("raw godbolt response: {:#?}", &response); + + Ok(if response.code == 0 { + Compilation::Success { + asm: response.asm.full_with_ansi_codes_stripped()?, + } + } else { + Compilation::Error { + stderr: response.stderr.full_with_ansi_codes_stripped()?, + } + }) +} + +// converts a rustc version number to a godbolt compiler id +fn rustc_to_godbolt(rustc_version: &str) -> Result { + match rustc_version { + "beta" => Ok("beta".to_string()), + "nightly" => Ok("nightly".to_string()), + // this heuristic is barebones but catches most obviously wrong things + // it doesn't know anything about valid rustc versions + ver if ver.contains('.') && !ver.contains(|c: char| c.is_alphabetic()) => { + let mut godbolt_version = "r".to_string(); + for segment in ver.split('.') { + godbolt_version.push_str(segment); + } + Ok(godbolt_version) + } + other => Err(format!("invalid rustc version: `{}`", other)), + } +} diff --git a/src/main.rs b/src/main.rs index 5ba6013..0b383b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod command_history; mod commands; mod crates; mod db; +mod godbolt; mod jobs; mod playground; mod schema; @@ -148,6 +149,35 @@ fn app() -> Result<(), Error> { }); } + cmds.add("?godbolt flags={} version={} ```\ncode```", |args| { + let flags = args + .params + .get("flags") + .unwrap_or(&"-Copt-level=3 --edition=2018"); + let rustc = args.params.get("rustc").unwrap_or(&"nightly"); + + let code = args + .params + .get("code") + .ok_or("Unable to retrieve param: code")?; + let (lang, text) = match godbolt::compile_rust_source(args.http, code, flags, rustc)? { + godbolt::Compilation::Success { asm } => ("x86asm", asm), + godbolt::Compilation::Error { stderr } => ("rust", stderr), + }; + + api::reply_potentially_long_text( + &args, + &format!("```{}\n{}", lang, text), + "\n```", + "Note: the output was truncated", + )?; + + Ok(()) + }); + cmds.help("?godbolt", "View assembly using Godbolt", |args| { + godbolt::help(args) + }); + // Slow mode. // 0 seconds disables slowmode cmds.add_protected("?slowmode {channel} {seconds}", api::slow_mode, api::is_mod);