From 843eabf18d83d6628e17a8717c76a20c37b1d086 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 24 Mar 2026 09:48:07 +0100 Subject: [PATCH 1/4] =?UTF-8?q?feat(cli):=20add=20run=20phase=20timings=20?= =?UTF-8?q?=E2=8F=B1=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ndc_bin/src/main.rs | 60 +++++++++++++++++++++++++++-- ndc_interpreter/src/lib.rs | 71 +++++++++++++++++++++++++---------- ndc_interpreter/src/timing.rs | 47 +++++++++++++++++++++++ 3 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 ndc_interpreter/src/timing.rs diff --git a/ndc_bin/src/main.rs b/ndc_bin/src/main.rs index bc89e30..768e642 100644 --- a/ndc_bin/src/main.rs +++ b/ndc_bin/src/main.rs @@ -4,9 +4,10 @@ use crate::docs::docs; use anyhow::{Context, anyhow}; use clap::{Parser, Subcommand}; use highlighter::AndycppHighlighter; -use ndc_interpreter::Interpreter; +use ndc_interpreter::{ExecutionTimings, Interpreter}; use std::path::PathBuf; use std::process; +use std::time::Duration; use std::{fs, io::Write}; mod diagnostic; @@ -32,6 +33,9 @@ enum Command { /// Execute an .ndc file or start the repl (this default action may be omitted) Run { file: Option, + /// Print total time spent in each interpreter phase + #[arg(long)] + time: bool, /// Print each instruction as it is dispatched #[cfg(feature = "trace")] #[arg(long)] @@ -78,6 +82,7 @@ impl Default for Command { fn default() -> Self { Self::Run { file: None, + time: false, #[cfg(feature = "trace")] trace_print: false, #[cfg(feature = "trace")] @@ -94,6 +99,7 @@ enum Action { RunLsp, RunFile { path: PathBuf, + time: bool, #[cfg(feature = "trace")] trace_print: bool, #[cfg(feature = "trace")] @@ -119,6 +125,7 @@ impl TryFrom for Action { let action = match value { Command::Run { file: Some(file), + time, #[cfg(feature = "trace")] trace_print, #[cfg(feature = "trace")] @@ -129,6 +136,7 @@ impl TryFrom for Action { trace_span, } => Self::RunFile { path: file, + time, #[cfg(feature = "trace")] trace_print, #[cfg(feature = "trace")] @@ -151,6 +159,7 @@ impl TryFrom for Action { } 1 => Self::RunFile { path: args[0].parse::().context("invalid path")?, + time: false, #[cfg(feature = "trace")] trace_print: false, #[cfg(feature = "trace")] @@ -176,6 +185,7 @@ fn main() -> anyhow::Result<()> { match action { Action::RunFile { path, + time, #[cfg(feature = "trace")] trace_print, #[cfg(feature = "trace")] @@ -217,9 +227,16 @@ fn main() -> anyhow::Result<()> { } let name = filename.as_deref().unwrap_or(""); - if let Err(err) = interpreter.eval_named(name, &string) { - diagnostic::emit_error(interpreter.source_db(), err); - process::exit(1); + match interpreter.eval_named_with_timings(name, &string) { + Ok((_, timings)) => { + if time { + print_phase_timings(&timings); + } + } + Err(err) => { + diagnostic::emit_error(interpreter.source_db(), err); + process::exit(1); + } } } Action::DisassembleFile(path) => { @@ -252,6 +269,41 @@ fn main() -> anyhow::Result<()> { Ok(()) } +fn format_duration(d: Duration) -> String { + let nanos = d.as_nanos(); + if nanos < 1_000 { + format!("{nanos}ns") + } else if nanos < 1_000_000 { + format!("{:.0}us", nanos as f64 / 1_000.0) + } else if nanos < 1_000_000_000 { + format!("{:.1}ms", nanos as f64 / 1_000_000.0) + } else { + format!("{:.2}s", d.as_secs_f64()) + } +} + +fn print_phase_timings(timings: &ExecutionTimings) { + let total = + timings.lexing + timings.parsing + timings.analysing + timings.compiling + timings.running; + + eprintln!("\n{:-<56}", ""); + eprintln!("Phase timings (total: {})", format_duration(total)); + eprintln!("{:-<56}", ""); + eprintln!(" {:<12} {}", "lexing", format_duration(timings.lexing)); + eprintln!(" {:<12} {}", "parsing", format_duration(timings.parsing)); + eprintln!( + " {:<12} {}", + "analyser", + format_duration(timings.analysing) + ); + eprintln!( + " {:<12} {}", + "compiling", + format_duration(timings.compiling) + ); + eprintln!(" {:<12} {}", "running", format_duration(timings.running)); +} + fn start_lsp() { #[allow( clippy::expect_used, diff --git a/ndc_interpreter/src/lib.rs b/ndc_interpreter/src/lib.rs index b1d0df7..79ddbd5 100644 --- a/ndc_interpreter/src/lib.rs +++ b/ndc_interpreter/src/lib.rs @@ -6,11 +6,15 @@ use ndc_vm::compiler::Compiler; use ndc_vm::value::CompiledFunction; use ndc_vm::{OutputSink, Vm}; use std::rc::Rc; +use timing::{Phase, measure}; pub use ndc_analyser::AnalysisResult; #[cfg(feature = "trace")] pub use ndc_vm::tracer; pub use ndc_vm::{NativeFunction, Value}; +pub use timing::ExecutionTimings; + +mod timing; pub struct Interpreter { registry: FunctionRegistry>, @@ -117,7 +121,7 @@ impl Interpreter { pub fn compile_str(&mut self, input: &str) -> Result { let source_id = self.source_db.add("", input); - let expressions = self.parse_and_analyse(input, source_id)?; + let (expressions, _) = self.parse_and_analyse(input, source_id)?; Ok(Compiler::compile(expressions.into_iter())?) } @@ -141,27 +145,51 @@ impl Interpreter { name: impl Into, input: &str, ) -> Result { + Ok(self.eval_named_with_timings(name, input)?.0) + } + + /// Execute source code with a custom source name for diagnostics and + /// return wall-clock timings for each pipeline phase. + pub fn eval_named_with_timings( + &mut self, + name: impl Into, + input: &str, + ) -> Result<(Value, ExecutionTimings), InterpreterError> { let source_id = self.source_db.add(name, input); - let expressions = self.parse_and_analyse(input, source_id)?; - self.interpret_vm(input, expressions.into_iter()) + let (expressions, mut timings) = self.parse_and_analyse(input, source_id)?; + let (value, vm_timings) = self.interpret_vm(input, expressions.into_iter())?; + timings.compiling = vm_timings.compiling; + timings.running = vm_timings.running; + Ok((value, timings)) } fn parse_and_analyse( &mut self, input: &str, source_id: SourceId, - ) -> Result, InterpreterError> { - let tokens = Lexer::new(input, source_id).collect::, _>>()?; - let mut expressions = ndc_parser::Parser::from_tokens(tokens).parse()?; + ) -> Result<(Vec, ExecutionTimings), InterpreterError> { + let mut timings = ExecutionTimings::default(); + + let tokens = measure(&mut timings, Phase::Lexing, || { + Lexer::new(input, source_id).collect::, _>>() + })?; + + let mut expressions = measure(&mut timings, Phase::Parsing, || { + ndc_parser::Parser::from_tokens(tokens).parse() + })?; let checkpoint = self.analyser.checkpoint(); - for e in &mut expressions { - // Hard errors (structural issues) still abort immediately. - if let Err(e) = self.analyser.analyse(e) { - self.analyser.restore(checkpoint); - return Err(InterpreterError::Resolver { causes: vec![e] }); + measure(&mut timings, Phase::Analysing, || { + for e in &mut expressions { + // Hard errors (structural issues) still abort immediately. + if let Err(e) = self.analyser.analyse(e) { + self.analyser.restore(checkpoint.clone()); + return Err(InterpreterError::Resolver { causes: vec![e] }); + } } - } + + Ok(()) + })?; if self.analyser.has_errors() { self.analyser.restore(checkpoint); @@ -171,7 +199,7 @@ impl Interpreter { }); } - Ok(expressions) + Ok((expressions, timings)) } fn interpret_vm( @@ -179,7 +207,7 @@ impl Interpreter { #[cfg(feature = "trace")] input: &str, #[cfg(not(feature = "trace"))] _input: &str, expressions: impl Iterator, - ) -> Result { + ) -> Result<(Value, ExecutionTimings), InterpreterError> { use ndc_vm::{Function as VmFunction, Object as VmObject, Value as VmValue}; let globals: Vec = self @@ -192,6 +220,7 @@ impl Interpreter { }) .collect(); + let mut timings = ExecutionTimings::default(); let result = match self.repl_state.take() { None => { let output = if self.capturing { @@ -199,7 +228,9 @@ impl Interpreter { } else { OutputSink::Stdout }; - let (code, checkpoint) = Compiler::compile_resumable(expressions)?; + let (code, checkpoint) = measure(&mut timings, Phase::Compiling, || { + Compiler::compile_resumable(expressions) + })?; let mut vm = Vm::new(code, globals).with_output(output); #[cfg(feature = "trace")] { @@ -208,7 +239,7 @@ impl Interpreter { vm = vm.with_tracer(tracer); } } - vm.run()?; + measure(&mut timings, Phase::Running, || vm.run())?; let result = vm.last_value(checkpoint.num_locals()); self.repl_state = Some((vm, checkpoint)); result @@ -216,20 +247,22 @@ impl Interpreter { Some((mut vm, checkpoint)) => { let resume_ip = checkpoint.halt_ip(); let prev_num_locals = checkpoint.num_locals(); - let (code, new_checkpoint) = checkpoint.resume(expressions)?; + let (code, new_checkpoint) = measure(&mut timings, Phase::Compiling, || { + checkpoint.resume(expressions) + })?; vm.resume_from_halt(code, globals, resume_ip, prev_num_locals); #[cfg(feature = "trace")] { vm.set_source(input); } - vm.run()?; + measure(&mut timings, Phase::Running, || vm.run())?; let result = vm.last_value(new_checkpoint.num_locals()); self.repl_state = Some((vm, new_checkpoint)); result } }; - Ok(result) + Ok((result, timings)) } } diff --git a/ndc_interpreter/src/timing.rs b/ndc_interpreter/src/timing.rs new file mode 100644 index 0000000..7071223 --- /dev/null +++ b/ndc_interpreter/src/timing.rs @@ -0,0 +1,47 @@ +use std::time::{Duration, Instant}; + +#[derive(Clone, Copy, Debug, Default)] +pub struct ExecutionTimings { + pub lexing: Duration, + pub parsing: Duration, + pub analysing: Duration, + pub compiling: Duration, + pub running: Duration, +} + +impl ExecutionTimings { + #[must_use] + pub fn total(&self) -> Duration { + self.lexing + self.parsing + self.analysing + self.compiling + self.running + } + + fn set(&mut self, phase: Phase, duration: Duration) { + match phase { + Phase::Lexing => self.lexing = duration, + Phase::Parsing => self.parsing = duration, + Phase::Analysing => self.analysing = duration, + Phase::Compiling => self.compiling = duration, + Phase::Running => self.running = duration, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Phase { + Lexing, + Parsing, + Analysing, + Compiling, + Running, +} + +pub fn measure( + timings: &mut ExecutionTimings, + phase: Phase, + f: impl FnOnce() -> Result, +) -> Result { + let start = Instant::now(); + let result = f(); + timings.set(phase, start.elapsed()); + result +} From cffd5f54d10abd68ffa06315511ca74056d01355 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 24 Mar 2026 10:05:33 +0100 Subject: [PATCH 2/4] =?UTF-8?q?refactor(cli):=20extract=20phase=20timing?= =?UTF-8?q?=20formatter=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ndc_bin/src/main.rs | 42 ++---------------- ndc_bin/src/phase_timing.rs | 87 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 ndc_bin/src/phase_timing.rs diff --git a/ndc_bin/src/main.rs b/ndc_bin/src/main.rs index 768e642..fc88492 100644 --- a/ndc_bin/src/main.rs +++ b/ndc_bin/src/main.rs @@ -4,13 +4,14 @@ use crate::docs::docs; use anyhow::{Context, anyhow}; use clap::{Parser, Subcommand}; use highlighter::AndycppHighlighter; -use ndc_interpreter::{ExecutionTimings, Interpreter}; +use ndc_interpreter::Interpreter; +use phase_timing::write_phase_timings; use std::path::PathBuf; use std::process; -use std::time::Duration; use std::{fs, io::Write}; mod diagnostic; +mod phase_timing; mod repl; mod docs; @@ -230,7 +231,7 @@ fn main() -> anyhow::Result<()> { match interpreter.eval_named_with_timings(name, &string) { Ok((_, timings)) => { if time { - print_phase_timings(&timings); + write_phase_timings(&mut std::io::stderr(), &timings)?; } } Err(err) => { @@ -269,41 +270,6 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn format_duration(d: Duration) -> String { - let nanos = d.as_nanos(); - if nanos < 1_000 { - format!("{nanos}ns") - } else if nanos < 1_000_000 { - format!("{:.0}us", nanos as f64 / 1_000.0) - } else if nanos < 1_000_000_000 { - format!("{:.1}ms", nanos as f64 / 1_000_000.0) - } else { - format!("{:.2}s", d.as_secs_f64()) - } -} - -fn print_phase_timings(timings: &ExecutionTimings) { - let total = - timings.lexing + timings.parsing + timings.analysing + timings.compiling + timings.running; - - eprintln!("\n{:-<56}", ""); - eprintln!("Phase timings (total: {})", format_duration(total)); - eprintln!("{:-<56}", ""); - eprintln!(" {:<12} {}", "lexing", format_duration(timings.lexing)); - eprintln!(" {:<12} {}", "parsing", format_duration(timings.parsing)); - eprintln!( - " {:<12} {}", - "analyser", - format_duration(timings.analysing) - ); - eprintln!( - " {:<12} {}", - "compiling", - format_duration(timings.compiling) - ); - eprintln!(" {:<12} {}", "running", format_duration(timings.running)); -} - fn start_lsp() { #[allow( clippy::expect_used, diff --git a/ndc_bin/src/phase_timing.rs b/ndc_bin/src/phase_timing.rs new file mode 100644 index 0000000..a5b5998 --- /dev/null +++ b/ndc_bin/src/phase_timing.rs @@ -0,0 +1,87 @@ +use ndc_interpreter::ExecutionTimings; +use std::io::{self, Write}; +use std::time::Duration; + +fn format_duration(d: Duration) -> String { + let nanos = d.as_nanos(); + if nanos < 1_000 { + format!("{nanos}ns") + } else if nanos < 1_000_000 { + format!("{:.0}us", nanos as f64 / 1_000.0) + } else if nanos < 1_000_000_000 { + format!("{:.1}ms", nanos as f64 / 1_000_000.0) + } else { + format!("{:.2}s", d.as_secs_f64()) + } +} + +pub fn write_phase_timings(writer: &mut W, timings: &ExecutionTimings) -> io::Result<()> { + writeln!(writer)?; + writeln!(writer, "{:-<56}", "")?; + writeln!( + writer, + "Phase timings (total: {})", + format_duration(timings.total()) + )?; + writeln!(writer, "{:-<56}", "")?; + writeln!( + writer, + " {:<12} {}", + "lexing", + format_duration(timings.lexing) + )?; + writeln!( + writer, + " {:<12} {}", + "parsing", + format_duration(timings.parsing) + )?; + writeln!( + writer, + " {:<12} {}", + "analyser", + format_duration(timings.analysing) + )?; + writeln!( + writer, + " {:<12} {}", + "compiling", + format_duration(timings.compiling) + )?; + writeln!( + writer, + " {:<12} {}", + "running", + format_duration(timings.running) + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::write_phase_timings; + use ndc_interpreter::ExecutionTimings; + use std::time::Duration; + + #[test] + fn writes_expected_summary() { + let timings = ExecutionTimings { + lexing: Duration::from_micros(10), + parsing: Duration::from_micros(20), + analysing: Duration::from_micros(30), + compiling: Duration::from_micros(40), + running: Duration::from_micros(50), + }; + + let mut output = Vec::new(); + write_phase_timings(&mut output, &timings).expect("timing output should write"); + let output = String::from_utf8(output).expect("timing output should be utf8"); + + assert!(output.contains("Phase timings (total: 150us)")); + assert!(output.contains("lexing 10us")); + assert!(output.contains("parsing 20us")); + assert!(output.contains("analyser 30us")); + assert!(output.contains("compiling 40us")); + assert!(output.contains("running 50us")); + } +} From 3ffed1cf86860a2a547d8a6c9c11e86cc354f7ca Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 24 Mar 2026 10:16:05 +0100 Subject: [PATCH 3/4] =?UTF-8?q?refactor(core):=20share=20duration=20format?= =?UTF-8?q?ter=20=E2=8F=B1=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ndc_bin/src/phase_timing.rs | 15 +-------------- ndc_core/src/duration.rs | 33 +++++++++++++++++++++++++++++++++ ndc_core/src/lib.rs | 1 + ndc_vm/src/tracer.rs | 22 +++------------------- 4 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 ndc_core/src/duration.rs diff --git a/ndc_bin/src/phase_timing.rs b/ndc_bin/src/phase_timing.rs index a5b5998..1e3e4b8 100644 --- a/ndc_bin/src/phase_timing.rs +++ b/ndc_bin/src/phase_timing.rs @@ -1,19 +1,6 @@ +use ndc_core::duration::format_duration; use ndc_interpreter::ExecutionTimings; use std::io::{self, Write}; -use std::time::Duration; - -fn format_duration(d: Duration) -> String { - let nanos = d.as_nanos(); - if nanos < 1_000 { - format!("{nanos}ns") - } else if nanos < 1_000_000 { - format!("{:.0}us", nanos as f64 / 1_000.0) - } else if nanos < 1_000_000_000 { - format!("{:.1}ms", nanos as f64 / 1_000_000.0) - } else { - format!("{:.2}s", d.as_secs_f64()) - } -} pub fn write_phase_timings(writer: &mut W, timings: &ExecutionTimings) -> io::Result<()> { writeln!(writer)?; diff --git a/ndc_core/src/duration.rs b/ndc_core/src/duration.rs new file mode 100644 index 0000000..c435129 --- /dev/null +++ b/ndc_core/src/duration.rs @@ -0,0 +1,33 @@ +use std::time::Duration; + +#[must_use] +pub fn format_duration(duration: Duration) -> String { + let nanos = duration.as_nanos(); + if nanos < 1_000 { + format!("{nanos}ns") + } else if nanos < 1_000_000 { + format!("{:.0}us", nanos as f64 / 1_000.0) + } else if nanos < 1_000_000_000 { + format!("{:.1}ms", nanos as f64 / 1_000_000.0) + } else { + format!("{:.2}s", duration.as_secs_f64()) + } +} + +#[cfg(test)] +mod tests { + use super::format_duration; + use std::time::Duration; + + #[test] + fn formats_small_durations() { + assert_eq!(format_duration(Duration::from_nanos(999)), "999ns"); + assert_eq!(format_duration(Duration::from_micros(10)), "10us"); + assert_eq!(format_duration(Duration::from_millis(12)), "12.0ms"); + } + + #[test] + fn formats_large_durations() { + assert_eq!(format_duration(Duration::from_secs(2)), "2.00s"); + } +} diff --git a/ndc_core/src/lib.rs b/ndc_core/src/lib.rs index 0394894..c214339 100644 --- a/ndc_core/src/lib.rs +++ b/ndc_core/src/lib.rs @@ -1,4 +1,5 @@ pub mod compare; +pub mod duration; pub mod hash_map; pub mod int; pub mod num; diff --git a/ndc_vm/src/tracer.rs b/ndc_vm/src/tracer.rs index c8cefa0..5e1d1c7 100644 --- a/ndc_vm/src/tracer.rs +++ b/ndc_vm/src/tracer.rs @@ -1,25 +1,9 @@ use crate::chunk::OpCode; +use ndc_core::duration::format_duration; use ndc_lexer::Span; use std::collections::HashMap; -use std::fmt; use std::time::{Duration, Instant}; -fn format_duration(d: &Duration) -> String { - let nanos = d.as_nanos(); - if nanos < 1_000 { - format!("{nanos}ns") - } else if nanos < 1_000_000 { - let micros = nanos as f64 / 1_000.0; - fmt::format(format_args!("{micros:.0}µs")) - } else if nanos < 1_000_000_000 { - let millis = nanos as f64 / 1_000_000.0; - fmt::format(format_args!("{millis:.1}ms")) - } else { - let secs = d.as_secs_f64(); - fmt::format(format_args!("{secs:.2}s")) - } -} - /// Context passed to a tracer on every dispatched instruction. pub struct InstructionContext<'a> { pub ip: usize, @@ -184,11 +168,11 @@ impl VmTracer for TimingTracer { let total: Duration = entries.iter().map(|(_, d)| d).sum(); eprintln!("\n{:-<60}", ""); - eprintln!("Instruction timing (total: {})", format_duration(&total)); + eprintln!("Instruction timing (total: {})", format_duration(total)); eprintln!("{:-<60}", ""); for (name, dur) in &entries { let pct = dur.as_secs_f64() / total.as_secs_f64() * 100.0; - eprintln!(" {name:<20} {:>10} ({pct:5.1}%)", format_duration(dur)); + eprintln!(" {name:<20} {:>10} ({pct:5.1}%)", format_duration(*dur)); } } } From ea044c4c302ed4743fec440438735ece29d93c8a Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Tue, 24 Mar 2026 12:32:28 +0100 Subject: [PATCH 4/4] =?UTF-8?q?fix(cli):=20honor=20implicit=20run=20flags?= =?UTF-8?q?=20and=20share=20run=20options=20=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ndc_bin/src/main.rs | 180 ++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 99 deletions(-) diff --git a/ndc_bin/src/main.rs b/ndc_bin/src/main.rs index fc88492..c249f6f 100644 --- a/ndc_bin/src/main.rs +++ b/ndc_bin/src/main.rs @@ -1,8 +1,8 @@ #![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] use crate::docs::docs; -use anyhow::{Context, anyhow}; -use clap::{Parser, Subcommand}; +use anyhow::anyhow; +use clap::{Args, Parser, Subcommand}; use highlighter::AndycppHighlighter; use ndc_interpreter::Interpreter; use phase_timing::write_phase_timings; @@ -34,25 +34,8 @@ enum Command { /// Execute an .ndc file or start the repl (this default action may be omitted) Run { file: Option, - /// Print total time spent in each interpreter phase - #[arg(long)] - time: bool, - /// Print each instruction as it is dispatched - #[cfg(feature = "trace")] - #[arg(long)] - trace_print: bool, - /// Print a histogram of instruction dispatch counts - #[cfg(feature = "trace")] - #[arg(long)] - trace_histogram: bool, - /// Print cumulative time spent per instruction type - #[cfg(feature = "trace")] - #[arg(long)] - trace_time: bool, - /// Render source as a heat map colored by time spent per span - #[cfg(feature = "trace")] - #[arg(long)] - trace_span: bool, + #[command(flatten)] + options: RunOptions, }, /// Output an .ndc file using the built-in syntax highlighting engine Highlight { file: PathBuf }, @@ -79,19 +62,42 @@ enum Command { Unknown(Vec), } +#[derive(Args, Clone, Copy, Default)] +struct RunOptions { + /// Print total time spent in each interpreter phase + #[arg(long)] + time: bool, + /// Print each instruction as it is dispatched + #[cfg(feature = "trace")] + #[arg(long)] + trace_print: bool, + /// Print a histogram of instruction dispatch counts + #[cfg(feature = "trace")] + #[arg(long)] + trace_histogram: bool, + /// Print cumulative time spent per instruction type + #[cfg(feature = "trace")] + #[arg(long)] + trace_time: bool, + /// Render source as a heat map colored by time spent per span + #[cfg(feature = "trace")] + #[arg(long)] + trace_span: bool, +} + +#[derive(Parser)] +#[command(name = "ndc", disable_help_subcommand = true)] +struct ImplicitRunArgs { + file: PathBuf, + #[command(flatten)] + options: RunOptions, +} + impl Default for Command { fn default() -> Self { Self::Run { file: None, - time: false, - #[cfg(feature = "trace")] - trace_print: false, - #[cfg(feature = "trace")] - trace_histogram: false, - #[cfg(feature = "trace")] - trace_time: false, - #[cfg(feature = "trace")] - trace_span: false, + options: RunOptions::default(), } } } @@ -100,15 +106,7 @@ enum Action { RunLsp, RunFile { path: PathBuf, - time: bool, - #[cfg(feature = "trace")] - trace_print: bool, - #[cfg(feature = "trace")] - trace_histogram: bool, - #[cfg(feature = "trace")] - trace_time: bool, - #[cfg(feature = "trace")] - trace_span: bool, + options: RunOptions, }, DisassembleFile(PathBuf), HighlightFile(PathBuf), @@ -126,51 +124,22 @@ impl TryFrom for Action { let action = match value { Command::Run { file: Some(file), - time, - #[cfg(feature = "trace")] - trace_print, - #[cfg(feature = "trace")] - trace_histogram, - #[cfg(feature = "trace")] - trace_time, - #[cfg(feature = "trace")] - trace_span, - } => Self::RunFile { - path: file, - time, - #[cfg(feature = "trace")] - trace_print, - #[cfg(feature = "trace")] - trace_histogram, - #[cfg(feature = "trace")] - trace_time, - #[cfg(feature = "trace")] - trace_span, - }, + options, + } => Self::RunFile { path: file, options }, Command::Run { file: None, .. } => Self::StartRepl, Command::Lsp { stdio: _ } => Self::RunLsp, Command::Disassemble { file } => Self::DisassembleFile(file), Command::Highlight { file } => Self::HighlightFile(file), Command::Docs { query, no_color } => Self::Docs { query, no_color }, Command::Unknown(args) => { - match args.len() { - 0 => { - // This case should have defaulted to `Command::Run { file: None }` - unreachable!("fallback case reached with 0 arguments (should never happen)") - } - 1 => Self::RunFile { - path: args[0].parse::().context("invalid path")?, - time: false, - #[cfg(feature = "trace")] - trace_print: false, - #[cfg(feature = "trace")] - trace_histogram: false, - #[cfg(feature = "trace")] - trace_time: false, - #[cfg(feature = "trace")] - trace_span: false, - }, - n => return Err(anyhow!("invalid number of arguments: {n}")), + let implicit_run = ImplicitRunArgs::try_parse_from( + std::iter::once("ndc").chain(args.iter().map(String::as_str)), + ) + .map_err(|err| anyhow!(err.render().to_string()))?; + + Self::RunFile { + path: implicit_run.file, + options: implicit_run.options, } } }; @@ -186,15 +155,7 @@ fn main() -> anyhow::Result<()> { match action { Action::RunFile { path, - time, - #[cfg(feature = "trace")] - trace_print, - #[cfg(feature = "trace")] - trace_histogram, - #[cfg(feature = "trace")] - trace_time, - #[cfg(feature = "trace")] - trace_span, + options, } => { let filename = path .file_name() @@ -210,16 +171,16 @@ fn main() -> anyhow::Result<()> { { use ndc_interpreter::tracer; let mut tracers: Vec> = Vec::new(); - if trace_print { + if options.trace_print { tracers.push(Box::new(tracer::PrintTracer)); } - if trace_histogram { + if options.trace_histogram { tracers.push(Box::new(tracer::HistogramTracer::new())); } - if trace_time { + if options.trace_time { tracers.push(Box::new(tracer::TimingTracer::new())); } - if trace_span { + if options.trace_span { tracers.push(Box::new(span_tracer::SpanTracer::new())); } if !tracers.is_empty() { @@ -228,16 +189,19 @@ fn main() -> anyhow::Result<()> { } let name = filename.as_deref().unwrap_or(""); - match interpreter.eval_named_with_timings(name, &string) { - Ok((_, timings)) => { - if time { + if options.time { + match interpreter.eval_named_with_timings(name, &string) { + Ok((_, timings)) => { write_phase_timings(&mut std::io::stderr(), &timings)?; } + Err(err) => { + diagnostic::emit_error(interpreter.source_db(), err); + process::exit(1); + } } - Err(err) => { - diagnostic::emit_error(interpreter.source_db(), err); - process::exit(1); - } + } else if let Err(err) = interpreter.eval_named(name, &string) { + diagnostic::emit_error(interpreter.source_db(), err); + process::exit(1); } } Action::DisassembleFile(path) => { @@ -289,11 +253,29 @@ fn start_lsp() { #[cfg(test)] mod test { use clap::CommandFactory; + use std::path::PathBuf; - use crate::Cli; + use crate::{Action, Cli, Command}; #[test] fn test_clap() { Cli::command().debug_assert(); } + + #[test] + fn implicit_run_honors_time_flag() { + let action = Action::try_from(Command::Unknown(vec![ + "script.ndc".to_string(), + "--time".to_string(), + ])) + .expect("implicit run flags should parse"); + + match action { + Action::RunFile { path, options } => { + assert_eq!(path, PathBuf::from("script.ndc")); + assert!(options.time); + } + _ => panic!("expected run action"), + } + } }