1use pest_derive::Parser;
2
3#[derive(Parser)]
4#[grammar = "trilogy.pest"]
5pub struct TrilogyParser;
6
7pub fn rule_calls(text: &str, max_calls: usize) -> Result<Option<usize>, Box<pest::error::Error<Rule>>> {
18 use pest::error::ErrorVariant;
19 use pest::Parser;
20 use std::num::NonZeroUsize;
21 use std::sync::Mutex;
22
23 static PROBE: Mutex<()> = Mutex::new(());
24 let _probe = PROBE.lock().unwrap_or_else(|e| e.into_inner());
25
26 let completes_within = |limit: usize| -> Result<bool, Box<pest::error::Error<Rule>>> {
27 pest::set_call_limit(NonZeroUsize::new(limit));
28 let result = TrilogyParser::parse(Rule::start, text);
29 pest::set_call_limit(None);
30 match result {
31 Ok(_) => Ok(true),
32 Err(e) => match &e.variant {
33 ErrorVariant::CustomError { message } if message == "call limit reached" => Ok(false),
34 _ => Err(Box::new(e)),
35 },
36 }
37 };
38 if !completes_within(max_calls)? {
39 return Ok(None);
40 }
41 let (mut low, mut high) = (1usize, max_calls);
42 while low < high {
43 let mid = low + (high - low) / 2;
44 if completes_within(mid)? {
45 high = mid;
46 } else {
47 low = mid + 1;
48 }
49 }
50 Ok(Some(low))
51}
52
53#[cfg(feature = "python")]
57mod python_impl {
58use super::{Rule, TrilogyParser};
59use pest::iterators::Pair;
60use pest::Parser;
61use pyo3::prelude::*;
62use pyo3::types::{PyList, PyString, PyTuple};
63use std::collections::HashMap;
64use std::sync::Mutex;
65
66fn rename_rule(name: &str) -> &str {
67 match name {
68 "attr_access_paren" => "attr_access",
69 "_and_conditional" => "conditional",
70 other => other,
71 }
72}
73
74fn is_token_rule(name: &str) -> bool {
75 name.chars()
76 .find(|c| c.is_alphabetic())
77 .map(|c| c.is_ascii_uppercase())
78 .unwrap_or(false)
79}
80
81fn is_silent_token_rule(name: &str) -> bool {
85 name.starts_with('_')
86}
87
88struct RuleInfo {
89 name_py: Py<PyString>,
90 is_token: bool,
91 is_silent: bool,
92}
93
94static RULE_INFO_CACHE: Mutex<Option<HashMap<Rule, RuleInfo>>> = Mutex::new(None);
97
98fn rule_info(py: Python<'_>, rule: Rule) -> (Py<PyString>, bool, bool) {
99 let mut borrowed = RULE_INFO_CACHE.lock().unwrap();
100 let map = borrowed.get_or_insert_with(HashMap::new);
101 if let Some(info) = map.get(&rule) {
102 return (info.name_py.clone_ref(py), info.is_token, info.is_silent);
103 }
104 let raw = format!("{:?}", rule);
105 let renamed = rename_rule(&raw);
106 let is_token = is_token_rule(&raw);
107 let is_silent = is_token && is_silent_token_rule(&raw);
108 let name_py = PyString::intern(py, renamed).unbind();
109 let cloned = name_py.clone_ref(py);
110 map.insert(
111 rule,
112 RuleInfo {
113 name_py,
114 is_token,
115 is_silent,
116 },
117 );
118 (cloned, is_token, is_silent)
119}
120
121static STATIC_STR_CACHE: Mutex<Option<HashMap<&'static str, Py<PyString>>>> = Mutex::new(None);
124
125fn intern_static(py: Python<'_>, s: &'static str) -> Py<PyString> {
126 let mut borrowed = STATIC_STR_CACHE.lock().unwrap();
127 let map = borrowed.get_or_insert_with(HashMap::new);
128 if let Some(existing) = map.get(s) {
129 return existing.clone_ref(py);
130 }
131 let py_str = PyString::intern(py, s).unbind();
132 map.insert(s, py_str.clone_ref(py));
133 py_str
134}
135
136#[pyclass(module = "_preql_import_resolver")]
137pub struct PestNode {
138 data_py: Py<PyString>,
139 children_list: Py<PyList>,
140 #[pyo3(get)]
141 pub line: usize,
142 #[pyo3(get)]
143 pub column: usize,
144 #[pyo3(get)]
145 pub end_line: usize,
146 #[pyo3(get)]
147 pub end_column: usize,
148 #[pyo3(get)]
149 pub start_pos: usize,
150 #[pyo3(get)]
151 pub end_pos: usize,
152}
153
154#[pymethods]
155impl PestNode {
156 #[getter]
157 fn data(&self, py: Python<'_>) -> Py<PyString> {
158 self.data_py.clone_ref(py)
159 }
160
161 #[getter]
162 fn children<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> {
163 self.children_list.bind(py).clone()
164 }
165
166 #[getter]
169 fn meta(slf: Py<Self>) -> Py<Self> {
170 slf
171 }
172
173 fn __repr__(&self, py: Python<'_>) -> String {
174 format!("PestNode({})", self.data_py.bind(py).to_string_lossy())
175 }
176}
177
178#[pyclass(module = "_preql_import_resolver")]
179pub struct PestToken {
180 type_name_py: Py<PyString>,
181 value_py: Py<PyString>,
182 #[pyo3(get)]
183 pub line: usize,
184 #[pyo3(get)]
185 pub column: usize,
186 #[pyo3(get)]
187 pub end_line: usize,
188 #[pyo3(get)]
189 pub end_column: usize,
190 #[pyo3(get)]
191 pub start_pos: usize,
192 #[pyo3(get)]
193 pub end_pos: usize,
194}
195
196#[pymethods]
197impl PestToken {
198 #[getter]
199 #[pyo3(name = "type")]
200 fn type_name(&self, py: Python<'_>) -> Py<PyString> {
201 self.type_name_py.clone_ref(py)
202 }
203
204 #[getter]
205 fn value(&self, py: Python<'_>) -> Py<PyString> {
206 self.value_py.clone_ref(py)
207 }
208
209 fn __repr__(&self, py: Python<'_>) -> String {
210 format!(
211 "PestToken({}={:?})",
212 self.type_name_py.bind(py).to_string_lossy(),
213 self.value_py.bind(py).to_string_lossy()
214 )
215 }
216
217 fn __str__(&self, py: Python<'_>) -> String {
218 self.value_py.bind(py).to_string_lossy().into_owned()
219 }
220}
221
222struct LineIndex {
228 char_byte_starts: Vec<usize>,
231 line_starts_char: Vec<usize>,
233}
234
235impl LineIndex {
236 fn new(text: &str) -> Self {
237 let mut char_byte_starts = Vec::with_capacity(text.len() + 1);
238 let mut line_starts_char = Vec::with_capacity(text.len() / 40 + 1);
239 line_starts_char.push(0);
240 let mut char_idx = 0usize;
241 for (byte_offset, ch) in text.char_indices() {
242 char_byte_starts.push(byte_offset);
243 if ch == '\n' {
244 line_starts_char.push(char_idx + 1);
245 }
246 char_idx += 1;
247 }
248 char_byte_starts.push(text.len());
249 Self {
250 char_byte_starts,
251 line_starts_char,
252 }
253 }
254
255 fn byte_to_char(&self, byte_pos: usize) -> usize {
256 match self.char_byte_starts.binary_search(&byte_pos) {
257 Ok(i) => i,
258 Err(i) => i - 1,
259 }
260 }
261
262 fn line_col(&self, byte_pos: usize) -> (usize, usize, usize) {
264 let char_pos = self.byte_to_char(byte_pos);
265 let idx = match self.line_starts_char.binary_search(&char_pos) {
266 Ok(i) => i,
267 Err(i) => i - 1,
268 };
269 let line_start = self.line_starts_char[idx];
270 (idx + 1, char_pos - line_start + 1, char_pos)
271 }
272}
273
274type SpanTuple = (usize, usize, usize, usize, usize, usize);
275
276fn span_positions(pair: &Pair<Rule>, idx: &LineIndex) -> SpanTuple {
277 let span = pair.as_span();
278 span_from_to(span.start(), span.end(), idx)
279}
280
281trait TreeBuilder {
285 fn node(&self, py: Python<'_>, name: Py<PyString>, children: Vec<Py<PyAny>>, span: SpanTuple) -> PyResult<Py<PyAny>>;
286 fn token(&self, py: Python<'_>, name: Py<PyString>, value: &str, span: SpanTuple) -> PyResult<Py<PyAny>>;
287}
288
289struct ClassBuilder;
290
291impl TreeBuilder for ClassBuilder {
292 fn node(&self, py: Python<'_>, name: Py<PyString>, children: Vec<Py<PyAny>>, span: SpanTuple) -> PyResult<Py<PyAny>> {
293 let (line, column, end_line, end_column, start_pos, end_pos) = span;
294 let node = PestNode {
295 data_py: name,
296 children_list: PyList::new(py, children)?.unbind(),
297 line,
298 column,
299 end_line,
300 end_column,
301 start_pos,
302 end_pos,
303 };
304 Ok(Py::new(py, node)?.into_any())
305 }
306
307 fn token(&self, py: Python<'_>, name: Py<PyString>, value: &str, span: SpanTuple) -> PyResult<Py<PyAny>> {
308 let (line, column, end_line, end_column, start_pos, end_pos) = span;
309 let token = PestToken {
310 type_name_py: name,
311 value_py: PyString::new(py, value).unbind(),
312 line,
313 column,
314 end_line,
315 end_column,
316 start_pos,
317 end_pos,
318 };
319 Ok(Py::new(py, token)?.into_any())
320 }
321}
322
323struct TupleBuilder;
329
330impl TreeBuilder for TupleBuilder {
331 fn node(&self, py: Python<'_>, name: Py<PyString>, children: Vec<Py<PyAny>>, span: SpanTuple) -> PyResult<Py<PyAny>> {
332 let (line, column, end_line, end_column, start_pos, end_pos) = span;
333 let children = PyTuple::new(py, children)?;
334 Ok((name, children, line, column, end_line, end_column, start_pos, end_pos)
335 .into_pyobject(py)?
336 .into_any()
337 .unbind())
338 }
339
340 fn token(&self, py: Python<'_>, name: Py<PyString>, value: &str, span: SpanTuple) -> PyResult<Py<PyAny>> {
341 let (line, column, end_line, end_column, start_pos, end_pos) = span;
342 let value_py = PyString::new(py, value).unbind();
343 Ok((name, value_py, line, column, end_line, end_column, start_pos, end_pos)
344 .into_pyobject(py)?
345 .into_any()
346 .unbind())
347 }
348}
349
350fn span_from_to(start_byte: usize, end_byte: usize, idx: &LineIndex) -> SpanTuple {
351 let (start_line, start_col, start_char) = idx.line_col(start_byte);
352 let (end_line, end_col, end_char) = idx.line_col(end_byte);
353 (start_line, start_col, end_line, end_col, start_char, end_char)
354}
355
356fn visible_children(pair: Pair<Rule>) -> Vec<Pair<Rule>> {
359 pair.into_inner()
360 .filter(|inner| {
361 let name = format!("{:?}", inner.as_rule());
362 !matches!(inner.as_rule(), Rule::EOI) && !(is_token_rule(&name) && is_silent_token_rule(&name))
363 })
364 .collect()
365}
366
367fn convert_children<B: TreeBuilder>(py: Python<'_>, pairs: Vec<Pair<Rule>>, idx: &LineIndex, b: &B) -> PyResult<Vec<Py<PyAny>>> {
368 pairs.into_iter().map(|p| convert_pair(py, p, idx, b)).collect()
369}
370
371fn paren_group_is_tuple(pair: &Pair<Rule>) -> bool {
376 let exprs: Vec<Pair<Rule>> = pair.clone().into_inner().collect();
377 if exprs.len() != 1 {
378 return true;
379 }
380 let text = pair.as_str();
381 let gap = &text[exprs[0].as_span().end() - pair.as_span().start()..];
382 let bytes = gap.as_bytes();
383 let mut i = 0;
384 while i < bytes.len() {
385 match bytes[i] {
386 b',' => return true,
387 b'#' => while i < bytes.len() && bytes[i] != b'\n' { i += 1 },
388 b'/' if bytes.get(i + 1) == Some(&b'/') => while i < bytes.len() && bytes[i] != b'\n' { i += 1 },
389 b'/' if bytes.get(i + 1) == Some(&b'*') => {
390 i += 2;
391 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') { i += 1 }
392 i += 1;
393 }
394 _ => {}
395 }
396 i += 1;
397 }
398 false
399}
400
401fn convert_paren_group<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
402 let span = span_positions(&pair, idx);
403 let name = if paren_group_is_tuple(&pair) { "expr_tuple" } else { "parenthetical" };
404 let items = convert_children(py, visible_children(pair), idx, b)?;
405 b.node(py, intern_static(py, name), items, span)
406}
407
408fn convert_access_chain<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
412 let span = span_positions(&pair, idx);
413 let mut inner = pair.into_inner();
414 let atom_pair = inner
415 .next()
416 .expect("access_chain must contain an atom as first child");
417
418 let mut inner = inner.peekable();
422 if matches!(inner.peek().map(|p| p.as_rule()), Some(Rule::conditional)) {
423 let conditional = inner.next().unwrap();
424 let bare = match atom_pair.as_rule() {
425 Rule::concept_lit => true,
426 Rule::paren_group => !paren_group_is_tuple(&atom_pair),
427 _ => false,
428 };
429 let head_pair = if bare {
430 atom_pair
431 .into_inner()
432 .next()
433 .expect("a parenthetical and a concept_lit each hold one child")
434 } else {
435 atom_pair
436 };
437 let head = convert_pair(py, head_pair, idx, b)?;
438 let cond = convert_pair(py, conditional, idx, b)?;
439 return b.node(py, intern_static(py, "filter_item"), vec![head, cond], span);
440 }
441
442 let mut tails: Vec<(Rule, Pair<Rule>)> = Vec::new();
443 for tail in inner {
444 let kind = tail.as_rule();
445 let payload = tail
446 .into_inner()
447 .next()
448 .expect("tail rule must have one payload child");
449 tails.push((kind, payload));
450 }
451
452 if tails.is_empty() {
453 return convert_pair(py, atom_pair, idx, b);
454 }
455
456 let atom_py = convert_pair(py, atom_pair, idx, b)?;
457
458 let tail_name = |kind: Rule, payload: &Pair<Rule>| -> &'static str {
459 match kind {
460 Rule::dot_tail => "attr_access",
461 Rule::bracket_tail => match payload.as_rule() {
462 Rule::int_lit => "index_access",
463 _ => "map_key_access",
464 },
465 Rule::dcolon_tail => "fcast",
466 _ => "chained_access",
467 }
468 };
469
470 let last_is_cast = matches!(tails.last().map(|(k, _)| *k), Some(Rule::dcolon_tail));
471
472 if tails.len() == 1 {
473 let (kind, payload) = tails.pop().unwrap();
474 let name = tail_name(kind, &payload);
475 let payload_py = convert_pair(py, payload, idx, b)?;
476 return b.node(py, intern_static(py, name), vec![atom_py, payload_py], span);
477 }
478
479 if last_is_cast {
480 let (_, cast_payload) = tails.pop().unwrap();
481 let inner_name: &'static str = if tails.len() == 1 {
482 let (k, p) = &tails[0];
483 tail_name(*k, p)
484 } else {
485 "chained_access"
486 };
487 let mut inner_items: Vec<Py<PyAny>> = Vec::with_capacity(tails.len() + 1);
488 inner_items.push(atom_py);
489 for (_, payload) in tails.into_iter() {
490 inner_items.push(convert_pair(py, payload, idx, b)?);
491 }
492 let inner_node = b.node(py, intern_static(py, inner_name), inner_items, span)?;
493 let cast_py = convert_pair(py, cast_payload, idx, b)?;
494 return b.node(py, intern_static(py, "fcast"), vec![inner_node, cast_py], span);
495 }
496
497 let mut items: Vec<Py<PyAny>> = Vec::with_capacity(tails.len() + 1);
498 items.push(atom_py);
499 for (_, payload) in tails.into_iter() {
500 items.push(convert_pair(py, payload, idx, b)?);
501 }
502 b.node(py, intern_static(py, "chained_access"), items, span)
503}
504
505fn convert_expr<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
510 let span = span_positions(&pair, idx);
511 let children = visible_children(pair);
512 let is_between = children.len() == 3 && matches!(children[1].as_rule(), Rule::sum_operator);
513 let name = if is_between { "between_comparison" } else { "comparison" };
514 let items = convert_children(py, children, idx, b)?;
515 b.node(py, intern_static(py, name), items, span)
516}
517
518fn convert_select_chain<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
522 let span = span_positions(&pair, idx);
523 let mut children = visible_children(pair);
524 if children.len() == 1 {
525 return convert_pair(py, children.pop().unwrap(), idx, b);
526 }
527 let items = convert_children(py, children, idx, b)?;
528 b.node(py, intern_static(py, "multi_select_statement"), items, span)
529}
530
531fn convert_aggregate_functions<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
538 let (start_byte, end_byte) = (pair.as_span().start(), pair.as_span().end());
539 let (name_py, _, _) = rule_info(py, pair.as_rule());
540 let mut children = visible_children(pair);
541 let over_pair = match children.last().map(|c| c.as_rule()) {
542 Some(Rule::window_sql_over) => children.pop(),
543 _ => None,
544 };
545 let call_end = match (children.last(), &over_pair) {
546 (Some(last), _) if matches!(last.as_rule(), Rule::aggregate_over) => last.as_span().end(),
547 (_, Some(over)) => over.as_span().start(),
548 _ => end_byte,
549 };
550 let call_items = convert_children(py, children, idx, b)?;
551 let call = b.node(py, name_py, call_items, span_from_to(start_byte, call_end, idx))?;
552 let Some(over_pair) = over_pair else {
553 return Ok(call);
554 };
555 let span = span_from_to(start_byte, end_byte, idx);
556 let over = convert_pair(py, over_pair, idx, b)?;
557 let windowed = b.node(py, intern_static(py, "window_item_sql_aggregate"), vec![call, over], span)?;
558 b.node(py, intern_static(py, "window_item"), vec![windowed], span)
559}
560
561fn convert_pair<B: TreeBuilder>(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex, b: &B) -> PyResult<Py<PyAny>> {
562 match pair.as_rule() {
563 Rule::access_chain => return convert_access_chain(py, pair, idx, b),
564 Rule::expr => return convert_expr(py, pair, idx, b),
565 Rule::select_chain => return convert_select_chain(py, pair, idx, b),
566 Rule::aggregate_functions => return convert_aggregate_functions(py, pair, idx, b),
567 Rule::paren_group => return convert_paren_group(py, pair, idx, b),
568 _ => {}
569 }
570
571 let (name_py, is_token, _is_silent) = rule_info(py, pair.as_rule());
572 let span = span_positions(&pair, idx);
573
574 if is_token {
575 return b.token(py, name_py, pair.as_str(), span);
576 }
577 let mut items: Vec<Py<PyAny>> = Vec::new();
578 for inner in pair.into_inner() {
579 if matches!(inner.as_rule(), Rule::EOI) {
580 continue;
581 }
582 let (_, _, inner_silent) = rule_info(py, inner.as_rule());
583 if inner_silent {
584 continue;
585 }
586 items.push(convert_pair(py, inner, idx, b)?);
587 }
588 b.node(py, name_py, items, span)
589}
590
591fn parse_root<'i>(text: &'i str) -> PyResult<Pair<'i, Rule>> {
592 let mut pairs = TrilogyParser::parse(Rule::start, text).map_err(|e| {
593 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))
594 })?;
595 pairs
596 .next()
597 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("empty parse result"))
598}
599
600#[pyfunction]
603pub fn parse_trilogy_syntax(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
604 let idx = LineIndex::new(text);
605 let start = parse_root(text)?;
606 convert_pair(py, start, &idx, &ClassBuilder)
607}
608
609#[pyfunction]
611pub fn parse_trilogy_syntax_tuple(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
612 let idx = LineIndex::new(text);
613 let start = parse_root(text)?;
614 convert_pair(py, start, &idx, &TupleBuilder)
615}
616
617#[pyfunction]
621pub fn parse_trilogy_syntax_calls(text: &str, max_calls: usize) -> PyResult<usize> {
622 match super::rule_calls(text, max_calls) {
623 Ok(Some(calls)) => Ok(calls),
624 Ok(None) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
625 "parse needs more than {max_calls} rule calls"
626 ))),
627 Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))),
628 }
629}
630
631#[pyfunction]
635pub fn parse_trilogy_syntax_count(text: &str) -> PyResult<usize> {
636 let mut pairs = TrilogyParser::parse(Rule::start, text).map_err(|e| {
637 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))
638 })?;
639 let start = pairs
640 .next()
641 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("empty parse result"))?;
642 fn walk(pair: Pair<Rule>) -> usize {
643 let mut n = 1;
644 for inner in pair.into_inner() {
645 n += walk(inner);
646 }
647 n
648 }
649 Ok(walk(start))
650}
651}
652
653#[cfg(feature = "python")]
654pub use python_impl::{
655 parse_trilogy_syntax, parse_trilogy_syntax_calls, parse_trilogy_syntax_count,
656 parse_trilogy_syntax_tuple, PestNode, PestToken,
657};