1use std::collections::{BTreeMap, HashMap, HashSet};
43use std::fmt;
44
45use harn_vm::VmValue;
46
47use super::symbol_graph::{EdgeKind, NodeId, NodeKind, SymbolGraph};
48
49pub const MAX_ROWS: usize = 10_000;
56
57pub const MAX_PATTERNS: usize = 3;
64
65const VAR_LENGTH_MAX_DEPTH: u32 = 4;
69
70#[derive(Debug, Clone, Eq, PartialEq)]
75pub enum CypherError {
76 LexError(String),
78 ParseError(String),
80 ExecError(String),
83}
84
85impl fmt::Display for CypherError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 CypherError::LexError(s) => write!(f, "lex error: {s}"),
89 CypherError::ParseError(s) => write!(f, "parse error: {s}"),
90 CypherError::ExecError(s) => write!(f, "exec error: {s}"),
91 }
92 }
93}
94
95impl std::error::Error for CypherError {}
96
97#[derive(Debug, Clone, Eq, PartialEq)]
99pub enum CypherValue {
100 Null,
102 String(String),
104 Int(i64),
106 Bool(bool),
108}
109
110impl CypherValue {
111 pub fn to_vm(&self) -> VmValue {
113 match self {
114 CypherValue::Null => VmValue::Nil,
115 CypherValue::String(s) => VmValue::String(arcstr::ArcStr::from(s.as_str())),
116 CypherValue::Int(n) => VmValue::Int(*n),
117 CypherValue::Bool(b) => VmValue::Bool(*b),
118 }
119 }
120}
121
122pub type CypherRow = BTreeMap<String, CypherValue>;
124
125pub fn execute(query: &str, graph: &SymbolGraph) -> Result<Vec<CypherRow>, CypherError> {
127 let tokens = lex(query)?;
128 let ast = parse(&tokens)?;
129 exec(&ast, graph)
130}
131
132#[derive(Debug, Clone, PartialEq)]
137enum Token {
138 Keyword(String),
140 Ident(String),
141 Str(String),
142 Int(i64),
143 LParen,
144 RParen,
145 LBrace,
146 RBrace,
147 LBracket,
148 RBracket,
149 Colon,
150 Comma,
151 Dot,
152 Eq,
153 Neq,
154 Lt,
155 Le,
156 Gt,
157 Ge,
158 Star,
159 DotDot,
160 Dash,
161 Arrow, LeftArrow, }
164
165#[expect(
166 clippy::string_slice,
167 reason = "every slice bound is at or right after an ASCII byte, hence a char boundary"
168)]
169fn lex(input: &str) -> Result<Vec<Token>, CypherError> {
170 let mut out: Vec<Token> = Vec::new();
171 let bytes = input.as_bytes();
172 let mut i = 0;
173 while i < bytes.len() {
174 let b = bytes[i];
175 if b.is_ascii_whitespace() {
176 i += 1;
177 continue;
178 }
179 if b == b'(' {
180 out.push(Token::LParen);
181 i += 1;
182 continue;
183 }
184 if b == b')' {
185 out.push(Token::RParen);
186 i += 1;
187 continue;
188 }
189 if b == b'{' {
190 out.push(Token::LBrace);
191 i += 1;
192 continue;
193 }
194 if b == b'}' {
195 out.push(Token::RBrace);
196 i += 1;
197 continue;
198 }
199 if b == b'[' {
200 out.push(Token::LBracket);
201 i += 1;
202 continue;
203 }
204 if b == b']' {
205 out.push(Token::RBracket);
206 i += 1;
207 continue;
208 }
209 if b == b':' {
210 out.push(Token::Colon);
211 i += 1;
212 continue;
213 }
214 if b == b',' {
215 out.push(Token::Comma);
216 i += 1;
217 continue;
218 }
219 if b == b'*' {
220 out.push(Token::Star);
221 i += 1;
222 continue;
223 }
224 if b == b'=' {
225 out.push(Token::Eq);
226 i += 1;
227 continue;
228 }
229 if b == b'.' {
230 if i + 1 < bytes.len() && bytes[i + 1] == b'.' {
231 out.push(Token::DotDot);
232 i += 2;
233 } else {
234 out.push(Token::Dot);
235 i += 1;
236 }
237 continue;
238 }
239 if b == b'<' {
240 if i + 1 < bytes.len() && bytes[i + 1] == b'-' {
241 out.push(Token::LeftArrow);
242 i += 2;
243 } else if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
244 out.push(Token::Le);
245 i += 2;
246 } else if i + 1 < bytes.len() && bytes[i + 1] == b'>' {
247 out.push(Token::Neq);
248 i += 2;
249 } else {
250 out.push(Token::Lt);
251 i += 1;
252 }
253 continue;
254 }
255 if b == b'>' {
256 if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
257 out.push(Token::Ge);
258 i += 2;
259 } else {
260 out.push(Token::Gt);
261 i += 1;
262 }
263 continue;
264 }
265 if b == b'!' {
266 if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
267 out.push(Token::Neq);
268 i += 2;
269 continue;
270 }
271 return Err(CypherError::LexError(format!(
272 "unexpected '!' at byte {i} (expected '!=')"
273 )));
274 }
275 if b == b'-' {
276 if i + 1 < bytes.len() && bytes[i + 1] == b'>' {
277 out.push(Token::Arrow);
278 i += 2;
279 } else {
280 out.push(Token::Dash);
281 i += 1;
282 }
283 continue;
284 }
285 if b == b'\'' || b == b'"' {
286 let quote = b;
287 let start = i + 1;
288 i += 1;
289 while i < bytes.len() && bytes[i] != quote {
290 if bytes[i] == b'\\' && i + 1 < bytes.len() {
291 i += 2;
292 } else {
293 i += 1;
294 }
295 }
296 if i >= bytes.len() {
297 return Err(CypherError::LexError("unterminated string literal".into()));
298 }
299 let raw = &input[start..i];
300 out.push(Token::Str(unescape(raw)));
301 i += 1;
302 continue;
303 }
304 if b.is_ascii_digit() {
305 let start = i;
306 while i < bytes.len() && bytes[i].is_ascii_digit() {
307 i += 1;
308 }
309 let n: i64 = input[start..i].parse().map_err(|err| {
310 CypherError::LexError(format!("bad integer at byte {start}: {err}"))
311 })?;
312 out.push(Token::Int(n));
313 continue;
314 }
315 if b.is_ascii_alphabetic() || b == b'_' {
316 let start = i;
317 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
318 i += 1;
319 }
320 let word = &input[start..i];
321 let upper = word.to_ascii_uppercase();
322 if is_keyword(&upper) {
323 out.push(Token::Keyword(upper));
324 } else {
325 out.push(Token::Ident(word.to_string()));
326 }
327 continue;
328 }
329 return Err(CypherError::LexError(format!(
330 "unexpected character `{}` at byte {i}",
331 char::from(b)
332 )));
333 }
334 Ok(out)
335}
336
337fn is_keyword(word: &str) -> bool {
338 matches!(
339 word,
340 "MATCH" | "WHERE" | "RETURN" | "AND" | "OR" | "AS" | "NOT" | "TRUE" | "FALSE"
341 )
342}
343
344fn unescape(raw: &str) -> String {
345 let mut out = String::with_capacity(raw.len());
346 let mut iter = raw.chars();
347 while let Some(c) = iter.next() {
348 if c == '\\' {
349 if let Some(next) = iter.next() {
350 out.push(match next {
351 'n' => '\n',
352 't' => '\t',
353 'r' => '\r',
354 '\\' => '\\',
355 '\'' => '\'',
356 '"' => '"',
357 other => other,
358 });
359 }
360 } else {
361 out.push(c);
362 }
363 }
364 out
365}
366
367#[derive(Debug, Clone)]
372struct Query {
373 matches: Vec<Pattern>,
374 where_clause: Option<Expr>,
375 projections: Vec<Projection>,
376}
377
378#[derive(Debug, Clone)]
379struct Pattern {
380 head: NodePat,
381 steps: Vec<RelStep>,
382}
383
384#[derive(Debug, Clone)]
385struct NodePat {
386 var: String,
387 label: Option<String>,
388 props: BTreeMap<String, Literal>,
389}
390
391#[derive(Debug, Clone)]
392struct RelStep {
393 forward: bool,
395 edge_label: String,
396 var_length: Option<(u32, u32)>,
397 target: NodePat,
398}
399
400#[derive(Debug, Clone)]
401enum Expr {
402 And(Box<Expr>, Box<Expr>),
403 Or(Box<Expr>, Box<Expr>),
404 Not(Box<Expr>),
405 Compare(Operand, CmpOp, Operand),
406 Bool(bool),
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410enum CmpOp {
411 Eq,
412 Neq,
413 Lt,
414 Le,
415 Gt,
416 Ge,
417}
418
419#[derive(Debug, Clone)]
420enum Operand {
421 Path {
423 var: String,
424 property: Option<String>,
425 },
426 Literal(Literal),
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
430enum Literal {
431 Str(String),
432 Int(i64),
433 Bool(bool),
434}
435
436#[derive(Debug, Clone)]
437struct Projection {
438 operand: Operand,
439 alias: String,
443 explicit_alias: bool,
448}
449
450struct Parser<'a> {
455 tokens: &'a [Token],
456 pos: usize,
457}
458
459fn parse(tokens: &[Token]) -> Result<Query, CypherError> {
460 let mut p = Parser { tokens, pos: 0 };
461 let q = p.parse_query()?;
462 if p.pos != tokens.len() {
463 return Err(CypherError::ParseError(format!(
464 "trailing tokens after RETURN at position {} ({:?})",
465 p.pos, tokens[p.pos]
466 )));
467 }
468 Ok(q)
469}
470
471impl<'a> Parser<'a> {
472 fn peek(&self) -> Option<&Token> {
473 self.tokens.get(self.pos)
474 }
475
476 fn advance(&mut self) -> Option<&Token> {
477 let t = self.tokens.get(self.pos);
478 self.pos += 1;
479 t
480 }
481
482 fn expect_keyword(&mut self, word: &str) -> Result<(), CypherError> {
483 match self.advance() {
484 Some(Token::Keyword(k)) if k == word => Ok(()),
485 other => Err(CypherError::ParseError(format!(
486 "expected `{word}`, got {other:?}"
487 ))),
488 }
489 }
490
491 fn match_keyword(&mut self, word: &str) -> bool {
492 if matches!(self.peek(), Some(Token::Keyword(k)) if k == word) {
493 self.pos += 1;
494 true
495 } else {
496 false
497 }
498 }
499
500 fn expect(&mut self, want: &Token) -> Result<(), CypherError> {
501 let next = self.advance();
502 if next == Some(want) {
503 Ok(())
504 } else {
505 Err(CypherError::ParseError(format!(
506 "expected {want:?}, got {next:?}"
507 )))
508 }
509 }
510
511 fn parse_query(&mut self) -> Result<Query, CypherError> {
512 self.expect_keyword("MATCH")?;
513 let mut matches = vec![self.parse_pattern()?];
514 while matches!(self.peek(), Some(Token::Comma)) {
515 self.advance();
516 matches.push(self.parse_pattern()?);
517 if matches.len() > MAX_PATTERNS {
518 return Err(CypherError::ParseError(format!(
519 "too many disjoint MATCH patterns (got {}, cap is {}); \
520 join through edge syntax (`-[:KIND]->`) instead of a cartesian product",
521 matches.len(),
522 MAX_PATTERNS
523 )));
524 }
525 }
526 let where_clause = if self.match_keyword("WHERE") {
527 Some(self.parse_expr()?)
528 } else {
529 None
530 };
531 self.expect_keyword("RETURN")?;
532 let mut projections = vec![self.parse_projection()?];
533 while matches!(self.peek(), Some(Token::Comma)) {
534 self.advance();
535 projections.push(self.parse_projection()?);
536 }
537 resolve_projection_aliases(&mut projections)?;
538 Ok(Query {
539 matches,
540 where_clause,
541 projections,
542 })
543 }
544
545 fn parse_pattern(&mut self) -> Result<Pattern, CypherError> {
546 let head = self.parse_node_pat()?;
547 let mut steps: Vec<RelStep> = Vec::new();
548 loop {
549 let forward = match self.peek() {
551 Some(Token::Dash) => true,
552 Some(Token::LeftArrow) => false,
553 _ => break,
554 };
555 self.advance(); self.expect(&Token::LBracket)?;
557 self.expect(&Token::Colon)?;
558 let edge_label = match self.advance() {
559 Some(Token::Ident(s)) => s.clone(),
560 other => {
561 return Err(CypherError::ParseError(format!(
562 "expected edge label, got {other:?}"
563 )))
564 }
565 };
566 let var_length = if matches!(self.peek(), Some(Token::Star)) {
567 self.advance();
568 let lo = if matches!(self.peek(), Some(Token::Int(_))) {
569 if let Some(Token::Int(n)) = self.advance() {
570 Some(*n)
571 } else {
572 None
573 }
574 } else {
575 None
576 };
577 let bounds = if matches!(self.peek(), Some(Token::DotDot)) {
578 self.advance();
579 let hi = if matches!(self.peek(), Some(Token::Int(_))) {
580 if let Some(Token::Int(n)) = self.advance() {
581 Some(*n)
582 } else {
583 None
584 }
585 } else {
586 None
587 };
588 let hi_v = hi.map(|n| n as u32).unwrap_or(VAR_LENGTH_MAX_DEPTH);
592 (lo.unwrap_or(1) as u32, hi_v)
593 } else {
594 let lo_v = lo.unwrap_or(1) as u32;
596 (lo_v, lo_v.max(3))
597 };
598 Some(bounds)
599 } else {
600 None
601 };
602 self.expect(&Token::RBracket)?;
603 if forward {
604 self.expect(&Token::Arrow)?;
605 } else {
606 self.expect(&Token::Dash)?;
607 }
608 let target = self.parse_node_pat()?;
609 steps.push(RelStep {
610 forward,
611 edge_label,
612 var_length,
613 target,
614 });
615 }
616 Ok(Pattern { head, steps })
617 }
618
619 fn parse_node_pat(&mut self) -> Result<NodePat, CypherError> {
620 self.expect(&Token::LParen)?;
621 let var = match self.advance() {
622 Some(Token::Ident(s)) => s.clone(),
623 other => {
624 return Err(CypherError::ParseError(format!(
625 "expected node variable, got {other:?}"
626 )))
627 }
628 };
629 let label = if matches!(self.peek(), Some(Token::Colon)) {
630 self.advance();
631 match self.advance() {
632 Some(Token::Ident(s)) => Some(s.clone()),
633 other => {
634 return Err(CypherError::ParseError(format!(
635 "expected node label, got {other:?}"
636 )))
637 }
638 }
639 } else {
640 None
641 };
642 let props = if matches!(self.peek(), Some(Token::LBrace)) {
643 self.parse_props()?
644 } else {
645 BTreeMap::new()
646 };
647 self.expect(&Token::RParen)?;
648 Ok(NodePat { var, label, props })
649 }
650
651 fn parse_props(&mut self) -> Result<BTreeMap<String, Literal>, CypherError> {
652 self.expect(&Token::LBrace)?;
653 let mut props: BTreeMap<String, Literal> = BTreeMap::new();
654 loop {
655 let key = match self.advance() {
656 Some(Token::Ident(s)) => s.clone(),
657 other => {
658 return Err(CypherError::ParseError(format!(
659 "expected property key, got {other:?}"
660 )))
661 }
662 };
663 self.expect(&Token::Colon)?;
664 let value = self.parse_literal()?;
665 props.insert(key, value);
666 match self.peek() {
667 Some(Token::Comma) => {
668 self.advance();
669 }
670 Some(Token::RBrace) => break,
671 other => {
672 return Err(CypherError::ParseError(format!(
673 "expected ',' or '}}', got {other:?}"
674 )))
675 }
676 }
677 }
678 self.expect(&Token::RBrace)?;
679 Ok(props)
680 }
681
682 fn parse_literal(&mut self) -> Result<Literal, CypherError> {
683 match self.advance() {
684 Some(Token::Str(s)) => Ok(Literal::Str(s.clone())),
685 Some(Token::Int(n)) => Ok(Literal::Int(*n)),
686 Some(Token::Keyword(k)) if k == "TRUE" => Ok(Literal::Bool(true)),
687 Some(Token::Keyword(k)) if k == "FALSE" => Ok(Literal::Bool(false)),
688 other => Err(CypherError::ParseError(format!(
689 "expected literal, got {other:?}"
690 ))),
691 }
692 }
693
694 fn parse_expr(&mut self) -> Result<Expr, CypherError> {
695 let mut left = self.parse_and_expr()?;
696 while self.match_keyword("OR") {
697 let right = self.parse_and_expr()?;
698 left = Expr::Or(Box::new(left), Box::new(right));
699 }
700 Ok(left)
701 }
702
703 fn parse_and_expr(&mut self) -> Result<Expr, CypherError> {
704 let mut left = self.parse_unary_expr()?;
705 while self.match_keyword("AND") {
706 let right = self.parse_unary_expr()?;
707 left = Expr::And(Box::new(left), Box::new(right));
708 }
709 Ok(left)
710 }
711
712 fn parse_unary_expr(&mut self) -> Result<Expr, CypherError> {
713 if self.match_keyword("NOT") {
714 let inner = self.parse_unary_expr()?;
715 return Ok(Expr::Not(Box::new(inner)));
716 }
717 self.parse_compare()
718 }
719
720 fn parse_compare(&mut self) -> Result<Expr, CypherError> {
721 if matches!(
722 self.peek(),
723 Some(Token::Keyword(k)) if k == "TRUE" || k == "FALSE"
724 ) {
725 let lit = self.parse_literal()?;
726 return Ok(match lit {
727 Literal::Bool(b) => Expr::Bool(b),
728 _ => unreachable!(),
729 });
730 }
731 let left = self.parse_operand()?;
732 let op = match self.peek() {
733 Some(Token::Eq) => Some(CmpOp::Eq),
734 Some(Token::Neq) => Some(CmpOp::Neq),
735 Some(Token::Lt) => Some(CmpOp::Lt),
736 Some(Token::Le) => Some(CmpOp::Le),
737 Some(Token::Gt) => Some(CmpOp::Gt),
738 Some(Token::Ge) => Some(CmpOp::Ge),
739 _ => None,
740 };
741 let Some(op) = op else {
742 return Err(CypherError::ParseError(
743 "expected comparison operator in WHERE clause".into(),
744 ));
745 };
746 self.advance();
747 let right = self.parse_operand()?;
748 Ok(Expr::Compare(left, op, right))
749 }
750
751 fn parse_operand(&mut self) -> Result<Operand, CypherError> {
752 match self.peek() {
753 Some(Token::Str(_)) | Some(Token::Int(_)) => {
754 let lit = self.parse_literal()?;
755 Ok(Operand::Literal(lit))
756 }
757 Some(Token::Keyword(k)) if k == "TRUE" || k == "FALSE" => {
758 let lit = self.parse_literal()?;
759 Ok(Operand::Literal(lit))
760 }
761 Some(Token::Ident(_)) => {
762 let var = if let Some(Token::Ident(s)) = self.advance() {
763 s.clone()
764 } else {
765 unreachable!()
766 };
767 let property = if matches!(self.peek(), Some(Token::Dot)) {
768 self.advance();
769 match self.advance() {
770 Some(Token::Ident(s)) => Some(s.clone()),
771 other => {
772 return Err(CypherError::ParseError(format!(
773 "expected property name after '.', got {other:?}"
774 )))
775 }
776 }
777 } else {
778 None
779 };
780 Ok(Operand::Path { var, property })
781 }
782 other => Err(CypherError::ParseError(format!(
783 "expected operand, got {other:?}"
784 ))),
785 }
786 }
787
788 fn parse_projection(&mut self) -> Result<Projection, CypherError> {
789 let operand = self.parse_operand()?;
790 let alias = if self.match_keyword("AS") {
791 match self.advance() {
792 Some(Token::Ident(s)) => Some(s.clone()),
793 other => {
794 return Err(CypherError::ParseError(format!(
795 "expected alias after AS, got {other:?}"
796 )))
797 }
798 }
799 } else {
800 None
801 };
802 let explicit_alias = alias.is_some();
803 Ok(Projection {
804 operand,
805 alias: alias.unwrap_or_default(),
806 explicit_alias,
807 })
808 }
809}
810
811fn default_alias(operand: &Operand) -> String {
812 match operand {
813 Operand::Path {
814 var,
815 property: None,
816 } => var.clone(),
817 Operand::Path {
818 var,
819 property: Some(p),
820 } => format!("{var}.{p}"),
821 Operand::Literal(_) => "value".to_string(),
822 }
823}
824
825fn resolve_projection_aliases(projections: &mut [Projection]) -> Result<(), CypherError> {
837 use std::collections::HashSet;
838
839 let mut seen: HashSet<String> = HashSet::new();
841 for proj in projections.iter() {
842 if proj.explicit_alias && !seen.insert(proj.alias.clone()) {
843 return Err(CypherError::ParseError(format!(
844 "duplicate alias `{}` in RETURN clause",
845 proj.alias
846 )));
847 }
848 }
849
850 let mut literal_count: usize = 0;
852 for proj in projections.iter_mut() {
853 if proj.explicit_alias {
854 continue;
855 }
856 let base = default_alias(&proj.operand);
857 let is_literal_default = matches!(proj.operand, Operand::Literal(_));
858 let mut candidate = base.clone();
859 if is_literal_default {
860 literal_count += 1;
861 if literal_count >= 2 {
862 candidate = format!("{base}_{literal_count}");
863 }
864 }
865 let mut bump: usize = 2;
868 while seen.contains(&candidate) {
869 candidate = format!("{base}_{bump}");
870 bump += 1;
871 }
872 seen.insert(candidate.clone());
873 proj.alias = candidate;
874 }
875 Ok(())
876}
877
878fn exec(query: &Query, graph: &SymbolGraph) -> Result<Vec<CypherRow>, CypherError> {
883 let mut all_rows: Vec<HashMap<String, NodeId>> = vec![HashMap::new()];
884 for pattern in &query.matches {
885 let mut new_rows: Vec<HashMap<String, NodeId>> = Vec::new();
886 for binding in &all_rows {
887 let candidates = match_pattern(pattern, graph, binding)?;
888 for cand in candidates {
889 let mut merged = binding.clone();
890 for (k, v) in cand {
891 merged.insert(k, v);
892 }
893 new_rows.push(merged);
894 if new_rows.len() > MAX_ROWS {
899 return Err(CypherError::ExecError(format!(
900 "row budget exceeded ({} > {}); narrow the MATCH or add a WHERE clause",
901 new_rows.len(),
902 MAX_ROWS
903 )));
904 }
905 }
906 }
907 all_rows = new_rows;
908 }
909
910 let mut out: Vec<CypherRow> = Vec::new();
911 for binding in all_rows {
912 if let Some(expr) = &query.where_clause {
913 if !eval_bool(expr, &binding, graph)? {
914 continue;
915 }
916 }
917 let mut row: CypherRow = BTreeMap::new();
918 for proj in &query.projections {
919 let v = eval_operand(&proj.operand, &binding, graph)?;
920 row.insert(proj.alias.clone(), v);
921 }
922 out.push(row);
923 if out.len() > MAX_ROWS {
927 return Err(CypherError::ExecError(format!(
928 "row budget exceeded ({} > {}); narrow the MATCH or add a WHERE clause",
929 out.len(),
930 MAX_ROWS
931 )));
932 }
933 }
934 Ok(out)
935}
936
937fn match_pattern(
938 pattern: &Pattern,
939 graph: &SymbolGraph,
940 seed: &HashMap<String, NodeId>,
941) -> Result<Vec<HashMap<String, NodeId>>, CypherError> {
942 let head_candidates: Vec<NodeId> = if let Some(existing) = seed.get(&pattern.head.var) {
943 if node_matches(graph, *existing, &pattern.head) {
945 vec![*existing]
946 } else {
947 vec![]
948 }
949 } else {
950 candidate_nodes(graph, &pattern.head)
951 };
952
953 let mut out: Vec<HashMap<String, NodeId>> = Vec::new();
954 for head_id in head_candidates {
955 let mut binding: HashMap<String, NodeId> = HashMap::new();
956 binding.insert(pattern.head.var.clone(), head_id);
957 extend_steps(graph, &pattern.steps, 0, head_id, binding, &mut out)?;
958 }
959 Ok(out)
960}
961
962fn extend_steps(
963 graph: &SymbolGraph,
964 steps: &[RelStep],
965 idx: usize,
966 cursor: NodeId,
967 binding: HashMap<String, NodeId>,
968 out: &mut Vec<HashMap<String, NodeId>>,
969) -> Result<(), CypherError> {
970 if idx == steps.len() {
971 out.push(binding);
972 return Ok(());
973 }
974 let step = &steps[idx];
975 let (kind, label_reversed) =
976 EdgeKind::parse_with_direction(&step.edge_label).ok_or_else(|| {
977 CypherError::ExecError(format!("unknown edge label `{}`", step.edge_label))
978 })?;
979 let forward = step.forward ^ label_reversed;
981 let (lo, hi) = step.var_length.unwrap_or((1, 1));
982 if hi > VAR_LENGTH_MAX_DEPTH {
983 return Err(CypherError::ExecError(format!(
984 "variable-length traversal capped at depth {VAR_LENGTH_MAX_DEPTH} (got `*{lo}..{hi}`)"
985 )));
986 }
987 let lo = lo.max(1);
988 let hi = hi.max(lo);
989
990 let mut visited: HashSet<NodeId> = HashSet::new();
992 visited.insert(cursor);
993 let mut stack: Vec<(NodeId, u32, HashSet<NodeId>)> = vec![(cursor, 0, visited)];
994 while let Some((node, depth, visited)) = stack.pop() {
995 if depth >= hi {
996 continue;
997 }
998 let edges = if forward {
999 graph.outgoing(node)
1000 } else {
1001 graph.incoming(node)
1002 };
1003 for edge in edges {
1004 if edge.kind != kind {
1005 continue;
1006 }
1007 let next = if forward { edge.to } else { edge.from };
1008 if visited.contains(&next) {
1009 continue;
1010 }
1011 let new_depth = depth + 1;
1012 if new_depth >= lo && node_matches(graph, next, &step.target) {
1013 let already_bound = binding.get(&step.target.var);
1014 if matches!(already_bound, Some(id) if *id != next) {
1015 } else {
1017 let mut next_binding = binding.clone();
1018 next_binding.insert(step.target.var.clone(), next);
1019 extend_steps(graph, steps, idx + 1, next, next_binding, out)?;
1020 }
1021 }
1022 if new_depth < hi {
1023 let mut next_visited = visited.clone();
1024 next_visited.insert(next);
1025 stack.push((next, new_depth, next_visited));
1026 }
1027 }
1028 }
1029 Ok(())
1030}
1031
1032fn candidate_nodes(graph: &SymbolGraph, pat: &NodePat) -> Vec<NodeId> {
1033 if let Some(Literal::Str(name)) = pat.props.get("name") {
1035 return graph
1036 .nodes_named(name)
1037 .iter()
1038 .copied()
1039 .filter(|nid| node_matches(graph, *nid, pat))
1040 .collect();
1041 }
1042 if let Some(label) = &pat.label {
1043 let kind = NodeKind::parse(label).unwrap_or(NodeKind::Module);
1044 return graph
1045 .nodes_of_kind(kind)
1046 .into_iter()
1047 .filter(|nid| node_matches(graph, *nid, pat))
1048 .collect();
1049 }
1050 graph
1051 .all_node_ids()
1052 .into_iter()
1053 .filter(|nid| node_matches(graph, *nid, pat))
1054 .collect()
1055}
1056
1057fn node_matches(graph: &SymbolGraph, id: NodeId, pat: &NodePat) -> bool {
1058 let Some(node) = graph.node(id) else {
1059 return false;
1060 };
1061 if let Some(label) = &pat.label {
1062 let Some(kind) = NodeKind::parse(label) else {
1063 return false;
1064 };
1065 if node.kind != kind {
1066 return false;
1067 }
1068 }
1069 for (key, expected) in &pat.props {
1070 let actual = property_value(node, key);
1071 if &actual != expected {
1072 return false;
1073 }
1074 }
1075 true
1076}
1077
1078fn property_value(node: &super::symbol_graph::Node, key: &str) -> Literal {
1079 match key {
1080 "name" => Literal::Str(node.name.clone()),
1081 "path" => Literal::Str(node.path.clone()),
1082 "language" => Literal::Str(node.language.clone()),
1083 "kind" => Literal::Str(node.kind.as_str().to_string()),
1084 "container" => Literal::Str(node.container.clone().unwrap_or_default()),
1085 "access_level" => Literal::Str(node.access_level.clone().unwrap_or_default()),
1086 "signature" => Literal::Str(node.signature.clone()),
1087 "line" => Literal::Int(node.line as i64),
1088 "file_id" => Literal::Int(node.file_id as i64),
1089 "id" => Literal::Int(node.id as i64),
1090 _ => Literal::Str(String::new()),
1091 }
1092}
1093
1094fn eval_bool(
1095 expr: &Expr,
1096 binding: &HashMap<String, NodeId>,
1097 graph: &SymbolGraph,
1098) -> Result<bool, CypherError> {
1099 match expr {
1100 Expr::Bool(b) => Ok(*b),
1101 Expr::And(a, b) => Ok(eval_bool(a, binding, graph)? && eval_bool(b, binding, graph)?),
1102 Expr::Or(a, b) => Ok(eval_bool(a, binding, graph)? || eval_bool(b, binding, graph)?),
1103 Expr::Not(inner) => Ok(!eval_bool(inner, binding, graph)?),
1104 Expr::Compare(l, op, r) => {
1105 let lv = lookup_operand(l, binding, graph)?;
1106 let rv = lookup_operand(r, binding, graph)?;
1107 Ok(apply_cmp(&lv, *op, &rv))
1108 }
1109 }
1110}
1111
1112fn apply_cmp(left: &Literal, op: CmpOp, right: &Literal) -> bool {
1113 match (left, right) {
1114 (Literal::Int(a), Literal::Int(b)) => match op {
1115 CmpOp::Eq => a == b,
1116 CmpOp::Neq => a != b,
1117 CmpOp::Lt => a < b,
1118 CmpOp::Le => a <= b,
1119 CmpOp::Gt => a > b,
1120 CmpOp::Ge => a >= b,
1121 },
1122 (Literal::Str(a), Literal::Str(b)) => match op {
1123 CmpOp::Eq => a == b,
1124 CmpOp::Neq => a != b,
1125 CmpOp::Lt => a < b,
1126 CmpOp::Le => a <= b,
1127 CmpOp::Gt => a > b,
1128 CmpOp::Ge => a >= b,
1129 },
1130 (Literal::Bool(a), Literal::Bool(b)) => match op {
1131 CmpOp::Eq => a == b,
1132 CmpOp::Neq => a != b,
1133 _ => false,
1134 },
1135 _ => matches!(op, CmpOp::Neq),
1136 }
1137}
1138
1139fn lookup_operand(
1140 op: &Operand,
1141 binding: &HashMap<String, NodeId>,
1142 graph: &SymbolGraph,
1143) -> Result<Literal, CypherError> {
1144 match op {
1145 Operand::Literal(lit) => Ok(lit.clone()),
1146 Operand::Path { var, property } => {
1147 let id = binding.get(var).copied().ok_or_else(|| {
1148 CypherError::ExecError(format!("unbound variable `{var}` in expression"))
1149 })?;
1150 let node = graph
1151 .node(id)
1152 .ok_or_else(|| CypherError::ExecError(format!("node id {id} not in graph")))?;
1153 match property {
1154 Some(p) => Ok(property_value(node, p)),
1155 None => Ok(Literal::Str(node.name.clone())),
1156 }
1157 }
1158 }
1159}
1160
1161fn eval_operand(
1162 op: &Operand,
1163 binding: &HashMap<String, NodeId>,
1164 graph: &SymbolGraph,
1165) -> Result<CypherValue, CypherError> {
1166 let lit = lookup_operand(op, binding, graph)?;
1167 Ok(match lit {
1168 Literal::Str(s) => {
1169 if s.is_empty() {
1170 CypherValue::Null
1171 } else {
1172 CypherValue::String(s)
1173 }
1174 }
1175 Literal::Int(n) => CypherValue::Int(n),
1176 Literal::Bool(b) => CypherValue::Bool(b),
1177 })
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183 use crate::ast::Language;
1184
1185 fn fixture() -> SymbolGraph {
1186 let mut g = SymbolGraph::new();
1187 g.rebuild_file(
1188 1,
1189 "src/a.rs",
1190 Language::Rust,
1191 "fn start() {}\nfn driver() { start(); }\n",
1192 &[],
1193 );
1194 g.rebuild_file(
1195 2,
1196 "src/b.rs",
1197 Language::Rust,
1198 "fn entry() { driver(); }\n",
1199 &[],
1200 );
1201 g
1202 }
1203
1204 #[test]
1205 fn lexer_recognises_arrows_and_keywords() {
1206 let toks = lex("MATCH (a)-[:CALLS]->(b) RETURN a.name").unwrap();
1207 assert!(toks.iter().any(|t| matches!(t, Token::Arrow)));
1208 assert!(toks
1209 .iter()
1210 .any(|t| matches!(t, Token::Keyword(k) if k == "MATCH")));
1211 }
1212
1213 #[test]
1214 fn returns_function_by_name() {
1215 let g = fixture();
1216 let rows = execute(
1217 "MATCH (f:Function {name: 'start'}) RETURN f.path AS path, f.line AS line",
1218 &g,
1219 )
1220 .unwrap();
1221 assert_eq!(rows.len(), 1);
1222 assert_eq!(
1223 rows[0].get("path"),
1224 Some(&CypherValue::String("src/a.rs".into()))
1225 );
1226 }
1227
1228 #[test]
1229 fn called_by_var_length_finds_indirect_callers() {
1230 let g = fixture();
1231 let rows = execute(
1232 "MATCH (f:Function {name: 'start'})<-[:CALLS*1..3]-(c:CallSite) RETURN c.path AS path",
1233 &g,
1234 )
1235 .unwrap();
1236 assert!(!rows.is_empty(), "expected at least one call-site caller");
1237 }
1238
1239 #[test]
1240 fn where_predicate_filters_results() {
1241 let g = fixture();
1242 let rows = execute(
1243 "MATCH (f:Function) WHERE f.name = 'driver' RETURN f.path AS path",
1244 &g,
1245 )
1246 .unwrap();
1247 assert_eq!(rows.len(), 1);
1248 assert_eq!(
1249 rows[0].get("path"),
1250 Some(&CypherValue::String("src/a.rs".into()))
1251 );
1252 }
1253
1254 #[test]
1255 fn literal_default_aliases_do_not_collide() {
1256 let g = fixture();
1261 let rows = execute("MATCH (f:Function {name: 'start'}) RETURN 1, 2, 3", &g).unwrap();
1262 assert_eq!(rows.len(), 1);
1263 let row = &rows[0];
1264 assert_eq!(row.get("value"), Some(&CypherValue::Int(1)));
1265 assert_eq!(row.get("value_2"), Some(&CypherValue::Int(2)));
1266 assert_eq!(row.get("value_3"), Some(&CypherValue::Int(3)));
1267 }
1268
1269 #[test]
1270 fn duplicate_explicit_aliases_are_rejected() {
1271 let g = fixture();
1272 let err = execute("MATCH (f:Function) RETURN f.name AS n, f.path AS n", &g).unwrap_err();
1273 assert!(
1274 matches!(err, CypherError::ParseError(_)),
1275 "expected ParseError for duplicate explicit alias, got {err:?}"
1276 );
1277 }
1278
1279 #[test]
1280 fn rejects_too_many_disjoint_patterns() {
1281 let g = fixture();
1285 let err = execute(
1286 "MATCH (a:Function),(b:Function),(c:Function),(d:Function) RETURN a.name",
1287 &g,
1288 )
1289 .unwrap_err();
1290 assert!(
1291 matches!(&err, CypherError::ParseError(msg) if msg.contains("too many disjoint MATCH patterns")),
1292 "expected ParseError for too many patterns, got {err:?}"
1293 );
1294 }
1295
1296 #[test]
1297 fn enforces_row_budget_on_cartesian_explosion() {
1298 let mut g = SymbolGraph::new();
1302 let mut source = String::new();
1303 for i in 0..25 {
1304 source.push_str(&format!("fn f{i}() {{}}\n"));
1305 }
1306 g.rebuild_file(1, "src/big.rs", Language::Rust, &source, &[]);
1307
1308 let err = execute(
1309 "MATCH (a:Function),(b:Function),(c:Function) RETURN a.name AS n",
1310 &g,
1311 )
1312 .unwrap_err();
1313 assert!(
1314 matches!(&err, CypherError::ExecError(msg) if msg.contains("row budget exceeded")),
1315 "expected ExecError for row budget, got {err:?}"
1316 );
1317 }
1318
1319 #[test]
1320 fn open_upper_bound_defaults_to_depth_cap() {
1321 let toks = lex("MATCH (a:Function)-[:CALLS*1..]->(b:Function) RETURN a.name AS n").unwrap();
1323 let q = parse(&toks).unwrap();
1324 let step = &q.matches[0].steps[0];
1325 assert_eq!(step.var_length, Some((1, VAR_LENGTH_MAX_DEPTH)));
1326
1327 let toks = lex("MATCH (a:Function)-[:CALLS*2..]->(b:Function) RETURN a.name AS n").unwrap();
1329 let q = parse(&toks).unwrap();
1330 let step = &q.matches[0].steps[0];
1331 assert_eq!(step.var_length, Some((2, VAR_LENGTH_MAX_DEPTH)));
1332 }
1333
1334 #[test]
1335 fn rejects_depth_above_four() {
1336 let g = fixture();
1337 let err = execute(
1338 "MATCH (a:Function)-[:CALLS*1..6]->(b:Function) RETURN a.name AS n",
1339 &g,
1340 )
1341 .unwrap_err();
1342 assert!(
1343 matches!(err, CypherError::ExecError(_)),
1344 "expected ExecError, got {err:?}"
1345 );
1346 }
1347}