1mod async_compiler;
2
3use std::{
4 env, fs,
5 path::{Path, PathBuf},
6 process::Command,
7 sync::{Mutex, mpsc},
8};
9
10use crate::async_compiler::{CompileRequest, Errors, Response};
11use clap::{Parser, ValueEnum};
12use mimium_audiodriver::{
13 AudioDriverOptions,
14 backends::{csv::csv_driver, local_buffer::LocalBufferDriver},
15 driver::{Driver, RuntimeData, SampleRate},
16 load_runtime_with_options,
17};
18use mimium_lang::{
19 Config, ExecContext,
20 compiler::{
21 self,
22 bytecodegen::SelfEvalMode,
23 emit_ast,
24 parser::{self as cst_parser, parser_errors_to_reportable},
25 },
26 log,
27 plugin::Plugin,
28 runtime::ProgramPayload,
29 utils::{
30 error::{ReportableError, report},
31 fileloader,
32 miniprint::MiniPrint,
33 },
34};
35#[cfg(target_os = "macos")]
36use notify::event::{AccessKind, EventKind, ModifyKind};
37#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
38use notify::event::{AccessKind, EventKind, ModifyKind};
39use notify::{Event, RecursiveMode, Watcher};
40use serde::{Deserialize, Serialize};
41
42#[cfg(not(target_arch = "wasm32"))]
43use mimium_lang::mir::StateType;
44#[cfg(not(target_arch = "wasm32"))]
45use mimium_lang::plugin::ExtFunTypeInfo;
46#[cfg(not(target_arch = "wasm32"))]
47use state_tree::StateStoragePatchPlan;
48#[cfg(not(target_arch = "wasm32"))]
49use state_tree::patch::CopyFromPatch;
50#[cfg(not(target_arch = "wasm32"))]
51use state_tree::tree::StateTreeSkeleton;
52
53#[derive(clap::Parser, Debug, Clone)]
54#[command(author, version, about, long_about = None)]
55pub struct Args {
56 #[command(flatten)]
57 pub mode: Mode,
58
59 #[clap(value_parser)]
61 pub file: Option<String>,
62
63 #[arg(long, short)]
65 pub output: Option<PathBuf>,
66
67 #[arg(long, default_value_t = 10)]
70 pub times: usize,
71
72 #[arg(long, value_enum)]
74 pub output_format: Option<OutputFileFormat>,
75
76 #[arg(long, default_value_t = false)]
78 pub no_gui: bool,
79
80 #[arg(long, value_enum, default_value_t = Backend::Wasm)]
82 pub backend: Backend,
83
84 #[arg(long)]
86 pub config: Option<PathBuf>,
87
88 #[arg(long, default_value_t = false)]
90 pub self_init_0: bool,
91}
92
93impl Args {
94 pub fn to_execctx_config(self) -> mimium_lang::Config {
95 mimium_lang::Config {
96 compiler: mimium_lang::compiler::Config {
97 self_eval_mode: if self.self_init_0 {
98 SelfEvalMode::ZeroAtInit
99 } else {
100 SelfEvalMode::SimpleState
101 },
102 },
103 }
104 }
105}
106
107#[derive(Clone, Debug, ValueEnum)]
108pub enum OutputFileFormat {
109 Csv,
110}
111
112#[derive(Clone, Copy, Debug, ValueEnum, Eq, PartialEq)]
113pub enum Backend {
114 Vm,
115 Wasm,
116}
117
118#[derive(Clone, Debug, Deserialize, Serialize, Default)]
119pub struct CliConfig {
120 #[serde(default)]
121 pub audio_setting: AudioSetting,
122}
123
124#[derive(Clone, Debug, Deserialize, Serialize)]
125#[serde(default)]
126#[serde(rename_all = "kebab-case")]
127pub struct AudioSetting {
128 pub input_device: String,
129 pub output_device: String,
130 pub buffer_size: u32,
131 pub sample_rate: u32,
132}
133
134impl Default for AudioSetting {
135 fn default() -> Self {
136 Self {
137 input_device: String::new(),
138 output_device: String::new(),
139 buffer_size: 512,
140 sample_rate: 48000,
141 }
142 }
143}
144
145impl AudioSetting {
146 fn to_driver_options(&self) -> AudioDriverOptions {
147 AudioDriverOptions {
148 input_device: (!self.input_device.trim().is_empty())
149 .then_some(self.input_device.clone()),
150 output_device: (!self.output_device.trim().is_empty())
151 .then_some(self.output_device.clone()),
152 buffer_size: (self.buffer_size > 0).then_some(self.buffer_size as usize),
153 }
154 }
155
156 fn effective_sample_rate(&self) -> u32 {
157 if self.sample_rate > 0 {
158 self.sample_rate
159 } else {
160 48000
161 }
162 }
163}
164
165fn home_dir() -> Option<PathBuf> {
166 env::var_os("HOME")
167 .map(PathBuf::from)
168 .or_else(|| env::var_os("USERPROFILE").map(PathBuf::from))
169}
170
171fn default_config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
172 home_dir()
173 .map(|home| home.join(".mimium").join("config.toml"))
174 .ok_or_else(|| "Could not resolve home directory for default config path".into())
175}
176
177fn expand_tilde(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
178 let raw = path.to_string_lossy();
179 if raw == "~" {
180 return home_dir().ok_or_else(|| "Could not resolve home directory".into());
181 }
182 if let Some(suffix) = raw.strip_prefix("~/").or_else(|| raw.strip_prefix("~\\")) {
183 return home_dir()
184 .map(|home| home.join(suffix))
185 .ok_or_else(|| "Could not resolve home directory".into());
186 }
187 Ok(path.to_path_buf())
188}
189
190fn resolve_config_path(path: Option<&PathBuf>) -> Result<PathBuf, Box<dyn std::error::Error>> {
191 path.map_or_else(default_config_path, |p| expand_tilde(p.as_path()))
192}
193
194fn load_or_create_cli_config(path: &Path) -> Result<CliConfig, Box<dyn std::error::Error>> {
195 if !path.exists() {
196 if let Some(parent) = path.parent() {
197 fs::create_dir_all(parent)?;
198 }
199 let default_cfg = CliConfig::default();
200 let serialized = toml::to_string_pretty(&default_cfg)?;
201 fs::write(path, serialized)?;
202 log::info!("Created default config at {}", path.display());
203 return Ok(default_cfg);
204 }
205
206 let content = fs::read_to_string(path)?;
207 let parsed: CliConfig = toml::from_str(&content)?;
208 Ok(parsed)
209}
210
211#[derive(clap::Args, Debug, Clone, Copy)]
212#[group(required = false, multiple = false)]
213pub struct Mode {
214 #[arg(long, default_value_t = false)]
216 pub emit_cst: bool,
217
218 #[arg(long, default_value_t = false)]
220 pub emit_ast: bool,
221
222 #[arg(long, default_value_t = false)]
224 pub emit_mir: bool,
225
226 #[arg(long, default_value_t = false)]
228 pub emit_bytecode: bool,
229
230 #[arg(long, default_value_t = false)]
232 pub emit_wasm: bool,
233}
234
235pub enum RunMode {
236 EmitCst,
237 EmitAst,
238 EmitMir,
239 EmitByteCode,
240 #[cfg(not(target_arch = "wasm32"))]
241 EmitWasm {
242 output: Option<PathBuf>,
243 },
244 NativeAudio,
245 #[cfg(not(target_arch = "wasm32"))]
246 WasmAudio,
247 WriteCsv {
248 times: usize,
249 output: Option<PathBuf>,
250 },
251}
252
253pub struct RunOptions {
255 mode: RunMode,
256 with_gui: bool,
257 use_wasm: bool,
259 audio_setting: AudioSetting,
260 config: Config,
261}
262
263impl RunOptions {
264 pub fn from_args(args: &Args, audio_setting: &AudioSetting) -> Self {
266 let config = args.clone().to_execctx_config();
267 #[cfg(not(target_arch = "wasm32"))]
268 let use_wasm_backend = matches!(args.backend, Backend::Wasm);
269 #[cfg(target_arch = "wasm32")]
270 let use_wasm_backend = false;
271
272 if args.mode.emit_cst {
273 return Self {
274 mode: RunMode::EmitCst,
275 with_gui: false,
276 use_wasm: false,
277 audio_setting: audio_setting.clone(),
278 config,
279 };
280 }
281
282 if args.mode.emit_ast {
283 return Self {
284 mode: RunMode::EmitAst,
285 with_gui: true,
286 use_wasm: false,
287 audio_setting: audio_setting.clone(),
288 config,
289 };
290 }
291
292 if args.mode.emit_mir {
293 return Self {
294 mode: RunMode::EmitMir,
295 with_gui: true,
296 use_wasm: false,
297 audio_setting: audio_setting.clone(),
298 config,
299 };
300 }
301
302 if args.mode.emit_bytecode {
303 return Self {
304 mode: RunMode::EmitByteCode,
305 with_gui: true,
306 use_wasm: false,
307 audio_setting: audio_setting.clone(),
308 config,
309 };
310 }
311
312 #[cfg(not(target_arch = "wasm32"))]
313 if args.mode.emit_wasm {
314 return Self {
315 mode: RunMode::EmitWasm {
316 output: args.output.clone(),
317 },
318 with_gui: false,
319 use_wasm: false,
320 audio_setting: audio_setting.clone(),
321 config,
322 };
323 }
324
325 #[cfg(not(target_arch = "wasm32"))]
326 if use_wasm_backend {
327 let mode = match (&args.output_format, args.output.as_ref()) {
329 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
330 times: args.times,
331 output: path.cloned(),
332 },
333 (None, Some(output))
334 if output.extension().and_then(|x| x.to_str()) == Some("csv") =>
335 {
336 RunMode::WriteCsv {
337 times: args.times,
338 output: Some(output.clone()),
339 }
340 }
341 _ => RunMode::WasmAudio,
342 };
343
344 let with_gui = match &mode {
345 RunMode::WasmAudio => !args.no_gui,
346 _ => false,
347 };
348
349 return Self {
350 mode,
351 with_gui,
352 use_wasm: true,
353 audio_setting: audio_setting.clone(),
354 config,
355 };
356 }
357
358 let mode = match (&args.output_format, args.output.as_ref()) {
359 (None, None) => RunMode::NativeAudio,
361 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
363 times: args.times,
364 output: path.cloned(),
365 },
366 (None, Some(output)) => match output.extension() {
368 Some(x) if &x.to_os_string() == "csv" => RunMode::WriteCsv {
369 times: args.times,
370 output: Some(output.clone()),
371 },
372 _ => panic!("cannot determine the output file format"),
373 },
374 };
375
376 let with_gui = match &mode {
377 RunMode::NativeAudio => !args.no_gui,
379 _ => false,
381 };
382
383 Self {
384 mode,
385 with_gui,
386 use_wasm: false,
387 audio_setting: audio_setting.clone(),
388 config,
389 }
390 }
391
392 fn get_driver(&self) -> Box<dyn Driver<Sample = f64>> {
393 match &self.mode {
394 RunMode::NativeAudio => {
395 load_runtime_with_options(&self.audio_setting.to_driver_options())
396 }
397 #[cfg(not(target_arch = "wasm32"))]
398 RunMode::WasmAudio => {
399 load_runtime_with_options(&self.audio_setting.to_driver_options())
400 }
401 RunMode::WriteCsv { times, output } => csv_driver(*times, output),
402 _ => unreachable!(),
403 }
404 }
405}
406
407pub fn get_default_context(
409 path: Option<PathBuf>,
410 with_gui: bool,
411 use_wasm_backend: bool,
412 config: Config,
413) -> ExecContext {
414 let plugins: Vec<Box<dyn Plugin>> = vec![];
415 let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
416
417 #[cfg(not(target_arch = "wasm32"))]
419 {
420 ctx.init_plugin_loader();
421
422 let mut loaded_count = 0;
423
424 if let Ok(exe_path) = std::env::current_exe()
426 && let Some(exe_dir) = exe_path.parent()
427 && let Some(loader) = ctx.get_plugin_loader_mut()
428 {
429 loaded_count = if use_wasm_backend {
432 loader
433 .load_plugins_from_dir_with_skip_substrings(exe_dir, &["symphonia"])
434 .unwrap_or(0)
435 } else {
436 loader.load_plugins_from_dir(exe_dir).unwrap_or(0)
437 };
438
439 if loaded_count > 0 {
440 log::debug!("Loaded {loaded_count} plugin(s) from executable directory");
441
442 if with_gui {
445 log::debug!("GUI mode: guitools will be provided as SystemPlugin");
448 }
449 }
450 }
451
452 if loaded_count == 0
454 && let Err(e) = if let Some(loader) = ctx.get_plugin_loader_mut() {
455 if use_wasm_backend {
456 loader.load_builtin_plugins_with_skip_substrings(&["symphonia"])
457 } else {
458 loader.load_builtin_plugins()
459 }
460 } else {
461 Ok(())
462 }
463 {
464 log::debug!("No builtin dynamic plugins found: {e:?}");
465 }
466 }
467
468 ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
469
470 if use_wasm_backend {
471 ctx.add_system_plugin(mimium_symphonia::SamplerPlugin::default());
472 }
473
474 if with_gui {
477 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
478 } else {
479 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::headless());
480 }
481
482 ctx
483}
484
485struct FileRunner {
486 pub tx_compiler: mpsc::Sender<CompileRequest>,
487 pub rx_compiler: mpsc::Receiver<Result<Response, Errors>>,
488 pub tx_prog: Option<mpsc::Sender<ProgramPayload>>,
489 pub fullpath: PathBuf,
490 pub use_wasm: bool,
492 #[cfg(not(target_arch = "wasm32"))]
497 old_program: Mutex<Option<OldWasmProgram>>,
498 #[cfg(not(target_arch = "wasm32"))]
503 retired_engine_receiver: Option<mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>>,
504}
505
506#[cfg(not(target_arch = "wasm32"))]
507#[derive(Clone)]
508struct OldWasmProgram {
509 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
511 ext_fns: Vec<ExtFunTypeInfo>,
513 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
515}
516
517#[cfg(not(target_arch = "wasm32"))]
518struct PreparedWasmSwapData {
519 prewarmed_global_state: Vec<u64>,
521 prepared_engine: Box<mimium_lang::runtime::wasm::engine::WasmEngine>,
523}
524
525struct FileWatcher {
526 pub rx: mpsc::Receiver<notify::Result<Event>>,
527 pub watcher: notify::RecommendedWatcher,
528}
529
530#[cfg(target_os = "macos")]
531fn should_recompile_on_event(event: &Event) -> bool {
532 matches!(
533 event.kind,
534 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
535 | EventKind::Modify(ModifyKind::Data(_))
536 | EventKind::Modify(ModifyKind::Any)
537 )
538}
539
540#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
541fn should_recompile_on_event(event: &Event) -> bool {
542 matches!(
543 event.kind,
544 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
545 | EventKind::Modify(ModifyKind::Data(_))
546 | EventKind::Modify(ModifyKind::Any)
547 )
548}
549
550#[cfg(target_os = "windows")]
551fn should_recompile_on_event(_event: &Event) -> bool {
552 true
553}
554
555impl FileRunner {
556 pub fn new(
557 compiler: compiler::Context,
558 path: PathBuf,
559 prog_tx: Option<mpsc::Sender<ProgramPayload>>,
560 use_wasm: bool,
561 #[cfg(not(target_arch = "wasm32"))] old_program: Option<OldWasmProgram>,
562 #[cfg(not(target_arch = "wasm32"))] retired_engine_receiver: Option<
563 mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>,
564 >,
565 ) -> Self {
566 let client = async_compiler::start_async_compiler_service(compiler);
567 Self {
568 tx_compiler: client.tx,
569 rx_compiler: client.rx,
570 tx_prog: prog_tx,
571 fullpath: path,
572 use_wasm,
573 #[cfg(not(target_arch = "wasm32"))]
574 old_program: Mutex::new(old_program),
575 #[cfg(not(target_arch = "wasm32"))]
576 retired_engine_receiver,
577 }
578 }
579 fn try_new_watcher(&self) -> Result<FileWatcher, notify::Error> {
580 let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
581 let mut watcher = notify::recommended_watcher(tx)?;
582 watcher.watch(Path::new(&self.fullpath), RecursiveMode::NonRecursive)?;
583 Ok(FileWatcher { rx, watcher })
584 }
585
586 #[cfg(not(target_arch = "wasm32"))]
587 fn try_compile_wasm_in_subprocess(&self) -> Result<Vec<u8>, String> {
588 let exe = env::current_exe().map_err(|e| format!("failed to resolve current exe: {e}"))?;
589 let output = Command::new(exe)
590 .arg(self.fullpath.as_os_str())
591 .arg("--backend=wasm")
592 .arg("--emit-wasm")
593 .output()
594 .map_err(|e| format!("failed to spawn compiler subprocess: {e}"))?;
595
596 if !output.status.success() {
597 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
598 return Err(format!(
599 "subprocess compile failed (status: {:?}): {}",
600 output.status.code(),
601 stderr
602 ));
603 }
604
605 if output.stdout.is_empty() {
606 return Err("subprocess compile succeeded but produced empty wasm stdout".to_string());
607 }
608
609 Ok(output.stdout)
610 }
611
612 #[cfg(not(target_arch = "wasm32"))]
613 fn try_prewarm_wasm_global_state(
614 wasm_bytes: &[u8],
615 ext_fns: &[ExtFunTypeInfo],
616 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
617 ) -> Result<PreparedWasmSwapData, String> {
618 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
619
620 let mut engine = WasmEngine::new(ext_fns, plugin_fns)
621 .map_err(|e| format!("failed to create prewarm wasm engine: {e}"))?;
622
623 engine
624 .load_module(wasm_bytes)
625 .map_err(|e| format!("failed to load module for prewarm: {e}"))?;
626
627 let mut runtime = WasmDspRuntime::new(engine, None, None);
628
629 runtime
630 .run_main()
631 .map_err(|e| format!("failed to run main for prewarm: {e}"))?;
632
633 let global_state = runtime
634 .engine_mut()
635 .get_global_state_data()
636 .map(|data| data.to_vec())
637 .ok_or_else(|| "missing global state after prewarm".to_string())?;
638
639 let prepared_engine = runtime.into_engine();
640
641 Ok(PreparedWasmSwapData {
642 prewarmed_global_state: global_state,
643 prepared_engine: Box::new(prepared_engine),
644 })
645 }
646
647 #[cfg(not(target_arch = "wasm32"))]
648 fn prepare_hot_swap_wasm_payload(
649 &self,
650 bytes: Vec<u8>,
651 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
652 ext_fns: Option<&[ExtFunTypeInfo]>,
653 ) -> Result<ProgramPayload, String> {
654 let old_program = self
655 .old_program
656 .lock()
657 .ok()
658 .and_then(|guard| (*guard).clone());
659 let previous_skeleton = old_program
660 .as_ref()
661 .and_then(|program| program.dsp_state_skeleton.clone());
662 let fallback_ext_fns: &[ExtFunTypeInfo] = old_program
663 .as_ref()
664 .map(|program| program.ext_fns.as_slice())
665 .unwrap_or(&[]);
666 let ext_fns = ext_fns.unwrap_or(fallback_ext_fns);
667 let plugin_fns = old_program
668 .as_ref()
669 .and_then(|program| program.plugin_fns.clone());
670
671 let prepared_swap_data =
672 Self::try_prewarm_wasm_global_state(&bytes, ext_fns, plugin_fns.clone())?;
673
674 let state_patch_plan = Self::build_required_state_patch_plan(
675 previous_skeleton,
676 dsp_state_skeleton.as_ref(),
677 prepared_swap_data.prewarmed_global_state.len(),
678 );
679 let payload = ProgramPayload::WasmModule {
680 bytes,
681 prepared_engine: prepared_swap_data.prepared_engine,
682 dsp_state_skeleton: dsp_state_skeleton.clone(),
683 state_patch_plan,
684 prewarmed_global_state: prepared_swap_data.prewarmed_global_state,
685 };
686 self.update_old_program(dsp_state_skeleton, ext_fns.to_vec(), plugin_fns);
687
688 Ok(payload)
689 }
690
691 #[cfg(not(target_arch = "wasm32"))]
692 fn build_required_state_patch_plan(
693 previous_skeleton: Option<StateTreeSkeleton<StateType>>,
694 new_skeleton: Option<&StateTreeSkeleton<StateType>>,
695 prewarmed_state_size: usize,
696 ) -> StateStoragePatchPlan {
697 if let (Some(old_skeleton), Some(new_skeleton)) = (previous_skeleton, new_skeleton.cloned())
698 {
699 let maybe_plan =
700 state_tree::build_state_storage_patch_plan(old_skeleton, new_skeleton.clone());
701 if let Some(plan) = maybe_plan {
702 return plan;
703 }
704 let total_size = new_skeleton.total_size() as usize;
705 return StateStoragePatchPlan {
706 total_size,
707 patches: vec![CopyFromPatch {
708 src_addr: 0,
709 dst_addr: 0,
710 size: total_size,
711 }],
712 };
713 }
714
715 StateStoragePatchPlan {
716 total_size: prewarmed_state_size,
717 patches: vec![],
718 }
719 }
720
721 #[cfg(not(target_arch = "wasm32"))]
722 fn update_old_program(
723 &self,
724 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
725 ext_fns: Vec<ExtFunTypeInfo>,
726 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
727 ) {
728 if let Ok(mut guard) = self.old_program.lock() {
729 *guard = Some(OldWasmProgram {
730 dsp_state_skeleton,
731 ext_fns,
732 plugin_fns,
733 });
734 }
735 }
736
737 fn recompile_file_inprocess(&self, new_content: String) {
738 #[cfg(not(target_arch = "wasm32"))]
739 let mode = RunMode::EmitByteCode;
740
741 #[cfg(target_arch = "wasm32")]
742 let mode = {
743 let _ = self.use_wasm;
744 RunMode::EmitByteCode
745 };
746 let _ = self.tx_compiler.send(CompileRequest {
747 source: new_content.clone(),
748 path: self.fullpath.clone(),
749 option: RunOptions {
750 mode,
751 with_gui: true,
752 use_wasm: self.use_wasm,
753 audio_setting: AudioSetting::default(),
754 config: Config::default(),
755 },
756 });
757 let _ = self.rx_compiler.recv().map(|res| match res {
758 Ok(Response::Ast(_)) | Ok(Response::Mir(_)) => {
759 log::warn!("unexpected response: AST/MIR");
760 }
761 Ok(Response::ByteCode(prog)) => {
762 log::info!("compiled successfully.");
763 if let Some(tx) = &self.tx_prog {
764 let _ = tx.send(ProgramPayload::VmProgram(prog));
765 }
766 }
767 #[cfg(not(target_arch = "wasm32"))]
768 Ok(Response::WasmModule(output)) => {
769 log::info!("WASM compiled successfully ({} bytes).", output.bytes.len());
770 if let Some(tx) = &self.tx_prog {
771 match self.prepare_hot_swap_wasm_payload(
772 output.bytes,
773 output.dsp_state_skeleton,
774 Some(&output.ext_fns),
775 ) {
776 Ok(payload) => {
777 let _ = tx.send(payload);
778 }
779 Err(e) => {
780 log::error!("WASM prepare_hot_swap failed; skip hot-swap by spec: {e}");
781 }
782 }
783 }
784 }
785 Err(errs) => {
786 let errs = errs
787 .into_iter()
788 .map(|e| Box::new(e) as Box<dyn ReportableError>)
789 .collect::<Vec<_>>();
790 report(&new_content, self.fullpath.clone(), &errs);
791 }
792 });
793 }
794
795 fn recompile_file(&self) {
796 match fileloader::load(&self.fullpath.to_string_lossy()) {
797 Ok(new_content) => {
798 #[cfg(not(target_arch = "wasm32"))]
799 {
800 if self.use_wasm {
801 match self.try_compile_wasm_in_subprocess() {
802 Ok(bytes) => {
803 log::info!(
804 "WASM compiled in subprocess successfully ({} bytes).",
805 bytes.len()
806 );
807 if let Some(tx) = &self.tx_prog {
808 match self.prepare_hot_swap_wasm_payload(bytes, None, None) {
809 Ok(payload) => {
810 let _ = tx.send(payload);
811 }
812 Err(e) => {
813 log::error!(
814 "WASM prepare_hot_swap failed; skip hot-swap by spec: {e}"
815 );
816 }
817 }
818 }
819 }
820 Err(e) => {
821 log::error!("{e}");
822 }
823 }
824 } else {
825 self.recompile_file_inprocess(new_content);
826 }
827 }
828
829 #[cfg(target_arch = "wasm32")]
830 {
831 self.recompile_file_inprocess(new_content);
832 }
833 }
834 Err(e) => {
835 log::error!(
836 "failed to reload the file {}: {}",
837 self.fullpath.display(),
838 e
839 );
840 }
841 }
842 }
843
844 #[cfg(not(target_arch = "wasm32"))]
845 fn drain_retired_engines(&self) {
846 if let Some(rx) = &self.retired_engine_receiver {
847 let mut dropped_count = 0usize;
848 while let Ok(_engine) = rx.try_recv() {
849 dropped_count += 1;
850 }
851 if dropped_count > 0 {
852 log::info!(
853 "WASM deferred drop: released {} retired engine(s) on non-RT thread",
854 dropped_count
855 );
856 }
857 }
858 }
859
860 pub fn cli_loop(&self) {
862 let file_watcher = match self.try_new_watcher() {
864 Ok(watcher) => watcher,
865 Err(e) => {
866 log::error!("Failed to watch file: {e}");
867 return;
868 }
869 };
870
871 loop {
872 #[cfg(not(target_arch = "wasm32"))]
873 self.drain_retired_engines();
874
875 match file_watcher
876 .rx
877 .recv_timeout(std::time::Duration::from_millis(100))
878 {
879 Ok(Ok(event)) => {
880 if should_recompile_on_event(&event) {
881 log::info!("File event detected ({:?}), recompiling...", event.kind);
882 self.recompile_file();
883 } else {
884 log::debug!("Ignored file event: {:?}", event.kind);
885 }
886 }
887 Ok(Err(e)) => {
888 log::error!("watch error event: {e}");
889 }
890 Err(mpsc::RecvTimeoutError::Timeout) => {
891 continue;
892 }
893 Err(e) => {
894 log::error!("receiver error: {e}");
895 }
896 }
897 }
898 }
899}
900
901pub fn run_file(
903 options: RunOptions,
904 content: &str,
905 fullpath: &Path,
906) -> Result<(), Vec<Box<dyn ReportableError>>> {
907 log::debug!("Filename: {}", fullpath.display());
908
909 let mut ctx = get_default_context(
910 Some(PathBuf::from(fullpath)),
911 options.with_gui,
912 options.use_wasm || matches!(options.mode, RunMode::EmitWasm { .. }),
913 options.config,
914 );
915
916 match options.mode {
917 RunMode::EmitCst => {
918 let tokens = cst_parser::tokenize(content);
919 let preparsed = cst_parser::preparse(&tokens);
920 let (green_id, arena, tokens, errors) = cst_parser::parse_cst(tokens, &preparsed);
921
922 if !errors.is_empty() {
924 let reportable_errors =
925 parser_errors_to_reportable(content, fullpath.to_path_buf(), errors);
926 report(content, fullpath.to_path_buf(), &reportable_errors);
927 }
928
929 let tree_output = arena.print_tree(green_id, &tokens, content, 0);
931 println!("{tree_output}");
932 Ok(())
933 }
934 RunMode::EmitAst => {
935 let ast = emit_ast(content, Some(PathBuf::from(fullpath)))?;
936 println!("{}", ast.pretty_print());
937 Ok(())
938 }
939 RunMode::EmitMir => {
940 ctx.prepare_compiler();
941 let res = ctx.get_compiler().unwrap().emit_mir(content);
942 res.map(|r| {
943 println!("{r}");
944 })?;
945 Ok(())
946 }
947 RunMode::EmitByteCode => {
948 let localdriver = LocalBufferDriver::new(0);
950 let plug = localdriver.get_as_plugin();
951 ctx.add_plugin(plug);
952 ctx.prepare_machine(content)?;
953 println!("{}", ctx.get_vm().unwrap().prog);
954 Ok(())
955 }
956 #[cfg(not(target_arch = "wasm32"))]
957 RunMode::EmitWasm { output } => {
958 use mimium_lang::utils::metadata::Location;
959 use std::io::Write;
960 use std::sync::Arc;
961
962 ctx.prepare_compiler();
963 let ext_fns = ctx.get_extfun_types();
964 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
965
966 let mut generator = compiler::wasmgen::WasmGenerator::new(Arc::new(mir), &ext_fns);
968 let wasm_bytes = generator.generate().map_err(|e| {
969 vec![Box::new(mimium_lang::utils::error::SimpleError {
970 message: e,
971 span: Location::default(),
972 }) as Box<dyn ReportableError>]
973 })?;
974
975 if let Some(path) = output {
976 std::fs::write(&path, &wasm_bytes).map_err(|e| {
977 vec![Box::new(mimium_lang::utils::error::SimpleError {
978 message: e.to_string(),
979 span: Location::default(),
980 }) as Box<dyn ReportableError>]
981 })?;
982 println!("Written to: {}", path.display());
983 } else {
984 let mut stdout = std::io::stdout().lock();
985 stdout.write_all(&wasm_bytes).map_err(|e| {
986 vec![Box::new(mimium_lang::utils::error::SimpleError {
987 message: e.to_string(),
988 span: Location::default(),
989 }) as Box<dyn ReportableError>]
990 })?;
991 stdout.flush().map_err(|e| {
992 vec![Box::new(mimium_lang::utils::error::SimpleError {
993 message: e.to_string(),
994 span: Location::default(),
995 }) as Box<dyn ReportableError>]
996 })?;
997 }
998
999 Ok(())
1000 }
1001 #[cfg(not(target_arch = "wasm32"))]
1002 RunMode::WasmAudio => {
1003 use mimium_lang::compiler::wasmgen::WasmGenerator;
1004 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
1005 use mimium_lang::utils::metadata::Location;
1006 use std::sync::Arc;
1007
1008 ctx.prepare_compiler();
1009 let mut ext_fns = ctx.get_extfun_types();
1010 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
1013 ext_fns.dedup_by(|a, b| a.name == b.name);
1014
1015 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
1016
1017 let io_channels = mir.get_dsp_iochannels();
1018 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
1019
1020 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
1022 let wasm_bytes = generator.generate().map_err(|e| {
1023 vec![Box::new(mimium_lang::utils::error::SimpleError {
1024 message: e,
1025 span: Location::default(),
1026 }) as Box<dyn ReportableError>]
1027 })?;
1028
1029 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1030
1031 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1035 let plugin_fns_for_hotswap = plugin_fns.clone();
1036
1037 let wasm_workers = ctx.generate_wasm_audioworkers();
1039
1040 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1041 vec![Box::new(mimium_lang::utils::error::SimpleError {
1042 message: format!("Failed to create WASM engine: {e}"),
1043 span: Location::default(),
1044 }) as Box<dyn ReportableError>]
1045 })?;
1046
1047 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1048 vec![Box::new(mimium_lang::utils::error::SimpleError {
1049 message: format!("Failed to load WASM module: {e}"),
1050 span: Location::default(),
1051 }) as Box<dyn ReportableError>]
1052 })?;
1053
1054 let mut wasm_runtime =
1056 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1057 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1058 let (retire_tx, retire_rx) = mpsc::channel();
1059 wasm_runtime.set_engine_retire_sender(retire_tx);
1060 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1061 let _ = wasm_runtime.run_main();
1062 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1063
1064 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1065
1066 let mut driver = options.get_driver();
1068
1069 let with_gui = options.with_gui;
1070 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1071 if with_gui {
1072 loop {
1073 std::thread::sleep(std::time::Duration::from_millis(1000));
1074 }
1075 }
1076 }));
1077
1078 driver.init(
1079 runtimedata,
1080 Some(SampleRate::from(
1081 options.audio_setting.effective_sample_rate(),
1082 )),
1083 );
1084 driver.play();
1085
1086 let compiler = ctx.take_compiler().unwrap();
1088 let frunner = FileRunner::new(
1089 compiler,
1090 fullpath.to_path_buf(),
1091 driver.get_program_channel(),
1092 true,
1093 Some(OldWasmProgram {
1094 dsp_state_skeleton: dsp_skeleton,
1095 ext_fns,
1096 plugin_fns: plugin_fns_for_hotswap,
1097 }),
1098 Some(retire_rx),
1099 );
1100 if with_gui {
1101 std::thread::spawn(move || frunner.cli_loop());
1102 }
1103
1104 mainloop();
1105 Ok(())
1106 }
1107 #[cfg(not(target_arch = "wasm32"))]
1108 _ if options.use_wasm => {
1109 use mimium_lang::compiler::wasmgen::WasmGenerator;
1111 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
1112 use mimium_lang::utils::metadata::Location;
1113 use std::sync::Arc;
1114
1115 let mut driver = options.get_driver();
1116
1117 ctx.prepare_compiler();
1118 let mut ext_fns = ctx.get_extfun_types();
1119 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
1122 ext_fns.dedup_by(|a, b| a.name == b.name);
1123
1124 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
1125 let io_channels = mir.get_dsp_iochannels();
1126 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
1127
1128 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
1129 let wasm_bytes = generator.generate().map_err(|e| {
1130 vec![Box::new(mimium_lang::utils::error::SimpleError {
1131 message: e,
1132 span: Location::default(),
1133 }) as Box<dyn ReportableError>]
1134 })?;
1135
1136 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1137
1138 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1142 let plugin_fns_for_hotswap = plugin_fns.clone();
1143
1144 let wasm_workers = ctx.generate_wasm_audioworkers();
1146
1147 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1148 vec![Box::new(mimium_lang::utils::error::SimpleError {
1149 message: format!("Failed to create WASM engine: {e}"),
1150 span: Location::default(),
1151 }) as Box<dyn ReportableError>]
1152 })?;
1153
1154 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1155 vec![Box::new(mimium_lang::utils::error::SimpleError {
1156 message: format!("Failed to load WASM module: {e}"),
1157 span: Location::default(),
1158 }) as Box<dyn ReportableError>]
1159 })?;
1160
1161 let mut wasm_runtime =
1162 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1163 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1164 let (retire_tx, retire_rx) = mpsc::channel();
1165 wasm_runtime.set_engine_retire_sender(retire_tx);
1166 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1167 let _ = wasm_runtime.run_main();
1168 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1169
1170 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1171
1172 let with_gui = options.with_gui;
1174 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1175 if with_gui {
1176 loop {
1177 std::thread::sleep(std::time::Duration::from_millis(1000));
1178 }
1179 }
1180 }));
1181
1182 driver.init(
1183 runtimedata,
1184 Some(SampleRate::from(
1185 options.audio_setting.effective_sample_rate(),
1186 )),
1187 );
1188 driver.play();
1189
1190 let compiler = ctx.take_compiler().unwrap();
1192 let frunner = FileRunner::new(
1193 compiler,
1194 fullpath.to_path_buf(),
1195 driver.get_program_channel(),
1196 true,
1197 Some(OldWasmProgram {
1198 dsp_state_skeleton: dsp_skeleton,
1199 ext_fns,
1200 plugin_fns: plugin_fns_for_hotswap,
1201 }),
1202 Some(retire_rx),
1203 );
1204 if with_gui {
1205 std::thread::spawn(move || frunner.cli_loop());
1206 }
1207
1208 mainloop();
1209 Ok(())
1210 }
1211 _ => {
1212 let mut driver = options.get_driver();
1213 let audiodriver_plug = driver.get_as_plugin();
1214
1215 ctx.add_plugin(audiodriver_plug);
1216 ctx.prepare_machine(content)?;
1217 let _res = ctx.run_main();
1218
1219 let runtimedata = {
1220 let ctxmut: &mut ExecContext = &mut ctx;
1221 RuntimeData::try_from(ctxmut).unwrap()
1222 };
1223
1224 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1225 if options.with_gui {
1226 loop {
1227 std::thread::sleep(std::time::Duration::from_millis(1000));
1228 }
1229 }
1230 }));
1231 driver.init(
1233 runtimedata,
1234 Some(SampleRate::from(
1235 options.audio_setting.effective_sample_rate(),
1236 )),
1237 );
1238 driver.play();
1239
1240 let compiler = ctx.take_compiler().unwrap();
1241
1242 let frunner = FileRunner::new(
1243 compiler,
1244 fullpath.to_path_buf(),
1245 driver.get_program_channel(),
1246 false,
1247 None,
1248 None,
1249 );
1250 if options.with_gui {
1251 std::thread::spawn(move || frunner.cli_loop());
1252 }
1253 mainloop();
1254 Ok(())
1255 }
1256 }
1257}
1258pub fn lib_main() -> Result<(), Box<dyn std::error::Error>> {
1259 if cfg!(debug_assertions) | cfg!(test) {
1260 colog::default_builder()
1261 .filter_level(log::LevelFilter::Trace)
1262 .init();
1263 } else {
1264 colog::default_builder().init();
1265 }
1266
1267 let args = Args::parse();
1268 let config_path = resolve_config_path(args.config.as_ref())?;
1269 let cli_config = load_or_create_cli_config(&config_path)?;
1270
1271 match &args.file {
1272 Some(file) => {
1273 let fullpath = fileloader::get_canonical_path(".", file)?;
1274 let content = fileloader::load(fullpath.to_str().unwrap())?;
1275 let options = RunOptions::from_args(&args, &cli_config.audio_setting);
1276 match run_file(options, &content, &fullpath) {
1277 Ok(()) => {}
1278 Err(e) => {
1279 report(&content, fullpath, &e);
1284 return Err(format!("Failed to process {file}").into());
1285 }
1286 }
1287 }
1288 None => {
1289 }
1291 }
1292 Ok(())
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::get_default_context;
1298 use mimium_lang::Config;
1299 use std::path::PathBuf;
1300
1301 #[test]
1302 fn default_cli_context_compiles_lift_array_code_source() {
1303 let src = r#"
1304// @test {"times":1,"stereo":false,"expected":[31.0],"web":true}
1305
1306#stage(macro)
1307fn mk_functions(){
1308 let funcs = [
1309 `|x| x + 1.0,
1310 `|x| x * 2.0,
1311 ]
1312 funcs |> lift_array_code
1313}
1314
1315#stage(main)
1316fn dsp(){
1317 let funcs = mk_functions!()
1318 funcs[0](10.0) + funcs[1](10.0)
1319}
1320"#;
1321 let mut ctx = get_default_context(
1322 Some(PathBuf::from("tmp/lift_array_code_test.mmm")),
1323 false,
1324 false,
1325 Config::default(),
1326 );
1327 ctx.prepare_compiler();
1328 let result = ctx.get_compiler().unwrap().emit_mir(src);
1329 assert!(result.is_ok(), "emit_mir failed: {result:?}");
1330 }
1331}