diff --git a/assyst-common/src/markdown.rs b/assyst-common/src/markdown.rs index e0de1df..7c984e1 100644 --- a/assyst-common/src/markdown.rs +++ b/assyst-common/src/markdown.rs @@ -152,6 +152,8 @@ pub fn parse_codeblock(input: String) -> String { let new = r.split(" ").skip(1).collect::>(); let joined = new.join(" "); joined[..joined.len() - 3].to_owned() + } else if input.trim().starts_with("`") && input.trim().ends_with("`") { + input[1..input.len() - 1].to_owned() } else { input } diff --git a/assyst-core/Cargo.toml b/assyst-core/Cargo.toml index fe2bf5d..3714a5d 100644 --- a/assyst-core/Cargo.toml +++ b/assyst-core/Cargo.toml @@ -38,3 +38,5 @@ rand = "0.8.5" assyst-tag = { path = "../assyst-tag" } urlencoding = "2.1.3" twilight-util = { version = "=0.16.0-rc.1", features = ["builder"] } +dash_vm = { git = "https://github.com/y21/dash" } +dash_rt = { git = "https://github.com/y21/dash" } diff --git a/assyst-core/src/command/flags.rs b/assyst-core/src/command/flags.rs index 4841e1c..51739df 100644 --- a/assyst-core/src/command/flags.rs +++ b/assyst-core/src/command/flags.rs @@ -81,6 +81,8 @@ flag_parse_argument! { RustFlags } #[derive(Default)] pub struct LangFlags { pub verbose: bool, + pub llir: bool, + pub opt: u64, } impl FlagDecode for LangFlags { fn from_str(input: &str) -> anyhow::Result @@ -89,10 +91,22 @@ impl FlagDecode for LangFlags { { let mut valid_flags = HashMap::new(); valid_flags.insert("verbose", FlagType::NoValue); + valid_flags.insert("llir", FlagType::NoValue); + valid_flags.insert("opt", FlagType::WithValue); let raw_decode = flags_from_str(input, valid_flags)?; + let opt = raw_decode + .get("opt") + .map(|x| x.as_deref()) + .flatten() + .map(|x| x.parse::()) + .unwrap_or(Ok(0)) + .context("Failed to parse optimisation level")?; + let result = Self { verbose: raw_decode.get("verbose").is_some(), + llir: raw_decode.get("llir").is_some(), + opt, }; Ok(result) diff --git a/assyst-core/src/command/group.rs b/assyst-core/src/command/group.rs index fe80b5b..398441a 100644 --- a/assyst-core/src/command/group.rs +++ b/assyst-core/src/command/group.rs @@ -61,7 +61,8 @@ macro_rules! define_commandgroup { #[::async_trait::async_trait] impl crate::command::Command for [<$groupname _command>] { fn metadata(&self) -> &'static crate::command::CommandMetadata { - static META: crate::command::CommandMetadata = crate::command::CommandMetadata { + static META: std::sync::OnceLock = std::sync::OnceLock::new(); + META.get_or_init(|| crate::command::CommandMetadata { access: $crate::defaults!(access $($access)?), category: $category, aliases: $crate::defaults!(aliases $(&$aliases)?), @@ -72,8 +73,8 @@ macro_rules! define_commandgroup { age_restricted: $crate::defaults!(age_restricted $($age_restricted)?), usage: $crate::defaults!(usage $($usage)?), send_processing: $crate::defaults!(send_processing $($send_processing)?), - }; - &META + flag_descriptions: std::collections::HashMap::new() + }) } fn subcommands(&self) -> Option<&'static [(&'static str, crate::command::TCommand)]> { diff --git a/assyst-core/src/command/misc/help.rs b/assyst-core/src/command/misc/help.rs index 3abfc11..054ba53 100644 --- a/assyst-core/src/command/misc/help.rs +++ b/assyst-core/src/command/misc/help.rs @@ -43,7 +43,10 @@ pub async fn help(ctxt: CommandCtxt<'_>, labels: Vec) -> anyhow::Result<() if let Some(Word(base_command)) = labels.next() { // if the base is a command if let Some(mut command) = find_command_by_name(&base_command) { + let mut meta = command.metadata(); + let mut usage = format!("{}{}", "Usage: ".fg_yellow(), ctxt.data.calling_prefix); + let mut name_fmt = meta.name.to_owned(); // For better error reporting, store the "chain of commands" (e.g. `-t create`) let mut command_chain = command.metadata().name.to_owned(); @@ -74,13 +77,45 @@ pub async fn help(ctxt: CommandCtxt<'_>, labels: Vec) -> anyhow::Result<() command_chain += " "; command_chain += command.metadata().name; } - usage += command.metadata().name; + + meta = command.metadata(); + + usage += meta.name; usage += " "; - usage += command.metadata().usage; + usage += meta.usage; - let meta = command.metadata(); + name_fmt += " "; + name_fmt += meta.name; + + let flags_format = if !meta.flag_descriptions.is_empty() { + format!( + "\n{}", + meta.flag_descriptions + .iter() + .map(|(x, y)| { format!("--{}: {}", x, y) }) + .collect::>() + .join("\n") + ) + } else { + "None".to_owned() + }; + let flags = "Flags:".fg_cyan() + &flags_format; - let name_fmt = (meta.name.to_owned() + ":").fg_green(); + let examples_format = if !meta.examples.is_empty() { + format!( + "\n{}", + meta.examples + .iter() + .map(|x| { format!("{}{} {}", ctxt.data.calling_prefix, name_fmt, x) }) + .collect::>() + .join("\n") + ) + } else { + "None".to_owned() + }; + let examples = "Examples: ".fg_cyan() + &examples_format; + + name_fmt = (name_fmt.to_owned() + ":").fg_green(); let description = meta.description; let aliases = "Aliases: ".fg_yellow() + &(if !meta.aliases.is_empty() { @@ -100,23 +135,9 @@ pub async fn help(ctxt: CommandCtxt<'_>, labels: Vec) -> anyhow::Result<() String::new() }; - let examples_format = if !meta.examples.is_empty() { - format!( - "\n{}", - meta.examples - .iter() - .map(|x| { format!("{}{} {}", ctxt.data.calling_prefix, meta.name, x) }) - .collect::>() - .join("\n") - ) - } else { - "None".to_owned() - }; - let examples = "Examples: ".fg_cyan() + &examples_format; - ctxt.reply( format!( - "{name_fmt} {description}\n\n{aliases}\n{cooldown}\n{access}\n{usage}{subcommands}\n\n{examples}" + "{name_fmt} {description}\n\n{aliases}\n{cooldown}\n{access}\n{usage}{subcommands}\n\n{examples}\n\n{flags}" ) .trim() .codeblock("ansi"), diff --git a/assyst-core/src/command/misc/run.rs b/assyst-core/src/command/misc/run.rs index 36e144e..670cdad 100644 --- a/assyst-core/src/command/misc/run.rs +++ b/assyst-core/src/command/misc/run.rs @@ -1,69 +1,132 @@ -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::process::{ExitCode, ExitStatus}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use anyhow::bail; use assyst_common::markdown::Markdown; use assyst_common::util::process::{exec_sync, CommandOutput}; use assyst_proc_macro::command; +use dash_rt::format_value; +use dash_vm::eval::EvalError; +use dash_vm::value::Root; +use dash_vm::Vm; use crate::command::arguments::Codeblock; use crate::command::flags::{LangFlags, RustFlags}; +use crate::command::messagebuilder::{Attachment, MessageBuilder}; use crate::command::{Availability, Category, CommandCtxt}; use crate::define_commandgroup; use crate::rest::rust::{run_benchmark, run_binary, run_clippy, run_godbolt, run_miri, OptimizationLevel}; +/* +struct TempDrop(String); +impl Drop for TempDrop { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(self.0.clone()); + } +}*/ + #[command( description = "execute some lang", cooldown = Duration::from_millis(100), access = Availability::Dev, category = Category::Misc, - usage = "[script] ", + usage = "[script] ", examples = ["1"], - send_processing = true + send_processing = true, + flag_descriptions = [ + ("verbose", "Get verbose output"), + ("llir", "Output LLVM IR"), + ("opt [level:0|1|2|3]", "Set optimisation level of LLVM") + ] )] pub async fn lang(ctxt: CommandCtxt<'_>, script: Codeblock, flags: LangFlags) -> anyhow::Result<()> { - let dir = format!( - "/tmp/lang/{}", - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() - ); + let dir = "/tmp/lang".to_owned(); - exec_sync(&format!("git clone https://github.com/y21/lang.git {dir}"))?; - std::fs::write(format!("{dir}/input"), script.0)?; + //#[allow(unused)] + //let dir_temp_drop = TempDrop(dir.clone()); + if std::fs::metadata(format!("{dir}/.git")).is_err() { + std::fs::remove_dir_all(&dir)?; + exec_sync(&format!("git clone https://github.com/y21/lang.git {dir} --depth=0"))?; + }; + + exec_sync(&format!("cd {dir} && git pull"))?; + std::fs::write(format!("{dir}/input"), script.0)?; exec_sync(&format!("cd {dir} && npm i --save-dev @types/node && tsc"))?; - let result = exec_sync(&format!( - "cd {dir} && node . input {}", - if flags.verbose { "--verbose" } else { "" } - ))?; - let bin_result = if std::fs::metadata(format!("{dir}/a.out")).is_ok() { - Some(exec_sync(&format!("cd {dir} && ./a.out"))?) - } else { - None - }; + let commit_hash = exec_sync(&format!("cd {dir} && git rev-parse HEAD")) + .map(|x| x.stdout[..8].to_owned()) + .unwrap_or("Unknown".to_owned()); - let stdout = result.stdout + "\n" + &bin_result.clone().unwrap_or(CommandOutput::default()).stdout; - let stderr = result.stderr + "\n" + &bin_result.clone().unwrap_or(CommandOutput::default()).stderr; + let mut flags_string = String::new(); + if flags.verbose { + flags_string += "--verbose" + }; - let mut output = "".to_owned(); - if !stdout.trim().is_empty() { - output = format!("`stdout`: ```ansi\n{}```\n", stdout); + if flags.llir { + flags_string += " --print-llir-only --no-timings" } - if !stderr.trim().is_empty() { - output = format!("{}`stderr`: ```ansi\n{}```", output, stderr); - } - output.push_str(&format!( - "\nCompiler: {}\nExecutable: {}", - result.exit_code, - if let Some(b) = bin_result { - b.exit_code.to_string() + flags_string += &format!(" -O{}", flags.opt); + + let result = exec_sync(&format!("cd {dir} && node . input {}", flags_string.trim()))?; + + if !flags.llir { + let bin_start = Instant::now(); + let bin_result = if std::fs::metadata(format!("{dir}/a.out")).is_ok() { + Some(exec_sync(&format!("cd {dir} && ./a.out"))?) } else { - "N/A".to_owned() + None + }; + let bin_time = bin_start.elapsed(); + + let stdout = result.stdout + "\n" + &bin_result.clone().unwrap_or(CommandOutput::default()).stdout; + let stderr = result.stderr + "\n" + &bin_result.clone().unwrap_or(CommandOutput::default()).stderr; + + let mut output = "".to_owned(); + if !stdout.trim().is_empty() { + output = format!("`stdout`: {}\n", stdout.codeblock("ansi")); } - )); - // todo: delete `dir` + if !stderr.trim().is_empty() { + output = format!("{}`stderr`: {}\n", output, stderr.codeblock("ansi")); + } - ctxt.reply(output).await?; + output.push_str(&format!( + "\nCompiler: {}\nExecutable: {}\nCommit Hash: {commit_hash}", + result.exit_code, + if let Some(b) = bin_result { + format!("{} (execution time {:?})", b.exit_code.to_string(), bin_time) + } else { + "N/A".to_owned() + } + )); + + ctxt.reply(output).await?; + } else { + let stdout = result.stdout; + let stderr = result.stderr; + + if result.exit_code.code() != Some(0) { + ctxt.reply(format!("Compilation failed: {}", stderr.codeblock(""))) + .await?; + } else { + if stdout.split("\n").count() < 100 { + ctxt.reply(format!("{}", stdout.codeblock("llvm"))).await?; + } else { + ctxt.reply(MessageBuilder { + content: None, + attachment: Some(Attachment { + name: "out.txt".into(), + data: stdout.as_bytes().to_vec(), + }), + }) + .await?; + } + } + } + + // todo: delete `dir` Ok(()) } @@ -73,9 +136,16 @@ pub async fn lang(ctxt: CommandCtxt<'_>, script: Codeblock, flags: LangFlags) -> cooldown = Duration::from_millis(100), access = Availability::Public, category = Category::Misc, - usage = "[script] ", - examples = ["println!(\"Hello World!\")"], - send_processing = true + usage = "[script] ", + examples = ["println!(\"Hello, world!\")"], + send_processing = true, + flag_descriptions = [ + ("miri", "Run code in miri debugger"), + ("asm", "Output ASM of Rust code"), + ("clippy", "Lint code using Clippy"), + ("bench", "Run code as a benchmark"), + ("release", "Run code in release mode") + ] )] pub async fn rust(ctxt: CommandCtxt<'_>, script: Codeblock, flags: RustFlags) -> anyhow::Result<()> { let opt = if flags.release { @@ -99,6 +169,47 @@ pub async fn rust(ctxt: CommandCtxt<'_>, script: Codeblock, flags: RustFlags) -> ctxt.reply(result.format().codeblock("rs")).await } +#[command( + description = "execute some dash", + cooldown = Duration::from_millis(100), + access = Availability::Dev, + category = Category::Misc, + usage = "[script]", + examples = ["\"Hello, world!\""], + send_processing = true +)] +pub async fn dash(ctxt: CommandCtxt<'_>, script: Codeblock) -> anyhow::Result<()> { + let str_result = { + let mut vm = Vm::new(Default::default()); + let result = vm.eval(&script.0, Default::default()); + let mut scope = vm.scope(); + match result { + Ok(result) => { + let fmt = format_value(result.root(&mut scope), &mut scope); + if let Ok(f) = fmt { + f.to_string() + } else { + format!("{:?}", fmt.unwrap_err()) + } + }, + Err(err) => match err { + EvalError::Exception(unrooted) => { + let fmt = format_value(unrooted.root(&mut scope), &mut scope); + if let Ok(f) = fmt { + format!("Exception: {}", f.to_string()) + } else { + format!("Exception: {:?}", fmt.unwrap_err()) + } + }, + EvalError::Middle(middle) => format!("Middle error: {:?}", middle), + }, + } + }; + + ctxt.reply(str_result).await?; + Ok(()) +} + define_commandgroup! { name: run, access: Availability::Public, @@ -107,6 +218,7 @@ define_commandgroup! { usage: "[language/runtime] [code] <...flags>", commands: [ "lang" => lang, - "rust" => rust + "rust" => rust, + "dash" => dash ] } diff --git a/assyst-core/src/command/misc/stats.rs b/assyst-core/src/command/misc/stats.rs index e92d8f1..c541c30 100644 --- a/assyst-core/src/command/misc/stats.rs +++ b/assyst-core/src/command/misc/stats.rs @@ -2,7 +2,9 @@ use std::time::Duration; use assyst_common::ansi::Ansi; use assyst_common::markdown::Markdown; -use assyst_common::util::process::{get_processes_cpu_usage, get_processes_mem_usage, get_processes_uptimes}; +use assyst_common::util::process::{ + exec_sync, get_processes_cpu_usage, get_processes_mem_usage, get_processes_uptimes, +}; use assyst_proc_macro::command; use human_bytes::human_bytes; use twilight_model::gateway::SessionStartLimit; @@ -97,6 +99,9 @@ pub async fn stats(ctxt: CommandCtxt<'_>, option: Option) -> anyhow::Resul fn get_general_stats(ctxt: &CommandCtxt<'_>) -> String { let events_rate = ctxt.assyst().metrics_handler.get_events_rate().to_string(); let commands_rate = ctxt.assyst().metrics_handler.get_commands_rate().to_string(); + let commit = exec_sync("git rev-parse head") + .map(|x| x.stdout[..8].to_owned()) + .unwrap_or("Unknown".to_string()); let stats_table = key_value(&[ ( @@ -106,6 +111,7 @@ pub async fn stats(ctxt: CommandCtxt<'_>, option: Option) -> anyhow::Resul ("Shards".fg_cyan(), ctxt.assyst().shard_count.to_string()), ("Events".fg_cyan(), events_rate + "/sec"), ("Commands".fg_cyan(), commands_rate + "/min"), + ("Commit Hash".fg_cyan(), commit), ]); stats_table.codeblock("ansi") diff --git a/assyst-core/src/command/mod.rs b/assyst-core/src/command/mod.rs index 5655ebc..720a171 100644 --- a/assyst-core/src/command/mod.rs +++ b/assyst-core/src/command/mod.rs @@ -103,6 +103,7 @@ pub struct CommandMetadata { /// or to send a prelim response to an interaction (a.k.a., Assyst is thinking...) pub send_processing: bool, pub age_restricted: bool, + pub flag_descriptions: HashMap<&'static str, &'static str>, } #[derive(Debug)] diff --git a/assyst-core/src/command/services/mod.rs b/assyst-core/src/command/services/mod.rs index b34b766..39c14f9 100644 --- a/assyst-core/src/command/services/mod.rs +++ b/assyst-core/src/command/services/mod.rs @@ -57,9 +57,13 @@ pub async fn r34(ctxt: CommandCtxt<'_>, tags: Rest) -> anyhow::Result<()> { access = Availability::Public, cooldown = Duration::from_secs(2), category = Category::Services, - usage = "[url] ", - examples = ["https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://www.youtube.com/watch?v=dQw4w9WgXcQ audio", "https://www.youtube.com/watch?v=dQw4w9WgXcQ 480"], - send_processing = true + usage = "[url] ", + examples = ["https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://www.youtube.com/watch?v=dQw4w9WgXcQ --audio", "https://www.youtube.com/watch?v=dQw4w9WgXcQ --quality 480"], + send_processing = true, + flag_descriptions = [ + ("audio", "Get content as MP3"), + ("quality [quality:144|240|360|480|720|1080|max]", "Set resolution of output"), + ] )] pub async fn download(ctxt: CommandCtxt<'_>, url: Word, options: DownloadFlags) -> anyhow::Result<()> { let opts = WebDownloadOpts::from_download_flags(options); diff --git a/assyst-proc-macro/src/lib.rs b/assyst-proc-macro/src/lib.rs index 8c0aedb..b0073d1 100644 --- a/assyst-proc-macro/src/lib.rs +++ b/assyst-proc-macro/src/lib.rs @@ -8,8 +8,7 @@ use quote::{quote, quote_spanned, ToTokens}; use syn::punctuated::Punctuated; use syn::token::Bracket; use syn::{ - parse_macro_input, Expr, ExprArray, ExprLit, FnArg, Ident, Item, Lit, LitBool, LitStr, Meta, Pat, PatType, Token, - Type, + parse_macro_input, parse_quote, Expr, ExprArray, ExprLit, FnArg, Ident, Item, Lit, LitBool, LitStr, Meta, Pat, PatType, Token, Type }; struct CommandAttributes(syn::punctuated::Punctuated); @@ -110,6 +109,7 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { let usage = fields.remove("usage").expect("missing usage"); let send_processing = fields.remove("send_processing").unwrap_or_else(false_expr); let age_restricted = fields.remove("age_restricted").unwrap_or_else(false_expr); + let flag_descriptions = fields.remove("flag_descriptions").unwrap_or_else(empty_array_expr); let following = quote::quote! { pub struct #struct_name; @@ -117,7 +117,14 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { #[::async_trait::async_trait] impl crate::command::Command for #struct_name { fn metadata(&self) -> &'static crate::command::CommandMetadata { - static META: crate::command::CommandMetadata = crate::command::CommandMetadata { + use std::collections::HashMap; + let mut descriptions = HashMap::new(); + for (k, v) in #flag_descriptions { + descriptions.insert(k, v); + } + + static META: std::sync::OnceLock = std::sync::OnceLock::new(); + META.get_or_init(|| crate::command::CommandMetadata { description: #description, cooldown: #cooldown, access: #access, @@ -127,9 +134,9 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { examples: &#examples, usage: #usage, send_processing: #send_processing, - age_restricted: #age_restricted - }; - &META + age_restricted: #age_restricted, + flag_descriptions: descriptions + }) } fn subcommands(&self) -> Option<&'static [(&'static str, crate::command::TCommand)]> { @@ -245,6 +252,10 @@ fn empty_array_expr() -> Expr { }) } +fn empty_hashmap_expr() -> Expr { + parse_quote!{ HashMap::new() } +} + fn false_expr() -> Expr { Expr::Lit(ExprLit { attrs: Vec::new(),