1use pest_derive::Parser;
2
3#[derive(Parser)]
4#[grammar = "trilogy.pest"]
5pub struct TrilogyParser;
6
7#[cfg(feature = "python")]
11mod python_impl {
12use super::{Rule, TrilogyParser};
13use pest::iterators::Pair;
14use pest::Parser;
15use pyo3::prelude::*;
16use pyo3::types::{PyList, PyString, PyTuple};
17use std::collections::HashMap;
18use std::sync::Mutex;
19
20fn rename_rule(name: &str) -> &str {
21 match name {
22 "attr_access_paren" => "attr_access",
23 "_and_conditional" => "conditional",
24 other => other,
25 }
26}
27
28fn is_token_rule(name: &str) -> bool {
29 name.chars()
30 .find(|c| c.is_alphabetic())
31 .map(|c| c.is_ascii_uppercase())
32 .unwrap_or(false)
33}
34
35fn is_silent_token_rule(name: &str) -> bool {
39 name.starts_with('_')
40}
41
42struct RuleInfo {
43 name_py: Py<PyString>,
44 is_token: bool,
45 is_silent: bool,
46}
47
48static RULE_INFO_CACHE: Mutex<Option<HashMap<Rule, RuleInfo>>> = Mutex::new(None);
51
52fn rule_info(py: Python<'_>, rule: Rule) -> (Py<PyString>, bool, bool) {
53 let mut borrowed = RULE_INFO_CACHE.lock().unwrap();
54 let map = borrowed.get_or_insert_with(HashMap::new);
55 if let Some(info) = map.get(&rule) {
56 return (info.name_py.clone_ref(py), info.is_token, info.is_silent);
57 }
58 let raw = format!("{:?}", rule);
59 let renamed = rename_rule(&raw);
60 let is_token = is_token_rule(&raw);
61 let is_silent = is_token && is_silent_token_rule(&raw);
62 let name_py = PyString::intern(py, renamed).unbind();
63 let cloned = name_py.clone_ref(py);
64 map.insert(
65 rule,
66 RuleInfo {
67 name_py,
68 is_token,
69 is_silent,
70 },
71 );
72 (cloned, is_token, is_silent)
73}
74
75static STATIC_STR_CACHE: Mutex<Option<HashMap<&'static str, Py<PyString>>>> = Mutex::new(None);
78
79fn intern_static(py: Python<'_>, s: &'static str) -> Py<PyString> {
80 let mut borrowed = STATIC_STR_CACHE.lock().unwrap();
81 let map = borrowed.get_or_insert_with(HashMap::new);
82 if let Some(existing) = map.get(s) {
83 return existing.clone_ref(py);
84 }
85 let py_str = PyString::intern(py, s).unbind();
86 map.insert(s, py_str.clone_ref(py));
87 py_str
88}
89
90#[pyclass(module = "_preql_import_resolver")]
91pub struct PestNode {
92 data_py: Py<PyString>,
93 children_list: Py<PyList>,
94 #[pyo3(get)]
95 pub line: usize,
96 #[pyo3(get)]
97 pub column: usize,
98 #[pyo3(get)]
99 pub end_line: usize,
100 #[pyo3(get)]
101 pub end_column: usize,
102 #[pyo3(get)]
103 pub start_pos: usize,
104 #[pyo3(get)]
105 pub end_pos: usize,
106}
107
108#[pymethods]
109impl PestNode {
110 #[getter]
111 fn data(&self, py: Python<'_>) -> Py<PyString> {
112 self.data_py.clone_ref(py)
113 }
114
115 #[getter]
116 fn children<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> {
117 self.children_list.bind(py).clone()
118 }
119
120 #[getter]
123 fn meta(slf: Py<Self>) -> Py<Self> {
124 slf
125 }
126
127 fn __repr__(&self, py: Python<'_>) -> String {
128 format!("PestNode({})", self.data_py.bind(py).to_string_lossy())
129 }
130}
131
132#[pyclass(module = "_preql_import_resolver")]
133pub struct PestToken {
134 type_name_py: Py<PyString>,
135 value_py: Py<PyString>,
136 #[pyo3(get)]
137 pub line: usize,
138 #[pyo3(get)]
139 pub column: usize,
140 #[pyo3(get)]
141 pub end_line: usize,
142 #[pyo3(get)]
143 pub end_column: usize,
144 #[pyo3(get)]
145 pub start_pos: usize,
146 #[pyo3(get)]
147 pub end_pos: usize,
148}
149
150#[pymethods]
151impl PestToken {
152 #[getter]
153 #[pyo3(name = "type")]
154 fn type_name(&self, py: Python<'_>) -> Py<PyString> {
155 self.type_name_py.clone_ref(py)
156 }
157
158 #[getter]
159 fn value(&self, py: Python<'_>) -> Py<PyString> {
160 self.value_py.clone_ref(py)
161 }
162
163 fn __repr__(&self, py: Python<'_>) -> String {
164 format!(
165 "PestToken({}={:?})",
166 self.type_name_py.bind(py).to_string_lossy(),
167 self.value_py.bind(py).to_string_lossy()
168 )
169 }
170
171 fn __str__(&self, py: Python<'_>) -> String {
172 self.value_py.bind(py).to_string_lossy().into_owned()
173 }
174}
175
176struct LineIndex {
182 char_byte_starts: Vec<usize>,
185 line_starts_char: Vec<usize>,
187}
188
189impl LineIndex {
190 fn new(text: &str) -> Self {
191 let mut char_byte_starts = Vec::with_capacity(text.len() + 1);
192 let mut line_starts_char = Vec::with_capacity(text.len() / 40 + 1);
193 line_starts_char.push(0);
194 let mut char_idx = 0usize;
195 for (byte_offset, ch) in text.char_indices() {
196 char_byte_starts.push(byte_offset);
197 if ch == '\n' {
198 line_starts_char.push(char_idx + 1);
199 }
200 char_idx += 1;
201 }
202 char_byte_starts.push(text.len());
203 Self {
204 char_byte_starts,
205 line_starts_char,
206 }
207 }
208
209 fn byte_to_char(&self, byte_pos: usize) -> usize {
210 match self.char_byte_starts.binary_search(&byte_pos) {
211 Ok(i) => i,
212 Err(i) => i - 1,
213 }
214 }
215
216 fn line_col(&self, byte_pos: usize) -> (usize, usize, usize) {
218 let char_pos = self.byte_to_char(byte_pos);
219 let idx = match self.line_starts_char.binary_search(&char_pos) {
220 Ok(i) => i,
221 Err(i) => i - 1,
222 };
223 let line_start = self.line_starts_char[idx];
224 (idx + 1, char_pos - line_start + 1, char_pos)
225 }
226}
227
228fn span_positions(pair: &Pair<Rule>, idx: &LineIndex) -> (usize, usize, usize, usize, usize, usize) {
229 let span = pair.as_span();
230 let (start_line, start_col, start_char) = idx.line_col(span.start());
231 let (end_line, end_col, end_char) = idx.line_col(span.end());
232 (start_line, start_col, end_line, end_col, start_char, end_char)
233}
234
235type SpanTuple = (usize, usize, usize, usize, usize, usize);
236
237fn make_node(
238 py: Python<'_>,
239 name_py: Py<PyString>,
240 list: Bound<PyList>,
241 span: SpanTuple,
242) -> PyResult<Py<PyAny>> {
243 let (line, column, end_line, end_column, start_pos, end_pos) = span;
244 let node = PestNode {
245 data_py: name_py,
246 children_list: list.unbind(),
247 line,
248 column,
249 end_line,
250 end_column,
251 start_pos,
252 end_pos,
253 };
254 Ok(Py::new(py, node)?.into_any())
255}
256
257fn convert_access_chain(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex) -> PyResult<Py<PyAny>> {
261 let span = span_positions(&pair, idx);
262 let mut inner = pair.into_inner();
263 let atom_pair = inner
264 .next()
265 .expect("access_chain must contain an atom as first child");
266
267 let mut tails: Vec<(Rule, Pair<Rule>)> = Vec::new();
268 for tail in inner {
269 let kind = tail.as_rule();
270 let payload = tail
271 .into_inner()
272 .next()
273 .expect("tail rule must have one payload child");
274 tails.push((kind, payload));
275 }
276
277 if tails.is_empty() {
278 return convert_pair(py, atom_pair, idx);
279 }
280
281 let atom_py = convert_pair(py, atom_pair, idx)?;
282
283 let tail_name = |kind: Rule, payload: &Pair<Rule>| -> &'static str {
284 match kind {
285 Rule::dot_tail => "attr_access",
286 Rule::bracket_tail => match payload.as_rule() {
287 Rule::int_lit => "index_access",
288 _ => "map_key_access",
289 },
290 Rule::dcolon_tail => "fcast",
291 _ => "chained_access",
292 }
293 };
294
295 let last_is_cast = matches!(tails.last().map(|(k, _)| *k), Some(Rule::dcolon_tail));
296
297 if tails.len() == 1 {
298 let (kind, payload) = tails.pop().unwrap();
299 let name = tail_name(kind, &payload);
300 let list = PyList::empty(py);
301 list.append(atom_py)?;
302 list.append(convert_pair(py, payload, idx)?)?;
303 return make_node(py, intern_static(py, name), list, span);
304 }
305
306 if last_is_cast {
307 let (_, cast_payload) = tails.pop().unwrap();
308 let inner_name: &'static str = if tails.len() == 1 {
309 let (k, p) = &tails[0];
310 tail_name(*k, p)
311 } else {
312 "chained_access"
313 };
314 let inner_list = PyList::empty(py);
315 inner_list.append(atom_py)?;
316 for (_, payload) in tails.into_iter() {
317 inner_list.append(convert_pair(py, payload, idx)?)?;
318 }
319 let inner_node = make_node(py, intern_static(py, inner_name), inner_list, span)?;
320
321 let outer_list = PyList::empty(py);
322 outer_list.append(inner_node)?;
323 outer_list.append(convert_pair(py, cast_payload, idx)?)?;
324 return make_node(py, intern_static(py, "fcast"), outer_list, span);
325 }
326
327 let list = PyList::empty(py);
328 list.append(atom_py)?;
329 for (_, payload) in tails.into_iter() {
330 list.append(convert_pair(py, payload, idx)?)?;
331 }
332 make_node(py, intern_static(py, "chained_access"), list, span)
333}
334
335fn convert_pair(py: Python<'_>, pair: Pair<Rule>, idx: &LineIndex) -> PyResult<Py<PyAny>> {
336 let rule = pair.as_rule();
337 if matches!(rule, Rule::access_chain) {
338 return convert_access_chain(py, pair, idx);
339 }
340
341 let (name_py, is_token, _is_silent) = rule_info(py, rule);
342 let span = span_positions(&pair, idx);
343 let (line, column, end_line, end_column, start_pos, end_pos) = span;
344
345 if is_token {
346 let value_py = PyString::new(py, pair.as_str()).unbind();
347 let token = PestToken {
348 type_name_py: name_py,
349 value_py,
350 line,
351 column,
352 end_line,
353 end_column,
354 start_pos,
355 end_pos,
356 };
357 Ok(Py::new(py, token)?.into_any())
358 } else {
359 let list = PyList::empty(py);
360 for inner in pair.into_inner() {
361 if matches!(inner.as_rule(), Rule::EOI) {
362 continue;
363 }
364 let (_, _, inner_silent) = rule_info(py, inner.as_rule());
365 if inner_silent {
366 continue;
367 }
368 let child = convert_pair(py, inner, idx)?;
369 list.append(child)?;
370 }
371 make_node(py, name_py, list, span)
372 }
373}
374
375#[pyfunction]
378pub fn parse_trilogy_syntax(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
379 let idx = LineIndex::new(text);
380 let mut pairs = TrilogyParser::parse(Rule::start, text).map_err(|e| {
381 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))
382 })?;
383 let start = pairs
384 .next()
385 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("empty parse result"))?;
386 convert_pair(py, start, &idx)
387}
388
389fn convert_access_chain_tuple<'py>(
396 py: Python<'py>,
397 pair: Pair<Rule>,
398 idx: &LineIndex,
399) -> PyResult<Bound<'py, PyTuple>> {
400 let span = span_positions(&pair, idx);
401 let mut inner = pair.into_inner();
402 let atom_pair = inner
403 .next()
404 .expect("access_chain must contain an atom as first child");
405
406 let mut tails: Vec<(Rule, Pair<Rule>)> = Vec::new();
407 for tail in inner {
408 let kind = tail.as_rule();
409 let payload = tail
410 .into_inner()
411 .next()
412 .expect("tail rule must have one payload child");
413 tails.push((kind, payload));
414 }
415
416 if tails.is_empty() {
417 return convert_pair_tuple(py, atom_pair, idx);
418 }
419
420 let atom_tup = convert_pair_tuple(py, atom_pair, idx)?;
421
422 let tail_name = |kind: Rule, payload: &Pair<Rule>| -> &'static str {
423 match kind {
424 Rule::dot_tail => "attr_access",
425 Rule::bracket_tail => match payload.as_rule() {
426 Rule::int_lit => "index_access",
427 _ => "map_key_access",
428 },
429 Rule::dcolon_tail => "fcast",
430 _ => "chained_access",
431 }
432 };
433
434 let last_is_cast = matches!(tails.last().map(|(k, _)| *k), Some(Rule::dcolon_tail));
435
436 if tails.len() == 1 {
437 let (kind, payload) = tails.pop().unwrap();
438 let name = tail_name(kind, &payload);
439 let payload_tup = convert_pair_tuple(py, payload, idx)?;
440 let children = PyTuple::new(py, [atom_tup.into_any(), payload_tup.into_any()])?;
441 return make_node_tuple(py, intern_static(py, name), children, span);
442 }
443
444 if last_is_cast {
445 let (_, cast_payload) = tails.pop().unwrap();
446 let inner_name: &'static str = if tails.len() == 1 {
447 let (k, p) = &tails[0];
448 tail_name(*k, p)
449 } else {
450 "chained_access"
451 };
452 let mut inner_items: Vec<Py<PyAny>> = Vec::with_capacity(tails.len() + 1);
453 inner_items.push(atom_tup.into_any().unbind());
454 for (_, payload) in tails.into_iter() {
455 inner_items.push(convert_pair_tuple(py, payload, idx)?.into_any().unbind());
456 }
457 let inner_children = PyTuple::new(py, inner_items)?;
458 let inner_node = make_node_tuple(py, intern_static(py, inner_name), inner_children, span)?;
459
460 let cast_tup = convert_pair_tuple(py, cast_payload, idx)?;
461 let outer_children = PyTuple::new(py, [inner_node.into_any(), cast_tup.into_any()])?;
462 return make_node_tuple(py, intern_static(py, "fcast"), outer_children, span);
463 }
464
465 let mut items: Vec<Py<PyAny>> = Vec::with_capacity(tails.len() + 1);
466 items.push(atom_tup.into_any().unbind());
467 for (_, payload) in tails.into_iter() {
468 items.push(convert_pair_tuple(py, payload, idx)?.into_any().unbind());
469 }
470 let children = PyTuple::new(py, items)?;
471 make_node_tuple(py, intern_static(py, "chained_access"), children, span)
472}
473
474fn make_node_tuple<'py>(
475 py: Python<'py>,
476 name_py: Py<PyString>,
477 children: Bound<'py, PyTuple>,
478 span: SpanTuple,
479) -> PyResult<Bound<'py, PyTuple>> {
480 let (line, column, end_line, end_column, start_pos, end_pos) = span;
481 (
482 name_py,
483 children,
484 line,
485 column,
486 end_line,
487 end_column,
488 start_pos,
489 end_pos,
490 )
491 .into_pyobject(py)
492}
493
494fn convert_pair_tuple<'py>(
495 py: Python<'py>,
496 pair: Pair<Rule>,
497 idx: &LineIndex,
498) -> PyResult<Bound<'py, PyTuple>> {
499 let rule = pair.as_rule();
500 if matches!(rule, Rule::access_chain) {
501 return convert_access_chain_tuple(py, pair, idx);
502 }
503
504 let (name_py, is_token, _is_silent) = rule_info(py, rule);
505 let span = span_positions(&pair, idx);
506 let (line, column, end_line, end_column, start_pos, end_pos) = span;
507
508 if is_token {
509 let value_py = PyString::new(py, pair.as_str()).unbind();
510 (
511 name_py,
512 value_py,
513 line,
514 column,
515 end_line,
516 end_column,
517 start_pos,
518 end_pos,
519 )
520 .into_pyobject(py)
521 } else {
522 let mut items: Vec<Py<PyAny>> = Vec::new();
523 for inner in pair.into_inner() {
524 if matches!(inner.as_rule(), Rule::EOI) {
525 continue;
526 }
527 let (_, _, inner_silent) = rule_info(py, inner.as_rule());
528 if inner_silent {
529 continue;
530 }
531 items.push(convert_pair_tuple(py, inner, idx)?.into_any().unbind());
532 }
533 let children = PyTuple::new(py, items)?;
534 make_node_tuple(py, name_py, children, span)
535 }
536}
537
538#[pyfunction]
540pub fn parse_trilogy_syntax_tuple(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
541 let idx = LineIndex::new(text);
542 let mut pairs = TrilogyParser::parse(Rule::start, text).map_err(|e| {
543 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))
544 })?;
545 let start = pairs
546 .next()
547 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("empty parse result"))?;
548 Ok(convert_pair_tuple(py, start, &idx)?.into_any().unbind())
549}
550
551#[pyfunction]
555pub fn parse_trilogy_syntax_count(text: &str) -> PyResult<usize> {
556 let mut pairs = TrilogyParser::parse(Rule::start, text).map_err(|e| {
557 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e))
558 })?;
559 let start = pairs
560 .next()
561 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("empty parse result"))?;
562 fn walk(pair: Pair<Rule>) -> usize {
563 let mut n = 1;
564 for inner in pair.into_inner() {
565 n += walk(inner);
566 }
567 n
568 }
569 Ok(walk(start))
570}
571}
572
573#[cfg(feature = "python")]
574pub use python_impl::{
575 parse_trilogy_syntax, parse_trilogy_syntax_count, parse_trilogy_syntax_tuple, PestNode,
576 PestToken,
577};