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 #[clap(value_parser)]
38 pub file: Option<String>,
39
40 #[arg(long, short)]
42 pub output: Option<PathBuf>,
43
44 #[arg(long, default_value_t = 10)]
47 pub times: usize,
48
49 #[arg(long, value_enum)]
51 pub output_format: Option<OutputFileFormat>,
52
53 #[arg(long, default_value_t = false)]
55 pub no_gui: bool,
56
57 #[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 #[arg(long, default_value_t = false)]
86 pub emit_ast: bool,
87
88 #[arg(long, default_value_t = false)]
90 pub emit_mir: bool,
91
92 #[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
108pub struct RunOptions {
110 mode: RunMode,
111 with_gui: bool,
112 config: Config,
113}
114
115impl RunOptions {
116 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 (None, None) => RunMode::NativeAudio,
146 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
148 times: args.times,
149 output: path.cloned(),
150 },
151 (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 RunMode::NativeAudio => !args.no_gui,
164 _ => 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
184pub 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 pub fn cli_loop(&self) {
277 use notify::event::{EventKind, ModifyKind};
278 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
308pub 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 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 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 report(&content, fullpath, &e);
404 return Err(format!("Failed to process {file}").into());
405 }
406 }
407 }
408 None => {
409 }
411 }
412 Ok(())
413}