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::Vm)]
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: 4096,
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(path: Option<PathBuf>, with_gui: bool, config: Config) -> ExecContext {
409 let plugins: Vec<Box<dyn Plugin>> = vec![];
410 let mut ctx = ExecContext::new(plugins.into_iter(), path, config);
411
412 #[cfg(not(target_arch = "wasm32"))]
414 {
415 ctx.init_plugin_loader();
416
417 let mut loaded_count = 0;
418
419 if let Ok(exe_path) = std::env::current_exe()
421 && let Some(exe_dir) = exe_path.parent()
422 && let Some(loader) = ctx.get_plugin_loader_mut()
423 {
424 loaded_count = loader.load_plugins_from_dir(exe_dir).unwrap_or(0);
427
428 if loaded_count > 0 {
429 log::debug!("Loaded {loaded_count} plugin(s) from executable directory");
430
431 if with_gui {
434 log::debug!("GUI mode: guitools will be provided as SystemPlugin");
437 }
438 }
439 }
440
441 if loaded_count == 0
443 && let Err(e) = ctx.load_builtin_dynamic_plugins()
444 {
445 log::debug!("No builtin dynamic plugins found: {e:?}");
446 }
447 }
448
449 ctx.add_system_plugin(mimium_scheduler::get_default_scheduler_plugin());
450
451 if with_gui {
454 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::default());
455 } else {
456 ctx.add_system_plugin(mimium_guitools::GuiToolPlugin::headless());
457 }
458
459 ctx
460}
461
462struct FileRunner {
463 pub tx_compiler: mpsc::Sender<CompileRequest>,
464 pub rx_compiler: mpsc::Receiver<Result<Response, Errors>>,
465 pub tx_prog: Option<mpsc::Sender<ProgramPayload>>,
466 pub fullpath: PathBuf,
467 pub use_wasm: bool,
469 #[cfg(not(target_arch = "wasm32"))]
474 old_program: Mutex<Option<OldWasmProgram>>,
475 #[cfg(not(target_arch = "wasm32"))]
480 retired_engine_receiver: Option<mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>>,
481}
482
483#[cfg(not(target_arch = "wasm32"))]
484#[derive(Clone)]
485struct OldWasmProgram {
486 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
488 ext_fns: Vec<ExtFunTypeInfo>,
490 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
492}
493
494#[cfg(not(target_arch = "wasm32"))]
495struct PreparedWasmSwapData {
496 prewarmed_global_state: Vec<u64>,
498 prepared_engine: Box<mimium_lang::runtime::wasm::engine::WasmEngine>,
500}
501
502struct FileWatcher {
503 pub rx: mpsc::Receiver<notify::Result<Event>>,
504 pub watcher: notify::RecommendedWatcher,
505}
506
507#[cfg(target_os = "macos")]
508fn should_recompile_on_event(event: &Event) -> bool {
509 matches!(
510 event.kind,
511 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
512 | EventKind::Modify(ModifyKind::Data(_))
513 | EventKind::Modify(ModifyKind::Any)
514 )
515}
516
517#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
518fn should_recompile_on_event(event: &Event) -> bool {
519 matches!(
520 event.kind,
521 EventKind::Access(AccessKind::Close(notify::event::AccessMode::Write))
522 | EventKind::Modify(ModifyKind::Data(_))
523 | EventKind::Modify(ModifyKind::Any)
524 )
525}
526
527#[cfg(target_os = "windows")]
528fn should_recompile_on_event(_event: &Event) -> bool {
529 true
530}
531
532impl FileRunner {
533 pub fn new(
534 compiler: compiler::Context,
535 path: PathBuf,
536 prog_tx: Option<mpsc::Sender<ProgramPayload>>,
537 use_wasm: bool,
538 #[cfg(not(target_arch = "wasm32"))] old_program: Option<OldWasmProgram>,
539 #[cfg(not(target_arch = "wasm32"))] retired_engine_receiver: Option<
540 mpsc::Receiver<mimium_lang::runtime::wasm::engine::WasmEngine>,
541 >,
542 ) -> Self {
543 let client = async_compiler::start_async_compiler_service(compiler);
544 Self {
545 tx_compiler: client.tx,
546 rx_compiler: client.rx,
547 tx_prog: prog_tx,
548 fullpath: path,
549 use_wasm,
550 #[cfg(not(target_arch = "wasm32"))]
551 old_program: Mutex::new(old_program),
552 #[cfg(not(target_arch = "wasm32"))]
553 retired_engine_receiver,
554 }
555 }
556 fn try_new_watcher(&self) -> Result<FileWatcher, notify::Error> {
557 let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
558 let mut watcher = notify::recommended_watcher(tx)?;
559 watcher.watch(Path::new(&self.fullpath), RecursiveMode::NonRecursive)?;
560 Ok(FileWatcher { rx, watcher })
561 }
562
563 #[cfg(not(target_arch = "wasm32"))]
564 fn try_compile_wasm_in_subprocess(&self) -> Result<Vec<u8>, String> {
565 let exe = env::current_exe().map_err(|e| format!("failed to resolve current exe: {e}"))?;
566 let output = Command::new(exe)
567 .arg(self.fullpath.as_os_str())
568 .arg("--backend=wasm")
569 .arg("--emit-wasm")
570 .output()
571 .map_err(|e| format!("failed to spawn compiler subprocess: {e}"))?;
572
573 if !output.status.success() {
574 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
575 return Err(format!(
576 "subprocess compile failed (status: {:?}): {}",
577 output.status.code(),
578 stderr
579 ));
580 }
581
582 if output.stdout.is_empty() {
583 return Err("subprocess compile succeeded but produced empty wasm stdout".to_string());
584 }
585
586 Ok(output.stdout)
587 }
588
589 #[cfg(not(target_arch = "wasm32"))]
590 fn try_prewarm_wasm_global_state(
591 wasm_bytes: &[u8],
592 ext_fns: &[ExtFunTypeInfo],
593 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
594 ) -> Result<PreparedWasmSwapData, String> {
595 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
596
597 let mut engine = WasmEngine::new(ext_fns, plugin_fns)
598 .map_err(|e| format!("failed to create prewarm wasm engine: {e}"))?;
599
600 engine
601 .load_module(wasm_bytes)
602 .map_err(|e| format!("failed to load module for prewarm: {e}"))?;
603
604 let mut runtime = WasmDspRuntime::new(engine, None, None);
605
606 runtime
607 .run_main()
608 .map_err(|e| format!("failed to run main for prewarm: {e}"))?;
609
610 let global_state = runtime
611 .engine_mut()
612 .get_global_state_data()
613 .map(|data| data.to_vec())
614 .ok_or_else(|| "missing global state after prewarm".to_string())?;
615
616 let prepared_engine = runtime.into_engine();
617
618 Ok(PreparedWasmSwapData {
619 prewarmed_global_state: global_state,
620 prepared_engine: Box::new(prepared_engine),
621 })
622 }
623
624 #[cfg(not(target_arch = "wasm32"))]
625 fn prepare_hot_swap_wasm_payload(
626 &self,
627 bytes: Vec<u8>,
628 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
629 ext_fns: Option<&[ExtFunTypeInfo]>,
630 ) -> Result<ProgramPayload, String> {
631 let old_program = self
632 .old_program
633 .lock()
634 .ok()
635 .and_then(|guard| (*guard).clone());
636 let previous_skeleton = old_program
637 .as_ref()
638 .and_then(|program| program.dsp_state_skeleton.clone());
639 let fallback_ext_fns: &[ExtFunTypeInfo] = old_program
640 .as_ref()
641 .map(|program| program.ext_fns.as_slice())
642 .unwrap_or(&[]);
643 let ext_fns = ext_fns.unwrap_or(fallback_ext_fns);
644 let plugin_fns = old_program
645 .as_ref()
646 .and_then(|program| program.plugin_fns.clone());
647
648 let prepared_swap_data =
649 Self::try_prewarm_wasm_global_state(&bytes, ext_fns, plugin_fns.clone())?;
650
651 let state_patch_plan = Self::build_required_state_patch_plan(
652 previous_skeleton,
653 dsp_state_skeleton.as_ref(),
654 prepared_swap_data.prewarmed_global_state.len(),
655 );
656 let payload = ProgramPayload::WasmModule {
657 bytes,
658 prepared_engine: prepared_swap_data.prepared_engine,
659 dsp_state_skeleton: dsp_state_skeleton.clone(),
660 state_patch_plan,
661 prewarmed_global_state: prepared_swap_data.prewarmed_global_state,
662 };
663 self.update_old_program(dsp_state_skeleton, ext_fns.to_vec(), plugin_fns);
664
665 Ok(payload)
666 }
667
668 #[cfg(not(target_arch = "wasm32"))]
669 fn build_required_state_patch_plan(
670 previous_skeleton: Option<StateTreeSkeleton<StateType>>,
671 new_skeleton: Option<&StateTreeSkeleton<StateType>>,
672 prewarmed_state_size: usize,
673 ) -> StateStoragePatchPlan {
674 if let (Some(old_skeleton), Some(new_skeleton)) = (previous_skeleton, new_skeleton.cloned())
675 {
676 let maybe_plan =
677 state_tree::build_state_storage_patch_plan(old_skeleton, new_skeleton.clone());
678 if let Some(plan) = maybe_plan {
679 return plan;
680 }
681 let total_size = new_skeleton.total_size() as usize;
682 return StateStoragePatchPlan {
683 total_size,
684 patches: vec![CopyFromPatch {
685 src_addr: 0,
686 dst_addr: 0,
687 size: total_size,
688 }],
689 };
690 }
691
692 StateStoragePatchPlan {
693 total_size: prewarmed_state_size,
694 patches: vec![],
695 }
696 }
697
698 #[cfg(not(target_arch = "wasm32"))]
699 fn update_old_program(
700 &self,
701 dsp_state_skeleton: Option<StateTreeSkeleton<StateType>>,
702 ext_fns: Vec<ExtFunTypeInfo>,
703 plugin_fns: Option<mimium_lang::runtime::wasm::WasmPluginFnMap>,
704 ) {
705 if let Ok(mut guard) = self.old_program.lock() {
706 *guard = Some(OldWasmProgram {
707 dsp_state_skeleton,
708 ext_fns,
709 plugin_fns,
710 });
711 }
712 }
713
714 fn recompile_file_inprocess(&self, new_content: String) {
715 #[cfg(not(target_arch = "wasm32"))]
716 let mode = RunMode::EmitByteCode;
717
718 #[cfg(target_arch = "wasm32")]
719 let mode = {
720 let _ = self.use_wasm;
721 RunMode::EmitByteCode
722 };
723 let _ = self.tx_compiler.send(CompileRequest {
724 source: new_content.clone(),
725 path: self.fullpath.clone(),
726 option: RunOptions {
727 mode,
728 with_gui: true,
729 use_wasm: self.use_wasm,
730 audio_setting: AudioSetting::default(),
731 config: Config::default(),
732 },
733 });
734 let _ = self.rx_compiler.recv().map(|res| match res {
735 Ok(Response::Ast(_)) | Ok(Response::Mir(_)) => {
736 log::warn!("unexpected response: AST/MIR");
737 }
738 Ok(Response::ByteCode(prog)) => {
739 log::info!("compiled successfully.");
740 if let Some(tx) = &self.tx_prog {
741 let _ = tx.send(ProgramPayload::VmProgram(prog));
742 }
743 }
744 #[cfg(not(target_arch = "wasm32"))]
745 Ok(Response::WasmModule(output)) => {
746 log::info!("WASM compiled successfully ({} bytes).", output.bytes.len());
747 if let Some(tx) = &self.tx_prog {
748 match self.prepare_hot_swap_wasm_payload(
749 output.bytes,
750 output.dsp_state_skeleton,
751 Some(&output.ext_fns),
752 ) {
753 Ok(payload) => {
754 let _ = tx.send(payload);
755 }
756 Err(e) => {
757 log::error!("WASM prepare_hot_swap failed; skip hot-swap by spec: {e}");
758 }
759 }
760 }
761 }
762 Err(errs) => {
763 let errs = errs
764 .into_iter()
765 .map(|e| Box::new(e) as Box<dyn ReportableError>)
766 .collect::<Vec<_>>();
767 report(&new_content, self.fullpath.clone(), &errs);
768 }
769 });
770 }
771
772 fn recompile_file(&self) {
773 match fileloader::load(&self.fullpath.to_string_lossy()) {
774 Ok(new_content) => {
775 #[cfg(not(target_arch = "wasm32"))]
776 {
777 if self.use_wasm {
778 match self.try_compile_wasm_in_subprocess() {
779 Ok(bytes) => {
780 log::info!(
781 "WASM compiled in subprocess successfully ({} bytes).",
782 bytes.len()
783 );
784 if let Some(tx) = &self.tx_prog {
785 match self.prepare_hot_swap_wasm_payload(bytes, None, None) {
786 Ok(payload) => {
787 let _ = tx.send(payload);
788 }
789 Err(e) => {
790 log::error!(
791 "WASM prepare_hot_swap failed; skip hot-swap by spec: {e}"
792 );
793 }
794 }
795 }
796 }
797 Err(e) => {
798 log::error!("{e}");
799 }
800 }
801 } else {
802 self.recompile_file_inprocess(new_content);
803 }
804 }
805
806 #[cfg(target_arch = "wasm32")]
807 {
808 self.recompile_file_inprocess(new_content);
809 }
810 }
811 Err(e) => {
812 log::error!(
813 "failed to reload the file {}: {}",
814 self.fullpath.display(),
815 e
816 );
817 }
818 }
819 }
820
821 #[cfg(not(target_arch = "wasm32"))]
822 fn drain_retired_engines(&self) {
823 if let Some(rx) = &self.retired_engine_receiver {
824 let mut dropped_count = 0usize;
825 while let Ok(_engine) = rx.try_recv() {
826 dropped_count += 1;
827 }
828 if dropped_count > 0 {
829 log::info!(
830 "WASM deferred drop: released {} retired engine(s) on non-RT thread",
831 dropped_count
832 );
833 }
834 }
835 }
836
837 pub fn cli_loop(&self) {
839 let file_watcher = match self.try_new_watcher() {
841 Ok(watcher) => watcher,
842 Err(e) => {
843 log::error!("Failed to watch file: {e}");
844 return;
845 }
846 };
847
848 loop {
849 #[cfg(not(target_arch = "wasm32"))]
850 self.drain_retired_engines();
851
852 match file_watcher
853 .rx
854 .recv_timeout(std::time::Duration::from_millis(100))
855 {
856 Ok(Ok(event)) => {
857 if should_recompile_on_event(&event) {
858 log::info!("File event detected ({:?}), recompiling...", event.kind);
859 self.recompile_file();
860 } else {
861 log::debug!("Ignored file event: {:?}", event.kind);
862 }
863 }
864 Ok(Err(e)) => {
865 log::error!("watch error event: {e}");
866 }
867 Err(mpsc::RecvTimeoutError::Timeout) => {
868 continue;
869 }
870 Err(e) => {
871 log::error!("receiver error: {e}");
872 }
873 }
874 }
875 }
876}
877
878pub fn run_file(
880 options: RunOptions,
881 content: &str,
882 fullpath: &Path,
883) -> Result<(), Vec<Box<dyn ReportableError>>> {
884 log::debug!("Filename: {}", fullpath.display());
885
886 let mut ctx = get_default_context(
887 Some(PathBuf::from(fullpath)),
888 options.with_gui,
889 options.config,
890 );
891
892 match options.mode {
893 RunMode::EmitCst => {
894 let tokens = cst_parser::tokenize(content);
895 let preparsed = cst_parser::preparse(&tokens);
896 let (green_id, arena, tokens, errors) = cst_parser::parse_cst(tokens, &preparsed);
897
898 if !errors.is_empty() {
900 let reportable_errors =
901 parser_errors_to_reportable(content, fullpath.to_path_buf(), errors);
902 report(content, fullpath.to_path_buf(), &reportable_errors);
903 }
904
905 let tree_output = arena.print_tree(green_id, &tokens, content, 0);
907 println!("{tree_output}");
908 Ok(())
909 }
910 RunMode::EmitAst => {
911 let ast = emit_ast(content, Some(PathBuf::from(fullpath)))?;
912 println!("{}", ast.pretty_print());
913 Ok(())
914 }
915 RunMode::EmitMir => {
916 ctx.prepare_compiler();
917 let res = ctx.get_compiler().unwrap().emit_mir(content);
918 res.map(|r| {
919 println!("{r}");
920 })?;
921 Ok(())
922 }
923 RunMode::EmitByteCode => {
924 let localdriver = LocalBufferDriver::new(0);
926 let plug = localdriver.get_as_plugin();
927 ctx.add_plugin(plug);
928 ctx.prepare_machine(content)?;
929 println!("{}", ctx.get_vm().unwrap().prog);
930 Ok(())
931 }
932 #[cfg(not(target_arch = "wasm32"))]
933 RunMode::EmitWasm { output } => {
934 use mimium_lang::utils::metadata::Location;
935 use std::io::Write;
936 use std::sync::Arc;
937
938 ctx.prepare_compiler();
939 let ext_fns = ctx.get_extfun_types();
940 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
941
942 let mut generator = compiler::wasmgen::WasmGenerator::new(Arc::new(mir), &ext_fns);
944 let wasm_bytes = generator.generate().map_err(|e| {
945 vec![Box::new(mimium_lang::utils::error::SimpleError {
946 message: e,
947 span: Location::default(),
948 }) as Box<dyn ReportableError>]
949 })?;
950
951 if let Some(path) = output {
952 std::fs::write(&path, &wasm_bytes).map_err(|e| {
953 vec![Box::new(mimium_lang::utils::error::SimpleError {
954 message: e.to_string(),
955 span: Location::default(),
956 }) as Box<dyn ReportableError>]
957 })?;
958 println!("Written to: {}", path.display());
959 } else {
960 let mut stdout = std::io::stdout().lock();
961 stdout.write_all(&wasm_bytes).map_err(|e| {
962 vec![Box::new(mimium_lang::utils::error::SimpleError {
963 message: e.to_string(),
964 span: Location::default(),
965 }) as Box<dyn ReportableError>]
966 })?;
967 stdout.flush().map_err(|e| {
968 vec![Box::new(mimium_lang::utils::error::SimpleError {
969 message: e.to_string(),
970 span: Location::default(),
971 }) as Box<dyn ReportableError>]
972 })?;
973 }
974
975 Ok(())
976 }
977 #[cfg(not(target_arch = "wasm32"))]
978 RunMode::WasmAudio => {
979 use mimium_lang::compiler::wasmgen::WasmGenerator;
980 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
981 use mimium_lang::utils::metadata::Location;
982 use std::sync::Arc;
983
984 ctx.prepare_compiler();
985 let mut ext_fns = ctx.get_extfun_types();
986 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
989 ext_fns.dedup_by(|a, b| a.name == b.name);
990
991 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
992
993 let io_channels = mir.get_dsp_iochannels();
994 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
995
996 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
998 let wasm_bytes = generator.generate().map_err(|e| {
999 vec![Box::new(mimium_lang::utils::error::SimpleError {
1000 message: e,
1001 span: Location::default(),
1002 }) as Box<dyn ReportableError>]
1003 })?;
1004
1005 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1006
1007 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1011 let plugin_fns_for_hotswap = plugin_fns.clone();
1012
1013 let wasm_workers = ctx.generate_wasm_audioworkers();
1015
1016 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1017 vec![Box::new(mimium_lang::utils::error::SimpleError {
1018 message: format!("Failed to create WASM engine: {e}"),
1019 span: Location::default(),
1020 }) as Box<dyn ReportableError>]
1021 })?;
1022
1023 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1024 vec![Box::new(mimium_lang::utils::error::SimpleError {
1025 message: format!("Failed to load WASM module: {e}"),
1026 span: Location::default(),
1027 }) as Box<dyn ReportableError>]
1028 })?;
1029
1030 let mut wasm_runtime =
1032 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1033 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1034 let (retire_tx, retire_rx) = mpsc::channel();
1035 wasm_runtime.set_engine_retire_sender(retire_tx);
1036 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1037 let _ = wasm_runtime.run_main();
1038 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1039
1040 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1041
1042 let mut driver = options.get_driver();
1044
1045 let with_gui = options.with_gui;
1046 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1047 if with_gui {
1048 loop {
1049 std::thread::sleep(std::time::Duration::from_millis(1000));
1050 }
1051 }
1052 }));
1053
1054 driver.init(
1055 runtimedata,
1056 Some(SampleRate::from(
1057 options.audio_setting.effective_sample_rate(),
1058 )),
1059 );
1060 driver.play();
1061
1062 let compiler = ctx.take_compiler().unwrap();
1064 let frunner = FileRunner::new(
1065 compiler,
1066 fullpath.to_path_buf(),
1067 driver.get_program_channel(),
1068 true,
1069 Some(OldWasmProgram {
1070 dsp_state_skeleton: dsp_skeleton,
1071 ext_fns,
1072 plugin_fns: plugin_fns_for_hotswap,
1073 }),
1074 Some(retire_rx),
1075 );
1076 if with_gui {
1077 std::thread::spawn(move || frunner.cli_loop());
1078 }
1079
1080 mainloop();
1081 Ok(())
1082 }
1083 #[cfg(not(target_arch = "wasm32"))]
1084 _ if options.use_wasm => {
1085 use mimium_lang::compiler::wasmgen::WasmGenerator;
1087 use mimium_lang::runtime::wasm::engine::{WasmDspRuntime, WasmEngine};
1088 use mimium_lang::utils::metadata::Location;
1089 use std::sync::Arc;
1090
1091 let mut driver = options.get_driver();
1092
1093 ctx.prepare_compiler();
1094 let mut ext_fns = ctx.get_extfun_types();
1095 ext_fns.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
1098 ext_fns.dedup_by(|a, b| a.name == b.name);
1099
1100 let mir = ctx.get_compiler().unwrap().emit_mir(content)?;
1101 let io_channels = mir.get_dsp_iochannels();
1102 let dsp_skeleton = mir.get_dsp_state_skeleton().cloned();
1103
1104 let mut generator = WasmGenerator::new(Arc::new(mir), &ext_fns);
1105 let wasm_bytes = generator.generate().map_err(|e| {
1106 vec![Box::new(mimium_lang::utils::error::SimpleError {
1107 message: e,
1108 span: Location::default(),
1109 }) as Box<dyn ReportableError>]
1110 })?;
1111
1112 log::info!("Generated WASM module ({} bytes)", wasm_bytes.len());
1113
1114 let plugin_fns = ctx.freeze_wasm_plugin_fns();
1118 let plugin_fns_for_hotswap = plugin_fns.clone();
1119
1120 let wasm_workers = ctx.generate_wasm_audioworkers();
1122
1123 let mut wasm_engine = WasmEngine::new(&ext_fns, plugin_fns).map_err(|e| {
1124 vec![Box::new(mimium_lang::utils::error::SimpleError {
1125 message: format!("Failed to create WASM engine: {e}"),
1126 span: Location::default(),
1127 }) as Box<dyn ReportableError>]
1128 })?;
1129
1130 wasm_engine.load_module(&wasm_bytes).map_err(|e| {
1131 vec![Box::new(mimium_lang::utils::error::SimpleError {
1132 message: format!("Failed to load WASM module: {e}"),
1133 span: Location::default(),
1134 }) as Box<dyn ReportableError>]
1135 })?;
1136
1137 let mut wasm_runtime =
1138 WasmDspRuntime::new(wasm_engine, io_channels, dsp_skeleton.clone());
1139 wasm_runtime.set_wasm_audioworkers(wasm_workers);
1140 let (retire_tx, retire_rx) = mpsc::channel();
1141 wasm_runtime.set_engine_retire_sender(retire_tx);
1142 ctx.run_wasm_on_init(wasm_runtime.engine_mut());
1143 let _ = wasm_runtime.run_main();
1144 ctx.run_wasm_after_main(wasm_runtime.engine_mut());
1145
1146 let runtimedata = RuntimeData::new_from_runtime(Box::new(wasm_runtime));
1147
1148 let with_gui = options.with_gui;
1150 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1151 if with_gui {
1152 loop {
1153 std::thread::sleep(std::time::Duration::from_millis(1000));
1154 }
1155 }
1156 }));
1157
1158 driver.init(
1159 runtimedata,
1160 Some(SampleRate::from(
1161 options.audio_setting.effective_sample_rate(),
1162 )),
1163 );
1164 driver.play();
1165
1166 let compiler = ctx.take_compiler().unwrap();
1168 let frunner = FileRunner::new(
1169 compiler,
1170 fullpath.to_path_buf(),
1171 driver.get_program_channel(),
1172 true,
1173 Some(OldWasmProgram {
1174 dsp_state_skeleton: dsp_skeleton,
1175 ext_fns,
1176 plugin_fns: plugin_fns_for_hotswap,
1177 }),
1178 Some(retire_rx),
1179 );
1180 if with_gui {
1181 std::thread::spawn(move || frunner.cli_loop());
1182 }
1183
1184 mainloop();
1185 Ok(())
1186 }
1187 _ => {
1188 let mut driver = options.get_driver();
1189 let audiodriver_plug = driver.get_as_plugin();
1190
1191 ctx.add_plugin(audiodriver_plug);
1192 ctx.prepare_machine(content)?;
1193 let _res = ctx.run_main();
1194
1195 let runtimedata = {
1196 let ctxmut: &mut ExecContext = &mut ctx;
1197 RuntimeData::try_from(ctxmut).unwrap()
1198 };
1199
1200 let mainloop = ctx.try_get_main_loop().unwrap_or(Box::new(move || {
1201 if options.with_gui {
1202 loop {
1203 std::thread::sleep(std::time::Duration::from_millis(1000));
1204 }
1205 }
1206 }));
1207 driver.init(
1209 runtimedata,
1210 Some(SampleRate::from(
1211 options.audio_setting.effective_sample_rate(),
1212 )),
1213 );
1214 driver.play();
1215
1216 let compiler = ctx.take_compiler().unwrap();
1217
1218 let frunner = FileRunner::new(
1219 compiler,
1220 fullpath.to_path_buf(),
1221 driver.get_program_channel(),
1222 false,
1223 None,
1224 None,
1225 );
1226 if options.with_gui {
1227 std::thread::spawn(move || frunner.cli_loop());
1228 }
1229 mainloop();
1230 Ok(())
1231 }
1232 }
1233}
1234pub fn lib_main() -> Result<(), Box<dyn std::error::Error>> {
1235 if cfg!(debug_assertions) | cfg!(test) {
1236 colog::default_builder()
1237 .filter_level(log::LevelFilter::Trace)
1238 .init();
1239 } else {
1240 colog::default_builder().init();
1241 }
1242
1243 let args = Args::parse();
1244 let config_path = resolve_config_path(args.config.as_ref())?;
1245 let cli_config = load_or_create_cli_config(&config_path)?;
1246
1247 match &args.file {
1248 Some(file) => {
1249 let fullpath = fileloader::get_canonical_path(".", file)?;
1250 let content = fileloader::load(fullpath.to_str().unwrap())?;
1251 let options = RunOptions::from_args(&args, &cli_config.audio_setting);
1252 match run_file(options, &content, &fullpath) {
1253 Ok(()) => {}
1254 Err(e) => {
1255 report(&content, fullpath, &e);
1260 return Err(format!("Failed to process {file}").into());
1261 }
1262 }
1263 }
1264 None => {
1265 }
1267 }
1268 Ok(())
1269}