1
0
dust/dust-cli/src/main.rs

398 lines
9.6 KiB
Rust
Raw Normal View History

2025-01-01 21:53:54 -05:00
use std::{
fs::read_to_string,
io::{self, stdout, Read},
path::PathBuf,
time::{Duration, Instant},
};
2024-11-17 20:32:53 -05:00
2024-12-17 22:39:22 -05:00
use clap::{
2025-01-09 01:52:30 -05:00
builder::{styling::AnsiColor, Styles},
2025-01-01 21:53:54 -05:00
crate_authors, crate_description, crate_version,
error::ErrorKind,
Args, ColorChoice, Error, Parser, Subcommand, ValueHint,
2024-12-17 22:39:22 -05:00
};
use color_print::{cformat, cstr};
2024-12-17 22:39:22 -05:00
use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm};
2025-01-01 21:53:54 -05:00
use tracing::{subscriber::set_global_default, Level};
2024-12-21 13:20:57 -05:00
use tracing_subscriber::FmtSubscriber;
2024-11-17 20:32:53 -05:00
const ABOUT: &str = cstr!(
2024-12-18 08:49:45 -05:00
r#"
<bright-magenta,bold>Dust CLI
</>
2024-12-26 15:30:23 -05:00
{about}
2024-12-18 08:49:45 -05:00
<bold> Version:</> {version}
<bold>🦀 Author:</> {author}
<bold> License:</> GPL-3.0
<bold>🔬 Repository:</> https://git.jeffa.io/jeff/dust
2024-12-18 08:49:45 -05:00
"#
);
2024-12-18 06:00:42 -05:00
const PLAIN_ABOUT: &str = r#"
2024-12-26 15:30:23 -05:00
{about}
"#;
2024-12-18 06:00:42 -05:00
const USAGE: &str = cstr!(
r#"
<bright-magenta,bold>Usage:</> {usage}
"#
);
2024-12-18 08:49:45 -05:00
const SUBCOMMANDS: &str = cstr!(
r#"
<bright-magenta,bold>Modes:</>
{subcommands}
"#
);
2024-12-18 06:00:42 -05:00
const OPTIONS: &str = cstr!(
r#"
<bright-magenta,bold>Options:</>
2024-12-18 08:49:45 -05:00
{options}
"#
2024-12-18 06:00:42 -05:00
);
const CREATE_MAIN_HELP_TEMPLATE: fn() -> String =
|| cformat!("{ABOUT}{USAGE}{SUBCOMMANDS}{OPTIONS}");
const CREATE_MODE_HELP_TEMPLATE: fn(&str) -> String = |title| {
cformat!(
"\
<bright-magenta,bold>{title}\n</>\
{PLAIN_ABOUT}{USAGE}{OPTIONS}
"
)
};
2024-12-18 06:00:42 -05:00
const STYLES: Styles = Styles::styled()
.literal(AnsiColor::Cyan.on_default())
.placeholder(AnsiColor::Cyan.on_default())
.valid(AnsiColor::BrightCyan.on_default())
.invalid(AnsiColor::BrightYellow.on_default())
.error(AnsiColor::BrightRed.on_default());
2024-11-17 20:32:53 -05:00
#[derive(Parser)]
2024-12-01 21:17:22 -05:00
#[clap(
2024-12-17 22:39:22 -05:00
version = crate_version!(),
author = crate_authors!(),
about = crate_description!(),
color = ColorChoice::Auto,
help_template = CREATE_MAIN_HELP_TEMPLATE(),
2024-12-18 06:00:42 -05:00
styles = STYLES,
2024-12-01 21:17:22 -05:00
)]
2024-11-17 20:32:53 -05:00
struct Cli {
2024-12-18 08:49:45 -05:00
/// Overrides the DUST_LOG environment variable
2024-12-21 13:20:57 -05:00
#[arg(
short,
long,
value_parser = |input: &str| match input.to_uppercase().as_str() {
"TRACE" => Ok(Level::TRACE),
"DEBUG" => Ok(Level::DEBUG),
"INFO" => Ok(Level::INFO),
"WARN" => Ok(Level::WARN),
"ERROR" => Ok(Level::ERROR),
_ => Err(Error::new(ErrorKind::ValueValidation)),
}
)]
log_level: Option<Level>,
2024-12-04 07:38:24 -05:00
2024-12-18 08:49:45 -05:00
#[command(subcommand)]
2024-12-21 13:20:57 -05:00
mode: Option<Mode>,
#[command(flatten)]
run: Run,
2024-12-18 08:49:45 -05:00
}
#[derive(Args)]
struct Input {
2024-12-18 06:00:42 -05:00
/// Source code to run instead of a file
2024-12-18 08:49:45 -05:00
#[arg(short, long, value_hint = ValueHint::Other, value_name = "INPUT")]
command: Option<String>,
2024-11-17 20:32:53 -05:00
2024-12-17 22:39:22 -05:00
/// Read source code from stdin
#[arg(long)]
stdin: bool,
2024-12-01 23:20:27 -05:00
/// Path to a source code file
2024-12-17 22:39:22 -05:00
#[arg(value_hint = ValueHint::FilePath)]
2024-12-01 23:20:27 -05:00
file: Option<PathBuf>,
2024-12-01 21:17:22 -05:00
}
2024-11-17 20:32:53 -05:00
2024-12-21 13:20:57 -05:00
/// Compile and run the program (default)
#[derive(Args)]
#[command(
short_flag = 'r',
help_template = CREATE_MODE_HELP_TEMPLATE("Run Mode")
2024-12-21 13:20:57 -05:00
)]
struct Run {
/// Print the time taken for compilation and execution
#[arg(long)]
time: bool,
2024-12-18 06:00:42 -05:00
2024-12-21 13:20:57 -05:00
/// Do not print the program's return value
#[arg(long)]
no_output: bool,
2024-12-18 06:00:42 -05:00
2024-12-21 13:20:57 -05:00
/// Custom program name, overrides the file name
#[arg(long)]
name: Option<DustString>,
2024-12-18 08:49:45 -05:00
2024-12-21 13:20:57 -05:00
#[command(flatten)]
input: Input,
}
#[derive(Subcommand)]
#[clap(subcommand_value_name = "MODE", flatten_help = true)]
enum Mode {
Run(Run),
2024-12-18 06:00:42 -05:00
/// Compile and print the bytecode disassembly
2024-12-18 08:49:45 -05:00
#[command(
short_flag = 'd',
help_template = CREATE_MODE_HELP_TEMPLATE("Disassemble Mode")
2024-12-18 08:49:45 -05:00
)]
2024-12-18 06:00:42 -05:00
Disassemble {
/// Style disassembly output
#[arg(short, long, default_value = "true")]
style: bool,
/// Custom program name, overrides the file name
#[arg(long)]
name: Option<DustString>,
2024-12-18 08:49:45 -05:00
#[command(flatten)]
input: Input,
2024-12-18 06:00:42 -05:00
},
/// Lex the source code and print the tokens
2024-12-18 08:49:45 -05:00
#[command(
short_flag = 't',
help_template = CREATE_MODE_HELP_TEMPLATE("Tokenize Mode")
2024-12-18 08:49:45 -05:00
)]
2024-12-18 06:00:42 -05:00
Tokenize {
/// Style token output
#[arg(short, long, default_value = "true")]
style: bool,
2024-12-18 08:49:45 -05:00
#[command(flatten)]
input: Input,
2024-12-18 06:00:42 -05:00
},
}
2024-12-18 08:49:45 -05:00
fn get_source_and_file_name(input: Input) -> (String, Option<DustString>) {
if let Some(path) = input.file {
let source = read_to_string(&path).expect("Failed to read source file");
let file_name = path
.file_name()
.and_then(|os_str| os_str.to_str())
.map(DustString::from);
return (source, file_name);
}
2024-12-18 06:00:42 -05:00
2024-12-18 08:49:45 -05:00
if input.stdin {
2024-12-17 22:39:22 -05:00
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.expect("Failed to read from stdin");
2024-12-18 08:49:45 -05:00
return (source, None);
}
let source = input.command.expect("No source code provided");
(source, None)
}
2024-11-17 20:32:53 -05:00
2024-12-18 08:49:45 -05:00
fn main() {
let start_time = Instant::now();
2024-12-21 13:20:57 -05:00
let Cli {
log_level,
mode,
run,
} = Cli::parse();
let mode = mode.unwrap_or(Mode::Run(run));
let subscriber = FmtSubscriber::builder()
.with_max_level(log_level)
.with_thread_names(true)
2025-01-01 21:53:54 -05:00
.with_file(false)
2024-12-21 13:20:57 -05:00
.finish();
set_global_default(subscriber).expect("Failed to set tracing subscriber");
2024-11-17 20:32:53 -05:00
2024-12-18 08:49:45 -05:00
if let Mode::Disassemble { style, name, input } = mode {
let (source, file_name) = get_source_and_file_name(input);
2024-12-17 22:39:22 -05:00
let lexer = Lexer::new(&source);
2025-01-08 04:21:01 -05:00
let program_name = name.or(file_name);
let mut compiler = match Compiler::new(lexer, program_name, true) {
2024-12-17 22:39:22 -05:00
Ok(compiler) => compiler,
Err(error) => {
2024-12-17 22:39:22 -05:00
handle_compile_error(error, &source);
2024-12-01 21:17:22 -05:00
return;
2024-11-17 20:32:53 -05:00
}
2024-12-01 21:17:22 -05:00
};
2024-12-17 22:39:22 -05:00
match compiler.compile() {
Ok(()) => {}
Err(error) => {
handle_compile_error(error, &source);
return;
}
}
2025-01-08 04:21:01 -05:00
let chunk = compiler.finish();
let mut stdout = stdout().lock();
chunk
.disassembler(&mut stdout)
2024-12-21 13:20:57 -05:00
.width(65)
2024-12-18 06:00:42 -05:00
.style(style)
2024-12-02 01:20:05 -05:00
.source(&source)
.disassemble()
.expect("Failed to write disassembly to stdout");
2024-12-01 21:17:22 -05:00
return;
2024-11-17 20:32:53 -05:00
}
2024-12-18 08:49:45 -05:00
if let Mode::Tokenize { input, .. } = mode {
let (source, _) = get_source_and_file_name(input);
let mut lexer = Lexer::new(&source);
let mut next_token = || -> Option<(Token, Span, bool)> {
match lexer.next_token() {
Ok((token, position)) => Some((token, position, lexer.is_eof())),
Err(error) => {
let report = DustError::compile(CompileError::Lex(error), &source).report();
2024-12-01 21:17:22 -05:00
eprintln!("{report}");
None
}
2024-12-01 21:17:22 -05:00
}
};
println!("{:^66}", "Tokens");
for _ in 0..66 {
print!("-");
}
println!();
println!("{:^21}|{:^22}|{:^22}", "Kind", "Value", "Position");
for _ in 0..66 {
print!("-");
}
println!();
while let Some((token, position, is_eof)) = next_token() {
if is_eof {
break;
}
let token_kind = token.kind().to_string();
let token = token.to_string();
let position = position.to_string();
println!("{token_kind:^21}|{token:^22}|{position:^22}");
}
return;
2024-11-17 20:32:53 -05:00
}
2024-12-04 13:31:02 -05:00
2024-12-21 13:20:57 -05:00
if let Mode::Run(Run {
2024-12-18 08:49:45 -05:00
time,
no_output,
2024-12-21 13:20:57 -05:00
name,
2024-12-18 08:49:45 -05:00
input,
2024-12-21 13:20:57 -05:00
}) = mode
2024-12-18 06:00:42 -05:00
{
2024-12-18 08:49:45 -05:00
let (source, file_name) = get_source_and_file_name(input);
2024-12-18 06:00:42 -05:00
let lexer = Lexer::new(&source);
2025-01-08 04:21:01 -05:00
let program_name = name.or(file_name);
let mut compiler = match Compiler::new(lexer, program_name, true) {
2024-12-18 06:00:42 -05:00
Ok(compiler) => compiler,
Err(error) => {
handle_compile_error(error, &source);
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
return;
}
};
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
match compiler.compile() {
Ok(()) => {}
Err(error) => {
handle_compile_error(error, &source);
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
return;
}
2024-12-10 16:04:36 -05:00
}
2024-12-04 13:31:02 -05:00
2025-01-08 04:21:01 -05:00
let chunk = compiler.finish();
2024-12-18 06:00:42 -05:00
let compile_end = start_time.elapsed();
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
let vm = Vm::new(chunk);
let return_value = vm.run();
let run_end = start_time.elapsed();
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
if let Some(value) = return_value {
if !no_output {
println!("{}", value)
}
2024-12-04 13:31:02 -05:00
}
2024-12-10 16:04:36 -05:00
2024-12-18 06:00:42 -05:00
if time {
let run_time = run_end - compile_end;
2025-01-04 02:56:46 -05:00
let total_time = compile_end + run_time;
2024-12-10 16:04:36 -05:00
2025-01-04 02:56:46 -05:00
print_time("Compile Time", compile_end);
print_time("Run Time", run_time);
print_time("Total Time", total_time);
2024-12-18 06:00:42 -05:00
}
2024-12-17 22:39:22 -05:00
}
}
2025-01-04 02:56:46 -05:00
fn print_time(phase: &str, instant: Duration) {
2024-12-17 22:39:22 -05:00
let seconds = instant.as_secs_f64();
match seconds {
..=0.001 => {
println!(
2025-01-04 02:56:46 -05:00
"{phase:12}: {microseconds}µs",
microseconds = (seconds * 1_000_000.0).round()
2024-12-17 22:39:22 -05:00
);
}
2025-01-04 02:56:46 -05:00
..=0.199 => {
2024-12-17 22:39:22 -05:00
println!(
2025-01-04 02:56:46 -05:00
"{phase:12}: {milliseconds}ms",
milliseconds = (seconds * 1000.0).round()
2024-12-17 22:39:22 -05:00
);
}
_ => {
2025-01-04 02:56:46 -05:00
println!("{phase:12}: {seconds}s");
2024-12-17 22:39:22 -05:00
}
2024-12-10 16:04:36 -05:00
}
}
fn handle_compile_error(error: CompileError, source: &str) {
let dust_error = DustError::compile(error, source);
let report = dust_error.report();
eprintln!("{report}");
2024-11-17 20:32:53 -05:00
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}