From de6f1c68c6d7d88cfc74f5c64531c87b6297608b Mon Sep 17 00:00:00 2001 From: kangalioo Date: Fri, 29 Jan 2021 00:01:44 +0100 Subject: [PATCH 1/4] Add Godbolt command --- Cargo.lock | 26 +++++++++++++++++++++ Cargo.toml | 4 +++- src/godbolt.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/godbolt.rs 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/godbolt.rs b/src/godbolt.rs new file mode 100644 index 0000000..fb90b26 --- /dev/null +++ b/src/godbolt.rs @@ -0,0 +1,63 @@ +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, +} + +/// Compile a given Rust source code file on Godbolt using the latest nightly compiler with +/// full optimizations (-O3) +/// Returns a multiline string with the pretty printed assembly +pub fn compile_rust_source( + http: &reqwest::blocking::Client, + source_code: &str, +) -> Result { + let response: GodboltResponse = http + .execute( + http.post("https://godbolt.org/api/compiler/nightly/compile") + .query(&[("options", "-Copt-level=3")]) + .header(reqwest::header::ACCEPT, "application/json") + .body(source_code.to_owned()) + .build()?, + )? + .json()?; + + dbg!(&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()?, + } + }) +} diff --git a/src/main.rs b/src/main.rs index 5ba6013..1d43ebe 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,34 @@ fn app() -> Result<(), Error> { }); } + cmds.add("?godbolt ```\ncode```", |args| { + let code = args + .params + .get("code") + .ok_or("Unable to retrieve param: code")?; + let (lang, text) = match godbolt::compile_rust_source(args.http, code)? { + godbolt::Compilation::Success { asm } => ("x86asm", asm), + godbolt::Compilation::Error { stderr } => ("rust", stderr), + }; + + reply_potentially_long_text( + &args, + &format!("```{}\n{}", lang, text), + "\n```", + "Note: the output was truncated", + )?; + + Ok(()) + }); + cmds.help("?godbolt", "View assembly using Godbolt", |args| { + api::send_reply( + &args, + "Compile Rust code using https://rust.godbolt.org. Full optimizations are applied. \ + ```?godbolt ``\u{200B}`code``\u{200B}` ```", + )?; + Ok(()) + }); + // Slow mode. // 0 seconds disables slowmode cmds.add_protected("?slowmode {channel} {seconds}", api::slow_mode, api::is_mod); @@ -202,6 +231,40 @@ fn app() -> 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. +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) + }; + + api::send_reply(args, &msg) +} + fn main_menu(args: &Args, commands: &IndexMap<&str, (&str, GuardFn)>) -> String { let mut menu = format!("Commands:\n"); From 2d029fa8624710e94a232d95c47947914a905e2c Mon Sep 17 00:00:00 2001 From: tinaun Date: Tue, 2 Feb 2021 15:12:42 -0500 Subject: [PATCH 2/4] dbg! -> info! --- src/godbolt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/godbolt.rs b/src/godbolt.rs index fb90b26..63bd255 100644 --- a/src/godbolt.rs +++ b/src/godbolt.rs @@ -49,7 +49,7 @@ pub fn compile_rust_source( )? .json()?; - dbg!(&response); + info!("raw godbolt response: {:#?}", &response); Ok(if response.code == 0 { Compilation::Success { From f11853643f9e73b6a5290256d6718abee2c60ac7 Mon Sep 17 00:00:00 2001 From: tinaun Date: Thu, 17 Jun 2021 17:04:51 -0400 Subject: [PATCH 3/4] add arguments to godbolt --- src/crates.rs | 2 +- src/godbolt.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 17 +++++++++-------- 3 files changed, 55 insertions(+), 12 deletions(-) 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 index 63bd255..77b5c4a 100644 --- a/src/godbolt.rs +++ b/src/godbolt.rs @@ -1,3 +1,4 @@ +use crate::{api, commands::Args}; pub enum Compilation { Success { asm: String }, Error { stderr: String }, @@ -32,17 +33,40 @@ struct GodboltResponse { 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) +/// 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("https://godbolt.org/api/compiler/nightly/compile") - .query(&[("options", "-Copt-level=3")]) + 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()?, @@ -61,3 +85,21 @@ pub fn compile_rust_source( } }) } + +// 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 1d43ebe..1aa459d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,12 +149,18 @@ fn app() -> Result<(), Error> { }); } - cmds.add("?godbolt ```\ncode```", |args| { + 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)? { + 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), }; @@ -169,12 +175,7 @@ fn app() -> Result<(), Error> { Ok(()) }); cmds.help("?godbolt", "View assembly using Godbolt", |args| { - api::send_reply( - &args, - "Compile Rust code using https://rust.godbolt.org. Full optimizations are applied. \ - ```?godbolt ``\u{200B}`code``\u{200B}` ```", - )?; - Ok(()) + godbolt::help(args) }); // Slow mode. From af19a248b7d27f892aeb80b661335aa184b21b9d Mon Sep 17 00:00:00 2001 From: tinaun Date: Fri, 18 Jun 2021 11:13:54 -0400 Subject: [PATCH 4/4] move `reply_long` to api --- src/api.rs | 34 ++++++++++++++++++++++++++++++++++ src/main.rs | 36 +----------------------------------- 2 files changed, 35 insertions(+), 35 deletions(-) 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/main.rs b/src/main.rs index 1aa459d..0b383b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -165,7 +165,7 @@ fn app() -> Result<(), Error> { godbolt::Compilation::Error { stderr } => ("rust", stderr), }; - reply_potentially_long_text( + api::reply_potentially_long_text( &args, &format!("```{}\n{}", lang, text), "\n```", @@ -232,40 +232,6 @@ fn app() -> 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. -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) - }; - - api::send_reply(args, &msg) -} - fn main_menu(args: &Args, commands: &IndexMap<&str, (&str, GuardFn)>) -> String { let mut menu = format!("Commands:\n");