1use anyhow::{Context, Result};
10use clap::ValueEnum;
11use log::{debug, info};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::env;
15use std::fs;
16use std::path::{Path, PathBuf};
17
18const MIN_QUEUE_SIZE: usize = 8;
19const CONFIG_FILENAMES: &[&str] = &[
20 ".runmat",
21 ".runmat.toml",
22 ".runmat.yaml",
23 ".runmat.yml",
24 ".runmat.json",
25 "runmat.config.toml",
26 "runmat.config.yaml",
27 "runmat.config.yml",
28 "runmat.config.json",
29];
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct RunMatConfig {
34 pub runtime: RuntimeConfig,
36 #[serde(default)]
38 pub accelerate: AccelerateConfig,
39 #[serde(default)]
41 pub language: LanguageConfig,
42 #[serde(default)]
44 pub telemetry: TelemetryConfig,
45 pub jit: JitConfig,
47 pub gc: GcConfig,
49 pub plotting: PlottingConfig,
51 pub kernel: KernelConfig,
53 pub logging: LoggingConfig,
55 #[serde(default)]
57 pub packages: PackagesConfig,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct RuntimeConfig {
63 #[serde(default = "default_timeout")]
65 pub timeout: u64,
66 #[serde(default = "default_callstack_limit")]
68 pub callstack_limit: usize,
69 #[serde(default = "default_error_namespace")]
71 pub error_namespace: String,
72 #[serde(default)]
74 pub verbose: bool,
75 pub snapshot_path: Option<PathBuf>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80pub struct LanguageConfig {
81 #[serde(default)]
85 pub compat: LanguageCompatMode,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
89#[serde(rename_all = "kebab-case")]
90pub enum LanguageCompatMode {
91 #[serde(rename = "runmat", alias = "run-mat")]
92 #[value(name = "runmat")]
93 RunMat,
94 Matlab,
95 Strict,
96}
97
98impl Default for LanguageCompatMode {
99 fn default() -> Self {
100 Self::RunMat
101 }
102}
103
104pub fn error_namespace_for_language_compat(mode: LanguageCompatMode) -> &'static str {
105 match mode {
106 LanguageCompatMode::Matlab => "MATLAB",
107 LanguageCompatMode::RunMat | LanguageCompatMode::Strict => "RunMat",
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct AccelerateConfig {
114 #[serde(default = "default_true")]
116 pub enabled: bool,
117 #[serde(default)]
119 pub provider: AccelerateProviderPreference,
120 #[serde(default = "default_true")]
122 pub allow_inprocess_fallback: bool,
123 #[serde(default)]
125 pub wgpu_power_preference: AccelPowerPreference,
126 #[serde(default)]
128 pub wgpu_force_fallback_adapter: bool,
129 #[serde(default)]
131 pub auto_offload: AutoOffloadConfig,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
135#[serde(rename_all = "kebab-case")]
136pub enum AccelerateProviderPreference {
137 Auto,
138 Wgpu,
139 InProcess,
140}
141
142impl Default for AccelerateProviderPreference {
143 fn default() -> Self {
144 Self::Wgpu
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
149#[serde(rename_all = "kebab-case")]
150pub enum AccelPowerPreference {
151 Auto,
152 HighPerformance,
153 LowPower,
154}
155
156impl Default for AccelPowerPreference {
157 fn default() -> Self {
158 Self::Auto
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
163#[serde(rename_all = "kebab-case")]
164pub enum AutoOffloadLogLevel {
165 Off,
166 Info,
167 #[default]
168 Trace,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct TelemetryConfig {
174 #[serde(default = "default_true")]
176 pub enabled: bool,
177 #[serde(default)]
179 pub show_payloads: bool,
180 pub http_endpoint: Option<String>,
182 pub udp_endpoint: Option<String>,
184 #[serde(default = "default_telemetry_queue")]
186 pub queue_size: usize,
187 #[serde(default = "default_true")]
189 pub require_ingestion_key: bool,
190}
191
192impl Default for TelemetryConfig {
193 fn default() -> Self {
194 Self {
195 enabled: true,
196 show_payloads: false,
197 http_endpoint: None,
198 udp_endpoint: Some("udp.telemetry.runmat.com:7846".to_string()),
199 queue_size: default_telemetry_queue(),
200 require_ingestion_key: true,
201 }
202 }
203}
204
205impl Default for AccelerateConfig {
206 fn default() -> Self {
207 Self {
208 enabled: true,
209 provider: AccelerateProviderPreference::Wgpu,
210 allow_inprocess_fallback: true,
211 wgpu_power_preference: AccelPowerPreference::Auto,
212 wgpu_force_fallback_adapter: false,
213 auto_offload: AutoOffloadConfig::default(),
214 }
215 }
216}
217
218fn default_telemetry_queue() -> usize {
219 256
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct AutoOffloadConfig {
225 #[serde(default = "default_true")]
226 pub enabled: bool,
227 #[serde(default = "default_true")]
228 pub calibrate: bool,
229 pub profile_path: Option<PathBuf>,
230 #[serde(default)]
231 pub log_level: AutoOffloadLogLevel,
232}
233
234impl Default for AutoOffloadConfig {
235 fn default() -> Self {
236 Self {
237 enabled: true,
238 calibrate: true,
239 profile_path: None,
240 log_level: AutoOffloadLogLevel::Trace,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct JitConfig {
248 #[serde(default = "default_true")]
250 pub enabled: bool,
251 #[serde(default = "default_jit_threshold")]
253 pub threshold: u32,
254 #[serde(default)]
256 pub optimization_level: JitOptLevel,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, Default)]
261pub struct GcConfig {
262 pub preset: Option<GcPreset>,
264 pub young_size_mb: Option<usize>,
266 pub threads: Option<usize>,
268 #[serde(default)]
270 pub collect_stats: bool,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct PlottingConfig {
276 #[serde(default)]
278 pub mode: PlotMode,
279 #[serde(default)]
281 pub force_headless: bool,
282 #[serde(default)]
284 pub backend: PlotBackend,
285 pub gui: Option<GuiConfig>,
287 pub export: Option<ExportConfig>,
289 #[serde(default)]
291 pub scatter_target_points: Option<u32>,
292 #[serde(default)]
294 pub surface_vertex_budget: Option<u64>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct GuiConfig {
300 #[serde(default = "default_window_width")]
302 pub width: u32,
303 #[serde(default = "default_window_height")]
305 pub height: u32,
306 #[serde(default = "default_true")]
308 pub vsync: bool,
309 #[serde(default)]
311 pub maximized: bool,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ExportConfig {
317 #[serde(default)]
319 pub format: ExportFormat,
320 #[serde(default = "default_dpi")]
322 pub dpi: u32,
323 pub output_dir: Option<PathBuf>,
325 pub jupyter: Option<JupyterConfig>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct JupyterConfig {
332 #[serde(default)]
334 pub output_format: JupyterOutputFormat,
335 #[serde(default = "default_true")]
337 pub enable_widgets: bool,
338 #[serde(default = "default_true")]
340 pub enable_static_fallback: bool,
341 pub widget: Option<JupyterWidgetConfig>,
343 pub static_export: Option<JupyterStaticConfig>,
345 pub performance: Option<JupyterPerformanceConfig>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct JupyterWidgetConfig {
352 #[serde(default = "default_true")]
354 pub client_side_rendering: bool,
355 #[serde(default)]
357 pub server_side_streaming: bool,
358 #[serde(default = "default_widget_cache_size")]
360 pub cache_size_mb: u32,
361 #[serde(default = "default_widget_fps")]
363 pub update_fps: u32,
364 #[serde(default = "default_true")]
366 pub gpu_acceleration: bool,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct JupyterStaticConfig {
372 #[serde(default = "default_jupyter_width")]
374 pub width: u32,
375 #[serde(default = "default_jupyter_height")]
377 pub height: u32,
378 #[serde(default = "default_jupyter_quality")]
380 pub quality: f32,
381 #[serde(default = "default_true")]
383 pub include_metadata: bool,
384 #[serde(default)]
386 pub preferred_formats: Vec<JupyterOutputFormat>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct JupyterPerformanceConfig {
392 #[serde(default = "default_max_render_time")]
394 pub max_render_time_ms: u32,
395 #[serde(default = "default_true")]
397 pub progressive_rendering: bool,
398 #[serde(default = "default_lod_threshold")]
400 pub lod_threshold: u32,
401 #[serde(default = "default_true")]
403 pub texture_compression: bool,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct KernelConfig {
409 #[serde(default = "default_kernel_ip")]
411 pub ip: String,
412 pub key: Option<String>,
414 pub ports: Option<KernelPorts>,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct KernelPorts {
421 pub shell: Option<u16>,
422 pub iopub: Option<u16>,
423 pub stdin: Option<u16>,
424 pub control: Option<u16>,
425 pub heartbeat: Option<u16>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize, Default)]
430pub struct PackagesConfig {
431 #[serde(default = "default_true")]
433 pub enabled: bool,
434 #[serde(default = "default_registries")]
436 pub registries: Vec<Registry>,
437 #[serde(default)]
439 pub dependencies: HashMap<String, PackageSpec>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct Registry {
444 pub name: String,
446 pub url: String,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(tag = "source", rename_all = "kebab-case")]
453pub enum PackageSpec {
454 Registry {
456 version: String,
458 #[serde(default)]
460 registry: Option<String>,
461 #[serde(default)]
463 features: Vec<String>,
464 #[serde(default)]
466 optional: bool,
467 },
468 Git {
470 url: String,
471 #[serde(default)]
472 rev: Option<String>,
473 #[serde(default)]
474 features: Vec<String>,
475 #[serde(default)]
476 optional: bool,
477 },
478 Path {
480 path: String,
481 #[serde(default)]
482 features: Vec<String>,
483 #[serde(default)]
484 optional: bool,
485 },
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct LoggingConfig {
491 #[serde(default)]
493 pub level: LogLevel,
494 #[serde(default)]
496 pub debug: bool,
497 pub file: Option<PathBuf>,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
503#[serde(rename_all = "lowercase")]
504pub enum PlotMode {
505 Auto,
507 Gui,
509 Headless,
511 Jupyter,
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
517#[serde(rename_all = "lowercase")]
518pub enum PlotBackend {
519 Auto,
521 Wgpu,
523 Static,
525 Web,
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
531#[serde(rename_all = "lowercase")]
532pub enum ExportFormat {
533 Png,
534 Svg,
535 Pdf,
536 Html,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(rename_all = "lowercase")]
542pub enum JupyterOutputFormat {
543 Widget,
545 Png,
547 Svg,
549 Base64,
551 PlotlyJson,
553 Auto,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(rename_all = "lowercase")]
560pub enum JitOptLevel {
561 None,
562 Size,
563 Speed,
564 Aggressive,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
569#[serde(rename_all = "kebab-case")]
570pub enum GcPreset {
571 LowLatency,
572 HighThroughput,
573 LowMemory,
574 Debug,
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
579#[serde(rename_all = "lowercase")]
580pub enum LogLevel {
581 Error,
582 Warn,
583 Info,
584 Debug,
585 Trace,
586}
587
588fn default_timeout() -> u64 {
590 300
591}
592
593fn default_callstack_limit() -> usize {
594 200
595}
596
597fn default_error_namespace() -> String {
598 "".to_string()
599}
600fn default_true() -> bool {
601 true
602}
603fn default_jit_threshold() -> u32 {
604 10
605}
606fn default_window_width() -> u32 {
607 1200
608}
609fn default_window_height() -> u32 {
610 800
611}
612fn default_dpi() -> u32 {
613 300
614}
615fn default_kernel_ip() -> String {
616 "127.0.0.1".to_string()
617}
618
619fn default_widget_cache_size() -> u32 {
620 64 }
622
623fn default_widget_fps() -> u32 {
624 30 }
626
627fn default_jupyter_width() -> u32 {
628 800
629}
630
631fn default_jupyter_height() -> u32 {
632 600
633}
634
635fn default_jupyter_quality() -> f32 {
636 0.9 }
638
639fn default_max_render_time() -> u32 {
640 16 }
642
643fn default_lod_threshold() -> u32 {
644 10000 }
646
647fn default_registries() -> Vec<Registry> {
648 vec![Registry {
649 name: "runmat".to_string(),
650 url: "https://packages.runmat.com".to_string(),
651 }]
652}
653
654impl Default for RuntimeConfig {
655 fn default() -> Self {
656 Self {
657 timeout: default_timeout(),
658 callstack_limit: default_callstack_limit(),
659 error_namespace: default_error_namespace(),
660 verbose: false,
661 snapshot_path: None,
662 }
663 }
664}
665
666impl Default for JitConfig {
667 fn default() -> Self {
668 Self {
669 enabled: true,
670 threshold: default_jit_threshold(),
671 optimization_level: JitOptLevel::Speed,
672 }
673 }
674}
675
676impl Default for PlottingConfig {
677 fn default() -> Self {
678 Self {
679 mode: PlotMode::Auto,
680 force_headless: false,
681 backend: PlotBackend::Auto,
682 gui: Some(GuiConfig::default()),
683 export: Some(ExportConfig::default()),
684 scatter_target_points: None,
685 surface_vertex_budget: None,
686 }
687 }
688}
689
690impl Default for GuiConfig {
691 fn default() -> Self {
692 Self {
693 width: default_window_width(),
694 height: default_window_height(),
695 vsync: true,
696 maximized: false,
697 }
698 }
699}
700
701impl Default for ExportConfig {
702 fn default() -> Self {
703 Self {
704 format: ExportFormat::Png,
705 dpi: default_dpi(),
706 output_dir: None,
707 jupyter: Some(JupyterConfig::default()),
708 }
709 }
710}
711
712impl Default for KernelConfig {
713 fn default() -> Self {
714 Self {
715 ip: default_kernel_ip(),
716 key: None,
717 ports: None,
718 }
719 }
720}
721
722impl Default for LoggingConfig {
723 fn default() -> Self {
724 Self {
725 level: LogLevel::Warn,
726 debug: false,
727 file: None,
728 }
729 }
730}
731
732impl Default for PlotMode {
733 fn default() -> Self {
734 Self::Auto
735 }
736}
737
738impl Default for PlotBackend {
739 fn default() -> Self {
740 Self::Auto
741 }
742}
743
744impl Default for ExportFormat {
745 fn default() -> Self {
746 Self::Png
747 }
748}
749
750impl Default for JitOptLevel {
751 fn default() -> Self {
752 Self::Speed
753 }
754}
755
756impl Default for LogLevel {
757 fn default() -> Self {
758 Self::Info
759 }
760}
761
762impl Default for JupyterOutputFormat {
763 fn default() -> Self {
764 Self::Auto
765 }
766}
767
768impl Default for JupyterConfig {
769 fn default() -> Self {
770 Self {
771 output_format: JupyterOutputFormat::default(),
772 enable_widgets: true,
773 enable_static_fallback: true,
774 widget: Some(JupyterWidgetConfig::default()),
775 static_export: Some(JupyterStaticConfig::default()),
776 performance: Some(JupyterPerformanceConfig::default()),
777 }
778 }
779}
780
781impl Default for JupyterWidgetConfig {
782 fn default() -> Self {
783 Self {
784 client_side_rendering: true,
785 server_side_streaming: false,
786 cache_size_mb: default_widget_cache_size(),
787 update_fps: default_widget_fps(),
788 gpu_acceleration: true,
789 }
790 }
791}
792
793impl Default for JupyterStaticConfig {
794 fn default() -> Self {
795 Self {
796 width: default_jupyter_width(),
797 height: default_jupyter_height(),
798 quality: default_jupyter_quality(),
799 include_metadata: true,
800 preferred_formats: vec![
801 JupyterOutputFormat::Widget,
802 JupyterOutputFormat::Png,
803 JupyterOutputFormat::Svg,
804 ],
805 }
806 }
807}
808
809impl Default for JupyterPerformanceConfig {
810 fn default() -> Self {
811 Self {
812 max_render_time_ms: default_max_render_time(),
813 progressive_rendering: true,
814 lod_threshold: default_lod_threshold(),
815 texture_compression: true,
816 }
817 }
818}
819
820pub struct ConfigLoader;
822
823impl ConfigLoader {
824 pub fn load() -> Result<RunMatConfig> {
826 let mut config = Self::load_from_files()?;
827 Self::apply_environment_variables(&mut config)?;
828 Ok(config)
829 }
830
831 fn load_from_files() -> Result<RunMatConfig> {
833 let config_paths = Self::find_config_files();
835
836 for path in config_paths {
837 if path.is_dir() {
838 info!(
839 "Ignoring config directory path (expected file): {}",
840 path.display()
841 );
842 continue;
843 }
844 if path.exists() {
845 info!("Loading configuration from: {}", path.display());
846 return Self::load_from_file(&path);
847 }
848 }
849
850 debug!("No configuration file found, using defaults");
851 Ok(RunMatConfig::default())
852 }
853
854 fn find_config_files() -> Vec<PathBuf> {
856 let mut paths = Vec::new();
857
858 if let Some(config_path) = env_value("RUNMAT_CONFIG", &[]) {
860 paths.push(PathBuf::from(config_path));
861 }
862
863 if let Ok(current_dir) = env::current_dir() {
865 for name in CONFIG_FILENAMES {
866 paths.push(current_dir.join(name));
867 }
868 }
869
870 if let Some(home_dir) = dirs::home_dir() {
872 for name in CONFIG_FILENAMES {
873 paths.push(home_dir.join(name));
874 }
875 paths.push(home_dir.join(".config/runmat/config.yaml"));
876 paths.push(home_dir.join(".config/runmat/config.yml"));
877 paths.push(home_dir.join(".config/runmat/config.json"));
878 }
879
880 #[cfg(unix)]
882 {
883 paths.push(PathBuf::from("/etc/runmat/config.yaml"));
884 paths.push(PathBuf::from("/etc/runmat/config.yml"));
885 paths.push(PathBuf::from("/etc/runmat/config.json"));
886 }
887
888 paths
889 }
890
891 pub fn discover_config_path_from(start: &Path) -> Option<PathBuf> {
893 let mut current = if start.is_dir() {
894 start.to_path_buf()
895 } else {
896 start.parent().map(Path::to_path_buf)?
897 };
898 loop {
899 for name in CONFIG_FILENAMES {
900 let candidate = current.join(name);
901 if candidate.is_file() {
902 return Some(candidate);
903 }
904 }
905 if !current.pop() {
906 break;
907 }
908 }
909 None
910 }
911
912 pub fn load_from_file(path: &Path) -> Result<RunMatConfig> {
914 let content = fs::read_to_string(path)
915 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
916
917 let config = match path.extension().and_then(|ext| ext.to_str()) {
918 None if path.file_name().and_then(|n| n.to_str()) == Some(".runmat") => {
920 toml::from_str(&content).with_context(|| {
921 format!("Failed to parse .runmat (TOML) config: {}", path.display())
922 })?
923 }
924 Some("runmat") => toml::from_str(&content).with_context(|| {
925 format!("Failed to parse .runmat (TOML) config: {}", path.display())
926 })?,
927 Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
928 .with_context(|| format!("Failed to parse YAML config: {}", path.display()))?,
929 Some("json") => serde_json::from_str(&content)
930 .with_context(|| format!("Failed to parse JSON config: {}", path.display()))?,
931 Some("toml") => toml::from_str(&content)
932 .with_context(|| format!("Failed to parse TOML config: {}", path.display()))?,
933 _ => {
934 if let Ok(config) = toml::from_str(&content) {
936 config
937 } else if let Ok(config) = serde_yaml::from_str(&content) {
938 config
939 } else if let Ok(config) = serde_json::from_str(&content) {
940 config
941 } else {
942 return Err(anyhow::anyhow!(
943 "Could not parse config file {} (tried TOML, YAML, JSON)",
944 path.display()
945 ));
946 }
947 }
948 };
949
950 Ok(config)
951 }
952
953 fn apply_environment_variables(config: &mut RunMatConfig) -> Result<()> {
955 if let Some(timeout) = env_value("RUNMAT_TIMEOUT", &[]) {
957 if let Ok(timeout) = timeout.parse() {
958 config.runtime.timeout = timeout;
959 }
960 }
961
962 if let Some(limit) = env_value("RUNMAT_CALLSTACK_LIMIT", &[]) {
963 if let Ok(limit) = limit.parse() {
964 config.runtime.callstack_limit = limit;
965 }
966 }
967
968 if let Some(namespace) = env_value("RUNMAT_ERROR_NAMESPACE", &[]) {
969 let trimmed = namespace.trim();
970 if !trimmed.is_empty() {
971 config.runtime.error_namespace = trimmed.to_string();
972 }
973 }
974
975 if let Some(verbose) = env_bool("RUNMAT_VERBOSE", &[]) {
976 config.runtime.verbose = verbose;
977 }
978
979 if let Some(snapshot) = env_value("RUNMAT_SNAPSHOT_PATH", &[]) {
980 config.runtime.snapshot_path = Some(PathBuf::from(snapshot));
981 }
982
983 if let Some(flag) = env_bool("RUNMAT_TELEMETRY", &[]) {
985 config.telemetry.enabled = flag;
986 }
987 if let Some(flag) = env_bool("RUNMAT_NO_TELEMETRY", &[]) {
988 if flag {
989 config.telemetry.enabled = false;
990 }
991 }
992 if let Some(show) = env_bool("RUNMAT_TELEMETRY_SHOW", &[]) {
993 config.telemetry.show_payloads = show;
994 }
995 if let Some(endpoint) = env_value(
996 "RUNMAT_TELEMETRY_ENDPOINT",
997 &["RUNMAT_TELEMETRY_HTTP_ENDPOINT"],
998 ) {
999 let trimmed = endpoint.trim();
1000 if trimmed.is_empty() {
1001 config.telemetry.http_endpoint = None;
1002 } else {
1003 config.telemetry.http_endpoint = Some(trimmed.to_string());
1004 }
1005 }
1006 if let Some(udp) = env_value("RUNMAT_TELEMETRY_UDP_ENDPOINT", &[]) {
1007 let trimmed = udp.trim();
1008 if trimmed.is_empty() || trimmed == "0" || trimmed.eq_ignore_ascii_case("off") {
1009 config.telemetry.udp_endpoint = None;
1010 } else {
1011 config.telemetry.udp_endpoint = Some(trimmed.to_string());
1012 }
1013 }
1014 if let Some(queue) = env_value("RUNMAT_TELEMETRY_QUEUE_SIZE", &[]) {
1015 if let Ok(parsed) = queue.parse::<usize>() {
1016 config.telemetry.queue_size = parsed.max(MIN_QUEUE_SIZE);
1017 }
1018 }
1019
1020 if let Some(accel) = env_value("RUNMAT_ACCEL_ENABLE", &[]) {
1022 if let Some(flag) = parse_bool(&accel) {
1023 config.accelerate.enabled = flag;
1024 }
1025 }
1026
1027 if let Some(provider) = env_value("RUNMAT_ACCEL_PROVIDER", &[]) {
1028 if let Some(pref) = parse_provider_preference(&provider) {
1029 config.accelerate.provider = pref;
1030 }
1031 }
1032
1033 if let Some(force_inprocess) = env_bool("RUNMAT_ACCEL_FORCE_INPROCESS", &[]) {
1034 if force_inprocess {
1035 config.accelerate.provider = AccelerateProviderPreference::InProcess;
1036 }
1037 }
1038
1039 if let Some(wgpu_toggle) = env_bool("RUNMAT_ACCEL_WGPU", &[]) {
1040 config.accelerate.provider = if wgpu_toggle {
1041 AccelerateProviderPreference::Wgpu
1042 } else {
1043 AccelerateProviderPreference::InProcess
1044 };
1045 }
1046
1047 if let Some(fallback) = env_bool("RUNMAT_ACCEL_DISABLE_FALLBACK", &[]) {
1048 config.accelerate.allow_inprocess_fallback = !fallback;
1049 }
1050
1051 if let Some(force_fallback) = env_bool("RUNMAT_ACCEL_WGPU_FORCE_FALLBACK", &[]) {
1052 config.accelerate.wgpu_force_fallback_adapter = force_fallback;
1053 }
1054
1055 if let Some(power) = env_value("RUNMAT_ACCEL_WGPU_POWER", &[]) {
1056 if let Some(pref) = parse_power_preference(&power) {
1057 config.accelerate.wgpu_power_preference = pref;
1058 }
1059 }
1060
1061 if let Some(auto_enabled) = env_bool("RUNMAT_ACCEL_AUTO_OFFLOAD", &[]) {
1062 config.accelerate.auto_offload.enabled = auto_enabled;
1063 }
1064
1065 if let Some(auto_calibrate) = env_bool("RUNMAT_ACCEL_CALIBRATE", &[]) {
1066 config.accelerate.auto_offload.calibrate = auto_calibrate;
1067 }
1068
1069 if let Some(profile_path) = env_value("RUNMAT_ACCEL_PROFILE", &[]) {
1070 config.accelerate.auto_offload.profile_path = Some(PathBuf::from(profile_path));
1071 }
1072
1073 if let Some(auto_log) = env_value("RUNMAT_ACCEL_AUTO_LOG", &[]) {
1074 if let Some(level) = parse_auto_offload_log_level(&auto_log) {
1075 config.accelerate.auto_offload.log_level = level;
1076 }
1077 }
1078
1079 if let Some(jit_enabled) = env_bool("RUNMAT_JIT_ENABLE", &[]) {
1081 config.jit.enabled = jit_enabled;
1082 }
1083
1084 if let Some(jit_disabled) = env_bool("RUNMAT_JIT_DISABLE", &[]) {
1085 if jit_disabled {
1086 config.jit.enabled = false;
1087 }
1088 }
1089
1090 if let Some(threshold) = env_value("RUNMAT_JIT_THRESHOLD", &[]) {
1091 if let Ok(threshold) = threshold.parse() {
1092 config.jit.threshold = threshold;
1093 }
1094 }
1095
1096 if let Some(opt_level) = env_value("RUNMAT_JIT_OPT_LEVEL", &[]) {
1097 config.jit.optimization_level = match opt_level.to_lowercase().as_str() {
1098 "none" => JitOptLevel::None,
1099 "size" => JitOptLevel::Size,
1100 "speed" => JitOptLevel::Speed,
1101 "aggressive" => JitOptLevel::Aggressive,
1102 _ => config.jit.optimization_level,
1103 };
1104 }
1105
1106 if let Some(preset) = env_value("RUNMAT_GC_PRESET", &[]) {
1108 config.gc.preset = match preset.to_lowercase().as_str() {
1109 "low-latency" => Some(GcPreset::LowLatency),
1110 "high-throughput" => Some(GcPreset::HighThroughput),
1111 "low-memory" => Some(GcPreset::LowMemory),
1112 "debug" => Some(GcPreset::Debug),
1113 _ => config.gc.preset,
1114 };
1115 }
1116
1117 if let Some(young_size) = env_value("RUNMAT_GC_YOUNG_SIZE", &[]) {
1118 if let Ok(young_size) = young_size.parse() {
1119 config.gc.young_size_mb = Some(young_size);
1120 }
1121 }
1122
1123 if let Some(threads) = env_value("RUNMAT_GC_THREADS", &[]) {
1124 if let Ok(threads) = threads.parse() {
1125 config.gc.threads = Some(threads);
1126 }
1127 }
1128
1129 if let Some(stats) = env_bool("RUNMAT_GC_STATS", &[]) {
1130 config.gc.collect_stats = stats;
1131 }
1132
1133 if let Some(plot_mode) = env_value("RUNMAT_PLOT_MODE", &[]) {
1135 config.plotting.mode = match plot_mode.to_lowercase().as_str() {
1136 "auto" => PlotMode::Auto,
1137 "gui" => PlotMode::Gui,
1138 "headless" => PlotMode::Headless,
1139 "jupyter" => PlotMode::Jupyter,
1140 _ => config.plotting.mode,
1141 };
1142 }
1143
1144 if let Some(headless) = env_bool("RUNMAT_PLOT_HEADLESS", &[]) {
1145 config.plotting.force_headless = headless;
1146 }
1147
1148 if let Some(backend) = env_value("RUNMAT_PLOT_BACKEND", &[]) {
1149 config.plotting.backend = match backend.to_lowercase().as_str() {
1150 "auto" => PlotBackend::Auto,
1151 "wgpu" => PlotBackend::Wgpu,
1152 "static" => PlotBackend::Static,
1153 "web" => PlotBackend::Web,
1154 _ => config.plotting.backend,
1155 };
1156 }
1157
1158 if let Some(debug) = env_bool("RUNMAT_DEBUG", &[]) {
1160 config.logging.debug = debug;
1161 }
1162
1163 if let Some(log_level) = env_value("RUNMAT_LOG_LEVEL", &[]) {
1164 config.logging.level = match log_level.to_lowercase().as_str() {
1165 "error" => LogLevel::Error,
1166 "warn" => LogLevel::Warn,
1167 "info" => LogLevel::Info,
1168 "debug" => LogLevel::Debug,
1169 "trace" => LogLevel::Trace,
1170 _ => config.logging.level,
1171 };
1172 }
1173
1174 if let Some(ip) = env_value("RUNMAT_KERNEL_IP", &[]) {
1176 config.kernel.ip = ip;
1177 }
1178
1179 if let Some(key) = env_value("RUNMAT_KERNEL_KEY", &[]) {
1180 config.kernel.key = Some(key);
1181 }
1182
1183 Ok(())
1184 }
1185
1186 pub fn save_to_file(config: &RunMatConfig, path: &Path) -> Result<()> {
1188 let content = match path.extension().and_then(|ext| ext.to_str()) {
1189 Some("yaml") | Some("yml") => {
1190 serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
1191 }
1192 Some("json") => serde_json::to_string_pretty(config)
1193 .context("Failed to serialize config to JSON")?,
1194 Some("toml") => {
1195 toml::to_string_pretty(config).context("Failed to serialize config to TOML")?
1196 }
1197 _ => {
1198 serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
1200 }
1201 };
1202
1203 fs::write(path, content)
1204 .with_context(|| format!("Failed to write config file: {}", path.display()))?;
1205
1206 info!("Configuration saved to: {}", path.display());
1207 Ok(())
1208 }
1209
1210 pub fn generate_sample_config() -> String {
1212 let config = RunMatConfig::default();
1213 serde_yaml::to_string(&config).unwrap_or_else(|_| "# Failed to generate config".to_string())
1214 }
1215}
1216
1217fn parse_bool(s: &str) -> Option<bool> {
1219 match s.to_lowercase().as_str() {
1220 "1" | "true" | "yes" | "on" | "enable" | "enabled" => Some(true),
1221 "0" | "false" | "no" | "off" | "disable" | "disabled" => Some(false),
1222 "" => Some(false),
1223 _ => None,
1224 }
1225}
1226
1227fn parse_auto_offload_log_level(value: &str) -> Option<AutoOffloadLogLevel> {
1228 match value.trim().to_ascii_lowercase().as_str() {
1229 "off" => Some(AutoOffloadLogLevel::Off),
1230 "info" => Some(AutoOffloadLogLevel::Info),
1231 "trace" => Some(AutoOffloadLogLevel::Trace),
1232 _ => None,
1233 }
1234}
1235
1236fn parse_provider_preference(value: &str) -> Option<AccelerateProviderPreference> {
1237 match value.trim().to_ascii_lowercase().as_str() {
1238 "auto" => Some(AccelerateProviderPreference::Auto),
1239 "wgpu" => Some(AccelerateProviderPreference::Wgpu),
1240 "inprocess" | "cpu" | "host" => Some(AccelerateProviderPreference::InProcess),
1241 _ => None,
1242 }
1243}
1244
1245fn parse_power_preference(value: &str) -> Option<AccelPowerPreference> {
1246 match value.trim().to_ascii_lowercase().as_str() {
1247 "auto" => Some(AccelPowerPreference::Auto),
1248 "high" | "highperformance" | "performance" => Some(AccelPowerPreference::HighPerformance),
1249 "low" | "lowpower" | "battery" => Some(AccelPowerPreference::LowPower),
1250 _ => None,
1251 }
1252}
1253
1254fn env_value(primary: &str, aliases: &[&str]) -> Option<String> {
1255 env::var(primary)
1256 .ok()
1257 .or_else(|| aliases.iter().find_map(|alias| env::var(alias).ok()))
1258}
1259
1260fn env_bool(primary: &str, aliases: &[&str]) -> Option<bool> {
1261 env_value(primary, aliases).and_then(|value| parse_bool(&value))
1262}
1263
1264#[cfg(feature = "accelerate")]
1265mod accelerate_bridge {
1266 use super::{
1267 AccelPowerPreference, AccelerateConfig, AccelerateProviderPreference, AutoOffloadConfig,
1268 AutoOffloadLogLevel,
1269 };
1270 use runmat_accelerate::{
1271 AccelPowerPreference as RuntimePowerPreference, AccelerateInitOptions,
1272 AccelerateProviderPreference as RuntimeProviderPreference,
1273 AutoOffloadLogLevel as RuntimeAutoLogLevel, AutoOffloadOptions,
1274 };
1275
1276 impl From<AccelPowerPreference> for RuntimePowerPreference {
1277 fn from(pref: AccelPowerPreference) -> Self {
1278 match pref {
1279 AccelPowerPreference::Auto => RuntimePowerPreference::Auto,
1280 AccelPowerPreference::HighPerformance => RuntimePowerPreference::HighPerformance,
1281 AccelPowerPreference::LowPower => RuntimePowerPreference::LowPower,
1282 }
1283 }
1284 }
1285
1286 impl From<AccelerateProviderPreference> for RuntimeProviderPreference {
1287 fn from(pref: AccelerateProviderPreference) -> Self {
1288 match pref {
1289 AccelerateProviderPreference::Auto => RuntimeProviderPreference::Auto,
1290 AccelerateProviderPreference::Wgpu => RuntimeProviderPreference::Wgpu,
1291 AccelerateProviderPreference::InProcess => RuntimeProviderPreference::InProcess,
1292 }
1293 }
1294 }
1295
1296 impl From<AutoOffloadLogLevel> for RuntimeAutoLogLevel {
1297 fn from(level: AutoOffloadLogLevel) -> Self {
1298 match level {
1299 AutoOffloadLogLevel::Off => RuntimeAutoLogLevel::Off,
1300 AutoOffloadLogLevel::Info => RuntimeAutoLogLevel::Info,
1301 AutoOffloadLogLevel::Trace => RuntimeAutoLogLevel::Trace,
1302 }
1303 }
1304 }
1305
1306 impl From<&AutoOffloadConfig> for AutoOffloadOptions {
1307 fn from(cfg: &AutoOffloadConfig) -> Self {
1308 AutoOffloadOptions {
1309 enabled: cfg.enabled,
1310 calibrate: cfg.calibrate,
1311 profile_path: cfg.profile_path.clone(),
1312 log_level: cfg.log_level.into(),
1313 }
1314 }
1315 }
1316
1317 impl From<&AccelerateConfig> for AccelerateInitOptions {
1318 fn from(cfg: &AccelerateConfig) -> Self {
1319 AccelerateInitOptions {
1320 enabled: cfg.enabled,
1321 provider: cfg.provider.into(),
1322 allow_inprocess_fallback: cfg.allow_inprocess_fallback,
1323 wgpu_power_preference: cfg.wgpu_power_preference.into(),
1324 wgpu_force_fallback_adapter: cfg.wgpu_force_fallback_adapter,
1325 auto_offload: AutoOffloadOptions::from(&cfg.auto_offload),
1326 }
1327 }
1328 }
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333 use super::*;
1334 use once_cell::sync::Lazy;
1335 use std::sync::Mutex;
1336 use tempfile::TempDir;
1337
1338 static ENV_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
1339
1340 #[test]
1341 fn test_config_defaults() {
1342 let config = RunMatConfig::default();
1343 assert_eq!(config.runtime.timeout, 300);
1344 assert!(config.jit.enabled);
1345 assert_eq!(config.jit.threshold, 10);
1346 assert_eq!(config.plotting.mode, PlotMode::Auto);
1347 assert!(matches!(config.language.compat, LanguageCompatMode::RunMat));
1348 assert_eq!(config.runtime.error_namespace, "");
1349 }
1350
1351 #[test]
1352 fn test_yaml_serialization() {
1353 let config = RunMatConfig::default();
1354 let yaml = serde_yaml::to_string(&config).unwrap();
1355 let parsed: RunMatConfig = serde_yaml::from_str(&yaml).unwrap();
1356
1357 assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
1358 assert_eq!(parsed.jit.enabled, config.jit.enabled);
1359 assert_eq!(parsed.accelerate.provider, config.accelerate.provider);
1360 }
1361
1362 #[test]
1363 fn test_json_serialization() {
1364 let config = RunMatConfig::default();
1365 let json = serde_json::to_string_pretty(&config).unwrap();
1366 let parsed: RunMatConfig = serde_json::from_str(&json).unwrap();
1367
1368 assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
1369 assert_eq!(parsed.plotting.mode, config.plotting.mode);
1370 assert_eq!(parsed.accelerate.enabled, config.accelerate.enabled);
1371 }
1372
1373 #[test]
1374 fn test_parse_auto_offload_log_level_cases() {
1375 assert_eq!(
1376 parse_auto_offload_log_level("off"),
1377 Some(AutoOffloadLogLevel::Off)
1378 );
1379 assert_eq!(
1380 parse_auto_offload_log_level("INFO"),
1381 Some(AutoOffloadLogLevel::Info)
1382 );
1383 assert_eq!(
1384 parse_auto_offload_log_level("trace"),
1385 Some(AutoOffloadLogLevel::Trace)
1386 );
1387 assert_eq!(parse_auto_offload_log_level("unknown"), None);
1388 }
1389
1390 #[test]
1391 fn test_file_loading() {
1392 let temp_dir = TempDir::new().unwrap();
1393 let config_path = temp_dir.path().join(".runmat.yaml");
1394
1395 let mut config = RunMatConfig::default();
1396 config.runtime.timeout = 600;
1397 config.jit.threshold = 20;
1398
1399 ConfigLoader::save_to_file(&config, &config_path).unwrap();
1400 let loaded = ConfigLoader::load_from_file(&config_path).unwrap();
1401
1402 assert_eq!(loaded.runtime.timeout, 600);
1403 assert_eq!(loaded.jit.threshold, 20);
1404 }
1405
1406 #[test]
1407 fn test_bool_parsing() {
1408 assert_eq!(parse_bool("true"), Some(true));
1409 assert_eq!(parse_bool("1"), Some(true));
1410 assert_eq!(parse_bool("yes"), Some(true));
1411 assert_eq!(parse_bool("false"), Some(false));
1412 assert_eq!(parse_bool("0"), Some(false));
1413 assert_eq!(parse_bool("invalid"), None);
1414 }
1415
1416 #[test]
1417 fn telemetry_env_overrides_respect_empty_values() {
1418 let _lock = ENV_GUARD.lock().unwrap();
1419 std::env::set_var("RUNMAT_TELEMETRY_ENDPOINT", "https://custom.example/ingest");
1420 std::env::set_var("RUNMAT_TELEMETRY_UDP_ENDPOINT", "off");
1421 let mut config = RunMatConfig::default();
1422 ConfigLoader::apply_environment_variables(&mut config).unwrap();
1423 assert_eq!(
1424 config.telemetry.http_endpoint.as_deref(),
1425 Some("https://custom.example/ingest")
1426 );
1427 assert!(config.telemetry.udp_endpoint.is_none());
1428 std::env::remove_var("RUNMAT_TELEMETRY_ENDPOINT");
1429 std::env::remove_var("RUNMAT_TELEMETRY_UDP_ENDPOINT");
1430 }
1431}