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_rust: bool,
233
234 #[arg(long, default_value_t = false)]
236 pub emit_wasm: bool,
237}
238
239pub enum RunMode {
240 EmitCst,
241 EmitAst,
242 EmitMir,
243 EmitByteCode,
244 EmitRust,
245 #[cfg(not(target_arch = "wasm32"))]
246 EmitWasm {
247 output: Option<PathBuf>,
248 },
249 NativeAudio,
250 #[cfg(not(target_arch = "wasm32"))]
251 WasmAudio,
252 WriteCsv {
253 times: usize,
254 output: Option<PathBuf>,
255 },
256}
257
258pub struct RunOptions {
260 mode: RunMode,
261 with_gui: bool,
262 use_wasm: bool,
264 audio_setting: AudioSetting,
265 config: Config,
266}
267
268impl RunOptions {
269 pub fn from_args(args: &Args, audio_setting: &AudioSetting) -> Self {
271 let config = args.clone().to_execctx_config();
272 #[cfg(not(target_arch = "wasm32"))]
273 let use_wasm_backend = matches!(args.backend, Backend::Wasm);
274 #[cfg(target_arch = "wasm32")]
275 let use_wasm_backend = false;
276
277 if args.mode.emit_cst {
278 return Self {
279 mode: RunMode::EmitCst,
280 with_gui: false,
281 use_wasm: false,
282 audio_setting: audio_setting.clone(),
283 config,
284 };
285 }
286
287 if args.mode.emit_ast {
288 return Self {
289 mode: RunMode::EmitAst,
290 with_gui: true,
291 use_wasm: false,
292 audio_setting: audio_setting.clone(),
293 config,
294 };
295 }
296
297 if args.mode.emit_mir {
298 return Self {
299 mode: RunMode::EmitMir,
300 with_gui: true,
301 use_wasm: false,
302 audio_setting: audio_setting.clone(),
303 config,
304 };
305 }
306
307 if args.mode.emit_bytecode {
308 return Self {
309 mode: RunMode::EmitByteCode,
310 with_gui: true,
311 use_wasm: false,
312 audio_setting: audio_setting.clone(),
313 config,
314 };
315 }
316
317 if args.mode.emit_rust {
318 return Self {
319 mode: RunMode::EmitRust,
320 with_gui: false,
321 use_wasm: false,
322 audio_setting: audio_setting.clone(),
323 config,
324 };
325 }
326
327 #[cfg(not(target_arch = "wasm32"))]
328 if args.mode.emit_wasm {
329 return Self {
330 mode: RunMode::EmitWasm {
331 output: args.output.clone(),
332 },
333 with_gui: false,
334 use_wasm: false,
335 audio_setting: audio_setting.clone(),
336 config,
337 };
338 }
339
340 #[cfg(not(target_arch = "wasm32"))]
341 if use_wasm_backend {
342 let mode = match (&args.output_format, args.output.as_ref()) {
344 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
345 times: args.times,
346 output: path.cloned(),
347 },
348 (None, Some(output))
349 if output.extension().and_then(|x| x.to_str()) == Some("csv") =>
350 {
351 RunMode::WriteCsv {
352 times: args.times,
353 output: Some(output.clone()),
354 }
355 }
356 _ => RunMode::WasmAudio,
357 };
358
359 let with_gui = match &mode {
360 RunMode::WasmAudio => !args.no_gui,
361 _ => false,
362 };
363
364 return Self {
365 mode,
366 with_gui,
367 use_wasm: true,
368 audio_setting: audio_setting.clone(),
369 config,
370 };
371 }
372
373 let mode = match (&args.output_format, args.output.as_ref()) {
374 (None, None) => RunMode::NativeAudio,
376 (Some(OutputFileFormat::Csv), path) => RunMode::WriteCsv {
378 times: args.times,
379 output: path.cloned(),
380 },
381 (None, Some(output)) => match output.extension() {
383 Some(x) if &x.to_os_string() == "csv" => RunMode::WriteCsv {
384 times: args.times,
385 output: Some(output.clone()),
386 },
387 _ => panic!("cannot determine the output file format"),
388 },
389 };
390
391 let with_gui = match &mode {
392 RunMode::NativeAudio => !args.no_gui,
394 _ => false,
396 };
397
398 Self {
399 mode,
400 with_gui,
401 use_wasm: false,
402 audio_setting: audio_setting.clone(),
403 config,
404 }
405 }
406
407 fn get_driver(&self) -> Box<dyn Driver<Sample = f64>> {
408 match &self.mode {
409 RunMode::NativeAudio => {
410 load_runtime_with_options(&self.audio_setting.to_driver_options())
411 }
412 #[cfg(not(target_arch = "wasm32"))]
413 RunMode::WasmAudio => {
414 load_runtime_with_options(&self.audio_setting.to_driver_options())
415 }
416 RunMode::WriteCsv { times, output } => csv_driver(*times, output),
417 _ => unreachable!(),
418 }
419 }
420}
421
422pub fn get_default_context(
424 path: Option<PathBuf>,
425 with_gui: bool,
426 use_wasm_backend: bool,
427 config: Config,
428) -> ExecContext {
429 let plugins: Vec<Box<dyn Plugin>> = vec![];
430 let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
431
432 #[cfg(not(target_arch = "wasm32"))]
434 {
435 ctx.init_plugin_loader();
436
437 let mut loaded_count = 0;
438
439 if let Ok(exe_path) = std::env::current_exe()
441 && let Some(exe_dir) = exe_path.parent()
442 && let Some(loader) = ctx.get_plugin_loader_mut()
443 {
444 loaded_count = if use_wasm_backend {
447 loader
448 .load_plugins_from_dir_with_skip_substrings(exe_dir, &["symphonia"])
449 .unwrap_or(0)
450 } else {
451 loader.load_plugins_from_dir(exe_dir).unwrap_or(0)
452 };
453
454 if loaded_count > 0 {
455 log::debug!("Loaded {loaded_count} plugin(s) from executable directory");
456
457 if with_gui {
460 log::debug!("GUI mode: guitools will be provided as SystemPlugin");
463 }
464 }
465 }
466
467 if loaded_count == 0
469 && let Err(e) = if let Some(loader) = ctx.get_plugin_loader_mut() {
470 if use_wasm_backend {
471 loader.load_builtin_plugins_with_skip_substrings(&["symphonia"])
472 } else {
473 loader.load_builtin_plugins()
474 }
475 } else {
476 Ok(())
477 }
478 {
479 log::debug!("No builtin dynamic plugins found: {e:?}");
480 }
481 }
482
483 ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
484
485 if use_wasm_backend {
486 ctx.add_system_plugin(mimium_symphonia::SamplerPlugin::default());
487 }
488
489 if with_gui {
492 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
493 } else {
494 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::headless());
495 }
496
497 ctx
498}
499
500struct FileRunner {
501 pub tx_compiler: mpsc::Sender<CompileRequest>,
502 pub rx_compiler: mpsc::Receiver<Result<Response, Errors>>,
503 pub tx_prog: Option<mpsc::Sender<ProgramPayload>>,
504 pub fullpath: PathBuf,
505 pub use_wasm: bool,
507 #[cfg(not(target_arch = "wasm32"))]
512 old_program: Mutex<Option<OldWasmProgram>>,
513 #[cfg(not(target_arch = "wasm32"))]
518 retired_engine_receiver: Option<mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>>,
519}
520
521#[cfg(not(target_arch = "wasm32"))]
522#[derive(Clone)]
523struct OldWasmProgram {
524 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
526 ext_fns: Vec<ExtFunTypeInfo>,
528 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
530}
531
532#[cfg(not(target_arch = "wasm32"))]
533struct PreparedWasmSwapData {
534 prewarmed_global_state: Vec<u64>,
536 prepared_engine: Box<mimium_lang::runtime::wasm::engine::WasmEngine>,
538}
539
540struct FileWatcher {
541 pub rx: mpsc::Receiver<notify::Result<Event>>,
542 pub watcher: notify::RecommendedWatcher,
543}
544
545#[cfg(target_os = "macos")]
546fn should_recompile_on_event(event: &Event) -> bool {
547 matches!(
548 event.kind,
549 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
550 | EventKind::Modify(ModifyKind::Data(_))
551 | EventKind::Modify(ModifyKind::Any)
552 )
553}
554
555#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
556fn should_recompile_on_event(event: &Event) -> bool {
557 matches!(
558 event.kind,
559 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
560 | EventKind::Modify(ModifyKind::Data(_))
561 | EventKind::Modify(ModifyKind::Any)
562 )
563}
564
565#[cfg(target_os = "windows")]
566fn should_recompile_on_event(_event: &Event) -> bool {
567 true
568}
569
570impl FileRunner {
571 pub fn new(
572 compiler: compiler::Context,
573 path: PathBuf,
574 prog_tx: Option<mpsc::Sender<ProgramPayload>>,
575 use_wasm: bool,
576 #[cfg(not(target_arch = "wasm32"))] old_program: Option<OldWasmProgram>,
577 #[cfg(not(target_arch = "wasm32"))] retired_engine_receiver: Option<
578 mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>,
579 >,
580 ) -> Self {
581 let client = async_compiler::start_async_compiler_service(compiler);
582 Self {
583 tx_compiler: client.tx,
584 rx_compiler: client.rx,
585 tx_prog: prog_tx,
586 fullpath: path,
587 use_wasm,
588 #[cfg(not(target_arch = "wasm32"))]
589 old_program: Mutex::new(old_program),
590 #[cfg(not(target_arch = "wasm32"))]
591 retired_engine_receiver,
592 }
593 }
594 fn try_new_watcher(&self) -> Result<FileWatcher, notify::Error> {
595 let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
596 let mut watcher = notify::recommended_watcher(tx)?;
597 watcher.watch(Path::new(&self.fullpath), RecursiveMode::NonRecursive)?;
598 Ok(FileWatcher { rx, watcher })
599 }
600
601 #[cfg(not(target_arch = "wasm32"))]
602 fn try_compile_wasm_in_subprocess(&self) -> Result<Vec<u8>, String> {
603 let exe = env::current_exe().map_err(|e| format!("failed to resolve current exe: {e}"))?;
604 let output = Command::new(exe)
605 .arg(self.fullpath.as_os_str())
606 .arg("--backend=wasm")
607 .arg("--emit-wasm")
608 .output()
609 .map_err(|e| format!("failed to spawn compiler subprocess: {e}"))?;
610
611 if !output.status.success() {
612 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
613 return Err(format!(
614 "subprocess compile failed (status: {:?}): {}",
615 output.status.code(),
616 stderr
617 ));
618 }
619
620 if output.stdout.is_empty() {
621 return Err("subprocess compile succeeded but produced empty wasm stdout".to_string());
622 }
623
624 Ok(output.stdout)
625 }
626
627 #[cfg(not(target_arch = "wasm32"))]
628 fn try_prewarm_wasm_global_state(
629 wasm_bytes: &[u8],
630 ext_fns: &[ExtFunTypeInfo],
631 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
632 ) -> Result<PreparedWasmSwapData, String> {
633 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
634
635 let mut engine = WasmEngine::new(ext_fns, plugin_fns)
636 .map_err(|e| format!("failed to create prewarm wasm engine: {e}"))?;
637
638 engine
639 .load_module(wasm_bytes)
640 .map_err(|e| format!("failed to load module for prewarm: {e}"))?;
641
642 let mut runtime = WasmDspRuntime::new(engine, None, None);
643
644 runtime
645 .run_main()
646 .map_err(|e| format!("failed to run main for prewarm: {e}"))?;
647
648 let global_state = runtime
649 .engine_mut()
650 .get_global_state_data()
651 .map(|data| data.to_vec())
652 .ok_or_else(|| "missing global state after prewarm".to_string())?;
653
654 let prepared_engine = runtime.into_engine();
655
656 Ok(PreparedWasmSwapData {
657 prewarmed_global_state: global_state,
658 prepared_engine: Box::new(prepared_engine),
659 })
660 }
661
662 #[cfg(not(target_arch = "wasm32"))]
663 fn prepare_hot_swap_wasm_payload(
664 &self,
665 bytes: Vec<u8>,
666 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
667 ext_fns: Option<&[ExtFunTypeInfo]>,
668 ) -> Result<ProgramPayload, String> {
669 let old_program = self
670 .old_program
671 .lock()
672 .ok()
673 .and_then(|guard| (*guard).clone());
674 let previous_skeleton = old_program
675 .as_ref()
676 .and_then(|program| program.dsp_state_skeleton.clone());
677 let fallback_ext_fns: &[ExtFunTypeInfo] = old_program
678 .as_ref()
679 .map(|program| program.ext_fns.as_slice())
680 .unwrap_or(&[]);
681 let ext_fns = ext_fns.unwrap_or(fallback_ext_fns);
682 let plugin_fns = old_program
683 .as_ref()
684 .and_then(|program| program.plugin_fns.clone());
685
686 let prepared_swap_data =
687 Self::try_prewarm_wasm_global_state(&bytes, ext_fns, plugin_fns.clone())?;
688
689 let state_patch_plan = Self::build_required_state_patch_plan(
690 previous_skeleton,
691 dsp_state_skeleton.as_ref(),
692 prepared_swap_data.prewarmed_global_state.len(),
693 );
694 let payload = ProgramPayload::WasmModule {
695 bytes,
696 prepared_engine: prepared_swap_data.prepared_engine,
697 dsp_state_skeleton: dsp_state_skeleton.clone(),
698 state_patch_plan,
699 prewarmed_global_state: prepared_swap_data.prewarmed_global_state,
700 };
701 self.update_old_program(dsp_state_skeleton, ext_fns.to_vec(), plugin_fns);
702
703 Ok(payload)
704 }
705
706 #[cfg(not(target_arch = "wasm32"))]
707 fn build_required_state_patch_plan(
708 previous_skeleton: Option<StateTreeSkeleton<StateType>>,
709 new_skeleton: Option<&StateTreeSkeleton<StateType>>,
710 prewarmed_state_size: usize,
711 ) -> StateStoragePatchPlan {
712 if let (Some(old_skeleton), Some(new_skeleton)) = (previous_skeleton, new_skeleton.cloned())
713 {
714 let maybe_plan =
715 state_tree::build_state_storage_patch_plan(old_skeleton, new_skeleton.clone());
716 if let Some(plan) = maybe_plan {
717 return plan;
718 }
719 let total_size = new_skeleton.total_size() as usize;
720 return StateStoragePatchPlan {
721 total_size,
722 patches: vec![CopyFromPatch {
723 src_addr: 0,
724 dst_addr: 0,
725 size: total_size,
726 }],
727 };
728 }
729
730 StateStoragePatchPlan {
731 total_size: prewarmed_state_size,
732 patches: vec![],
733 }
734 }
735
736 #[cfg(not(target_arch = "wasm32"))]
737 fn update_old_program(
738 &self,
739 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
740 ext_fns: Vec<ExtFunTypeInfo>,
741 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
742 ) {
743 if let Ok(mut guard) = self.old_program.lock() {
744 *guard = Some(OldWasmProgram {
745 dsp_state_skeleton,
746 ext_fns,
747 plugin_fns,
748 });
749 }
750 }
751
752 fn recompile_file_inprocess(&self, new_content: String) {
753 #[cfg(not(target_arch = "wasm32"))]
754 let mode = RunMode::EmitByteCode;
755
756 #[cfg(target_arch = "wasm32")]
757 let mode = {
758 let _ = self.use_wasm;
759 RunMode::EmitByteCode
760 };
761 let _ = self.tx_compiler.send(CompileRequest {
762 source: new_content.clone(),
763 path: self.fullpath.clone(),
764 option: RunOptions {
765 mode,
766 with_gui: true,
767 use_wasm: self.use_wasm,
768 audio_setting: AudioSetting::default(),
769 config: Config::default(),
770 },
771 });
772 let _ = self.rx_compiler.recv().map(|res| match res {
773 Ok(Response::Ast(_)) | Ok(Response::Mir(_)) => {
774 log::warn!("unexpected response: AST/MIR");
775 }
776 Ok(Response::ByteCode(prog)) => {
777 log::info!("compiled successfully.");
778 if let Some(tx) = &self.tx_prog {
779 let _ = tx.send(ProgramPayload::VmProgram(prog));
780 }
781 }
782 #[cfg(not(target_arch = "wasm32"))]
783 Ok(Response::WasmModule(output)) => {
784 log::info!("WASM compiled successfully ({} bytes).", output.bytes.len());
785 if let Some(tx) = &self.tx_prog {
786 match self.prepare_hot_swap_wasm_payload(
787 output.bytes,
788 output.dsp_state_skeleton,
789 Some(&output.ext_fns),
790 ) {
791 Ok(payload) => {
792 let _ = tx.send(payload);
793 }
794 Err(e) => {
795 log::error!("WASM prepare_hot_swap failed; skip hot-swap by spec: {e}");
796 }
797 }
798 }
799 }
800 Err(errs) => {
801 let errs = errs
802 .into_iter()
803 .map(|e| Box::new(e) as Box<dyn ReportableError>)
804 .collect::<Vec<_>>();
805 report(&new_content, self.fullpath.clone(), &errs);
806 }
807 });
808 }
809
810 fn recompile_file(&self) {
811 match fileloader::load(&self.fullpath.to_string_lossy()) {
812 Ok(new_content) => {
813 #[cfg(not(target_arch = "wasm32"))]
814 {
815 if self.use_wasm {
816 match self.try_compile_wasm_in_subprocess() {
817 Ok(bytes) => {
818 log::info!(
819 "WASM compiled in subprocess successfully ({} bytes).",
820 bytes.len()
821 );
822 if let Some(tx) = &self.tx_prog {
823 match self.prepare_hot_swap_wasm_payload(bytes, None, None) {
824 Ok(payload) => {
825 let _ = tx.send(payload);
826 }
827 Err(e) => {
828 log::error!(
829 "WASM prepare_hot_swap failed; skip hot-swap by spec: {e}"
830 );
831 }
832 }
833 }
834 }
835 Err(e) => {
836 log::error!("{e}");
837 }
838 }
839 } else {
840 self.recompile_file_inprocess(new_content);
841 }
842 }
843
844 #[cfg(target_arch = "wasm32")]
845 {
846 self.recompile_file_inprocess(new_content);
847 }
848 }
849 Err(e) => {
850 log::error!(
851 "failed to reload the file {}: {}",
852 self.fullpath.display(),
853 e
854 );
855 }
856 }
857 }
858
859 #[cfg(not(target_arch = "wasm32"))]
860 fn drain_retired_engines(&self) {
861 if let Some(rx) = &self.retired_engine_receiver {
862 let mut dropped_count = 0usize;
863 while let Ok(_engine) = rx.try_recv() {
864 dropped_count += 1;
865 }
866 if dropped_count > 0 {
867 log::info!(
868 "WASM deferred drop: released {} retired engine(s) on non-RT thread",
869 dropped_count
870 );
871 }
872 }
873 }
874
875 pub fn cli_loop(&self) {
877 let file_watcher = match self.try_new_watcher() {
879 Ok(watcher) => watcher,
880 Err(e) => {
881 log::error!("Failed to watch file: {e}");
882 return;
883 }
884 };
885
886 loop {
887 #[cfg(not(target_arch = "wasm32"))]
888 self.drain_retired_engines();
889
890 match file_watcher
891 .rx
892 .recv_timeout(std::time::Duration::from_millis(100))
893 {
894 Ok(Ok(event)) => {
895 if should_recompile_on_event(&event) {
896 log::info!("File event detected ({:?}), recompiling...", event.kind);
897 self.recompile_file();
898 } else {
899 log::debug!("Ignored file event: {:?}", event.kind);
900 }
901 }
902 Ok(Err(e)) => {
903 log::error!("watch error event: {e}");
904 }
905 Err(mpsc::RecvTimeoutError::Timeout) => {
906 continue;
907 }
908 Err(e) => {
909 log::error!("receiver error: {e}");
910 }
911 }
912 }
913 }
914}
915
916pub fn run_file(
918 options: RunOptions,
919 content: &str,
920 fullpath: &Path,
921) -> Result<(), Vec<Box<dyn ReportableError>>> {
922 log::debug!("Filename: {}", fullpath.display());
923
924 let mut ctx = get_default_context(
925 Some(PathBuf::from(fullpath)),
926 options.with_gui,
927 options.use_wasm || matches!(options.mode, RunMode::EmitWasm { .. }),
928 options.config,
929 );
930
931 match options.mode {
932 RunMode::EmitCst => {
933 let tokens = cst_parser::tokenize(content);
934 let preparsed = cst_parser::preparse(&tokens);
935 let (green_id, arena, tokens, errors) = cst_parser::parse_cst(tokens, &preparsed);
936
937 if !errors.is_empty() {
939 let reportable_errors =
940 parser_errors_to_reportable(content, fullpath.to_path_buf(), errors);
941 report(content, fullpath.to_path_buf(), &reportable_errors);
942 }
943
944 let tree_output = arena.print_tree(green_id, &tokens, content, 0);
946 println!("{tree_output}");
947 Ok(())
948 }
949 RunMode::EmitAst => {
950 let ast = emit_ast(content, Some(PathBuf::from(fullpath)))?;
951 println!("{}", ast.pretty_print());
952 Ok(())
953 }
954 RunMode::EmitMir => {
955 ctx.prepare_compiler();
956 let res = ctx.get_compiler().unwrap().emit_mir(content);
957 res.map(|r| {
958 println!("{r}");
959 })?;
960 Ok(())
961 }
962 RunMode::EmitByteCode => {
963 let localdriver = LocalBufferDriver::new(0);
965 let plug = localdriver.get_as_plugin();
966 ctx.add_plugin(plug);
967 ctx.prepare_machine(content)?;
968 println!("{}", ctx.get_vm().unwrap().prog);
969 Ok(())
970 }
971 RunMode::EmitRust => {
972 ctx.prepare_compiler();
973 let output = ctx.get_compiler().unwrap().emit_rust(content)?;
974 println!("{}", output.source);
975 Ok(())
976 }
977 #[cfg(not(target_arch = "wasm32"))]
978 RunMode::EmitWasm { output } => {
979 use mimium_lang::utils::metadata::Location;
980 use std::io::Write;
981 use std::sync::Arc;
982
983 ctx.prepare_compiler();
984 let ext_fns = ctx.get_extfun_types();
985 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
986
987 let mut generator = compiler::wasmgen::WasmGenerator::new(Arc::new(mir), &ext_fns);
989 let wasm_bytes = generator.generate().map_err(|e| {
990 vec![Box::new(mimium_lang::utils::error::SimpleError {
991 message: e,
992 span: Location::default(),
993 }) as Box<dyn ReportableError>]
994 })?;
995
996 if let Some(path) = output {
997 std::fs::write(&path, &wasm_bytes).map_err(|e| {
998 vec![Box::new(mimium_lang::utils::error::SimpleError {
999 message: e.to_string(),
1000 span: Location::default(),
1001 }) as Box<dyn ReportableError>]
1002 })?;
1003 println!("Written to: {}", path.display());
1004 } else {
1005 let mut stdout = std::io::stdout().lock();
1006 stdout.write_all(&wasm_bytes).map_err(|e| {
1007 vec![Box::new(mimium_lang::utils::error::SimpleError {
1008 message: e.to_string(),
1009 span: Location::default(),
1010 }) as Box<dyn ReportableError>]
1011 })?;
1012 stdout.flush().map_err(|e| {
1013 vec![Box::new(mimium_lang::utils::error::SimpleError {
1014 message: e.to_string(),
1015 span: Location::default(),
1016 }) as Box<dyn ReportableError>]
1017 })?;
1018 }
1019
1020 Ok(())
1021 }
1022 #[cfg(not(target_arch = "wasm32"))]
1023 RunMode::WasmAudio => {
1024 use mimium_lang::compiler::wasmgen::WasmGenerator;
1025 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
1026 use mimium_lang::utils::metadata::Location;
1027 use std::sync::Arc;
1028
1029 ctx.prepare_compiler();
1030 let mut ext_fns = ctx.get_extfun_types();
1031 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
1034 ext_fns.dedup_by(|a, b| a.name == b.name);
1035
1036 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
1037
1038 let io_channels = mir.get_dsp_iochannels();
1039 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
1040
1041 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
1043 let wasm_bytes = generator.generate().map_err(|e| {
1044 vec![Box::new(mimium_lang::utils::error::SimpleError {
1045 message: e,
1046 span: Location::default(),
1047 }) as Box<dyn ReportableError>]
1048 })?;
1049
1050 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1051
1052 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1056 let plugin_fns_for_hotswap = plugin_fns.clone();
1057
1058 let wasm_workers = ctx.generate_wasm_audioworkers();
1060
1061 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1062 vec![Box::new(mimium_lang::utils::error::SimpleError {
1063 message: format!("Failed to create WASM engine: {e}"),
1064 span: Location::default(),
1065 }) as Box<dyn ReportableError>]
1066 })?;
1067
1068 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1069 vec![Box::new(mimium_lang::utils::error::SimpleError {
1070 message: format!("Failed to load WASM module: {e}"),
1071 span: Location::default(),
1072 }) as Box<dyn ReportableError>]
1073 })?;
1074
1075 let mut wasm_runtime =
1077 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1078 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1079 let (retire_tx, retire_rx) = mpsc::channel();
1080 wasm_runtime.set_engine_retire_sender(retire_tx);
1081 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1082 let _ = wasm_runtime.run_main();
1083 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1084
1085 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1086
1087 let mut driver = options.get_driver();
1089
1090 let with_gui = options.with_gui;
1091 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1092 if with_gui {
1093 loop {
1094 std::thread::sleep(std::time::Duration::from_millis(1000));
1095 }
1096 }
1097 }));
1098
1099 driver.init(
1100 runtimedata,
1101 Some(SampleRate::from(
1102 options.audio_setting.effective_sample_rate(),
1103 )),
1104 );
1105 driver.play();
1106
1107 let compiler = ctx.take_compiler().unwrap();
1109 let frunner = FileRunner::new(
1110 compiler,
1111 fullpath.to_path_buf(),
1112 driver.get_program_channel(),
1113 true,
1114 Some(OldWasmProgram {
1115 dsp_state_skeleton: dsp_skeleton,
1116 ext_fns,
1117 plugin_fns: plugin_fns_for_hotswap,
1118 }),
1119 Some(retire_rx),
1120 );
1121 if with_gui {
1122 std::thread::spawn(move || frunner.cli_loop());
1123 }
1124
1125 mainloop();
1126 Ok(())
1127 }
1128 #[cfg(not(target_arch = "wasm32"))]
1129 _ if options.use_wasm => {
1130 use mimium_lang::compiler::wasmgen::WasmGenerator;
1132 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
1133 use mimium_lang::utils::metadata::Location;
1134 use std::sync::Arc;
1135
1136 let mut driver = options.get_driver();
1137
1138 ctx.prepare_compiler();
1139 let mut ext_fns = ctx.get_extfun_types();
1140 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
1143 ext_fns.dedup_by(|a, b| a.name == b.name);
1144
1145 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
1146 let io_channels = mir.get_dsp_iochannels();
1147 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
1148
1149 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
1150 let wasm_bytes = generator.generate().map_err(|e| {
1151 vec![Box::new(mimium_lang::utils::error::SimpleError {
1152 message: e,
1153 span: Location::default(),
1154 }) as Box<dyn ReportableError>]
1155 })?;
1156
1157 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1158
1159 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1163 let plugin_fns_for_hotswap = plugin_fns.clone();
1164
1165 let wasm_workers = ctx.generate_wasm_audioworkers();
1167
1168 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1169 vec![Box::new(mimium_lang::utils::error::SimpleError {
1170 message: format!("Failed to create WASM engine: {e}"),
1171 span: Location::default(),
1172 }) as Box<dyn ReportableError>]
1173 })?;
1174
1175 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1176 vec![Box::new(mimium_lang::utils::error::SimpleError {
1177 message: format!("Failed to load WASM module: {e}"),
1178 span: Location::default(),
1179 }) as Box<dyn ReportableError>]
1180 })?;
1181
1182 let mut wasm_runtime =
1183 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1184 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1185 let (retire_tx, retire_rx) = mpsc::channel();
1186 wasm_runtime.set_engine_retire_sender(retire_tx);
1187 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1188 let _ = wasm_runtime.run_main();
1189 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1190
1191 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1192
1193 let with_gui = options.with_gui;
1195 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1196 if with_gui {
1197 loop {
1198 std::thread::sleep(std::time::Duration::from_millis(1000));
1199 }
1200 }
1201 }));
1202
1203 driver.init(
1204 runtimedata,
1205 Some(SampleRate::from(
1206 options.audio_setting.effective_sample_rate(),
1207 )),
1208 );
1209 driver.play();
1210
1211 let compiler = ctx.take_compiler().unwrap();
1213 let frunner = FileRunner::new(
1214 compiler,
1215 fullpath.to_path_buf(),
1216 driver.get_program_channel(),
1217 true,
1218 Some(OldWasmProgram {
1219 dsp_state_skeleton: dsp_skeleton,
1220 ext_fns,
1221 plugin_fns: plugin_fns_for_hotswap,
1222 }),
1223 Some(retire_rx),
1224 );
1225 if with_gui {
1226 std::thread::spawn(move || frunner.cli_loop());
1227 }
1228
1229 mainloop();
1230 Ok(())
1231 }
1232 _ => {
1233 let mut driver = options.get_driver();
1234 let audiodriver_plug = driver.get_as_plugin();
1235
1236 ctx.add_plugin(audiodriver_plug);
1237 ctx.prepare_machine(content)?;
1238 let _res = ctx.run_main();
1239
1240 let runtimedata = {
1241 let ctxmut: &mut ExecContext = &mut ctx;
1242 RuntimeData::try_from(ctxmut).unwrap()
1243 };
1244
1245 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1246 if options.with_gui {
1247 loop {
1248 std::thread::sleep(std::time::Duration::from_millis(1000));
1249 }
1250 }
1251 }));
1252 driver.init(
1254 runtimedata,
1255 Some(SampleRate::from(
1256 options.audio_setting.effective_sample_rate(),
1257 )),
1258 );
1259 driver.play();
1260
1261 let compiler = ctx.take_compiler().unwrap();
1262
1263 let frunner = FileRunner::new(
1264 compiler,
1265 fullpath.to_path_buf(),
1266 driver.get_program_channel(),
1267 false,
1268 None,
1269 None,
1270 );
1271 if options.with_gui {
1272 std::thread::spawn(move || frunner.cli_loop());
1273 }
1274 mainloop();
1275 Ok(())
1276 }
1277 }
1278}
1279pub fn lib_main() -> Result<(), Box<dyn std::error::Error>> {
1280 if cfg!(debug_assertions) | cfg!(test) {
1281 colog::default_builder()
1282 .filter_level(log::LevelFilter::Trace)
1283 .init();
1284 } else {
1285 colog::default_builder().init();
1286 }
1287
1288 let args = Args::parse();
1289 let config_path = resolve_config_path(args.config.as_ref())?;
1290 let cli_config = load_or_create_cli_config(&config_path)?;
1291
1292 match &args.file {
1293 Some(file) => {
1294 let fullpath = fileloader::get_canonical_path(".", file)?;
1295 let content = fileloader::load(fullpath.to_str().unwrap())?;
1296 let options = RunOptions::from_args(&args, &cli_config.audio_setting);
1297 match run_file(options, &content, &fullpath) {
1298 Ok(()) => {}
1299 Err(e) => {
1300 report(&content, fullpath, &e);
1305 return Err(format!("Failed to process {file}").into());
1306 }
1307 }
1308 }
1309 None => {
1310 }
1312 }
1313 Ok(())
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::get_default_context;
1319 use mimium_lang::Config;
1320 use std::path::PathBuf;
1321
1322 #[test]
1323 fn default_cli_context_compiles_lift_array_code_source() {
1324 let src = r#"
1325// @test {"times":1,"stereo":false,"expected":[31.0],"web":true}
1326
1327#stage(macro)
1328fn mk_functions(){
1329 let funcs = [
1330 `|x| x + 1.0,
1331 `|x| x * 2.0,
1332 ]
1333 funcs |> lift_array_code
1334}
1335
1336#stage(main)
1337fn dsp(){
1338 let funcs = mk_functions!()
1339 funcs[0](10.0) + funcs[1](10.0)
1340}
1341"#;
1342 let mut ctx = get_default_context(
1343 Some(PathBuf::from("tmp/lift_array_code_test.mmm")),
1344 false,
1345 false,
1346 Config::default(),
1347 );
1348 ctx.prepare_compiler();
1349 let result = ctx.get_compiler().unwrap().emit_mir(src);
1350 assert!(result.is_ok(), "emit_mir failed: {result:?}");
1351 }
1352}