mimium_cli/
lib.rs

1mod async_compiler;
2
3use std::{
4    path::{Path, PathBuf},
5    sync::mpsc,
6};
7
8use crate::async_compiler::{CompileRequest, Errors, Response};
9use clap::{Parser, ValueEnum};
10use mimium_audiodriver::{
11    backends::{csv::csv_driver, local_buffer::LocalBufferDriver},
12    driver::{Driver, RuntimeData, SampleRate},
13    load_default_runtime,
14};
15use mimium_lang::{
16    Config, ExecContext,
17    compiler::{self, bytecodegen::SelfEvalMode, emit_ast},
18    log,
19    plugin::Plugin,
20    runtime::vm,
21    utils::{
22        error::{ReportableError, report},
23        fileloader,
24        miniprint::MiniPrint,
25    },
26};
27use mimium_symphonia::SamplerPlugin;
28use notify::{Event, RecursiveMode, Watcher};
29
30#[derive(clap::Parser, Debug, Clone)]
31#[command(author, version, about, long_about = None)]
32pub struct Args {
33    #[command(flatten)]
34    pub mode: Mode,
35
36    /// File name
37    #[clap(value_parser)]
38    pub file: Option<String>,
39
40    /// Write out the signal values to a file (e.g. out.csv).
41    #[arg(long, short)]
42    pub output: Option<PathBuf>,
43
44    /// How many times to execute the code. This is only effective when --output
45    /// is specified.
46    #[arg(long, default_value_t = 10)]
47    pub times: usize,
48
49    /// Output format
50    #[arg(long, value_enum)]
51    pub output_format: Option<OutputFileFormat>,
52
53    /// Don't launch GUI
54    #[arg(long, default_value_t = false)]
55    pub no_gui: bool,
56
57    /// 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.
58    #[arg(long, default_value_t = false)]
59    pub self_init_0: bool,
60}
61
62impl Args {
63    pub fn to_execctx_config(self) -> mimium_lang::Config {
64        mimium_lang::Config {
65            compiler: mimium_lang::compiler::Config {
66                self_eval_mode: if self.self_init_0 {
67                    SelfEvalMode::ZeroAtInit
68                } else {
69                    SelfEvalMode::SimpleState
70                },
71            },
72        }
73    }
74}
75
76#[derive(Clone, Debug, ValueEnum)]
77pub enum OutputFileFormat {
78    Csv,
79}
80
81#[derive(clap::Args, Debug, Clone, Copy)]
82#[group(required = false, multiple = false)]
83pub struct Mode {
84    /// Print AST and exit
85    #[arg(long, default_value_t = false)]
86    pub emit_ast: bool,
87
88    /// Print MIR and exit
89    #[arg(long, default_value_t = false)]
90    pub emit_mir: bool,
91
92    /// Print bytecode and exit
93    #[arg(long, default_value_t = false)]
94    pub emit_bytecode: bool,
95}
96
97pub enum RunMode {
98    EmitAst,
99    EmitMir,
100    EmitByteCode,
101    NativeAudio,
102    WriteCsv {
103        times: usize,
104        output: Option<PathBuf>,
105    },
106}
107
108/// Execution options derived from CLI arguments.
109pub struct RunOptions {
110    mode: RunMode,
111    with_gui: bool,
112    config: Config,
113}
114
115impl RunOptions {
116    /// Convert parsed command line arguments into [`RunOptions`].
117    pub fn from_args(args: &Args) -> Self {
118        let config = args.clone().to_execctx_config();
119        if args.mode.emit_ast {
120            return Self {
121                mode: RunMode::EmitAst,
122                with_gui: true,
123                config,
124            };
125        }
126
127        if args.mode.emit_mir {
128            return Self {
129                mode: RunMode::EmitMir,
130                with_gui: true,
131                config,
132            };
133        }
134
135        if args.mode.emit_bytecode {
136            return Self {
137                mode: RunMode::EmitByteCode,
138                with_gui: true,
139                config,
140            };
141        }
142
143        let mode = match (&args.output_format, args.output.as_ref()) {
144            // if none of the output options is specified, make sounds.
145            (None, None) => RunMode::NativeAudio,
146            // When --output-format is explicitly specified, use it.
147            (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
148                times: args.times,
149                output: path.cloned(),
150            },
151            // Otherwise, guess from the file extension.
152            (None, Some(output)) => match output.extension() {
153                Some(x) if &x.to_os_string() == "csv" => RunMode::WriteCsv {
154                    times: args.times,
155                    output: Some(output.clone()),
156                },
157                _ => panic!("cannot determine the output file format"),
158            },
159        };
160
161        let with_gui = match &mode {
162            // launch except when --no-gui is specified
163            RunMode::NativeAudio => !args.no_gui,
164            // do not launch in other mode
165            _ => false,
166        };
167
168        Self {
169            mode,
170            with_gui,
171            config,
172        }
173    }
174
175    fn get_driver(&self) -> Box<dyn Driver<Sample = f64>> {
176        match &self.mode {
177            RunMode::NativeAudio => load_default_runtime(),
178            RunMode::WriteCsv { times, output } => csv_driver(*times, output),
179            _ => unreachable!(),
180        }
181    }
182}
183
184/// Construct an [`ExecContext`] with the default set of plugins.
185pub fn get_default_context(path: Option<PathBuf>, with_gui: bool, config: Config) -> ExecContext {
186    let plugins: Vec<Box<dyn Plugin>> = vec![];
187    let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
188    ctx.add_system_plugin(SamplerPlugin::default());
189    ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
190    if let Some(midi_plug) = mimium_midi::MidiPlugin::try_new() {
191        ctx.add_system_plugin(midi_plug);
192    } else {
193        log::warn!("Midi is not supported on this platform.")
194    }
195
196    if with_gui {
197        #[cfg(not(target_arch = "wasm32"))]
198        ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
199    }
200
201    ctx
202}
203
204struct FileRunner {
205    pub tx_compiler: mpsc::Sender<CompileRequest>,
206    pub rx_compiler: mpsc::Receiver<Result<Response, Errors>>,
207    pub tx_prog: Option<mpsc::Sender<vm::Program>>,
208    pub fullpath: PathBuf,
209}
210
211struct FileWatcher {
212    pub rx: mpsc::Receiver<notify::Result<Event>>,
213    pub watcher: notify::RecommendedWatcher,
214}
215impl FileRunner {
216    pub fn new(
217        compiler: compiler::Context,
218        path: PathBuf,
219        prog_tx: Option<mpsc::Sender<vm::Program>>,
220    ) -> Self {
221        let client = async_compiler::start_async_compiler_service(compiler);
222        Self {
223            tx_compiler: client.tx,
224            rx_compiler: client.rx,
225            tx_prog: prog_tx,
226            fullpath: path,
227        }
228    }
229    fn try_new_watcher(&self) -> Result<FileWatcher, notify::Error> {
230        let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
231        let mut watcher = notify::recommended_watcher(tx)?;
232        watcher.watch(Path::new(&self.fullpath), RecursiveMode::NonRecursive)?;
233        Ok(FileWatcher { rx, watcher })
234    }
235    fn recompile_file(&self) {
236        match fileloader::load(&self.fullpath.to_string_lossy()) {
237            Ok(new_content) => {
238                let _ = self.tx_compiler.send(CompileRequest {
239                    source: new_content.clone(),
240                    path: self.fullpath.clone(),
241                    option: RunOptions {
242                        mode: RunMode::EmitByteCode,
243                        with_gui: true,
244                        config: Config::default(),
245                    },
246                });
247                let _ = self.rx_compiler.recv().map(|res| match res {
248                    Ok(Response::Ast(_)) | Ok(Response::Mir(_)) => {
249                        log::warn!("unexpected response: AST/MIR");
250                    }
251                    Ok(Response::ByteCode(prog)) => {
252                        log::info!("compiled successfully.");
253                        if let Some(tx) = &self.tx_prog {
254                            let _ = tx.send(prog);
255                        }
256                    }
257                    Err(errs) => {
258                        let errs = errs
259                            .into_iter()
260                            .map(|e| Box::new(e) as Box<dyn ReportableError>)
261                            .collect::<Vec<_>>();
262                        report(&new_content, self.fullpath.clone(), &errs);
263                    }
264                });
265            }
266            Err(e) => {
267                log::error!(
268                    "failed to reload the file {}: {}",
269                    self.fullpath.display(),
270                    e
271                );
272            }
273        }
274    }
275    //this api never returns
276    pub fn cli_loop(&self) {
277        use notify::event::{EventKind, ModifyKind};
278        //watcher instance lives only this context
279        let file_watcher = match self.try_new_watcher() {
280            Ok(watcher) => watcher,
281            Err(e) => {
282                log::error!("Failed to watch file: {e}");
283                return;
284            }
285        };
286
287        loop {
288            match file_watcher.rx.recv() {
289                Ok(Ok(Event {
290                    kind: EventKind::Modify(ModifyKind::Data(_)),
291                    ..
292                })) => {
293                    log::info!("File changed, recompiling...");
294                    self.recompile_file();
295                }
296                Ok(Err(e)) => {
297                    log::error!("watch error event: {e}");
298                }
299                Ok(_) => {}
300                Err(e) => {
301                    log::error!("receiver error: {e}");
302                }
303            }
304        }
305    }
306}
307
308/// Compile and run a single source file according to the provided options.
309pub fn run_file(
310    options: RunOptions,
311    content: &str,
312    fullpath: &Path,
313) -> Result<(), Vec<Box<dyn ReportableError>>> {
314    log::debug!("Filename: {}", fullpath.display());
315
316    let mut ctx = get_default_context(
317        Some(PathBuf::from(fullpath)),
318        options.with_gui,
319        options.config,
320    );
321
322    match options.mode {
323        RunMode::EmitAst => {
324            let ast = emit_ast(content, Some(PathBuf::from(fullpath)))?;
325            println!("{}", ast.pretty_print());
326            Ok(())
327        }
328        RunMode::EmitMir => {
329            ctx.prepare_compiler();
330            let res = ctx.get_compiler().unwrap().emit_mir(content);
331            res.map(|r| {
332                println!("{r}");
333            })?;
334            Ok(())
335        }
336        RunMode::EmitByteCode => {
337            // need to prepare dummy audio plugin to link `now` and `samplerate`
338            let localdriver = LocalBufferDriver::new(0);
339            let plug = localdriver.get_as_plugin();
340            ctx.add_plugin(plug);
341            ctx.prepare_machine(content)?;
342            println!("{}", ctx.get_vm().unwrap().prog);
343            Ok(())
344        }
345        _ => {
346            let mut driver = options.get_driver();
347            let audiodriver_plug = driver.get_as_plugin();
348
349            ctx.add_plugin(audiodriver_plug);
350            ctx.prepare_machine(content)?;
351            let _res = ctx.run_main();
352
353            let runtimedata = {
354                let ctxmut: &mut ExecContext = &mut ctx;
355                RuntimeData::try_from(ctxmut).unwrap()
356            };
357
358            let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
359                if options.with_gui {
360                    loop {
361                        std::thread::sleep(std::time::Duration::from_millis(1000));
362                    }
363                }
364            }));
365            //this takes ownership of ctx
366            driver.init(runtimedata, Some(SampleRate::from(48000)));
367            driver.play();
368
369            let compiler = ctx.take_compiler().unwrap();
370
371            let frunner =
372                FileRunner::new(compiler, fullpath.to_path_buf(), driver.get_vm_channel());
373            if options.with_gui {
374                std::thread::spawn(move || frunner.cli_loop());
375            }
376            mainloop();
377            Ok(())
378        }
379    }
380}
381pub fn lib_main() -> Result<(), Box<dyn std::error::Error>> {
382    if cfg!(debug_assertions) | cfg!(test) {
383        colog::default_builder()
384            .filter_level(log::LevelFilter::Trace)
385            .init();
386    } else {
387        colog::default_builder().init();
388    }
389
390    let args = Args::parse();
391    match &args.file {
392        Some(file) => {
393            let fullpath = fileloader::get_canonical_path(".", file)?;
394            let content = fileloader::load(fullpath.to_str().unwrap())?;
395            let options = RunOptions::from_args(&args);
396            match run_file(options, &content, &fullpath) {
397                Ok(()) => {}
398                Err(e) => {
399                    // Note: I was hoping to implement std::error::Error for a
400                    // struct around ReportableError and directly return it,
401                    // however, std::error::Error cannot be so color-rich as
402                    // ariadne because it just uses std::fmt::Display.
403                    report(&content, fullpath, &e);
404                    return Err(format!("Failed to process {file}").into());
405                }
406            }
407        }
408        None => {
409            // repl::run_repl();
410        }
411    }
412    Ok(())
413}