Parse node: (variable:Label {props}) */
| 569 | |
| 570 | /* Parse node: (variable:Label {props}) */ |
| 571 | static int parse_node(parser_t *p, cbm_node_pattern_t *out) { |
| 572 | memset(out, 0, sizeof(*out)); |
| 573 | if (!expect(p, TOK_LPAREN)) { |
| 574 | return CBM_NOT_FOUND; |
| 575 | } |
| 576 | |
| 577 | /* Optional variable */ |
| 578 | if (check(p, TOK_IDENT)) { |
| 579 | /* Lookahead: if next is COLON, this is a variable */ |
| 580 | /* Or if next is RPAREN/LBRACE, this is a variable without label */ |
| 581 | out->variable = heap_strdup(advance(p)->text); |
| 582 | } |
| 583 | |
| 584 | /* Optional :Label, with openCypher label alternation :A|B|C (#242). |
| 585 | * Stored as a single "A|B|C" string; the matcher splits on '|'. */ |
| 586 | if (match(p, TOK_COLON)) { |
| 587 | const cbm_token_t *label = expect(p, TOK_IDENT); |
| 588 | if (!label) { |
| 589 | return CBM_NOT_FOUND; |
| 590 | } |
| 591 | char lbuf[CBM_SZ_256]; |
| 592 | int ll = snprintf(lbuf, sizeof(lbuf), "%s", label->text); |
| 593 | while (match(p, TOK_PIPE)) { |
| 594 | const cbm_token_t *alt = expect(p, TOK_IDENT); |
| 595 | if (!alt) { |
| 596 | return CBM_NOT_FOUND; |
| 597 | } |
| 598 | int w = snprintf(lbuf + ll, (ll < (int)sizeof(lbuf)) ? sizeof(lbuf) - (size_t)ll : 0, |
| 599 | "|%s", alt->text); |
| 600 | if (w > 0) { |
| 601 | ll += w; |
| 602 | } |
| 603 | if (ll >= (int)sizeof(lbuf)) { |
| 604 | break; /* buffer full */ |
| 605 | } |
| 606 | } |
| 607 | out->label = heap_strdup(lbuf); |
| 608 | } |
| 609 | |
| 610 | /* Optional {props} */ |
| 611 | if (check(p, TOK_LBRACE)) { |
| 612 | if (parse_props(p, &out->props, &out->prop_count) < 0) { |
| 613 | return CBM_NOT_FOUND; |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | if (!expect(p, TOK_RPAREN)) { |
| 618 | return CBM_NOT_FOUND; |
| 619 | } |
| 620 | return 0; |
| 621 | } |
| 622 | |
| 623 | /* Parse *min..max hop range after the star token has been consumed */ |
| 624 | static void parse_hop_range(parser_t *p, int *min_hops, int *max_hops) { |
no test coverage detected