Skip to main content

presolve_compiler/
purity_effect.rs

1//! Conservative purity and effect classification over function summaries.
2
3use serde::Serialize;
4
5use crate::{
6    ControlFlowAccessKindV1, ControlFlowGraphV1, FunctionCallCoverageV1, FunctionSummaryGraphV1,
7};
8
9pub const PURITY_EFFECT_SCHEMA_VERSION: u32 = 1;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum FunctionPurityV1 {
14    Pure,
15    Impure,
16    Unknown,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum FunctionEffectKindV1 {
22    StorageWrite,
23    ContextSlotWrite,
24    ObservableInstruction,
25    ResourceRead,
26    UnknownCall,
27    UnavailableCallCoverage,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
31pub struct FunctionEffectFactV1 {
32    pub kind: FunctionEffectKindV1,
33    pub id: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct FunctionPurityEffectV1 {
38    pub function: String,
39    pub purity: FunctionPurityV1,
40    pub effects: Vec<FunctionEffectFactV1>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44pub struct PurityEffectGraphV1 {
45    pub schema_version: u32,
46    pub functions: Vec<FunctionPurityEffectV1>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum PurityEffectErrorV1 {
51    MissingSummary(String),
52}
53
54impl std::fmt::Display for PurityEffectErrorV1 {
55    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::MissingSummary(function) => {
58                write!(formatter, "missing function summary for {function}")
59            }
60        }
61    }
62}
63
64impl std::error::Error for PurityEffectErrorV1 {}
65
66/// Classifies only effects directly encoded by canonical CFG and summary facts.
67pub fn build_purity_effect_graph_v1(
68    control_flow: &ControlFlowGraphV1,
69    summaries: &FunctionSummaryGraphV1,
70) -> Result<PurityEffectGraphV1, PurityEffectErrorV1> {
71    let summaries = summaries
72        .summaries
73        .iter()
74        .map(|summary| (summary.id.as_str(), summary))
75        .collect::<std::collections::BTreeMap<_, _>>();
76    let mut functions = Vec::new();
77    for function in &control_flow.functions {
78        let Some(summary) = summaries.get(function.id.as_str()) else {
79            return Err(PurityEffectErrorV1::MissingSummary(function.id.clone()));
80        };
81        let mut effects = std::collections::BTreeSet::new();
82        for access in &summary.direct_writes {
83            let kind = match access.kind {
84                ControlFlowAccessKindV1::Storage => Some(FunctionEffectKindV1::StorageWrite),
85                ControlFlowAccessKindV1::ContextSlot => {
86                    Some(FunctionEffectKindV1::ContextSlotWrite)
87                }
88                ControlFlowAccessKindV1::Value
89                | ControlFlowAccessKindV1::Computed
90                | ControlFlowAccessKindV1::Resource => None,
91            };
92            if let Some(kind) = kind {
93                effects.insert(FunctionEffectFactV1 {
94                    kind,
95                    id: access.id.clone(),
96                });
97            }
98        }
99        for access in &summary.direct_reads {
100            if access.kind == ControlFlowAccessKindV1::Resource {
101                effects.insert(FunctionEffectFactV1 {
102                    kind: FunctionEffectKindV1::ResourceRead,
103                    id: access.id.clone(),
104                });
105            }
106        }
107        for block in &function.blocks {
108            for instruction in &block.observable_instructions {
109                effects.insert(FunctionEffectFactV1 {
110                    kind: FunctionEffectKindV1::ObservableInstruction,
111                    id: instruction.clone(),
112                });
113            }
114        }
115        if summary.has_direct_unknown_call {
116            effects.insert(FunctionEffectFactV1 {
117                kind: FunctionEffectKindV1::UnknownCall,
118                id: function.id.clone(),
119            });
120        }
121        if summary.call_coverage == FunctionCallCoverageV1::Unavailable {
122            effects.insert(FunctionEffectFactV1 {
123                kind: FunctionEffectKindV1::UnavailableCallCoverage,
124                id: function.id.clone(),
125            });
126        }
127        let effects = effects.into_iter().collect::<Vec<_>>();
128        let impure = effects.iter().any(|effect| {
129            matches!(
130                effect.kind,
131                FunctionEffectKindV1::StorageWrite
132                    | FunctionEffectKindV1::ContextSlotWrite
133                    | FunctionEffectKindV1::ObservableInstruction
134            )
135        });
136        let unknown = effects.iter().any(|effect| {
137            matches!(
138                effect.kind,
139                FunctionEffectKindV1::ResourceRead
140                    | FunctionEffectKindV1::UnknownCall
141                    | FunctionEffectKindV1::UnavailableCallCoverage
142            )
143        });
144        functions.push(FunctionPurityEffectV1 {
145            function: function.id.clone(),
146            purity: if impure {
147                FunctionPurityV1::Impure
148            } else if unknown {
149                FunctionPurityV1::Unknown
150            } else {
151                FunctionPurityV1::Pure
152            },
153            effects,
154        });
155    }
156    functions.sort_by(|left, right| left.function.cmp(&right.function));
157    Ok(PurityEffectGraphV1 {
158        schema_version: PURITY_EFFECT_SCHEMA_VERSION,
159        functions,
160    })
161}
162
163#[cfg(test)]
164mod tests {
165    use crate::{
166        ControlFlowAccessKindV1, ControlFlowAccessV1, ControlFlowBlockV1,
167        ControlFlowCoverageStatusV1, ControlFlowCoverageV1, ControlFlowFunctionV1,
168        ControlFlowGraphV1, ControlFlowProvenanceV1, FunctionCallCoverageV1,
169        FunctionSummaryGraphV1, FunctionSummaryV1,
170    };
171
172    use super::{build_purity_effect_graph_v1, FunctionPurityV1};
173
174    fn provenance() -> ControlFlowProvenanceV1 {
175        ControlFlowProvenanceV1 {
176            path: "src/App.tsx".into(),
177            start: 0,
178            end: 1,
179            line: 1,
180            column: 1,
181        }
182    }
183
184    fn graph() -> ControlFlowGraphV1 {
185        ControlFlowGraphV1 {
186            schema_version: 1,
187            functions: vec![ControlFlowFunctionV1 {
188                module_path: "src/App.tsx".into(),
189                id: "app".into(),
190                name: "app".into(),
191                provenance: provenance(),
192                entry_block: "app/entry".into(),
193                blocks: vec![ControlFlowBlockV1 {
194                    id: "app/entry".into(),
195                    provenance: provenance(),
196                    reads: Vec::new(),
197                    writes: Vec::new(),
198                    observable_instructions: vec!["app/effect".into()],
199                }],
200                branch_edges: Vec::new(),
201                loops: Vec::new(),
202                coverage: ControlFlowCoverageV1 {
203                    branch_topology: ControlFlowCoverageStatusV1::Available,
204                    definite_dataflow: ControlFlowCoverageStatusV1::Available,
205                    natural_loops: ControlFlowCoverageStatusV1::Available,
206                    exception_paths: ControlFlowCoverageStatusV1::Unavailable,
207                    async_suspension: ControlFlowCoverageStatusV1::Unavailable,
208                    unknown_calls: ControlFlowCoverageStatusV1::Unavailable,
209                    capture_escape: ControlFlowCoverageStatusV1::Unavailable,
210                    resource_cancellation: ControlFlowCoverageStatusV1::Unavailable,
211                },
212            }],
213        }
214    }
215
216    #[test]
217    fn classifies_observable_writes_as_impure_and_incomplete_calls_as_unknown() {
218        let summary = FunctionSummaryGraphV1 {
219            schema_version: 1,
220            summaries: vec![FunctionSummaryV1 {
221                id: "app".into(),
222                module_path: "src/App.tsx".into(),
223                direct_reads: vec![ControlFlowAccessV1 {
224                    kind: ControlFlowAccessKindV1::Resource,
225                    id: "resource:profile".into(),
226                }],
227                direct_writes: Vec::new(),
228                direct_callees: Vec::new(),
229                has_direct_unknown_call: false,
230                call_coverage: FunctionCallCoverageV1::Unavailable,
231                transitive_reads: None,
232                transitive_writes: None,
233                transitive_callees: None,
234                has_transitive_unknown_call: None,
235            }],
236        };
237        let classified =
238            build_purity_effect_graph_v1(&graph(), &summary).expect("matching summary");
239        assert_eq!(classified.functions[0].purity, FunctionPurityV1::Impure);
240        assert_eq!(classified.functions[0].effects.len(), 3);
241    }
242}