Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Godbolt Command #77

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MessageId> {
let data = args.cx.data.read();
let history = data.get::<CommandHistory>().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion src/crates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn get_crate(args: &Args) -> Result<Option<Crate>, Error> {
.send()?
.json::<Crates>()?;

Ok(crate_list.crates.into_iter().nth(0))
Ok(crate_list.crates.into_iter().next())
}

pub fn search(args: Args) -> Result<(), Error> {
Expand Down
105 changes: 105 additions & 0 deletions src/godbolt.rs
Original file line number Diff line number Diff line change
@@ -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<GodboltOutputSegment>);

impl GodboltOutput {
pub fn full_with_ansi_codes_stripped(&self) -> Result<String, crate::Error> {
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 <https://rust.godbolt.org/>. 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<Compilation, crate::Error> {
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<String, String> {
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)),
}
}
30 changes: 30 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod command_history;
mod commands;
mod crates;
mod db;
mod godbolt;
mod jobs;
mod playground;
mod schema;
Expand Down Expand Up @@ -148,6 +149,35 @@ fn app() -> Result<(), Error> {
});
}

cmds.add("?godbolt flags={} version={} ```\ncode```", |args| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to not use a closure here unless its necessary. Please move these into a fn and pass the fn pointer here instead.

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);
Expand Down