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 #[clap(value_parser)]
43 pub file: Option<String>,
44
45 #[arg(long, short)]
47 pub output: Option<PathBuf>,
48
49 #[arg(long, default_value_t = 10)]
52 pub times: usize,
53
54 #[arg(long, value_enum)]
56 pub output_format: Option<OutputFileFormat>,
57
58 #[arg(long, default_value_t = false)]
60 pub no_gui: bool,
61
62 #[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 #[arg(long, default_value_t = false)]
91 pub emit_cst: bool,
92
93 #[arg(long, default_value_t = false)]
95 pub emit_ast: bool,
96
97 #[arg(long, default_value_t = false)]
99 pub emit_mir: bool,
100
101 #[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
118pub struct RunOptions {
120 mode: RunMode,
121 with_gui: bool,
122 config: Config,
123}
124
125impl RunOptions {
126 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 (None, None) => RunMode::NativeAudio,
164 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
166 times: args.times,
167 output: path.cloned(),
168 },
169 (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 RunMode::NativeAudio => !args.no_gui,
182 _ => 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
202pub 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 pub fn cli_loop(&self) {
295 use notify::event::{EventKind, ModifyKind};
296 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
326pub 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 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 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 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 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 report(&content, fullpath, &e);
439 return Err(format!("Failed to process {file}").into());
440 }
441 }
442 }
443 None => {
444 }
446 }
447 Ok(())
448}