mimium_cli/
lib.rs

1use std::{
2    io::stdin,
3    path::{Path, PathBuf},
4};
5
6use clap::{Parser, ValueEnum};
7use mimium_audiodriver::{
8    backends::{csv::csv_driver, local_buffer::LocalBufferDriver},
9    driver::{Driver, SampleRate},
10    load_default_runtime,
11};
12use mimium_lang::{
13    compiler::{bytecodegen::SelfEvalMode, emit_ast}, interner::{Symbol, ToSymbol}, log, plugin::Plugin, utils::{error::{report, ReportableError}, fileloader, miniprint::MiniPrint}, Config, ExecContext
14};
15use mimium_symphonia::SamplerPlugin;
16
17#[derive(clap::Parser, Debug, Clone)]
18#[command(author, version, about, long_about = None)]
19pub struct Args {
20    #[command(flatten)]
21    pub mode: Mode,
22
23    /// File name
24    #[clap(value_parser)]
25    pub file: Option<String>,
26
27    /// Write out the signal values to a file (e.g. out.csv).
28    #[arg(long, short)]
29    pub output: Option<PathBuf>,
30
31    /// How many times to execute the code. This is only effective when --output
32    /// is specified.
33    #[arg(long, default_value_t = 10)]
34    pub times: usize,
35
36    /// Output format
37    #[arg(long, value_enum)]
38    pub output_format: Option<OutputFileFormat>,
39
40    /// Don't launch GUI
41    #[arg(long, default_value_t = false)]
42    pub no_gui: bool,
43
44    /// Change the behavior of `self` in the code. It this is set to true, `| | {self+1}` will return 0 at t=0, which normally returns 1.
45    #[arg(long, default_value_t = false)]
46    pub self_init_0: bool,
47}
48
49impl Args {
50    pub fn to_execctx_config(self) -> mimium_lang::Config {
51        mimium_lang::Config {
52            compiler: mimium_lang::compiler::Config {
53                self_eval_mode: if self.self_init_0 {
54                    SelfEvalMode::ZeroAtInit
55                } else {
56                    SelfEvalMode::SimpleState
57                },
58            },
59        }
60    }
61}
62
63#[derive(Clone, Debug, ValueEnum)]
64pub enum OutputFileFormat {
65    Csv,
66}
67
68#[derive(clap::Args, Debug, Clone, Copy)]
69#[group(required = false, multiple = false)]
70pub struct Mode {
71    /// Print AST and exit
72    #[arg(long, default_value_t = false)]
73    pub emit_ast: bool,
74
75    /// Print MIR and exit
76    #[arg(long, default_value_t = false)]
77    pub emit_mir: bool,
78
79    /// Print bytecode and exit
80    #[arg(long, default_value_t = false)]
81    pub emit_bytecode: bool,
82}
83
84pub enum RunMode {
85    EmitAst,
86    EmitMir,
87    EmitByteCode,
88    NativeAudio,
89    WriteCsv {
90        times: usize,
91        output: Option<PathBuf>,
92    },
93}
94
95/// Execution options derived from CLI arguments.
96pub struct RunOptions {
97    mode: RunMode,
98    with_gui: bool,
99    config: Config,
100}
101
102impl RunOptions {
103    /// Convert parsed command line arguments into [`RunOptions`].
104    pub fn from_args(args: &Args) -> Self {
105        let config = args.clone().to_execctx_config();
106        if args.mode.emit_ast {
107            return Self {
108                mode: RunMode::EmitAst,
109                with_gui: true,
110                config,
111            };
112        }
113
114        if args.mode.emit_mir {
115            return Self {
116                mode: RunMode::EmitMir,
117                with_gui: false,
118                config,
119            };
120        }
121
122        if args.mode.emit_bytecode {
123            return Self {
124                mode: RunMode::EmitByteCode,
125                with_gui: true,
126                config,
127            };
128        }
129
130        let mode = match (&args.output_format, args.output.as_ref()) {
131            // if none of the output options is specified, make sounds.
132            (None, None) => RunMode::NativeAudio,
133            // When --output-format is explicitly specified, use it.
134            (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
135                times: args.times,
136                output: path.cloned(),
137            },
138            // Otherwise, guess from the file extension.
139            (None, Some(output)) => match output.extension() {
140                Some(x) if &x.to_os_string() == "csv" => RunMode::WriteCsv {
141                    times: args.times,
142                    output: Some(output.clone()),
143                },
144                _ => panic!("cannot determine the output file format"),
145            },
146        };
147
148        let with_gui = match &mode {
149            // launch except when --no-gui is specified
150            RunMode::NativeAudio => !args.no_gui,
151            // do not launch in other mode
152            _ => false,
153        };
154
155        Self {
156            mode,
157            with_gui,
158            config,
159        }
160    }
161
162    fn get_driver(&self) -> Box<dyn Driver<Sample = f64>> {
163        match &self.mode {
164            RunMode::NativeAudio => load_default_runtime(),
165            RunMode::WriteCsv { times, output } => csv_driver(*times, output),
166            _ => unreachable!(),
167        }
168    }
169}
170
171/// Construct an [`ExecContext`] with the default set of plugins.
172pub fn get_default_context(path: Option<Symbol>, with_gui: bool, config: Config) -> ExecContext {
173    let plugins: Vec<Box<dyn Plugin>> = vec![Box::new(SamplerPlugin)];
174    let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
175    ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
176    if let Some(midi_plug) = mimium_midi::MidiPlugin::try_new() {
177        ctx.add_system_plugin(midi_plug);
178    } else {
179        log::warn!("Midi is not supported on this platform.")
180    }
181
182    if with_gui {
183        #[cfg(not(target_arch = "wasm32"))]
184        ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
185    }
186
187    ctx
188}
189
190/// Compile and run a single source file according to the provided options.
191pub fn run_file(
192    options: RunOptions,
193    content: &str,
194    fullpath: &Path,
195) -> Result<(), Vec<Box<dyn ReportableError>>> {
196    log::debug!("Filename: {}", fullpath.display());
197    let path_sym = fullpath.to_string_lossy().to_symbol();
198    let mut ctx = get_default_context(Some(path_sym), options.with_gui, options.config);
199
200    match options.mode {
201        RunMode::EmitAst => {
202            let ast = emit_ast(content, Some(path_sym))?;
203            println!("{}", ast.pretty_print());
204            Ok(())
205        }
206        RunMode::EmitMir => {
207            ctx.prepare_compiler();
208            let res = ctx.get_compiler().unwrap().emit_mir(content);
209            res.map(|r| {
210                println!("{r}");
211            })
212        }
213        RunMode::EmitByteCode => {
214            // need to prepare dummy audio plugin to link `now` and `samplerate`
215            let localdriver = LocalBufferDriver::new(0);
216            let plug = localdriver.get_as_plugin();
217            ctx.add_plugin(plug);
218            ctx.prepare_machine(content)?;
219            Ok(println!("{}", ctx.get_vm().unwrap().prog))
220        }
221        _ => {
222            let mut driver = options.get_driver();
223            let audiodriver_plug = driver.get_as_plugin();
224            ctx.add_plugin(audiodriver_plug);
225            ctx.prepare_machine(content)?;
226            let _res = ctx.run_main();
227            let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(|| {
228                //wait until input something
229                let mut dummy = String::new();
230                eprintln!("Press Enter to exit");
231                let _size = stdin().read_line(&mut dummy).expect("stdin read error.");
232            }));
233            driver.init(ctx, Some(SampleRate::from(48000)));
234            driver.play();
235            mainloop();
236            Ok(())
237        }
238    }
239}
240
241
242pub fn lib_main() -> Result<(), Box<dyn std::error::Error>> {
243    if cfg!(debug_assertions) | cfg!(test) {
244        colog::default_builder()
245            .filter_level(log::LevelFilter::Trace)
246            .init();
247    } else {
248        colog::default_builder().init();
249    }
250
251    let args = Args::parse();
252    match &args.file {
253        Some(file) => {
254            let fullpath = fileloader::get_canonical_path(".", file)?;
255            let content = fileloader::load(fullpath.to_str().unwrap())?;
256            let options = RunOptions::from_args(&args);
257            match run_file(options, &content, &fullpath) {
258                Ok(_) => {}
259                Err(e) => {
260                    // Note: I was hoping to implement std::error::Error for a
261                    // struct around ReportableError and directly return it,
262                    // however, std::error::Error cannot be so color-rich as
263                    // ariadne because it just uses std::fmt::Display.
264                    report(&content, fullpath.to_string_lossy().to_symbol(), &e);
265                    return Err(format!("Failed to process {file}").into());
266                }
267            }
268        }
269        None => {
270            // repl::run_repl();
271        }
272    }
273    Ok(())
274}