Skip to main content

runmat_config/
lib.rs

1//! Configuration system for RunMat
2//!
3//! Supports multiple configuration sources with proper precedence:
4//! 1. Command-line arguments (highest priority)
5//! 2. Environment variables  
6//! 3. Configuration files (.runmat.yaml, .runmat.json, etc.)
7//! 4. Built-in defaults (lowest priority)
8
9use 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/// Main RunMat configuration
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct RunMatConfig {
34    /// Runtime configuration
35    pub runtime: RuntimeConfig,
36    /// Acceleration configuration
37    #[serde(default)]
38    pub accelerate: AccelerateConfig,
39    /// Language compatibility configuration
40    #[serde(default)]
41    pub language: LanguageConfig,
42    /// Telemetry configuration
43    #[serde(default)]
44    pub telemetry: TelemetryConfig,
45    /// JIT compiler configuration
46    pub jit: JitConfig,
47    /// Garbage collector configuration
48    pub gc: GcConfig,
49    /// Plotting configuration
50    pub plotting: PlottingConfig,
51    /// Kernel configuration
52    pub kernel: KernelConfig,
53    /// Logging configuration
54    pub logging: LoggingConfig,
55    /// Package manager configuration
56    #[serde(default)]
57    pub packages: PackagesConfig,
58}
59
60/// Runtime execution configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct RuntimeConfig {
63    /// Execution timeout in seconds
64    #[serde(default = "default_timeout")]
65    pub timeout: u64,
66    /// Maximum number of call stack frames to record
67    #[serde(default = "default_callstack_limit")]
68    pub callstack_limit: usize,
69    /// Namespace prefix for runtime/semantic error identifiers
70    #[serde(default = "default_error_namespace")]
71    pub error_namespace: String,
72    /// Enable verbose output
73    #[serde(default)]
74    pub verbose: bool,
75    /// Snapshot file to preload
76    pub snapshot_path: Option<PathBuf>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80pub struct LanguageConfig {
81    /// Compatibility mode for MATLAB command syntax and legacy behaviors.
82    /// Default: "runmat" (RunMat identifiers with MATLAB-compatible command syntax).
83    /// "strict" disables command syntax; require `hold(\"on\")` style.
84    #[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/// Acceleration (GPU) configuration
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct AccelerateConfig {
114    /// Enable acceleration subsystem
115    #[serde(default = "default_true")]
116    pub enabled: bool,
117    /// Preferred provider (auto, wgpu, inprocess)
118    #[serde(default)]
119    pub provider: AccelerateProviderPreference,
120    /// Allow automatic fallback to the in-process provider when hardware backend fails
121    #[serde(default = "default_true")]
122    pub allow_inprocess_fallback: bool,
123    /// Preferred WGPU power profile
124    #[serde(default)]
125    pub wgpu_power_preference: AccelPowerPreference,
126    /// Force use of WGPU fallback adapter even if a high-performance adapter exists
127    #[serde(default)]
128    pub wgpu_force_fallback_adapter: bool,
129    /// Auto-offload planner configuration
130    #[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/// Telemetry configuration
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct TelemetryConfig {
174    /// Enable runtime telemetry
175    #[serde(default = "default_true")]
176    pub enabled: bool,
177    /// Echo each payload to stdout for transparency
178    #[serde(default)]
179    pub show_payloads: bool,
180    /// Optional HTTP endpoint override
181    pub http_endpoint: Option<String>,
182    /// Optional UDP endpoint override (host:port)
183    pub udp_endpoint: Option<String>,
184    /// Bounded queue size for async delivery
185    #[serde(default = "default_telemetry_queue")]
186    pub queue_size: usize,
187    /// Require ingestion key (self-built binaries default to false)
188    #[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/// Auto-offload planner configuration
223#[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/// JIT compiler configuration
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct JitConfig {
248    /// Enable JIT compilation
249    #[serde(default = "default_true")]
250    pub enabled: bool,
251    /// JIT compilation threshold
252    #[serde(default = "default_jit_threshold")]
253    pub threshold: u32,
254    /// JIT optimization level
255    #[serde(default)]
256    pub optimization_level: JitOptLevel,
257}
258
259/// GC configuration
260#[derive(Debug, Clone, Serialize, Deserialize, Default)]
261pub struct GcConfig {
262    /// GC preset
263    pub preset: Option<GcPreset>,
264    /// Young generation size in MB
265    pub young_size_mb: Option<usize>,
266    /// Number of GC threads
267    pub threads: Option<usize>,
268    /// Enable GC statistics collection
269    #[serde(default)]
270    pub collect_stats: bool,
271}
272
273/// Plotting configuration
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct PlottingConfig {
276    /// Plotting mode
277    #[serde(default)]
278    pub mode: PlotMode,
279    /// Force headless mode
280    #[serde(default)]
281    pub force_headless: bool,
282    /// Default plot backend
283    #[serde(default)]
284    pub backend: PlotBackend,
285    /// GUI settings
286    pub gui: Option<GuiConfig>,
287    /// Export settings
288    pub export: Option<ExportConfig>,
289    /// Target scatter point budget for GPU decimation overrides
290    #[serde(default)]
291    pub scatter_target_points: Option<u32>,
292    /// Surface vertex budget override for LOD selection
293    #[serde(default)]
294    pub surface_vertex_budget: Option<u64>,
295}
296
297/// GUI configuration
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct GuiConfig {
300    /// Window width
301    #[serde(default = "default_window_width")]
302    pub width: u32,
303    /// Window height
304    #[serde(default = "default_window_height")]
305    pub height: u32,
306    /// Enable VSync
307    #[serde(default = "default_true")]
308    pub vsync: bool,
309    /// Enable maximized window
310    #[serde(default)]
311    pub maximized: bool,
312}
313
314/// Export configuration
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ExportConfig {
317    /// Default export format
318    #[serde(default)]
319    pub format: ExportFormat,
320    /// Default DPI for raster exports
321    #[serde(default = "default_dpi")]
322    pub dpi: u32,
323    /// Default output directory
324    pub output_dir: Option<PathBuf>,
325    /// Jupyter notebook configuration
326    pub jupyter: Option<JupyterConfig>,
327}
328
329/// Jupyter notebook integration configuration
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct JupyterConfig {
332    /// Default output format for Jupyter cells
333    #[serde(default)]
334    pub output_format: JupyterOutputFormat,
335    /// Enable interactive widgets
336    #[serde(default = "default_true")]
337    pub enable_widgets: bool,
338    /// Enable static image fallback
339    #[serde(default = "default_true")]
340    pub enable_static_fallback: bool,
341    /// Widget configuration
342    pub widget: Option<JupyterWidgetConfig>,
343    /// Static export configuration
344    pub static_export: Option<JupyterStaticConfig>,
345    /// Performance settings
346    pub performance: Option<JupyterPerformanceConfig>,
347}
348
349/// Jupyter widget configuration
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct JupyterWidgetConfig {
352    /// Enable client-side rendering (WebAssembly)
353    #[serde(default = "default_true")]
354    pub client_side_rendering: bool,
355    /// Enable server-side streaming
356    #[serde(default)]
357    pub server_side_streaming: bool,
358    /// Widget cache size in MB
359    #[serde(default = "default_widget_cache_size")]
360    pub cache_size_mb: u32,
361    /// Update frequency for animations (FPS)
362    #[serde(default = "default_widget_fps")]
363    pub update_fps: u32,
364    /// Enable GPU acceleration in browser
365    #[serde(default = "default_true")]
366    pub gpu_acceleration: bool,
367}
368
369/// Jupyter static export configuration
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct JupyterStaticConfig {
372    /// Image width in pixels
373    #[serde(default = "default_jupyter_width")]
374    pub width: u32,
375    /// Image height in pixels
376    #[serde(default = "default_jupyter_height")]
377    pub height: u32,
378    /// Image quality (0.0-1.0)
379    #[serde(default = "default_jupyter_quality")]
380    pub quality: f32,
381    /// Include metadata in exports
382    #[serde(default = "default_true")]
383    pub include_metadata: bool,
384    /// Preferred formats in order of preference
385    #[serde(default)]
386    pub preferred_formats: Vec<JupyterOutputFormat>,
387}
388
389/// Jupyter performance configuration
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct JupyterPerformanceConfig {
392    /// Maximum render time per frame (ms)
393    #[serde(default = "default_max_render_time")]
394    pub max_render_time_ms: u32,
395    /// Enable progressive rendering
396    #[serde(default = "default_true")]
397    pub progressive_rendering: bool,
398    /// LOD (Level of Detail) threshold
399    #[serde(default = "default_lod_threshold")]
400    pub lod_threshold: u32,
401    /// Enable texture compression
402    #[serde(default = "default_true")]
403    pub texture_compression: bool,
404}
405
406/// Kernel configuration
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct KernelConfig {
409    /// Default IP address
410    #[serde(default = "default_kernel_ip")]
411    pub ip: String,
412    /// Authentication key
413    pub key: Option<String>,
414    /// Port configuration
415    pub ports: Option<KernelPorts>,
416}
417
418/// Kernel port configuration
419#[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/// Package manager configuration
429#[derive(Debug, Clone, Serialize, Deserialize, Default)]
430pub struct PackagesConfig {
431    /// Enable package manager
432    #[serde(default = "default_true")]
433    pub enabled: bool,
434    /// Registries to search for packages (first match wins)
435    #[serde(default = "default_registries")]
436    pub registries: Vec<Registry>,
437    /// Dependencies declared by the workspace (name -> spec)
438    #[serde(default)]
439    pub dependencies: HashMap<String, PackageSpec>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct Registry {
444    /// Registry logical name
445    pub name: String,
446    /// Base URL for index/API (e.g., https://packages.runmat.com)
447    pub url: String,
448}
449
450/// Package specification
451#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(tag = "source", rename_all = "kebab-case")]
453pub enum PackageSpec {
454    /// Resolve from a registry by name
455    Registry {
456        /// Semver range (e.g. "^1.2"), or exact version
457        version: String,
458        /// Optional registry override (defaults to first registry)
459        #[serde(default)]
460        registry: Option<String>,
461        /// Optional feature flags
462        #[serde(default)]
463        features: Vec<String>,
464        /// Optional mark for optional dependency
465        #[serde(default)]
466        optional: bool,
467    },
468    /// Git repository
469    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    /// Local path dependency (useful for development)
479    Path {
480        path: String,
481        #[serde(default)]
482        features: Vec<String>,
483        #[serde(default)]
484        optional: bool,
485    },
486}
487
488/// Logging configuration
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct LoggingConfig {
491    /// Log level
492    #[serde(default)]
493    pub level: LogLevel,
494    /// Enable debug logging
495    #[serde(default)]
496    pub debug: bool,
497    /// Log file path
498    pub file: Option<PathBuf>,
499}
500
501/// Plotting mode enumeration
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
503#[serde(rename_all = "lowercase")]
504pub enum PlotMode {
505    /// Automatic detection based on environment
506    Auto,
507    /// Force GUI mode
508    Gui,
509    /// Force headless/static mode
510    Headless,
511    /// Jupyter notebook mode
512    Jupyter,
513}
514
515/// Plot backend enumeration
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
517#[serde(rename_all = "lowercase")]
518pub enum PlotBackend {
519    /// Automatic backend selection
520    Auto,
521    /// WGPU GPU-accelerated backend
522    Wgpu,
523    /// Static plotters backend
524    Static,
525    /// Web/browser backend
526    Web,
527}
528
529/// Export format enumeration
530#[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/// Jupyter-specific output formats
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(rename_all = "lowercase")]
542pub enum JupyterOutputFormat {
543    /// Interactive HTML widget with WebAssembly
544    Widget,
545    /// Static PNG image
546    Png,
547    /// Static SVG image
548    Svg,
549    /// Base64-encoded image
550    Base64,
551    /// Plotly-compatible JSON
552    PlotlyJson,
553    /// Auto-detect based on environment
554    Auto,
555}
556
557/// JIT optimization level
558#[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/// GC preset
568#[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/// Log level
578#[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
588// Default value functions
589fn 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 // 64MB cache
621}
622
623fn default_widget_fps() -> u32 {
624    30 // 30 FPS for smooth animations
625}
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 // High quality (0.0-1.0)
637}
638
639fn default_max_render_time() -> u32 {
640    16 // 16ms for 60 FPS
641}
642
643fn default_lod_threshold() -> u32 {
644    10000 // Points threshold for LOD
645}
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
820/// Configuration loader with multiple source support
821pub struct ConfigLoader;
822
823impl ConfigLoader {
824    /// Load configuration from all sources with proper precedence
825    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    /// Find and load configuration from files
832    fn load_from_files() -> Result<RunMatConfig> {
833        // Try to find config file in order of preference
834        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    /// Find potential configuration file paths
855    fn find_config_files() -> Vec<PathBuf> {
856        let mut paths = Vec::new();
857
858        // 1. Environment variable override
859        if let Some(config_path) = env_value("RUNMAT_CONFIG", &[]) {
860            paths.push(PathBuf::from(config_path));
861        }
862
863        // 2. Current directory
864        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        // 3. Home directory
871        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        // 4. System-wide configurations
881        #[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    /// Walk up from the provided directory looking for the first config file.
892    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    /// Load configuration from a specific file
913    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            // `.runmat` is a TOML alias by default (single canonical format)
919            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                // Try auto-detect (prefer TOML for unknown/no extension)
935                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    /// Apply environment variable overrides
954    fn apply_environment_variables(config: &mut RunMatConfig) -> Result<()> {
955        // Runtime settings
956        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        // Telemetry settings
984        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        // Acceleration settings
1021        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        // JIT settings
1080        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        // GC settings
1107        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        // Plotting settings
1134        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        // Logging settings
1159        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        // Kernel settings
1175        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    /// Save configuration to a file
1187    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                // Default to YAML
1199                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    /// Generate a sample configuration file
1211    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
1217/// Parse a boolean value from string with various formats
1218fn 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}