Skip to main content

conjure_cp_essence_parser/parser/
util.rs

1use std::collections::BTreeMap;
2use std::sync::{Mutex, OnceLock};
3
4use tree_sitter::{Node, Parser, Tree};
5use tree_sitter_essence::LANGUAGE;
6
7use super::traversal::WalkDFS;
8use crate::diagnostics::diagnostics_api::SymbolKind;
9use crate::diagnostics::source_map::{HoverInfo, SourceMap, SpanId, span_with_hover};
10use crate::errors::RecoverableParseError;
11use conjure_cp_core::ast::{Name, SymbolTablePtr};
12
13/// Context for parsing, containing shared state passed through parser functions.
14pub struct ParseContext<'a> {
15    pub source_code: &'a str,
16    pub root: &'a Node<'a>,
17    pub symbols: Option<SymbolTablePtr>,
18    pub errors: &'a mut Vec<RecoverableParseError>,
19    pub source_map: &'a mut SourceMap,
20    pub decl_spans: &'a mut BTreeMap<Name, SpanId>,
21    /// What type the current expression/literal itself should be
22    pub typechecking_context: TypecheckingContext,
23    /// What type the elements within a collection should be
24    pub inner_typechecking_context: TypecheckingContext,
25}
26
27impl<'a> ParseContext<'a> {
28    pub fn new(
29        source_code: &'a str,
30        root: &'a Node<'a>,
31        symbols: Option<SymbolTablePtr>,
32        errors: &'a mut Vec<RecoverableParseError>,
33        source_map: &'a mut SourceMap,
34        decl_spans: &'a mut BTreeMap<Name, SpanId>,
35    ) -> Self {
36        Self {
37            source_code,
38            root,
39            symbols,
40            errors,
41            source_map,
42            decl_spans,
43            typechecking_context: TypecheckingContext::Unknown,
44            inner_typechecking_context: TypecheckingContext::Unknown,
45        }
46    }
47
48    pub fn record_error(&mut self, error: RecoverableParseError) {
49        self.errors.push(error);
50    }
51
52    /// Create a new ParseContext with different symbols but sharing source_code, root, errors, and source_map.
53    pub fn with_new_symbols(&mut self, symbols: Option<SymbolTablePtr>) -> ParseContext<'_> {
54        ParseContext {
55            source_code: self.source_code,
56            root: self.root,
57            symbols,
58            errors: self.errors,
59            source_map: self.source_map,
60            decl_spans: self.decl_spans,
61            typechecking_context: self.typechecking_context,
62            inner_typechecking_context: self.inner_typechecking_context,
63        }
64    }
65
66    pub fn save_decl_span(&mut self, name: Name, span_id: SpanId) {
67        self.decl_spans.insert(name, span_id);
68    }
69
70    pub fn lookup_decl_span(&self, name: &Name) -> Option<SpanId> {
71        self.decl_spans.get(name).copied()
72    }
73
74    pub fn lookup_decl_line(&self, name: &Name) -> Option<u32> {
75        let span_id = self.lookup_decl_span(name)?;
76        let span = self.source_map.spans.get(span_id as usize)?;
77        Some(span.start_point.line + 1)
78    }
79
80    /// Helper to add to span and documentation hover info into the source map
81    pub fn add_span_and_doc_hover(
82        &mut self,
83        node: &tree_sitter::Node,
84        doc_key: &str, // name of the documentation file in Bits
85        kind: SymbolKind,
86        ty: Option<String>,
87        decl_span: Option<u32>,
88    ) {
89        let hover = HoverInfo {
90            description: String::new(),
91            doc_key: Some(normalise_documentation_key(doc_key)),
92            kind: Some(kind),
93            ty,
94            decl_span,
95        };
96        span_with_hover(node, self.source_code, self.source_map, hover);
97    }
98}
99
100// Used to detect type mismatches during parsing.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum TypecheckingContext {
103    Boolean,
104    Arithmetic,
105    Set,
106    SetOrMatrix,
107    MSet,
108    Matrix,
109    Tuple,
110    Record,
111    Partition,
112    Permutation,
113    Sequence,
114    Function,
115    Relation,
116    /// Context is unknown or flexible
117    Unknown,
118}
119
120/// Parse the given source code into a syntax tree using tree-sitter.
121///
122/// If successful, returns a tuple containing the syntax tree and the raw source code.
123/// If the source code is not valid Essence, returns None.
124pub fn get_tree(src: &str) -> Option<(Tree, String)> {
125    let mut parser = Parser::new();
126    parser.set_language(&LANGUAGE.into()).unwrap();
127
128    parser.parse(src, None).and_then(|tree| {
129        let root = tree.root_node();
130        if root.is_error() {
131            return None;
132        }
133        Some((tree, src.to_string()))
134    })
135}
136
137/// Parse an expression fragment, allowing a dummy prefix for error recovery.
138///
139/// NOTE: The new source code may be different from the original source code.
140///       See implementation for details.
141pub fn get_expr_tree(src: &str) -> Option<(Tree, String)> {
142    let mut parser = Parser::new();
143    parser.set_language(&LANGUAGE.into()).unwrap();
144
145    parser.parse(src, None).and_then(|tree| {
146        let root = tree.root_node();
147        if root.is_error() {
148            return None;
149        }
150
151        let children: Vec<_> = named_children(&root).collect();
152        let first_child = children.first()?;
153
154        // HACK: Tree-sitter can only parse a complete program from top to bottom, not an individual bit of syntax.
155        // See: https://github.com/tree-sitter/tree-sitter/issues/711 and linked issues.
156        // However, we can use a dummy _FRAGMENT_EXPRESSION prefix (which we insert as necessary)
157        // to trick the parser into accepting an isolated expression.
158        // This way we can parse an isolated expression and it is only slightly cursed :)
159        if first_child.is_error() {
160            if src.starts_with("_FRAGMENT_EXPRESSION") {
161                None
162            } else {
163                get_expr_tree(&format!("_FRAGMENT_EXPRESSION {src}"))
164            }
165        } else {
166            Some((tree, src.to_string()))
167        }
168    })
169}
170
171/// Get the named children of a node
172pub fn named_children<'a>(node: &'a Node<'a>) -> impl Iterator<Item = Node<'a>> + 'a {
173    (0..node.named_child_count())
174        .filter_map(|i| u32::try_from(i).ok().and_then(|i| node.named_child(i)))
175}
176
177pub fn node_is_expression(node: &Node) -> bool {
178    matches!(
179        node.kind(),
180        "bool_expr" | "arithmetic_expr" | "comparison_expr" | "annotation_expr" | "atom"
181    )
182}
183
184/// Get all top-level nodes that match the given predicate
185pub fn query_toplevel<'a>(
186    node: &'a Node<'a>,
187    predicate: &'a dyn Fn(&Node<'a>) -> bool,
188) -> impl Iterator<Item = Node<'a>> + 'a {
189    WalkDFS::with_retract(node, predicate).filter(|n| n.is_named() && predicate(n))
190}
191
192/// Get all meta-variable names in a node
193pub fn get_metavars<'a>(node: &'a Node<'a>, src: &'a str) -> impl Iterator<Item = String> + 'a {
194    query_toplevel(node, &|n| n.kind() == "metavar").filter_map(|child| {
195        child
196            .named_child(0)
197            .map(|name| src[name.start_byte()..name.end_byte()].to_string())
198    })
199}
200
201/// Fetch Essence syntax documentation from Conjure's `docs/bits/` folder on GitHub.
202///
203/// `name` is the name of the documentation file (without .md suffix). If the file is not found or an error occurs, returns None.
204pub fn get_documentation(name: &str) -> Option<String> {
205    static DOCUMENTATION_CACHE: OnceLock<Mutex<BTreeMap<String, Option<String>>>> = OnceLock::new();
206
207    let base = normalise_documentation_key(name);
208    let cache = DOCUMENTATION_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
209
210    if let Some(cached) = cache.lock().ok()?.get(&base).cloned() {
211        return cached;
212    }
213
214    // This url is for raw Markdown bytes
215    let url =
216        format!("https://raw.githubusercontent.com/conjure-cp/conjure/main/docs/bits/{base}.md");
217
218    let output = std::process::Command::new("curl")
219        .args(["-fsSL", &url])
220        .output()
221        .ok();
222
223    let documentation = output
224        .filter(|output| output.status.success())
225        .and_then(|output| String::from_utf8(output.stdout).ok());
226
227    if let Ok(mut cache) = cache.lock() {
228        cache.insert(base, documentation.clone());
229    }
230
231    documentation
232}
233
234fn normalise_documentation_key(name: &str) -> String {
235    name.strip_suffix(".md").unwrap_or(name).to_string()
236}
237
238mod test {
239    #[allow(unused)]
240    use super::*;
241
242    #[test]
243    fn test_get_metavars() {
244        let src = "such that &x = y";
245        let (tree, _) = get_tree(src).unwrap();
246        let root = tree.root_node();
247        let metavars = get_metavars(&root, src).collect::<Vec<_>>();
248        assert_eq!(metavars, vec!["x"]);
249    }
250}