1use 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
66pub 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}