Skip to main content

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