use anyhow::{Context, Result};
use clap::ValueEnum;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const MIN_QUEUE_SIZE: usize = 8;
const CONFIG_FILENAMES: &[&str] = &[
".runmat",
".runmat.toml",
".runmat.yaml",
".runmat.yml",
".runmat.json",
"runmat.config.toml",
"runmat.config.yaml",
"runmat.config.yml",
"runmat.config.json",
];
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RunMatConfig {
pub runtime: RuntimeConfig,
#[serde(default)]
pub accelerate: AccelerateConfig,
#[serde(default)]
pub language: LanguageConfig,
#[serde(default)]
pub telemetry: TelemetryConfig,
pub jit: JitConfig,
pub gc: GcConfig,
pub plotting: PlottingConfig,
pub kernel: KernelConfig,
pub logging: LoggingConfig,
#[serde(default)]
pub packages: PackagesConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeConfig {
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default = "default_callstack_limit")]
pub callstack_limit: usize,
#[serde(default = "default_error_namespace")]
pub error_namespace: String,
#[serde(default)]
pub verbose: bool,
pub snapshot_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LanguageConfig {
#[serde(default)]
pub compat: LanguageCompatMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageCompatMode {
#[serde(rename = "runmat", alias = "run-mat")]
#[value(name = "runmat")]
RunMat,
Matlab,
Strict,
}
impl Default for LanguageCompatMode {
fn default() -> Self {
Self::RunMat
}
}
pub fn error_namespace_for_language_compat(mode: LanguageCompatMode) -> &'static str {
match mode {
LanguageCompatMode::Matlab => "MATLAB",
LanguageCompatMode::RunMat | LanguageCompatMode::Strict => "RunMat",
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccelerateConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub provider: AccelerateProviderPreference,
#[serde(default = "default_true")]
pub allow_inprocess_fallback: bool,
#[serde(default)]
pub wgpu_power_preference: AccelPowerPreference,
#[serde(default)]
pub wgpu_force_fallback_adapter: bool,
#[serde(default)]
pub auto_offload: AutoOffloadConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum AccelerateProviderPreference {
Auto,
Wgpu,
InProcess,
}
impl Default for AccelerateProviderPreference {
fn default() -> Self {
Self::Wgpu
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum AccelPowerPreference {
Auto,
HighPerformance,
LowPower,
}
impl Default for AccelPowerPreference {
fn default() -> Self {
Self::Auto
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, Default)]
#[serde(rename_all = "kebab-case")]
pub enum AutoOffloadLogLevel {
Off,
Info,
#[default]
Trace,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub show_payloads: bool,
pub http_endpoint: Option<String>,
pub udp_endpoint: Option<String>,
#[serde(default = "default_telemetry_queue")]
pub queue_size: usize,
#[serde(default = "default_true")]
pub require_ingestion_key: bool,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: true,
show_payloads: false,
http_endpoint: None,
udp_endpoint: Some("udp.telemetry.runmat.com:7846".to_string()),
queue_size: default_telemetry_queue(),
require_ingestion_key: true,
}
}
}
impl Default for AccelerateConfig {
fn default() -> Self {
Self {
enabled: true,
provider: AccelerateProviderPreference::Wgpu,
allow_inprocess_fallback: true,
wgpu_power_preference: AccelPowerPreference::Auto,
wgpu_force_fallback_adapter: false,
auto_offload: AutoOffloadConfig::default(),
}
}
}
fn default_telemetry_queue() -> usize {
256
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoOffloadConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_true")]
pub calibrate: bool,
pub profile_path: Option<PathBuf>,
#[serde(default)]
pub log_level: AutoOffloadLogLevel,
}
impl Default for AutoOffloadConfig {
fn default() -> Self {
Self {
enabled: true,
calibrate: true,
profile_path: None,
log_level: AutoOffloadLogLevel::Trace,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JitConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_jit_threshold")]
pub threshold: u32,
#[serde(default)]
pub optimization_level: JitOptLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GcConfig {
pub preset: Option<GcPreset>,
pub young_size_mb: Option<usize>,
pub threads: Option<usize>,
#[serde(default)]
pub collect_stats: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlottingConfig {
#[serde(default)]
pub mode: PlotMode,
#[serde(default)]
pub force_headless: bool,
#[serde(default)]
pub backend: PlotBackend,
pub gui: Option<GuiConfig>,
pub export: Option<ExportConfig>,
#[serde(default)]
pub scatter_target_points: Option<u32>,
#[serde(default)]
pub surface_vertex_budget: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuiConfig {
#[serde(default = "default_window_width")]
pub width: u32,
#[serde(default = "default_window_height")]
pub height: u32,
#[serde(default = "default_true")]
pub vsync: bool,
#[serde(default)]
pub maximized: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
#[serde(default)]
pub format: ExportFormat,
#[serde(default = "default_dpi")]
pub dpi: u32,
pub output_dir: Option<PathBuf>,
pub jupyter: Option<JupyterConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterConfig {
#[serde(default)]
pub output_format: JupyterOutputFormat,
#[serde(default = "default_true")]
pub enable_widgets: bool,
#[serde(default = "default_true")]
pub enable_static_fallback: bool,
pub widget: Option<JupyterWidgetConfig>,
pub static_export: Option<JupyterStaticConfig>,
pub performance: Option<JupyterPerformanceConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterWidgetConfig {
#[serde(default = "default_true")]
pub client_side_rendering: bool,
#[serde(default)]
pub server_side_streaming: bool,
#[serde(default = "default_widget_cache_size")]
pub cache_size_mb: u32,
#[serde(default = "default_widget_fps")]
pub update_fps: u32,
#[serde(default = "default_true")]
pub gpu_acceleration: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterStaticConfig {
#[serde(default = "default_jupyter_width")]
pub width: u32,
#[serde(default = "default_jupyter_height")]
pub height: u32,
#[serde(default = "default_jupyter_quality")]
pub quality: f32,
#[serde(default = "default_true")]
pub include_metadata: bool,
#[serde(default)]
pub preferred_formats: Vec<JupyterOutputFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JupyterPerformanceConfig {
#[serde(default = "default_max_render_time")]
pub max_render_time_ms: u32,
#[serde(default = "default_true")]
pub progressive_rendering: bool,
#[serde(default = "default_lod_threshold")]
pub lod_threshold: u32,
#[serde(default = "default_true")]
pub texture_compression: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelConfig {
#[serde(default = "default_kernel_ip")]
pub ip: String,
pub key: Option<String>,
pub ports: Option<KernelPorts>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KernelPorts {
pub shell: Option<u16>,
pub iopub: Option<u16>,
pub stdin: Option<u16>,
pub control: Option<u16>,
pub heartbeat: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PackagesConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_registries")]
pub registries: Vec<Registry>,
#[serde(default)]
pub dependencies: HashMap<String, PackageSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Registry {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "kebab-case")]
pub enum PackageSpec {
Registry {
version: String,
#[serde(default)]
registry: Option<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
Git {
url: String,
#[serde(default)]
rev: Option<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
Path {
path: String,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
optional: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default)]
pub level: LogLevel,
#[serde(default)]
pub debug: bool,
pub file: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PlotMode {
Auto,
Gui,
Headless,
Jupyter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PlotBackend {
Auto,
Wgpu,
Static,
Web,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExportFormat {
Png,
Svg,
Pdf,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JupyterOutputFormat {
Widget,
Png,
Svg,
Base64,
PlotlyJson,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JitOptLevel {
None,
Size,
Speed,
Aggressive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GcPreset {
LowLatency,
HighThroughput,
LowMemory,
Debug,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
fn default_timeout() -> u64 {
300
}
fn default_callstack_limit() -> usize {
200
}
fn default_error_namespace() -> String {
"".to_string()
}
fn default_true() -> bool {
true
}
fn default_jit_threshold() -> u32 {
10
}
fn default_window_width() -> u32 {
1200
}
fn default_window_height() -> u32 {
800
}
fn default_dpi() -> u32 {
300
}
fn default_kernel_ip() -> String {
"127.0.0.1".to_string()
}
fn default_widget_cache_size() -> u32 {
64 }
fn default_widget_fps() -> u32 {
30 }
fn default_jupyter_width() -> u32 {
800
}
fn default_jupyter_height() -> u32 {
600
}
fn default_jupyter_quality() -> f32 {
0.9 }
fn default_max_render_time() -> u32 {
16 }
fn default_lod_threshold() -> u32 {
10000 }
fn default_registries() -> Vec<Registry> {
vec![Registry {
name: "runmat".to_string(),
url: "https://packages.runmat.com".to_string(),
}]
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
timeout: default_timeout(),
callstack_limit: default_callstack_limit(),
error_namespace: default_error_namespace(),
verbose: false,
snapshot_path: None,
}
}
}
impl Default for JitConfig {
fn default() -> Self {
Self {
enabled: true,
threshold: default_jit_threshold(),
optimization_level: JitOptLevel::Speed,
}
}
}
impl Default for PlottingConfig {
fn default() -> Self {
Self {
mode: PlotMode::Auto,
force_headless: false,
backend: PlotBackend::Auto,
gui: Some(GuiConfig::default()),
export: Some(ExportConfig::default()),
scatter_target_points: None,
surface_vertex_budget: None,
}
}
}
impl Default for GuiConfig {
fn default() -> Self {
Self {
width: default_window_width(),
height: default_window_height(),
vsync: true,
maximized: false,
}
}
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
format: ExportFormat::Png,
dpi: default_dpi(),
output_dir: None,
jupyter: Some(JupyterConfig::default()),
}
}
}
impl Default for KernelConfig {
fn default() -> Self {
Self {
ip: default_kernel_ip(),
key: None,
ports: None,
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: LogLevel::Warn,
debug: false,
file: None,
}
}
}
impl Default for PlotMode {
fn default() -> Self {
Self::Auto
}
}
impl Default for PlotBackend {
fn default() -> Self {
Self::Auto
}
}
impl Default for ExportFormat {
fn default() -> Self {
Self::Png
}
}
impl Default for JitOptLevel {
fn default() -> Self {
Self::Speed
}
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl Default for JupyterOutputFormat {
fn default() -> Self {
Self::Auto
}
}
impl Default for JupyterConfig {
fn default() -> Self {
Self {
output_format: JupyterOutputFormat::default(),
enable_widgets: true,
enable_static_fallback: true,
widget: Some(JupyterWidgetConfig::default()),
static_export: Some(JupyterStaticConfig::default()),
performance: Some(JupyterPerformanceConfig::default()),
}
}
}
impl Default for JupyterWidgetConfig {
fn default() -> Self {
Self {
client_side_rendering: true,
server_side_streaming: false,
cache_size_mb: default_widget_cache_size(),
update_fps: default_widget_fps(),
gpu_acceleration: true,
}
}
}
impl Default for JupyterStaticConfig {
fn default() -> Self {
Self {
width: default_jupyter_width(),
height: default_jupyter_height(),
quality: default_jupyter_quality(),
include_metadata: true,
preferred_formats: vec![
JupyterOutputFormat::Widget,
JupyterOutputFormat::Png,
JupyterOutputFormat::Svg,
],
}
}
}
impl Default for JupyterPerformanceConfig {
fn default() -> Self {
Self {
max_render_time_ms: default_max_render_time(),
progressive_rendering: true,
lod_threshold: default_lod_threshold(),
texture_compression: true,
}
}
}
pub struct ConfigLoader;
impl ConfigLoader {
pub fn load() -> Result<RunMatConfig> {
let mut config = Self::load_from_files()?;
Self::apply_environment_variables(&mut config)?;
Ok(config)
}
fn load_from_files() -> Result<RunMatConfig> {
let config_paths = Self::find_config_files();
for path in config_paths {
if path.is_dir() {
info!(
"Ignoring config directory path (expected file): {}",
path.display()
);
continue;
}
if path.exists() {
info!("Loading configuration from: {}", path.display());
return Self::load_from_file(&path);
}
}
debug!("No configuration file found, using defaults");
Ok(RunMatConfig::default())
}
fn find_config_files() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(config_path) = env_value("RUNMAT_CONFIG", &[]) {
paths.push(PathBuf::from(config_path));
}
if let Ok(current_dir) = env::current_dir() {
for name in CONFIG_FILENAMES {
paths.push(current_dir.join(name));
}
}
if let Some(home_dir) = dirs::home_dir() {
for name in CONFIG_FILENAMES {
paths.push(home_dir.join(name));
}
paths.push(home_dir.join(".config/runmat/config.yaml"));
paths.push(home_dir.join(".config/runmat/config.yml"));
paths.push(home_dir.join(".config/runmat/config.json"));
}
#[cfg(unix)]
{
paths.push(PathBuf::from("/etc/runmat/config.yaml"));
paths.push(PathBuf::from("/etc/runmat/config.yml"));
paths.push(PathBuf::from("/etc/runmat/config.json"));
}
paths
}
pub fn discover_config_path_from(start: &Path) -> Option<PathBuf> {
let mut current = if start.is_dir() {
start.to_path_buf()
} else {
start.parent().map(Path::to_path_buf)?
};
loop {
for name in CONFIG_FILENAMES {
let candidate = current.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
if !current.pop() {
break;
}
}
None
}
pub fn load_from_file(path: &Path) -> Result<RunMatConfig> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config = match path.extension().and_then(|ext| ext.to_str()) {
None if path.file_name().and_then(|n| n.to_str()) == Some(".runmat") => {
toml::from_str(&content).with_context(|| {
format!("Failed to parse .runmat (TOML) config: {}", path.display())
})?
}
Some("runmat") => toml::from_str(&content).with_context(|| {
format!("Failed to parse .runmat (TOML) config: {}", path.display())
})?,
Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
.with_context(|| format!("Failed to parse YAML config: {}", path.display()))?,
Some("json") => serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON config: {}", path.display()))?,
Some("toml") => toml::from_str(&content)
.with_context(|| format!("Failed to parse TOML config: {}", path.display()))?,
_ => {
if let Ok(config) = toml::from_str(&content) {
config
} else if let Ok(config) = serde_yaml::from_str(&content) {
config
} else if let Ok(config) = serde_json::from_str(&content) {
config
} else {
return Err(anyhow::anyhow!(
"Could not parse config file {} (tried TOML, YAML, JSON)",
path.display()
));
}
}
};
Ok(config)
}
fn apply_environment_variables(config: &mut RunMatConfig) -> Result<()> {
if let Some(timeout) = env_value("RUNMAT_TIMEOUT", &[]) {
if let Ok(timeout) = timeout.parse() {
config.runtime.timeout = timeout;
}
}
if let Some(limit) = env_value("RUNMAT_CALLSTACK_LIMIT", &[]) {
if let Ok(limit) = limit.parse() {
config.runtime.callstack_limit = limit;
}
}
if let Some(namespace) = env_value("RUNMAT_ERROR_NAMESPACE", &[]) {
let trimmed = namespace.trim();
if !trimmed.is_empty() {
config.runtime.error_namespace = trimmed.to_string();
}
}
if let Some(verbose) = env_bool("RUNMAT_VERBOSE", &[]) {
config.runtime.verbose = verbose;
}
if let Some(snapshot) = env_value("RUNMAT_SNAPSHOT_PATH", &[]) {
config.runtime.snapshot_path = Some(PathBuf::from(snapshot));
}
if let Some(flag) = env_bool("RUNMAT_TELEMETRY", &[]) {
config.telemetry.enabled = flag;
}
if let Some(flag) = env_bool("RUNMAT_NO_TELEMETRY", &[]) {
if flag {
config.telemetry.enabled = false;
}
}
if let Some(show) = env_bool("RUNMAT_TELEMETRY_SHOW", &[]) {
config.telemetry.show_payloads = show;
}
if let Some(endpoint) = env_value(
"RUNMAT_TELEMETRY_ENDPOINT",
&["RUNMAT_TELEMETRY_HTTP_ENDPOINT"],
) {
let trimmed = endpoint.trim();
if trimmed.is_empty() {
config.telemetry.http_endpoint = None;
} else {
config.telemetry.http_endpoint = Some(trimmed.to_string());
}
}
if let Some(udp) = env_value("RUNMAT_TELEMETRY_UDP_ENDPOINT", &[]) {
let trimmed = udp.trim();
if trimmed.is_empty() || trimmed == "0" || trimmed.eq_ignore_ascii_case("off") {
config.telemetry.udp_endpoint = None;
} else {
config.telemetry.udp_endpoint = Some(trimmed.to_string());
}
}
if let Some(queue) = env_value("RUNMAT_TELEMETRY_QUEUE_SIZE", &[]) {
if let Ok(parsed) = queue.parse::<usize>() {
config.telemetry.queue_size = parsed.max(MIN_QUEUE_SIZE);
}
}
if let Some(accel) = env_value("RUNMAT_ACCEL_ENABLE", &[]) {
if let Some(flag) = parse_bool(&accel) {
config.accelerate.enabled = flag;
}
}
if let Some(provider) = env_value("RUNMAT_ACCEL_PROVIDER", &[]) {
if let Some(pref) = parse_provider_preference(&provider) {
config.accelerate.provider = pref;
}
}
if let Some(force_inprocess) = env_bool("RUNMAT_ACCEL_FORCE_INPROCESS", &[]) {
if force_inprocess {
config.accelerate.provider = AccelerateProviderPreference::InProcess;
}
}
if let Some(wgpu_toggle) = env_bool("RUNMAT_ACCEL_WGPU", &[]) {
config.accelerate.provider = if wgpu_toggle {
AccelerateProviderPreference::Wgpu
} else {
AccelerateProviderPreference::InProcess
};
}
if let Some(fallback) = env_bool("RUNMAT_ACCEL_DISABLE_FALLBACK", &[]) {
config.accelerate.allow_inprocess_fallback = !fallback;
}
if let Some(force_fallback) = env_bool("RUNMAT_ACCEL_WGPU_FORCE_FALLBACK", &[]) {
config.accelerate.wgpu_force_fallback_adapter = force_fallback;
}
if let Some(power) = env_value("RUNMAT_ACCEL_WGPU_POWER", &[]) {
if let Some(pref) = parse_power_preference(&power) {
config.accelerate.wgpu_power_preference = pref;
}
}
if let Some(auto_enabled) = env_bool("RUNMAT_ACCEL_AUTO_OFFLOAD", &[]) {
config.accelerate.auto_offload.enabled = auto_enabled;
}
if let Some(auto_calibrate) = env_bool("RUNMAT_ACCEL_CALIBRATE", &[]) {
config.accelerate.auto_offload.calibrate = auto_calibrate;
}
if let Some(profile_path) = env_value("RUNMAT_ACCEL_PROFILE", &[]) {
config.accelerate.auto_offload.profile_path = Some(PathBuf::from(profile_path));
}
if let Some(auto_log) = env_value("RUNMAT_ACCEL_AUTO_LOG", &[]) {
if let Some(level) = parse_auto_offload_log_level(&auto_log) {
config.accelerate.auto_offload.log_level = level;
}
}
if let Some(jit_enabled) = env_bool("RUNMAT_JIT_ENABLE", &[]) {
config.jit.enabled = jit_enabled;
}
if let Some(jit_disabled) = env_bool("RUNMAT_JIT_DISABLE", &[]) {
if jit_disabled {
config.jit.enabled = false;
}
}
if let Some(threshold) = env_value("RUNMAT_JIT_THRESHOLD", &[]) {
if let Ok(threshold) = threshold.parse() {
config.jit.threshold = threshold;
}
}
if let Some(opt_level) = env_value("RUNMAT_JIT_OPT_LEVEL", &[]) {
config.jit.optimization_level = match opt_level.to_lowercase().as_str() {
"none" => JitOptLevel::None,
"size" => JitOptLevel::Size,
"speed" => JitOptLevel::Speed,
"aggressive" => JitOptLevel::Aggressive,
_ => config.jit.optimization_level,
};
}
if let Some(preset) = env_value("RUNMAT_GC_PRESET", &[]) {
config.gc.preset = match preset.to_lowercase().as_str() {
"low-latency" => Some(GcPreset::LowLatency),
"high-throughput" => Some(GcPreset::HighThroughput),
"low-memory" => Some(GcPreset::LowMemory),
"debug" => Some(GcPreset::Debug),
_ => config.gc.preset,
};
}
if let Some(young_size) = env_value("RUNMAT_GC_YOUNG_SIZE", &[]) {
if let Ok(young_size) = young_size.parse() {
config.gc.young_size_mb = Some(young_size);
}
}
if let Some(threads) = env_value("RUNMAT_GC_THREADS", &[]) {
if let Ok(threads) = threads.parse() {
config.gc.threads = Some(threads);
}
}
if let Some(stats) = env_bool("RUNMAT_GC_STATS", &[]) {
config.gc.collect_stats = stats;
}
if let Some(plot_mode) = env_value("RUNMAT_PLOT_MODE", &[]) {
config.plotting.mode = match plot_mode.to_lowercase().as_str() {
"auto" => PlotMode::Auto,
"gui" => PlotMode::Gui,
"headless" => PlotMode::Headless,
"jupyter" => PlotMode::Jupyter,
_ => config.plotting.mode,
};
}
if let Some(headless) = env_bool("RUNMAT_PLOT_HEADLESS", &[]) {
config.plotting.force_headless = headless;
}
if let Some(backend) = env_value("RUNMAT_PLOT_BACKEND", &[]) {
config.plotting.backend = match backend.to_lowercase().as_str() {
"auto" => PlotBackend::Auto,
"wgpu" => PlotBackend::Wgpu,
"static" => PlotBackend::Static,
"web" => PlotBackend::Web,
_ => config.plotting.backend,
};
}
if let Some(debug) = env_bool("RUNMAT_DEBUG", &[]) {
config.logging.debug = debug;
}
if let Some(log_level) = env_value("RUNMAT_LOG_LEVEL", &[]) {
config.logging.level = match log_level.to_lowercase().as_str() {
"error" => LogLevel::Error,
"warn" => LogLevel::Warn,
"info" => LogLevel::Info,
"debug" => LogLevel::Debug,
"trace" => LogLevel::Trace,
_ => config.logging.level,
};
}
if let Some(ip) = env_value("RUNMAT_KERNEL_IP", &[]) {
config.kernel.ip = ip;
}
if let Some(key) = env_value("RUNMAT_KERNEL_KEY", &[]) {
config.kernel.key = Some(key);
}
Ok(())
}
pub fn save_to_file(config: &RunMatConfig, path: &Path) -> Result<()> {
let content = match path.extension().and_then(|ext| ext.to_str()) {
Some("yaml") | Some("yml") => {
serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
}
Some("json") => serde_json::to_string_pretty(config)
.context("Failed to serialize config to JSON")?,
Some("toml") => {
toml::to_string_pretty(config).context("Failed to serialize config to TOML")?
}
_ => {
serde_yaml::to_string(config).context("Failed to serialize config to YAML")?
}
};
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {}", path.display()))?;
info!("Configuration saved to: {}", path.display());
Ok(())
}
pub fn generate_sample_config() -> String {
let config = RunMatConfig::default();
serde_yaml::to_string(&config).unwrap_or_else(|_| "# Failed to generate config".to_string())
}
}
fn parse_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "enable" | "enabled" => Some(true),
"0" | "false" | "no" | "off" | "disable" | "disabled" => Some(false),
"" => Some(false),
_ => None,
}
}
fn parse_auto_offload_log_level(value: &str) -> Option<AutoOffloadLogLevel> {
match value.trim().to_ascii_lowercase().as_str() {
"off" => Some(AutoOffloadLogLevel::Off),
"info" => Some(AutoOffloadLogLevel::Info),
"trace" => Some(AutoOffloadLogLevel::Trace),
_ => None,
}
}
fn parse_provider_preference(value: &str) -> Option<AccelerateProviderPreference> {
match value.trim().to_ascii_lowercase().as_str() {
"auto" => Some(AccelerateProviderPreference::Auto),
"wgpu" => Some(AccelerateProviderPreference::Wgpu),
"inprocess" | "cpu" | "host" => Some(AccelerateProviderPreference::InProcess),
_ => None,
}
}
fn parse_power_preference(value: &str) -> Option<AccelPowerPreference> {
match value.trim().to_ascii_lowercase().as_str() {
"auto" => Some(AccelPowerPreference::Auto),
"high" | "highperformance" | "performance" => Some(AccelPowerPreference::HighPerformance),
"low" | "lowpower" | "battery" => Some(AccelPowerPreference::LowPower),
_ => None,
}
}
fn env_value(primary: &str, aliases: &[&str]) -> Option<String> {
env::var(primary)
.ok()
.or_else(|| aliases.iter().find_map(|alias| env::var(alias).ok()))
}
fn env_bool(primary: &str, aliases: &[&str]) -> Option<bool> {
env_value(primary, aliases).and_then(|value| parse_bool(&value))
}
#[cfg(feature = "accelerate")]
mod accelerate_bridge {
use super::{
AccelPowerPreference, AccelerateConfig, AccelerateProviderPreference, AutoOffloadConfig,
AutoOffloadLogLevel,
};
use runmat_accelerate::{
AccelPowerPreference as RuntimePowerPreference, AccelerateInitOptions,
AccelerateProviderPreference as RuntimeProviderPreference,
AutoOffloadLogLevel as RuntimeAutoLogLevel, AutoOffloadOptions,
};
impl From<AccelPowerPreference> for RuntimePowerPreference {
fn from(pref: AccelPowerPreference) -> Self {
match pref {
AccelPowerPreference::Auto => RuntimePowerPreference::Auto,
AccelPowerPreference::HighPerformance => RuntimePowerPreference::HighPerformance,
AccelPowerPreference::LowPower => RuntimePowerPreference::LowPower,
}
}
}
impl From<AccelerateProviderPreference> for RuntimeProviderPreference {
fn from(pref: AccelerateProviderPreference) -> Self {
match pref {
AccelerateProviderPreference::Auto => RuntimeProviderPreference::Auto,
AccelerateProviderPreference::Wgpu => RuntimeProviderPreference::Wgpu,
AccelerateProviderPreference::InProcess => RuntimeProviderPreference::InProcess,
}
}
}
impl From<AutoOffloadLogLevel> for RuntimeAutoLogLevel {
fn from(level: AutoOffloadLogLevel) -> Self {
match level {
AutoOffloadLogLevel::Off => RuntimeAutoLogLevel::Off,
AutoOffloadLogLevel::Info => RuntimeAutoLogLevel::Info,
AutoOffloadLogLevel::Trace => RuntimeAutoLogLevel::Trace,
}
}
}
impl From<&AutoOffloadConfig> for AutoOffloadOptions {
fn from(cfg: &AutoOffloadConfig) -> Self {
AutoOffloadOptions {
enabled: cfg.enabled,
calibrate: cfg.calibrate,
profile_path: cfg.profile_path.clone(),
log_level: cfg.log_level.into(),
}
}
}
impl From<&AccelerateConfig> for AccelerateInitOptions {
fn from(cfg: &AccelerateConfig) -> Self {
AccelerateInitOptions {
enabled: cfg.enabled,
provider: cfg.provider.into(),
allow_inprocess_fallback: cfg.allow_inprocess_fallback,
wgpu_power_preference: cfg.wgpu_power_preference.into(),
wgpu_force_fallback_adapter: cfg.wgpu_force_fallback_adapter,
auto_offload: AutoOffloadOptions::from(&cfg.auto_offload),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use once_cell::sync::Lazy;
use std::sync::Mutex;
use tempfile::TempDir;
static ENV_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
#[test]
fn test_config_defaults() {
let config = RunMatConfig::default();
assert_eq!(config.runtime.timeout, 300);
assert!(config.jit.enabled);
assert_eq!(config.jit.threshold, 10);
assert_eq!(config.plotting.mode, PlotMode::Auto);
assert!(matches!(config.language.compat, LanguageCompatMode::RunMat));
assert_eq!(config.runtime.error_namespace, "");
}
#[test]
fn test_yaml_serialization() {
let config = RunMatConfig::default();
let yaml = serde_yaml::to_string(&config).unwrap();
let parsed: RunMatConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
assert_eq!(parsed.jit.enabled, config.jit.enabled);
assert_eq!(parsed.accelerate.provider, config.accelerate.provider);
}
#[test]
fn test_json_serialization() {
let config = RunMatConfig::default();
let json = serde_json::to_string_pretty(&config).unwrap();
let parsed: RunMatConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.runtime.timeout, config.runtime.timeout);
assert_eq!(parsed.plotting.mode, config.plotting.mode);
assert_eq!(parsed.accelerate.enabled, config.accelerate.enabled);
}
#[test]
fn test_parse_auto_offload_log_level_cases() {
assert_eq!(
parse_auto_offload_log_level("off"),
Some(AutoOffloadLogLevel::Off)
);
assert_eq!(
parse_auto_offload_log_level("INFO"),
Some(AutoOffloadLogLevel::Info)
);
assert_eq!(
parse_auto_offload_log_level("trace"),
Some(AutoOffloadLogLevel::Trace)
);
assert_eq!(parse_auto_offload_log_level("unknown"), None);
}
#[test]
fn test_file_loading() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join(".runmat.yaml");
let mut config = RunMatConfig::default();
config.runtime.timeout = 600;
config.jit.threshold = 20;
ConfigLoader::save_to_file(&config, &config_path).unwrap();
let loaded = ConfigLoader::load_from_file(&config_path).unwrap();
assert_eq!(loaded.runtime.timeout, 600);
assert_eq!(loaded.jit.threshold, 20);
}
#[test]
fn test_bool_parsing() {
assert_eq!(parse_bool("true"), Some(true));
assert_eq!(parse_bool("1"), Some(true));
assert_eq!(parse_bool("yes"), Some(true));
assert_eq!(parse_bool("false"), Some(false));
assert_eq!(parse_bool("0"), Some(false));
assert_eq!(parse_bool("invalid"), None);
}
#[test]
fn telemetry_env_overrides_respect_empty_values() {
let _lock = ENV_GUARD.lock().unwrap();
std::env::set_var("RUNMAT_TELEMETRY_ENDPOINT", "https://custom.example/ingest");
std::env::set_var("RUNMAT_TELEMETRY_UDP_ENDPOINT", "off");
let mut config = RunMatConfig::default();
ConfigLoader::apply_environment_variables(&mut config).unwrap();
assert_eq!(
config.telemetry.http_endpoint.as_deref(),
Some("https://custom.example/ingest")
);
assert!(config.telemetry.udp_endpoint.is_none());
std::env::remove_var("RUNMAT_TELEMETRY_ENDPOINT");
std::env::remove_var("RUNMAT_TELEMETRY_UDP_ENDPOINT");
}
}