Skip to main content

saf_core/
config.rs

1//! SAF configuration contract (SRS Section 6).
2//!
3//! Config is JSON-serializable and hashed into cache keys. All fields
4//! have documented defaults matching the SRS specification.
5
6use serde::{Deserialize, Serialize};
7
8/// Which frontend to use for ingestion.
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum Frontend {
12    /// LLVM bitcode / IR frontend.
13    #[default]
14    Llvm,
15    /// AIR-JSON frontend.
16    #[serde(rename = "air-json")]
17    AirJson,
18    /// Custom frontend — the string is the frontend identifier.
19    /// New frontends can use any string without modifying `saf-core`.
20    #[serde(untagged)]
21    Other(String),
22}
23
24/// Analysis mode controlling the speed/precision trade-off.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum AnalysisMode {
28    /// Fast mode: fewer iterations, less precision.
29    Fast,
30    /// Precise mode: full fixed-point iteration.
31    #[default]
32    Precise,
33}
34
35/// Field sensitivity level for the top-level config.
36///
37/// This is a simpler enum than `saf_analysis::pta::config::FieldSensitivity`,
38/// which additionally carries `max_depth`. This enum represents the
39/// serializable config value only.
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum ConfigFieldSensitivity {
43    /// No field sensitivity -- treat structs as monolithic objects.
44    None,
45    /// Track individual struct fields.
46    #[default]
47    StructFields,
48}
49
50/// External call side-effect model.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ExternalSideEffects {
54    /// No side effects assumed for external calls.
55    None,
56    /// External calls may write to unknown locations.
57    UnknownWrite,
58    /// External calls may read from and write to unknown locations.
59    #[default]
60    UnknownReadwrite,
61}
62
63/// Top-level configuration for a SAF analysis run.
64#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
65pub struct Config {
66    /// Which frontend to use: `Llvm` or `AirJson`.
67    #[serde(default)]
68    pub frontend: Frontend,
69
70    /// Analysis sub-config.
71    #[serde(default)]
72    pub analysis: AnalysisConfig,
73
74    /// External call side-effect model.
75    #[serde(default)]
76    pub external_side_effects: ExternalSideEffects,
77
78    /// Path normalization sub-config.
79    #[serde(default)]
80    pub paths: PathsConfig,
81
82    /// Rust-specific sub-config.
83    #[serde(default)]
84    pub rust: RustConfig,
85
86    /// Incremental analysis sub-config.
87    #[serde(default)]
88    pub incremental: IncrementalConfig,
89}
90
91/// Analysis sub-configuration.
92#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
93pub struct AnalysisConfig {
94    /// Analysis mode: `Fast` or `Precise`.
95    #[serde(default)]
96    pub mode: AnalysisMode,
97}
98
99/// Path normalization sub-configuration.
100#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
101pub struct PathsConfig {
102    /// Prefixes to strip from source paths in outputs.
103    #[serde(default)]
104    pub strip_prefixes: Vec<String>,
105
106    /// Whether to normalize path separators to `/`.
107    #[serde(default)]
108    pub normalize_separators: bool,
109}
110
111/// Rust-specific sub-configuration.
112#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
113pub struct RustConfig {
114    /// Whether to demangle Rust symbols in outputs.
115    #[serde(default)]
116    pub demangle: bool,
117}
118
119/// Which PTA solver backend to use.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub enum PtaSolver {
122    /// Worklist-based imperative solver (default — matches SVF performance).
123    #[default]
124    Worklist,
125    /// Datalog fixpoint solver (Ascent).
126    Datalog,
127}
128
129/// Configuration for incremental analysis (Plan 165).
130///
131/// When `enabled` is false (default), the system behaves identically to
132/// non-incremental analysis. All fields are ignored unless `enabled` is true.
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct IncrementalConfig {
135    /// Master switch for incremental analysis.
136    pub enabled: bool,
137
138    /// Cache directory for per-module AIR bundles and analysis products.
139    pub cache_dir: std::path::PathBuf,
140
141    /// How to split a single pre-linked input into logical modules.
142    pub split_strategy: crate::program::SplitStrategy,
143}
144
145impl Default for IncrementalConfig {
146    fn default() -> Self {
147        Self {
148            enabled: false,
149            cache_dir: std::path::PathBuf::from(".saf-cache"),
150            split_strategy: crate::program::SplitStrategy::Auto,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn default_config_roundtrips() {
161        let config = Config::default();
162        let json = serde_json::to_string(&config).unwrap();
163        let back: Config = serde_json::from_str(&json).unwrap();
164        assert_eq!(config, back);
165    }
166
167    #[test]
168    fn default_values_match_srs() {
169        let config = Config::default();
170        assert_eq!(config.frontend, Frontend::Llvm);
171        assert_eq!(config.analysis.mode, AnalysisMode::Precise);
172        assert_eq!(
173            config.external_side_effects,
174            ExternalSideEffects::UnknownReadwrite
175        );
176    }
177
178    #[test]
179    fn serde_json_backward_compatibility() {
180        // Verify that the JSON serialized form matches the old string values.
181        let config = Config::default();
182        let json = serde_json::to_value(&config).unwrap();
183        assert_eq!(json["frontend"], "llvm");
184        assert_eq!(json["analysis"]["mode"], "precise");
185        assert_eq!(json["external_side_effects"], "unknown_readwrite");
186
187        // Verify air-json variant preserves hyphen.
188        let air_json = serde_json::to_value(Frontend::AirJson).unwrap();
189        assert_eq!(air_json, "air-json");
190
191        // Verify deserialization from old string values still works.
192        let old_json = r#"{
193            "frontend": "air-json",
194            "analysis": { "mode": "fast" },
195            "external_side_effects": "unknown_write"
196        }"#;
197        let parsed: Config = serde_json::from_str(old_json).unwrap();
198        assert_eq!(parsed.frontend, Frontend::AirJson);
199        assert_eq!(parsed.analysis.mode, AnalysisMode::Fast);
200        assert_eq!(
201            parsed.external_side_effects,
202            ExternalSideEffects::UnknownWrite
203        );
204    }
205
206    #[test]
207    fn custom_frontend_roundtrips() {
208        let frontend = Frontend::Other("my-custom-frontend".to_string());
209        let json = serde_json::to_string(&frontend).unwrap();
210        // Untagged variant serializes as just the string
211        assert_eq!(json, "\"my-custom-frontend\"");
212
213        let parsed: Frontend = serde_json::from_str(&json).unwrap();
214        assert_eq!(parsed, Frontend::Other("my-custom-frontend".to_string()));
215    }
216
217    #[test]
218    fn known_frontends_still_deserialize_correctly() {
219        // "llvm" should still deserialize to Frontend::Llvm, not Frontend::Other("llvm")
220        let parsed: Frontend = serde_json::from_str("\"llvm\"").unwrap();
221        assert_eq!(parsed, Frontend::Llvm);
222
223        let parsed: Frontend = serde_json::from_str("\"air-json\"").unwrap();
224        assert_eq!(parsed, Frontend::AirJson);
225    }
226}