Line data Source code
1 : //! The script cursor and the location lookups it builds on: the symbol, comment and AST node at a
2 : //! text offset and the path from a node to its statement root.
3 :
4 : use crate::analyzer::{AnalyzedScript, ExprId, ScopeId, TableRefId};
5 : use crate::catalog::CatalogSnapshot;
6 : use dashql_buffers::cursor::RelativeSymbolPosition;
7 : use dashql_buffers::parser::{NodeType, SymbolSpan};
8 : use dashql_parser::names::NameId;
9 : use dashql_parser::parser::context::resolve_text_span;
10 : use dashql_parser::parser::ParsedScript;
11 : use dashql_parser::scanner::{ScannedScript, Symbol};
12 : use dashql_parser::symbol_kind::*;
13 : use std::sync::Arc;
14 :
15 : /// The state of a script that a cursor points into (the parts of `Script` the cursor reads)
16 : #[derive(Clone, Copy)]
17 : pub struct ScriptView<'a> {
18 : /// The scanned script, if scanned
19 : pub scanned: Option<&'a Arc<ScannedScript>>,
20 : /// The parsed script, if parsed
21 : pub parsed: Option<&'a Arc<ParsedScript>>,
22 : /// The analyzed script, if analyzed, possibly from an older parsed script
23 : pub analyzed: Option<&'a Arc<AnalyzedScript>>,
24 : /// The current snapshot of the catalog, for scripts without analysis
25 : pub catalog: &'a CatalogSnapshot,
26 : }
27 :
28 : impl<'a> ScriptView<'a> {
29 : /// The catalog snapshot the script was analyzed against, the current one without analysis. The
30 : /// catalog locations of the analysis refer to this snapshot.
31 1052 : pub fn catalog(&self) -> &'a CatalogSnapshot {
32 1052 : self.analyzed.map_or(self.catalog, |analyzed| &analyzed.snapshot)
33 1052 : }
34 :
35 : /// The analyzed script if it was analyzed from the current parsed script
36 373 : pub fn current_analysis(&self) -> Option<&'a Arc<AnalyzedScript>> {
37 373 : match (self.parsed, self.analyzed) {
38 373 : (Some(p), Some(a)) if Arc::ptr_eq(p, &a.parsed) => Some(a),
39 0 : _ => None,
40 : }
41 373 : }
42 : }
43 :
44 : /// A scanner symbol and the position of a text offset relative to it
45 : #[derive(Clone, Copy, Debug)]
46 : pub struct SymbolLocation {
47 : /// The symbol index
48 : pub symbol_id: u32,
49 : /// The scanner symbol
50 : pub symbol: Symbol,
51 : /// The text offset
52 : pub text_offset: u32,
53 : /// The position of the text offset relative to the symbol
54 : pub relative_pos: RelativeSymbolPosition,
55 : }
56 :
57 : impl SymbolLocation {
58 1399 : pub fn is_dot(&self) -> bool {
59 1399 : self.symbol.kind == S_DOT || self.symbol.kind == S_DOT_STAR_LA
60 1399 : }
61 1474 : pub fn is_trailing_dot(&self) -> bool {
62 1474 : self.symbol.kind == S_DOT_TRAILING
63 1474 : }
64 : }
65 :
66 : /// The symbol at a text offset and its predecessor
67 : #[derive(Clone, Copy, Debug)]
68 : pub struct LocationInfo {
69 : /// The last symbol that does not begin after the text offset
70 : pub current: SymbolLocation,
71 : /// The previous symbol, which might end at the text offset
72 : pub previous: Option<SymbolLocation>,
73 : }
74 :
75 : /// The position of a text offset relative to a symbol
76 1078 : fn relative_position(symbols: &[Symbol], text_offset: u32, symbol_id: u32) -> RelativeSymbolPosition {
77 1078 : let Some(symbol) = symbols.get(symbol_id as usize) else {
78 0 : return RelativeSymbolPosition::AFTER_SYMBOL;
79 : };
80 1078 : let begin = symbol.location.offset;
81 1078 : let end = symbol.location.offset + symbol.location.length;
82 1078 : if text_offset < begin {
83 54 : RelativeSymbolPosition::BEFORE_SYMBOL
84 1024 : } else if text_offset == begin {
85 35 : RelativeSymbolPosition::BEGIN_OF_SYMBOL
86 989 : } else if text_offset == end {
87 489 : RelativeSymbolPosition::END_OF_SYMBOL
88 500 : } else if text_offset < end {
89 29 : RelativeSymbolPosition::MID_OF_SYMBOL
90 : } else {
91 471 : RelativeSymbolPosition::AFTER_SYMBOL
92 : }
93 1078 : }
94 :
95 : /// Find the symbol at a text offset
96 545 : pub fn find_symbol(scanned: &ScannedScript, text_offset: usize) -> LocationInfo {
97 545 : let symbols = &scanned.symbols;
98 545 : let text_offset = text_offset.min(scanned.text.len()) as u32;
99 545 : let location = |id: u32| SymbolLocation {
100 1078 : symbol_id: id,
101 1078 : symbol: symbols[id as usize],
102 1078 : text_offset,
103 1078 : relative_pos: relative_position(symbols, text_offset, id),
104 1078 : };
105 : // The last symbol that does not begin after the offset
106 2136 : let mut id = symbols.partition_point(|s| s.location.offset <= text_offset).saturating_sub(1) as u32;
107 : // Hit EOF? Use the last symbol before it
108 545 : if symbols[id as usize].kind == S_YYEOF {
109 399 : if id == 0 {
110 : // The very first symbol is EOF, the script is empty
111 6 : let current = SymbolLocation {
112 6 : symbol_id: 0,
113 6 : symbol: symbols[0],
114 6 : text_offset,
115 6 : relative_pos: RelativeSymbolPosition::BEFORE_SYMBOL,
116 6 : };
117 6 : return LocationInfo { current, previous: None };
118 393 : }
119 393 : id -= 1;
120 146 : }
121 539 : let current = location(id);
122 539 : let previous = location(id.saturating_sub(1));
123 539 : LocationInfo {
124 539 : current,
125 539 : previous: Some(previous),
126 539 : }
127 545 : }
128 :
129 : /// Is a text offset strictly inside a comment?
130 : ///
131 : /// The begin of a comment is never inside. A line comment ends at its newline, which is not part of
132 : /// its span, a block comment ends with its closing `*/`.
133 499 : pub fn is_inside_comment(scanned: &ScannedScript, text_offset: usize) -> bool {
134 499 : let text_offset = text_offset as u32;
135 499 : let i = scanned.comments.partition_point(|c| c.offset <= text_offset);
136 499 : if i == 0 {
137 469 : return false;
138 30 : }
139 30 : let comment = scanned.comments[i - 1];
140 30 : let begin = comment.offset;
141 30 : let end = begin + comment.length;
142 30 : if text_offset <= begin {
143 1 : return false;
144 29 : }
145 29 : let text = scanned.text.as_bytes();
146 29 : let is_line_comment = comment.length >= 2 && text.get(begin as usize) == Some(&b'-') && text.get(begin as usize + 1) == Some(&b'-');
147 29 : if is_line_comment {
148 4 : text_offset <= end
149 : } else {
150 25 : text_offset < end
151 : }
152 499 : }
153 :
154 : /// Find the statement and the innermost AST node at a text offset
155 553 : pub fn find_node_at_offset(parsed: &ParsedScript, text_offset: usize) -> Option<(u32, u32)> {
156 553 : if parsed.statements.is_empty() {
157 148 : return None;
158 405 : }
159 405 : let scanned = &parsed.scanned;
160 2453 : let resolve = |span: SymbolSpan| resolve_text_span(&scanned.text, &scanned.symbols, span);
161 405 : let text_offset = text_offset as u64;
162 : // The predecessor of the first statement that begins after the offset
163 405 : let mut statement_id = 0;
164 820 : while statement_id < parsed.statements.len() {
165 442 : let root = parsed.statements[statement_id].root;
166 442 : let ts = resolve(*parsed.nodes[root as usize].symbol_span());
167 442 : if ts.offset as u64 > text_offset {
168 27 : break;
169 415 : }
170 415 : statement_id += 1;
171 : }
172 405 : if statement_id == 0 {
173 27 : return None;
174 378 : }
175 378 : statement_id -= 1;
176 : // Traverse down the AST
177 378 : let mut iter = parsed.statements[statement_id].root;
178 : loop {
179 1653 : let node = &parsed.nodes[iter as usize];
180 1653 : if node.children_count() == 0 {
181 307 : break;
182 1346 : }
183 : // Prefer a child that contains the offset over a child that ends at it.
184 : // AST nodes have holes, e.g. a select clause has no node for the FROM keyword.
185 1346 : let mut child_exact = None;
186 1346 : let mut child_end = None;
187 2011 : for i in 0..node.children_count() {
188 2011 : let ci = node.children_begin_or_value() + i;
189 2011 : let ts = resolve(*parsed.nodes[ci as usize].symbol_span());
190 2011 : let begin = ts.offset as u64;
191 2011 : let end = begin + ts.length as u64;
192 2011 : if begin <= text_offset {
193 1877 : if end > text_offset {
194 251 : child_exact = Some(ci);
195 1650 : } else if end == text_offset {
196 1041 : child_end = Some(ci);
197 1058 : }
198 134 : }
199 : }
200 1346 : match child_exact.or(child_end) {
201 1275 : Some(child) => iter = child,
202 71 : None => break,
203 : }
204 : }
205 378 : Some((statement_id as u32, iter))
206 553 : }
207 :
208 : /// Collect the path from a node to its statement root and the name scopes on that path
209 373 : pub fn follow_path_upwards(analyzed: &AnalyzedScript, ast_node_id: u32, path: &mut Vec<u32>, scopes: &mut Vec<ScopeId>) {
210 373 : path.clear();
211 373 : scopes.clear();
212 373 : let nodes = &analyzed.parsed.nodes;
213 373 : let mut node = ast_node_id;
214 : loop {
215 1623 : path.push(node);
216 1623 : if let Some(&scope) = analyzed.name_scopes_by_root_node.get(&node) {
217 387 : scopes.push(scope);
218 1236 : }
219 1623 : let parent = nodes[node as usize].parent();
220 : // Statement roots are their own parents
221 1623 : if parent == node {
222 373 : break;
223 1250 : }
224 1250 : node = parent;
225 : }
226 373 : }
227 :
228 : /// The kind of a name path component
229 : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
230 : pub enum NameComponentType {
231 : Name,
232 : Star,
233 : TrailingDot,
234 : Index,
235 : }
236 :
237 : /// A component of a name path
238 : #[derive(Clone, Copy, Debug)]
239 : pub struct NameComponent {
240 : /// The symbol span of the component
241 : pub loc: SymbolSpan,
242 : /// The kind of the component
243 : pub component_type: NameComponentType,
244 : /// The name id, set for name components only
245 : pub name: Option<NameId>,
246 : }
247 :
248 : /// What the cursor points at
249 : #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
250 : pub enum CursorContext {
251 : #[default]
252 : None,
253 : /// A table reference
254 : TableRef { table_reference_id: TableRefId, at_alias: bool },
255 : /// A column reference
256 : ColumnRef { expression_id: ExprId },
257 : /// An `alias.` that the parser recovered without a column ref node
258 : AliasDot,
259 : }
260 :
261 : /// A cursor in a script
262 : pub struct ScriptCursor<'a> {
263 : /// The script the cursor points into
264 : pub script: ScriptView<'a>,
265 : /// The UTF-8 byte offset of the cursor in the script text
266 : pub text_offset: usize,
267 : /// The symbol at the cursor, if the script was scanned
268 : pub scanner_location: Option<LocationInfo>,
269 : /// The statement at the cursor
270 : pub statement_id: Option<u32>,
271 : /// The innermost AST node at the cursor
272 : pub ast_node_id: Option<u32>,
273 : /// The AST nodes from the cursor node to the statement root
274 : pub ast_path_to_root: Vec<u32>,
275 : /// The name scopes that contain the cursor, innermost first
276 : pub name_scopes: Vec<ScopeId>,
277 : /// What the cursor points at
278 : pub context: CursorContext,
279 : }
280 :
281 : impl<'a> ScriptCursor<'a> {
282 : /// Place a cursor in a script
283 545 : pub fn place(script: ScriptView<'a>, text_offset: usize) -> Self {
284 545 : let mut cursor = ScriptCursor {
285 545 : script,
286 545 : text_offset,
287 545 : scanner_location: None,
288 545 : statement_id: None,
289 545 : ast_node_id: None,
290 545 : ast_path_to_root: Vec::new(),
291 545 : name_scopes: Vec::new(),
292 545 : context: CursorContext::None,
293 545 : };
294 545 : if let Some(scanned) = script.scanned {
295 545 : cursor.scanner_location = Some(find_symbol(scanned, text_offset));
296 545 : }
297 545 : let Some(parsed) = script.parsed else { return cursor };
298 :
299 545 : let mut ast_node = find_node_at_offset(parsed, text_offset);
300 : // A trailing dot may span into following whitespace. Anchor the lookup at the dot so that the
301 : // cursor stays associated with the qualifier.
302 545 : if let Some(location) = &cursor.scanner_location {
303 545 : if location.current.is_trailing_dot() {
304 8 : if let Some(qualifier) = find_node_at_offset(parsed, location.current.symbol.location.offset as usize) {
305 5 : ast_node = Some(qualifier);
306 8 : }
307 537 : }
308 0 : }
309 545 : if let Some((statement_id, node_id)) = ast_node {
310 373 : cursor.statement_id = Some(statement_id);
311 373 : cursor.ast_node_id = Some(node_id);
312 373 : if let Some(analyzed) = script.current_analysis() {
313 373 : follow_path_upwards(analyzed, node_id, &mut cursor.ast_path_to_root, &mut cursor.name_scopes);
314 373 : cursor.find_context(analyzed, parsed);
315 373 : }
316 172 : }
317 545 : cursor.recover_dot_context();
318 545 : cursor
319 545 : }
320 :
321 : /// Find a table or column ref in the innermost scope
322 373 : fn find_context(&mut self, analyzed: &AnalyzedScript, parsed: &ParsedScript) {
323 373 : let Some(&innermost) = self.name_scopes.first() else { return };
324 373 : let scope = &analyzed.name_scopes[innermost as usize];
325 1029 : for &node_id in &self.ast_path_to_root {
326 1029 : let mut matched = false;
327 1029 : match parsed.nodes[node_id as usize].node_type() {
328 : NodeType::OBJECT_SQL_COLUMN_REF => {
329 113 : matched = true;
330 150 : for &expr in &scope.expressions {
331 150 : let expression = &analyzed.expressions[expr as usize];
332 150 : if expression.ast_node_id == node_id && expression.column_ref().is_some() {
333 112 : self.context = CursorContext::ColumnRef {
334 112 : expression_id: expression.expression_id,
335 112 : };
336 112 : }
337 : }
338 : }
339 : NodeType::OBJECT_SQL_TABLEREF => {
340 161 : matched = true;
341 171 : for &table_ref_id in &scope.table_references {
342 169 : let table_ref = &analyzed.table_references[table_ref_id as usize];
343 169 : if table_ref.ast_node_id != node_id {
344 16 : continue;
345 153 : }
346 153 : let mut at_alias = false;
347 153 : if let Some((_, alias_loc)) = &table_ref.alias {
348 25 : let scanned = &parsed.scanned;
349 25 : let alias_ts = resolve_text_span(&scanned.text, &scanned.symbols, *alias_loc);
350 25 : at_alias = self.text_offset >= alias_ts.offset as usize;
351 129 : }
352 153 : self.context = CursorContext::TableRef {
353 153 : table_reference_id: table_ref.table_reference_id.object,
354 153 : at_alias,
355 153 : };
356 : }
357 : }
358 755 : _ => {}
359 : }
360 : // Stop at the root of the innermost scope
361 1029 : if matched || node_id == scope.ast_node_id {
362 373 : break;
363 656 : }
364 : }
365 373 : }
366 :
367 : /// The dot symbol at or before the cursor
368 289 : pub fn dot_symbol(location: &LocationInfo, current: &SymbolLocation) -> Option<SymbolLocation> {
369 289 : if current.is_dot() || current.is_trailing_dot() {
370 17 : return Some(*current);
371 272 : }
372 272 : location.previous.filter(|p| p.is_dot() || p.is_trailing_dot())
373 289 : }
374 :
375 : /// In incomplete SQL, the parser can omit the column ref node of `alias.` although name
376 : /// resolution registered the alias in a scope. Recover a synthetic column ref context.
377 545 : fn recover_dot_context(&mut self) {
378 545 : if self.context != CursorContext::None {
379 265 : return;
380 280 : }
381 280 : let (Some(location), Some(analyzed), Some(scanned)) = (self.scanner_location, self.script.analyzed, self.script.scanned) else {
382 0 : return;
383 : };
384 280 : let Some(dot) = Self::dot_symbol(&location, &location.current) else { return };
385 12 : let qualifier = qualifier_before(&scanned.text, dot.symbol.location.offset as usize);
386 12 : let qualifier_text = &scanned.text[qualifier.0..qualifier.1];
387 :
388 12 : let mut best_scope = None;
389 12 : for &scope in &self.name_scopes {
390 7 : if analyzed.name_scopes[scope as usize].source(qualifier_text).is_some() {
391 6 : best_scope = Some(scope);
392 6 : break;
393 1 : }
394 : }
395 : // Prefer the narrowest scope containing the cursor, the same alias can occur in nested queries
396 12 : if best_scope.is_none() && self.name_scopes.is_empty() {
397 5 : let mut best_length = usize::MAX;
398 5 : let parsed = &analyzed.parsed;
399 5 : for scope in &analyzed.name_scopes {
400 5 : if scope.source(qualifier_text).is_none() {
401 0 : continue;
402 5 : }
403 5 : let span = resolve_text_span(&scanned.text, &scanned.symbols, *parsed.nodes[scope.ast_node_id as usize].symbol_span());
404 5 : let scope_end = span.offset as usize + span.length as usize;
405 5 : let contains_cursor = span.offset as usize <= self.text_offset && self.text_offset <= scope_end;
406 5 : if contains_cursor && (span.length as usize) < best_length {
407 4 : best_scope = Some(scope.name_scope_id);
408 4 : best_length = span.length as usize;
409 4 : } else if best_scope.is_none() && self.statement_id.is_none_or(|s| scope.ast_statement_id == Some(s)) {
410 1 : best_scope = Some(scope.name_scope_id);
411 1 : }
412 : }
413 7 : }
414 12 : if let Some(scope) = best_scope {
415 11 : if self.name_scopes.is_empty() {
416 5 : self.name_scopes.push(scope);
417 10 : }
418 11 : self.context = CursorContext::AliasDot;
419 1 : }
420 545 : }
421 :
422 : /// Read the name path at the cursor
423 92 : pub fn read_cursor_name_path(&self, name_path_loc: &mut SymbolSpan) -> Vec<NameComponent> {
424 92 : let (Some(analyzed), Some(parsed)) = (self.script.analyzed, self.script.parsed) else {
425 0 : return Vec::new();
426 : };
427 92 : let name_node = match self.context {
428 57 : CursorContext::TableRef { table_reference_id, .. } => analyzed.table_references[table_reference_id as usize]
429 57 : .inner
430 57 : .as_ref()
431 57 : .and_then(|r| r.table_name.ast_node_id),
432 35 : CursorContext::ColumnRef { expression_id } => {
433 35 : analyzed.expressions[expression_id as usize].column_ref().and_then(|c| c.column_name.ast_node_id)
434 : }
435 0 : _ => None,
436 : };
437 92 : let Some(name_node) = name_node else { return Vec::new() };
438 92 : let nodes = &parsed.nodes;
439 92 : let node = &nodes[name_node as usize];
440 92 : if node.node_type() != NodeType::ARRAY {
441 0 : return Vec::new();
442 92 : }
443 92 : *name_path_loc = *node.symbol_span();
444 92 : let begin = node.children_begin_or_value() as usize;
445 92 : let mut components = Vec::new();
446 203 : for child in &nodes[begin..begin + node.children_count() as usize] {
447 203 : let component_type = match child.node_type() {
448 174 : NodeType::NAME => NameComponentType::Name,
449 0 : NodeType::OBJECT_SQL_INDIRECTION_STAR => NameComponentType::Star,
450 0 : NodeType::OBJECT_SQL_INDIRECTION_INDEX => NameComponentType::Index,
451 29 : NodeType::OBJECT_EXT_TRAILING_DOT => NameComponentType::TrailingDot,
452 0 : _ => return Vec::new(),
453 : };
454 203 : components.push(NameComponent {
455 203 : loc: *child.symbol_span(),
456 203 : component_type,
457 203 : name: (component_type == NameComponentType::Name).then(|| child.children_begin_or_value()),
458 : });
459 203 : if component_type == NameComponentType::TrailingDot {
460 29 : return components;
461 174 : }
462 : }
463 63 : components
464 92 : }
465 : }
466 :
467 : /// The identifier before a dot at a byte offset: skips dots, then takes `[A-Za-z0-9_]*`.
468 : /// Returns the byte range.
469 21 : pub fn qualifier_before(text: &str, dot_offset: usize) -> (usize, usize) {
470 21 : let bytes = text.as_bytes();
471 21 : let mut end = dot_offset.min(bytes.len());
472 21 : while end > 0 && bytes[end - 1] == b'.' {
473 0 : end -= 1;
474 0 : }
475 21 : let mut begin = end;
476 65 : while begin > 0 && (bytes[begin - 1].is_ascii_alphanumeric() || bytes[begin - 1] == b'_') {
477 44 : begin -= 1;
478 44 : }
479 21 : (begin, end)
480 21 : }
|