Skip to main content

presolve_compiler/
computed_value.rs

1use std::collections::BTreeMap;
2
3use crate::{ComponentNode, ExecutionBoundary, SemanticId, SemanticOwner, SourceProvenance};
4
5/// Compiler-owned cache behavior for a computed value.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ComputedCachePolicy {
8    Memoized,
9}
10
11/// Compiler classification of a computed getter's purity.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ComputedPurity {
14    Unclassified,
15    Pure,
16    Impure,
17}
18
19/// Stable compiler diagnostics emitted for computed declarations and values.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub enum ComputedDiagnosticCode {
22    PurityViolation,
23    DependencyCycle,
24    InvalidDeclaration,
25    UnsupportedBody,
26    UnresolvedRead,
27    TypeMismatch,
28    SerializationViolation,
29}
30
31impl ComputedDiagnosticCode {
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::PurityViolation => "PSC1034",
36            Self::DependencyCycle => "PSC1035",
37            Self::InvalidDeclaration => "PSC1036",
38            Self::UnsupportedBody => "PSC1037",
39            Self::UnresolvedRead => "PSC1038",
40            Self::TypeMismatch => "PSC1039",
41            Self::SerializationViolation => "PSC1040",
42        }
43    }
44}
45
46/// Unsupported behavior that makes a computed getter impure.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
48pub enum ComputedPurityViolationKind {
49    StateMutation,
50    Action,
51    Effect,
52    Async,
53    Resource,
54    ArbitraryMethodCall,
55    NondeterministicOperation,
56}
57
58impl ComputedPurityViolationKind {
59    #[must_use]
60    pub const fn description(self) -> &'static str {
61        match self {
62            Self::StateMutation => "state mutation",
63            Self::Action => "action invocation",
64            Self::Effect => "effectful operation",
65            Self::Async => "async execution",
66            Self::Resource => "resource access",
67            Self::ArbitraryMethodCall => "arbitrary method call",
68            Self::NondeterministicOperation => "nondeterministic operation",
69        }
70    }
71}
72
73/// One compiler-owned purity violation with its authored location.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct ComputedPurityViolation {
76    pub kind: ComputedPurityViolationKind,
77    pub provenance: SourceProvenance,
78}
79
80/// A first-class compiler semantic entity for one `@computed()` getter.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ComputedValue {
83    pub id: SemanticId,
84    pub owner: SemanticOwner,
85    pub method: SemanticId,
86    pub name: String,
87    pub cache_policy: ComputedCachePolicy,
88    pub purity: ComputedPurity,
89    pub purity_violations: Vec<ComputedPurityViolation>,
90    pub execution_boundary: ExecutionBoundary,
91    pub provenance: SourceProvenance,
92}
93
94/// Collect canonical computed entities in stable semantic-ID order.
95///
96/// # Panics
97///
98/// Panics when a computed method has no canonical source provenance.
99#[must_use]
100pub fn collect_computed_values(
101    components: &[ComponentNode],
102    provenance: &BTreeMap<SemanticId, SourceProvenance>,
103) -> BTreeMap<SemanticId, ComputedValue> {
104    components
105        .iter()
106        .flat_map(|component| {
107            component
108                .methods
109                .iter()
110                .filter(|method| method.is_computed())
111                .map(move |method| {
112                    let id = component.id.computed(&method.name);
113                    let provenance = provenance
114                        .get(&method.id)
115                        .expect("computed methods should have canonical provenance")
116                        .clone();
117                    (
118                        id.clone(),
119                        ComputedValue {
120                            id,
121                            owner: SemanticOwner::entity(component.id.clone()),
122                            method: method.id.clone(),
123                            name: method.name.clone(),
124                            cache_policy: ComputedCachePolicy::Memoized,
125                            purity: ComputedPurity::Unclassified,
126                            purity_violations: Vec::new(),
127                            execution_boundary: ExecutionBoundary::Client,
128                            provenance,
129                        },
130                    )
131                })
132        })
133        .collect()
134}
135
136#[cfg(test)]
137mod tests {
138    use crate::{
139        build_component_graph, collect_computed_values, ComputedCachePolicy, ComputedPurity,
140        ExecutionBoundary,
141    };
142
143    #[test]
144    fn collects_stable_computed_entities_from_decorated_getters() {
145        let parsed = presolve_parser::parse_file(
146            "src/Computed.tsx",
147            r#"
148@component("x-computed")
149class Computed extends Component {
150  @computed()
151  get remainingCount(): number { return 1; }
152}
153"#,
154        );
155        let graph = build_component_graph(&parsed);
156        let component = &graph.components[0];
157        let computed = collect_computed_values(&graph.components, &graph.provenance)
158            .into_values()
159            .next()
160            .expect("computed entity");
161
162        assert_eq!(
163            computed.id.as_str(),
164            "component:x-computed/computed:remainingCount"
165        );
166        assert_eq!(computed.method, component.methods[0].id);
167        assert_eq!(computed.owner, component.methods[0].owner);
168        assert_eq!(computed.cache_policy, ComputedCachePolicy::Memoized);
169        assert_eq!(computed.purity, ComputedPurity::Unclassified);
170        assert!(computed.purity_violations.is_empty());
171        assert_eq!(computed.execution_boundary, ExecutionBoundary::Client);
172    }
173}