Skip to content

silent accumulator overwrite in build_range_literal and ListSlice (defensive; unreachable today but fragile) #47

Description

@arvindsh

Summary

src/ast/build_cst.rs::build_range_literal and the ListSlice arm of build_binary_expr accumulate the start and end components of a *N..M quantifier (or list[start..end] slice) into Option<…> slots, but a second value on either side of the DOT_DOT token silently overwrites the previously stored one. There is no diagnostic and no defensive check.

Today the grammar does not actually emit two INTEGER (or two Expression) tokens on the same side of .., so the bug is unreachable from valid Cypher. It is reported as a defensive concern: any future grammar/CST refactor that does emit duplicate tokens would silently drop the first one and produce a wrong AST without surfacing the conflict.

This is the same shape as the duplicate-RETURN accumulator-overwrite bug reported separately (single-part query body), so I'm bundling them together for visibility.

Affected version

decypher 0.2.0-alpha.6.

Locations

Location 1src/ast/build_cst.rs, in build_range_literal():

fn build_range_literal(rl: RangeLiteral) -> Result<ast_c::RangeLiteral> {
    let mut start: Option<i64> = None;
    let mut end: Option<i64> = None;
    let mut seen_dot_dot = false;
    let mut seen_star = false;
    for child in rl.syntax().children_with_tokens() {
        if let Some(tok) = child.as_token() {
            match tok.kind() {
                SyntaxKind::STAR => seen_star = true,
                SyntaxKind::DOT_DOT => seen_dot_dot = true,
                SyntaxKind::INTEGER => {
                    if let Some(val) = parse_integer(tok.text()) {
                        if !seen_dot_dot {
                            start = Some(val);   // ← silent overwrite if multiple INTEGERs before DOT_DOT
                        } else {
                            end = Some(val);     // ← silent overwrite if multiple INTEGERs after DOT_DOT
                        }
                    }
                }
                _ => {}
            }
        } else if let Some(node) = child.as_node() {
            for inner in node.children_with_tokens() {
                if let Some(tok) = inner.as_token()
                    && tok.kind() == SyntaxKind::INTEGER
                    && let Some(val) = parse_integer(tok.text())
                {
                    if !seen_dot_dot {
                        start = Some(val);       // same overwrite, inner loop
                    } else {
                        end = Some(val);
                    }
                }
            }
        }
    }
    ...
}

Location 2src/ast/build_cst.rs, in build_binary_expr(), ListSlice arm (around line 1311):

let mut seen_dot_dot = false;
let mut start_expr: Option<ast_c::Expression> = None;
let mut end_expr: Option<ast_c::Expression> = None;
for child in b.syntax().children_with_tokens() {
    if let Some(tok) = child.as_token() {
        if tok.kind() == SyntaxKind::DOT_DOT {
            seen_dot_dot = true;
        }
        continue;
    }
    if let Some(node) = child.as_node()
        && let Some(e) = Expression::cast(node.clone())
    {
        let built = build_expression(e)?;
        if !seen_dot_dot {
            start_expr = Some(built);    // ← silent overwrite if multiple Expressions before DOT_DOT
        } else {
            end_expr = Some(built);      // ← silent overwrite if multiple Expressions after DOT_DOT
        }
    }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingkilo-auto-fixAuto-generated label by Kilokilo-triagedAuto-generated label by KilorustPull requests that update rust code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions