Skip to main content

logicaffeine_compile/extraction/
collector.rs

1//! Dependency collection for program extraction.
2//!
3//! Collects all transitive dependencies needed to extract a definition.
4
5use crate::kernel::{Context, Term};
6use std::collections::{HashMap, HashSet};
7
8/// Whether `name` extracts to self-contained Rust: a constructor of an emittable
9/// inductive, an emittable inductive (has constructors) or mapped primitive, or a
10/// definition whose body only references extractable things. Axioms / declarations
11/// / tactics (no extractable body, e.g. `syn_diag`, `try_auto`) are NOT extractable
12/// — extracting them would emit Rust referencing undefined symbols.
13pub fn is_extractable(ctx: &Context, name: &str) -> bool {
14    extractable(ctx, name, &mut HashMap::new())
15}
16
17fn extractable(ctx: &Context, name: &str, memo: &mut HashMap<String, bool>) -> bool {
18    // `_` is a placeholder (e.g. a match-motive binder) that surfaces as a Global
19    // but is never emitted — treat it as benign.
20    if name == "_" {
21        return true;
22    }
23    if let Some(&b) = memo.get(name) {
24        return b;
25    }
26    // Optimistic for recursive/cyclic references (a fixpoint that can't lower).
27    memo.insert(name.to_string(), true);
28    let result = if ctx.is_constructor(name) {
29        ctx.constructor_inductive(name)
30            .map(|ind| !ctx.get_constructors(ind).is_empty())
31            .unwrap_or(false)
32    } else if ctx.is_inductive(name) {
33        (!ctx.get_constructors(name).is_empty() && !is_logical_type(name)) || is_mapped_primitive(name)
34    } else if let Some(body) = ctx.get_definition_body(name) {
35        let mut refs = Vec::new();
36        collect_globals(body, &mut refs);
37        refs.iter().all(|g| extractable(ctx, g, memo))
38            && matches_inferable(ctx, body)
39            // Arithmetic builtins must be fully (binary) applied — a bare/partial
40            // `add` has no Rust function to call.
41            && super::codegen::arith_uses_well_formed(body)
42    } else {
43        // A declaration / axiom / primitive op with no body. The mapped primitives
44        // and the arithmetic builtins (`add`/`sub`/… → Rust operators) are the only
45        // bodyless globals that extract to real Rust; everything else (tactics,
46        // reflection axioms) would emit references to undefined symbols.
47        is_mapped_primitive(name) || super::codegen::arith_operator(name).is_some()
48    };
49    memo.insert(name.to_string(), result);
50    result
51}
52
53/// Whether every `match` in a term has an inductive the extractor can recognize
54/// (from the motive or the discriminant). Literate-generated matches sometimes
55/// lack a readable motive, which would extract to an empty (non-exhaustive) match;
56/// such definitions are not cleanly extractable.
57fn matches_inferable(ctx: &Context, term: &Term) -> bool {
58    match term {
59        Term::Match { discriminant, motive, cases } => {
60            if motive_inductive(ctx, motive)
61                .or_else(|| disc_inductive(ctx, discriminant))
62                .is_none()
63            {
64                return false;
65            }
66            matches_inferable(ctx, discriminant)
67                && matches_inferable(ctx, motive)
68                && cases.iter().all(|c| matches_inferable(ctx, c))
69        }
70        Term::Lambda { param_type, body, .. } => {
71            matches_inferable(ctx, param_type) && matches_inferable(ctx, body)
72        }
73        Term::Pi { param_type, body_type, .. } => {
74            matches_inferable(ctx, param_type) && matches_inferable(ctx, body_type)
75        }
76        Term::App(f, a) => matches_inferable(ctx, f) && matches_inferable(ctx, a),
77        Term::Fix { body, .. } => matches_inferable(ctx, body),
78        _ => true,
79    }
80}
81
82fn motive_inductive(ctx: &Context, motive: &Term) -> Option<String> {
83    if let Term::Lambda { param_type, .. } = motive {
84        if let Term::Global(name) = param_type.as_ref() {
85            if ctx.is_inductive(name) {
86                return Some(name.clone());
87            }
88        }
89    }
90    None
91}
92
93fn disc_inductive(ctx: &Context, term: &Term) -> Option<String> {
94    match term {
95        Term::Global(name) => {
96            if ctx.is_constructor(name) {
97                ctx.constructor_inductive(name).map(|s| s.to_string())
98            } else if ctx.is_inductive(name) {
99                Some(name.clone())
100            } else {
101                None
102            }
103        }
104        Term::App(f, _) => disc_inductive(ctx, f),
105        _ => None,
106    }
107}
108
109fn is_mapped_primitive(name: &str) -> bool {
110    matches!(
111        name,
112        "Int" | "Float" | "Text" | "Bool" | "Duration" | "Date" | "Moment"
113    )
114}
115
116/// StandardLibrary types that encode logic/proof/reflection rather than user data
117/// (`Syntax` ASTs, `Eq`/`And`/`Ex` propositions, `Derivation` proofs, …). Values of
118/// these are not the "compile my data to Rust" use case and don't extract cleanly,
119/// so definitions over them are left as a note instead of emitting broken Rust.
120pub fn is_logical_type(name: &str) -> bool {
121    matches!(
122        name,
123        "Syntax" | "Derivation" | "Eq" | "And" | "Or" | "Iff" | "Ex" | "Not"
124            | "True" | "False" | "Entity" | "Univ" | "Prop"
125    )
126}
127
128/// Collect all dependencies of a definition.
129///
130/// Starting from an entry point, recursively collects all global names
131/// that are referenced, including inductives, constructors, and definitions.
132pub fn collect_dependencies(ctx: &Context, entry: &str) -> HashSet<String> {
133    let mut visited = HashSet::new();
134    let mut to_visit = vec![entry.to_string()];
135
136    while let Some(name) = to_visit.pop() {
137        if visited.contains(&name) {
138            continue;
139        }
140        visited.insert(name.clone());
141
142        // Get the term to analyze based on what kind of global it is
143        if let Some(body) = ctx.get_definition_body(&name) {
144            collect_globals(body, &mut to_visit);
145            // Also collect from the type
146            if let Some(ty) = ctx.get_definition_type(&name) {
147                collect_globals(ty, &mut to_visit);
148            }
149        }
150
151        if ctx.is_inductive(&name) {
152            // Add constructors and their types
153            for (ctor_name, ctor_ty) in ctx.get_constructors(&name) {
154                collect_globals(ctor_ty, &mut to_visit);
155                to_visit.push(ctor_name.to_string());
156            }
157        }
158
159        if ctx.is_constructor(&name) {
160            // Add the inductive type
161            if let Some(ind) = ctx.constructor_inductive(&name) {
162                to_visit.push(ind.to_string());
163            }
164        }
165    }
166
167    visited
168}
169
170/// Recursively collect all Global references from a term.
171pub(crate) fn collect_globals(term: &Term, deps: &mut Vec<String>) {
172    match term {
173        Term::Global(name) => deps.push(name.clone()),
174        Term::Const { name, .. } => deps.push(name.clone()),
175        Term::App(f, a) => {
176            collect_globals(f, deps);
177            collect_globals(a, deps);
178        }
179        Term::Lambda {
180            param_type, body, ..
181        } => {
182            collect_globals(param_type, deps);
183            collect_globals(body, deps);
184        }
185        Term::Pi {
186            param_type,
187            body_type,
188            ..
189        } => {
190            collect_globals(param_type, deps);
191            collect_globals(body_type, deps);
192        }
193        Term::Fix { body, .. } => collect_globals(body, deps),
194        Term::Match {
195            discriminant,
196            motive,
197            cases,
198        } => {
199            collect_globals(discriminant, deps);
200            collect_globals(motive, deps);
201            for case in cases {
202                collect_globals(case, deps);
203            }
204        }
205        Term::Let {
206            ty, value, body, ..
207        } => {
208            collect_globals(ty, deps);
209            collect_globals(value, deps);
210            collect_globals(body, deps);
211        }
212        Term::MutualFix { defs, .. } => {
213            for (_, body) in defs {
214                collect_globals(body, deps);
215            }
216        }
217        // Base cases: no dependencies
218        Term::Sort(_) | Term::Var(_) | Term::Lit(_) | Term::Hole => {}
219    }
220}