diff --git a/Cargo.lock b/Cargo.lock index 2f7f60e..94f61d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,6 +102,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bumpalo" version = "3.16.0" @@ -132,7 +138,7 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "textwrap", "unicode-width", ] @@ -157,6 +163,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -345,6 +352,16 @@ dependencies = [ "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]] name = "getrandom" version = "0.2.15" @@ -450,6 +467,12 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -647,6 +670,19 @@ dependencies = [ "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]] name = "ryu" version = "1.0.18" @@ -775,6 +811,16 @@ dependencies = [ "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]] name = "textwrap" version = "0.11.0" diff --git a/bench/addictive_addition/addictive_addition.ds b/bench/addictive_addition/addictive_addition.ds index cba1b4c..8b6b2d2 100644 --- a/bench/addictive_addition/addictive_addition.ds +++ b/bench/addictive_addition/addictive_addition.ds @@ -1,5 +1,9 @@ let mut i = 0 while i < 5_000_000 { + + if i % 100000 == 0 { + write_line(i) + } i += 1 } diff --git a/dust-cli/Cargo.toml b/dust-cli/Cargo.toml index fab72b3..b1f3b83 100644 --- a/dust-cli/Cargo.toml +++ b/dust-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dust-cli" -description = "The Dust Programming Language CLI" +description = "Dust Programming Language CLI" authors = ["Jeff Anderson"] edition.workspace = true license.workspace = true @@ -13,7 +13,7 @@ name = "dust" path = "src/main.rs" [dependencies] -clap = { version = "4.5.14", features = ["derive"] } +clap = { version = "4.5.14", features = ["cargo", "color", "derive", "help", "wrap_help"] } colored = "2.1.0" dust-lang = { path = "../dust-lang" } env_logger = "0.11.5" diff --git a/dust-cli/src/main.rs b/dust-cli/src/main.rs index 53528a7..a7a54cb 100644 --- a/dust-cli/src/main.rs +++ b/dust-cli/src/main.rs @@ -1,77 +1,137 @@ -use std::io::{stdout, Write}; -use std::time::Instant; +use std::io::{self, stdout, Read, Write}; +use std::time::{Duration, Instant}; 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 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}; -const DEFAULT_PROGRAM_NAME: &str = "Dust CLI Input"; +const HELP_TEMPLATE: &str = "\ +{about} +{version} +{author} + +{usage-heading} +{usage} + +{all-args} +"; #[derive(Parser)] #[clap( - name = env!("CARGO_PKG_NAME"), - version = env!("CARGO_PKG_VERSION"), - author = env!("CARGO_PKG_AUTHORS"), - about = env!("CARGO_PKG_DESCRIPTION"), + version = crate_version!(), + author = crate_authors!(), + about = crate_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 { /// Log level, overrides the DUST_LOG environment variable - /// - /// Possible values: trace, debug, info, warn, error, off - #[arg(short, long, value_name = "LOG_LEVEL")] + #[arg( + short, + long, + value_name = "LOG_LEVEL", + value_parser = ["info", "trace", "debug"], + )] + #[clap(help_heading = Some("- Options"))] log: Option, + #[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)] - mode: ModeFlags, + mode: Modes, #[command(flatten)] source: Source, } #[derive(Args)] -#[group(multiple = false)] -struct ModeFlags { - /// Run the source code (default) - #[arg(short, long)] - run: bool, - - /// Print the time taken to compile and run the source code - #[arg(long, requires("run"))] +#[group(multiple = true, requires = "run")] +struct RunOptions { + /// Print the time taken for compilation and execution + #[arg(long)] + #[clap(help_heading = Some("- Run Options"))] time: bool, - #[arg(long, requires("run"))] /// Do not print the run result + #[arg(long)] + #[clap(help_heading = Some("- Run Options"))] no_output: bool, + /// Custom program name, overrides the file name + #[arg(long)] + #[clap(help_heading = Some("- Run Options"))] + program_name: Option, +} + +#[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 #[arg(short, long)] + #[clap(help_heading = Some("- Modes"))] disassemble: bool, /// Lex and display tokens from the source code #[arg(short, long)] + #[clap(help_heading = Some("- Modes"))] tokenize: bool, /// Style disassembly or tokenization output - #[arg( - short, - long, - default_value = "true", - requires("disassemble"), - requires("tokenize") - )] + #[arg(short, long, default_value = "true")] + #[clap(help_heading = Some("- Modes"))] style: bool, } -#[derive(Args)] +#[derive(Args, Clone)] #[group(required = true, multiple = false)] struct Source { - /// Source code - #[arg(short, long, value_name = "SOURCE")] + /// Source code to use instead of a file + #[arg(short, long, value_hint = ValueHint::Other, value_name = "SOURCE")] + #[clap(help_heading = Some("- Input"))] command: Option, + /// Read source code from stdin + #[arg(long)] + #[clap(help_heading = Some("- Input"))] + stdin: bool, + /// Path to a source code file + #[arg(value_hint = ValueHint::FilePath)] + #[clap(help_heading = Some("- Input"))] file: Option, } @@ -95,8 +155,13 @@ fn main() { let Cli { log, - source: Source { command, file }, + source: Source { + command, + file, + stdin, + }, mode, + .. } = Cli::parse(); if let Some(level) = log { @@ -105,29 +170,56 @@ fn main() { logger.parse_env("DUST_LOG").init(); } - let source = if let Some(source) = command { - source + let (source, file_name) = if let Some(source) = command { + (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 { 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 { - let chunk = match compile(&source) { - Ok(chunk) => chunk, + let lexer = Lexer::new(&source); + let mut compiler = match Compiler::new(lexer) { + Ok(compiler) => compiler, Err(error) => { - eprintln!("{}", error); + handle_compile_error(error, &source); return; } }; + + match compiler.compile() { + Ok(()) => {} + Err(error) => { + handle_compile_error(error, &source); + + return; + } + } + + let chunk = compiler.finish(program_name); let mut stdout = stdout().lock(); chunk .disassembler(&mut stdout) .style(mode.style) .source(&source) + .width(70) .disassemble() .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(); + if mode.run_options.time { + print_time(compile_end); + } + let vm = Vm::new(chunk); let return_value = vm.run(); let run_end = start_time.elapsed(); if let Some(value) = return_value { - if !mode.no_output { + if !mode.run_options.no_output { println!("{}", value) } } - if mode.time { - let compile_time = compile_end.as_micros(); + if mode.run_options.time { let run_time = run_end - compile_end; - println!( - "Compile time: {compile_time}µs Run time: {}s{}ms{}µs", - run_time.as_secs(), - run_time.subsec_millis(), - run_time.subsec_micros() - ); + print_time(run_time); + } +} + +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"); + } } } diff --git a/dust-lang/src/chunk/mod.rs b/dust-lang/src/chunk/mod.rs index 07a91a0..ec67719 100644 --- a/dust-lang/src/chunk/mod.rs +++ b/dust-lang/src/chunk/mod.rs @@ -3,7 +3,7 @@ //! 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 //! 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. //! //! 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 //! 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 -//! 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 local; mod scope;