rust-lang/rust-analyzer · error

duplicate rule: `{}`

Error message

duplicate rule: `{}`

What it means

`node` throws `duplicate rule: \`{name}\`` when a node name appears as a left-hand side more than once in the grammar. Each node is interned once; if `p.grammar[node].rule` is already set (not DUMMY_RULE), a second `Name = ...` definition would silently overwrite the first, so the parser rejects it instead.

Source

Thrown at lib/ungrammar/src/parser.rs:96

    fn intern_token(&mut self, name: String) -> Token {
        let len = self.token_table.len();
        let grammar = &mut self.grammar;
        *self.token_table.entry(name.clone()).or_insert_with(|| {
            grammar.tokens.push(TokenData { name });
            Token(len)
        })
    }
}

fn node(p: &mut Parser) -> Result<()> {
    let token = p.bump()?;
    let node = match token.kind {
        TokenKind::Node(it) => p.intern_node(it),
        _ => bail!(token.loc, "expected ident"),
    };
    p.expect(TokenKind::Eq, "=")?;
    if !matches!(p.grammar[node].rule, DUMMY_RULE) {
        bail!(token.loc, "duplicate rule: `{}`", p.grammar[node].name)
    }

    let rule = rule(p)?;
    p.grammar.nodes[node.0].rule = rule;
    Ok(())
}

fn rule(p: &mut Parser) -> Result<Rule> {
    if let Some(lexer::Token { kind: TokenKind::Pipe, loc }) = p.peek() {
        bail!(
            *loc,
            "The first element in a sequence of productions or alternatives \
            must not have a leading pipe (`|`)"
        );
    }

    let lhs = seq_rule(p)?;
    let mut alt = vec![lhs];

View on GitHub (pinned to e8f7e90aa3)

Solutions

  1. Search the .ungram file for the duplicated node name and delete or rename one of the definitions.
  2. If the intent is an alternative production, merge them into one rule using `|` alternatives instead of a second definition.
  3. Uniquely rename one node if two concepts genuinely share a name.

Example fix

// before (.ungram)
Expr = PathExpr | Literal
Expr = BinExpr

// after (.ungram)
Expr = PathExpr | Literal | BinExpr
Defensive patterns

Strategy: validation

Validate before calling

// detect duplicate node names before parsing
fn has_duplicate_nodes(src: &str) -> bool {
    let mut names: Vec<&str> = src.lines()
        .filter(|l| l.contains('=') && !l.trim_start().starts_with('|'))
        .filter_map(|l| l.split('=').next())
        .map(|s| s.trim())
        .collect();
    let n = names.len();
    names.sort_unstable();
    names.dedup().len() != n
}

Try / catch

match ungrammar::parse(src) {
    Ok(g) => g,
    Err(e) if e.to_string().starts_with("duplicate rule") => {
        eprintln!("dedupe your grammar: {}", e);
        ungrammar::parse(&dedupe(src)).expect("deduped grammar parses")
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Parsing a `.ungram` file containing two definitions for the same node, e.g. `Expr = ...` appearing twice at top level. Triggered via `parse` whenever an identifier token maps to an already-defined node.

Common situations: Merging two grammar files without deduplication, copy-pasting a block that redefines an existing node, refactoring node names and forgetting to delete the old definition.

Related errors


AI-assisted analysis of rust-lang/rust-analyzer@e8f7e90aa3 (2026-09-03). Data as JSON: /api/errors/adf7e9f2124bbe8a. Report an issue: GitHub.