1
0

Snazz up the CLI

This commit is contained in:
Jeff 2024-12-17 22:39:22 -05:00
parent d7289414f4
commit cee9f0d95c
5 changed files with 216 additions and 54 deletions

48
Cargo.lock generated
View File

@ -102,6 +102,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@ -132,7 +138,7 @@ version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
] ]
@ -157,6 +163,7 @@ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim", "strsim",
"terminal_size",
] ]
[[package]] [[package]]
@ -345,6 +352,16 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "errno"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -450,6 +467,12 @@ version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@ -647,6 +670,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -775,6 +811,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "terminal_size"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [
"rustix",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"

View File

@ -1,5 +1,9 @@
let mut i = 0 let mut i = 0
while i < 5_000_000 { while i < 5_000_000 {
if i % 100000 == 0 {
write_line(i)
}
i += 1 i += 1
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "dust-cli" name = "dust-cli"
description = "The Dust Programming Language CLI" description = "Dust Programming Language CLI"
authors = ["Jeff Anderson"] authors = ["Jeff Anderson"]
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -13,7 +13,7 @@ name = "dust"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
clap = { version = "4.5.14", features = ["derive"] } clap = { version = "4.5.14", features = ["cargo", "color", "derive", "help", "wrap_help"] }
colored = "2.1.0" colored = "2.1.0"
dust-lang = { path = "../dust-lang" } dust-lang = { path = "../dust-lang" }
env_logger = "0.11.5" env_logger = "0.11.5"

View File

@ -1,77 +1,137 @@
use std::io::{stdout, Write}; use std::io::{self, stdout, Read, Write};
use std::time::Instant; use std::time::{Duration, Instant};
use std::{fs::read_to_string, path::PathBuf}; use std::{fs::read_to_string, path::PathBuf};
use clap::{Args, Parser}; use clap::builder::StyledStr;
use clap::{
builder::{styling::AnsiColor, Styles},
ArgAction, Args, ColorChoice, Parser, ValueHint,
};
use clap::{crate_authors, crate_description, crate_version};
use colored::Colorize; use colored::Colorize;
use dust_lang::{compile, CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm}; use dust_lang::{CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm};
use log::{Level, LevelFilter}; use log::{Level, LevelFilter};
const DEFAULT_PROGRAM_NAME: &str = "Dust CLI Input"; const HELP_TEMPLATE: &str = "\
{about}
{version}
{author}
{usage-heading}
{usage}
{all-args}
";
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
name = env!("CARGO_PKG_NAME"), version = crate_version!(),
version = env!("CARGO_PKG_VERSION"), author = crate_authors!(),
author = env!("CARGO_PKG_AUTHORS"), about = crate_description!(),
about = env!("CARGO_PKG_DESCRIPTION"), term_width = 80,
color = ColorChoice::Auto,
styles = Styles::styled()
.header(AnsiColor::BrightMagenta.on_default().bold())
.usage(AnsiColor::BrightWhite.on_default().bold())
.literal(AnsiColor::BrightCyan.on_default())
.placeholder(AnsiColor::BrightGreen.on_default())
.error(AnsiColor::BrightRed.on_default().bold())
.valid(AnsiColor::Blue.on_default())
.invalid(AnsiColor::BrightRed.on_default()),
disable_help_flag = true,
disable_version_flag = true,
help_template = StyledStr::from(HELP_TEMPLATE.bright_white().bold().to_string()),
)] )]
struct Cli { struct Cli {
/// Log level, overrides the DUST_LOG environment variable /// Log level, overrides the DUST_LOG environment variable
/// #[arg(
/// Possible values: trace, debug, info, warn, error, off short,
#[arg(short, long, value_name = "LOG_LEVEL")] long,
value_name = "LOG_LEVEL",
value_parser = ["info", "trace", "debug"],
)]
#[clap(help_heading = Some("- Options"))]
log: Option<LevelFilter>, log: Option<LevelFilter>,
#[arg(short, long, action = ArgAction::Help)]
#[clap(help_heading = Some("- Options"))]
help: bool,
#[arg(short, long, action = ArgAction::Version)]
#[clap(help_heading = Some("- Options"))]
version: bool,
#[command(flatten)] #[command(flatten)]
mode: ModeFlags, mode: Modes,
#[command(flatten)] #[command(flatten)]
source: Source, source: Source,
} }
#[derive(Args)] #[derive(Args)]
#[group(multiple = false)] #[group(multiple = true, requires = "run")]
struct ModeFlags { struct RunOptions {
/// Run the source code (default) /// Print the time taken for compilation and execution
#[arg(short, long)] #[arg(long)]
run: bool, #[clap(help_heading = Some("- Run Options"))]
/// Print the time taken to compile and run the source code
#[arg(long, requires("run"))]
time: bool, time: bool,
#[arg(long, requires("run"))]
/// Do not print the run result /// Do not print the run result
#[arg(long)]
#[clap(help_heading = Some("- Run Options"))]
no_output: bool, no_output: bool,
/// Custom program name, overrides the file name
#[arg(long)]
#[clap(help_heading = Some("- Run Options"))]
program_name: Option<DustString>,
}
#[derive(Args)]
#[group(multiple = false)]
struct Modes {
/// Run the source code (default)
///
/// Use the RUN OPTIONS to control this mode
#[arg(short, long, default_value = "true")]
#[clap(help_heading = Some("- Modes"))]
run: bool,
#[command(flatten)]
run_options: RunOptions,
/// Compile a chunk and show the disassembly /// Compile a chunk and show the disassembly
#[arg(short, long)] #[arg(short, long)]
#[clap(help_heading = Some("- Modes"))]
disassemble: bool, disassemble: bool,
/// Lex and display tokens from the source code /// Lex and display tokens from the source code
#[arg(short, long)] #[arg(short, long)]
#[clap(help_heading = Some("- Modes"))]
tokenize: bool, tokenize: bool,
/// Style disassembly or tokenization output /// Style disassembly or tokenization output
#[arg( #[arg(short, long, default_value = "true")]
short, #[clap(help_heading = Some("- Modes"))]
long,
default_value = "true",
requires("disassemble"),
requires("tokenize")
)]
style: bool, style: bool,
} }
#[derive(Args)] #[derive(Args, Clone)]
#[group(required = true, multiple = false)] #[group(required = true, multiple = false)]
struct Source { struct Source {
/// Source code /// Source code to use instead of a file
#[arg(short, long, value_name = "SOURCE")] #[arg(short, long, value_hint = ValueHint::Other, value_name = "SOURCE")]
#[clap(help_heading = Some("- Input"))]
command: Option<String>, command: Option<String>,
/// Read source code from stdin
#[arg(long)]
#[clap(help_heading = Some("- Input"))]
stdin: bool,
/// Path to a source code file /// Path to a source code file
#[arg(value_hint = ValueHint::FilePath)]
#[clap(help_heading = Some("- Input"))]
file: Option<PathBuf>, file: Option<PathBuf>,
} }
@ -95,8 +155,13 @@ fn main() {
let Cli { let Cli {
log, log,
source: Source { command, file }, source: Source {
command,
file,
stdin,
},
mode, mode,
..
} = Cli::parse(); } = Cli::parse();
if let Some(level) = log { if let Some(level) = log {
@ -105,29 +170,56 @@ fn main() {
logger.parse_env("DUST_LOG").init(); logger.parse_env("DUST_LOG").init();
} }
let source = if let Some(source) = command { let (source, file_name) = if let Some(source) = command {
source (source, None)
} else if stdin {
let mut source = String::new();
io::stdin()
.read_to_string(&mut source)
.expect("Failed to read from stdin");
(source, None)
} else { } else {
let path = file.expect("Path is required when command is not provided"); let path = file.expect("Path is required when command is not provided");
let source = read_to_string(&path).expect("Failed to read file");
let file_name = path
.file_name()
.and_then(|os_str| os_str.to_str())
.map(DustString::from);
read_to_string(path).expect("Failed to read file") (source, file_name)
}; };
let program_name = mode.run_options.program_name.or(file_name);
if mode.disassemble { if mode.disassemble {
let chunk = match compile(&source) { let lexer = Lexer::new(&source);
Ok(chunk) => chunk, let mut compiler = match Compiler::new(lexer) {
Ok(compiler) => compiler,
Err(error) => { Err(error) => {
eprintln!("{}", error); handle_compile_error(error, &source);
return; return;
} }
}; };
match compiler.compile() {
Ok(()) => {}
Err(error) => {
handle_compile_error(error, &source);
return;
}
}
let chunk = compiler.finish(program_name);
let mut stdout = stdout().lock(); let mut stdout = stdout().lock();
chunk chunk
.disassembler(&mut stdout) .disassembler(&mut stdout)
.style(mode.style) .style(mode.style)
.source(&source) .source(&source)
.width(70)
.disassemble() .disassemble()
.expect("Failed to write disassembly to stdout"); .expect("Failed to write disassembly to stdout");
@ -198,29 +290,49 @@ fn main() {
} }
} }
let chunk = compiler.finish(Some(DEFAULT_PROGRAM_NAME)); let chunk = compiler.finish(program_name);
let compile_end = start_time.elapsed(); let compile_end = start_time.elapsed();
if mode.run_options.time {
print_time(compile_end);
}
let vm = Vm::new(chunk); let vm = Vm::new(chunk);
let return_value = vm.run(); let return_value = vm.run();
let run_end = start_time.elapsed(); let run_end = start_time.elapsed();
if let Some(value) = return_value { if let Some(value) = return_value {
if !mode.no_output { if !mode.run_options.no_output {
println!("{}", value) println!("{}", value)
} }
} }
if mode.time { if mode.run_options.time {
let compile_time = compile_end.as_micros();
let run_time = run_end - compile_end; let run_time = run_end - compile_end;
println!( print_time(run_time);
"Compile time: {compile_time}µs Run time: {}s{}ms{}µs", }
run_time.as_secs(), }
run_time.subsec_millis(),
run_time.subsec_micros() fn print_time(instant: Duration) {
); let seconds = instant.as_secs_f64();
match seconds {
..=0.001 => {
println!(
"Compile time: {microseconds} microseconds",
microseconds = seconds * 1_000_000.0
);
}
..=0.1 => {
println!(
"Compile time: {milliseconds} milliseconds",
milliseconds = seconds * 1000.0
);
}
_ => {
println!("Compile time: {seconds} seconds");
}
} }
} }

View File

@ -3,7 +3,7 @@
//! A chunk is output by the compiler to represent all of the information needed to execute a Dust //! A chunk is output by the compiler to represent all of the information needed to execute a Dust
//! program. In addition to the program itself, each function in the source is compiled into its own //! program. In addition to the program itself, each function in the source is compiled into its own
//! chunk and stored in the `prototypes` field of its parent. Thus, a chunk is also the //! chunk and stored in the `prototypes` field of its parent. Thus, a chunk is also the
//! representation of a function prototype, i.e. a function declaration as opposed to an individual //! representation of a function prototype, i.e. a function declaration, as opposed to an individual
//! instance. //! instance.
//! //!
//! Chunks have a name when they belong to a named function. They also have a type, so the input //! Chunks have a name when they belong to a named function. They also have a type, so the input
@ -12,7 +12,7 @@
//! cannot be instantiated directly and must be created by the compiler. However, when the Rust //! cannot be instantiated directly and must be created by the compiler. However, when the Rust
//! compiler is in the "test" configuration (used for all types of test), [`Chunk::with_data`] can //! compiler is in the "test" configuration (used for all types of test), [`Chunk::with_data`] can
//! be used to create a chunk for comparison to the compiler output. Do not try to run these chunks //! be used to create a chunk for comparison to the compiler output. Do not try to run these chunks
//! in a virtual machine. Due to their missing stack size, they will cause a panic. //! in a virtual machine. Due to their missing stack size and record index, they will cause a panic.
mod disassembler; mod disassembler;
mod local; mod local;
mod scope; mod scope;