Skip to main content

semantic/parser/
parser_core.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Core parsing implementation.
3
4use std::sync::{Arc, OnceLock};
5
6use objects::object::{ContentHash, OccurrenceRole, ScopeEntry, ScopeKind};
7use tree_sitter::{Node, Tree as TSTree};
8
9use super::{
10    parser_language::Language,
11    parser_pool::{parse_fresh, parse_fresh_bounded},
12    parser_types::{CallSite, FunctionDef, Import},
13    syntax_index::{FunctionRef, ImportRef, SyntaxIndex},
14};
15
16/// Cooperative parser cancellation and a monotonic execution deadline.
17#[derive(Clone)]
18pub struct ParseBudget {
19    pub cancelled: Arc<std::sync::atomic::AtomicBool>,
20    pub deadline: std::time::Instant,
21}
22impl ParseBudget {
23    pub fn interrupted(&self) -> bool {
24        self.cancelled.load(std::sync::atomic::Ordering::Acquire)
25            || std::time::Instant::now() >= self.deadline
26    }
27}
28
29/// A parsed file with its tree-sitter AST and Heddle-owned syntax index.
30#[derive(Debug)]
31pub struct ParsedFile {
32    pub language: Language,
33    pub source: Arc<str>,
34    content_hash: ContentHash,
35    tree: TSTree,
36    index: OnceLock<SyntaxIndex>,
37}
38
39impl ParsedFile {
40    /// Parse a file's contents.
41    pub fn parse(source: impl AsRef<str>, language: Language) -> Option<Self> {
42        let source = Arc::<str>::from(source.as_ref());
43        let content_hash = ContentHash::compute(source.as_bytes());
44        Self::parse_with_hash(source, language, content_hash)
45    }
46
47    pub fn parse_bounded(source: &str, language: Language, budget: &ParseBudget) -> Option<Self> {
48        let tree = parse_fresh_bounded(source.as_bytes(), language, Some(budget))?;
49        if tree.root_node().has_error() || budget.interrupted() {
50            return None;
51        }
52        Some(Self {
53            language,
54            source: Arc::from(source),
55            content_hash: ContentHash::compute(source.as_bytes()),
56            tree,
57            index: OnceLock::new(),
58        })
59    }
60
61    /// Parse already-owned contents without copying the source string.
62    pub fn parse_owned(source: String, language: Language) -> Option<Self> {
63        let content_hash = ContentHash::compute(source.as_bytes());
64        Self::parse_with_hash(Arc::<str>::from(source), language, content_hash)
65    }
66
67    pub(crate) fn parse_with_hash(
68        source: Arc<str>,
69        language: Language,
70        content_hash: ContentHash,
71    ) -> Option<Self> {
72        let tree = parse_fresh(source.as_bytes(), language)?;
73        if tree.root_node().has_error() {
74            return None;
75        }
76
77        Some(Self {
78            language,
79            source,
80            content_hash,
81            tree,
82            index: OnceLock::new(),
83        })
84    }
85
86    /// Stable content identity for caches and sidecars.
87    pub fn content_hash(&self) -> ContentHash {
88        self.content_hash
89    }
90
91    /// Borrow the source text without cloning.
92    pub fn source(&self) -> &str {
93        &self.source
94    }
95
96    /// Get the root node of the AST.
97    pub fn root_node(&self) -> Node<'_> {
98        self.tree.root_node()
99    }
100
101    /// Compact Heddle-owned syntax data derived from the AST.
102    pub fn syntax_index(&self) -> &SyntaxIndex {
103        self.index.get_or_init(|| {
104            SyntaxIndex::build(self.language, self.source.as_ref(), self.root_node())
105        })
106    }
107
108    /// Borrow indexed function definitions.
109    pub fn functions(&self) -> impl Iterator<Item = FunctionRef<'_>> + '_ {
110        self.syntax_index().functions(self.source.as_ref())
111    }
112
113    /// Borrow indexed imports.
114    pub fn imports(&self) -> impl Iterator<Item = ImportRef<'_>> + '_ {
115        self.syntax_index().imports(self.source.as_ref())
116    }
117
118    /// Extract function definitions from the file.
119    pub fn extract_functions(&self) -> Vec<FunctionDef> {
120        self.functions().map(FunctionRef::to_owned).collect()
121    }
122
123    /// Call expressions from the syntax index. Comments and string
124    /// literals are not call nodes, so they do not appear here.
125    pub fn extract_calls(&self) -> Vec<CallSite> {
126        self.syntax_index()
127            .occurrences()
128            .iter()
129            .filter(|occurrence| occurrence.role == OccurrenceRole::Call)
130            .map(|occurrence| CallSite {
131                name: occurrence.name.clone(),
132                qualifier: occurrence.qualifier.clone(),
133            })
134            .collect()
135    }
136
137    /// Calls owned by the outermost function in this parse.
138    /// Nested function and closure bodies are excluded.
139    pub fn extract_own_calls(&self) -> Vec<CallSite> {
140        let index = self.syntax_index();
141        let scopes = index.semantic_scopes();
142        let Some(owner) = outermost_function_scope(scopes) else {
143            return Vec::new();
144        };
145        index
146            .occurrences()
147            .iter()
148            .filter(|occurrence| {
149                occurrence.role == OccurrenceRole::Call
150                    && nearest_function_scope(scopes, occurrence.scope) == Some(owner)
151            })
152            .map(|occurrence| CallSite {
153                name: occurrence.name.clone(),
154                qualifier: occurrence.qualifier.clone(),
155            })
156            .collect()
157    }
158
159    /// Extract imports from the file.
160    pub fn extract_imports(&self) -> Vec<Import> {
161        self.imports().map(ImportRef::to_owned).collect()
162    }
163
164    /// Check if a node kind string represents a function definition in the given language.
165    pub fn is_function_kind(kind: &str, language: Language) -> bool {
166        super::syntax_index::is_function_kind(kind, language)
167    }
168}
169
170fn outermost_function_scope(scopes: &[ScopeEntry]) -> Option<u32> {
171    scopes.iter().find_map(|scope| {
172        if scope.kind != ScopeKind::Function {
173            return None;
174        }
175        let mut parent = scope.parent;
176        while let Some(id) = parent {
177            let entry = scopes.get(id as usize)?;
178            if entry.kind == ScopeKind::Function {
179                return None;
180            }
181            parent = entry.parent;
182        }
183        Some(scope.local_id)
184    })
185}
186
187fn nearest_function_scope(scopes: &[ScopeEntry], scope: u32) -> Option<u32> {
188    let mut current = Some(scope);
189    while let Some(id) = current {
190        let entry = scopes.get(id as usize)?;
191        if entry.kind == ScopeKind::Function {
192            return Some(id);
193        }
194        current = entry.parent;
195    }
196    None
197}