Gestion des branches avec nom : alt et permutation

Introduction

La plupart des formats de données exigent de différencier plusieurs structures possibles au même endroit : une valeur peut être un objet, un tableau, une chaîne ou un nombre, et un fichier de configuration peut contenir des options dans un ordre quelconque. Le module nom::branch propose deux combinateurs pour traiter ces cas sans conditionnelles explicites : alt et permutation.

alt : sélectionner la première branche qui réussit

alt prend une liste de parseurs et renvoie le résultat du premier qui réussit. C’est l’outil naturel pour des alternativse exclusives.

Exemple de base

use nom::branch::alt;
use nom::bytes::complete::tag;

fn parse_keyword(i: &str) -> nom::IResult<&str, &str> {
    alt((tag("fn"), tag("let"), tag("use"))).parse(i)
}

assert_eq!(parse_keyword("let x = 1"), Ok((" x = 1", "let")));
assert_eq!(parse_keyword("use std::io"), Ok((" std::io", "use")));

Différentes formes d’entrée

alt accepte un n-uplet, un tableau ou une tranche mutable :

// N-uplet
alt((tag("red"), tag("green"), tag("blue"))).parse(i);

// Tableau
const COLORS: [fn(&str) -> nom::IResult<&str, &str>; 3] =
    [tag("red"), tag("green"), tag("blue")];
alt(COLORS).parse(i);

// Tranche mutable
let mut parsers = [tag("one"), tag("two"), tag("three")];
alt(&mut parsers[..]).parse(i);

permutation : toutes les branches, dans n’importe quel ordre

Contrairement à alt, permutation exige que tous les parseurs réusissent, mais ils peuvent apparaître dans n’importe quel ordre.

use nom::branch::permutation;
use nom::bytes::complete::tag;

fn parse_flags(i: &[u8]) -> nom::IResult<&[u8], (&[u8], &[u8], &[u8])> {
    permutation((
        tag(b"fast"),
        tag(b"safe"),
        tag(b"quiet"),
    ))
    .parse(i)
}

assert_eq!(parse_flags(b"fastsafequiet"), Ok((&b""[..], (b"fast", b"safe", b"quiet"))));
assert_eq!(parse_flags(b"quietfastsafe"), Ok((&b""[..], (b"fast", b"safe", b"quiet"))));

Affiner les erreurs avec un type personnalisé

Par défaut, alt renvoie l’erreur de la dernière branche testée. Un type d’erreur personnalisé permet d’accumuler les échecs.

use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::error::{ErrorKind, ParseError};

#[derive(Debug, PartialEq)]
struct BranchError(String);

impl<'a> ParseError<&'a str> for BranchError {
    fn from_error_kind(input: &'a str, kind: ErrorKind) -> Self {
        BranchError(format!("échec sur {:?} ({:?})", input, kind))
    }

    fn append(input: &'a str, kind: ErrorKind, other: Self) -> Self {
        BranchError(format!("{} ; aussi {:?} ({:?})", other.0, input, kind))
    }
}

fn parse_color(i: &str) -> nom::IResult<&str, &str, BranchError> {
    alt((tag("rouge"), tag("vert"), tag("bleu"))).parse(i)
}

Exemples concrets

Valeurs JSON

use nom::branch::alt;
use nom::combinator::map;

fn json_value(i: &str) -> nom::IResult<&str, JsonValue> {
    alt((
        map(parse_object, JsonValue::Object),
        map(parse_array, JsonValue::Array),
        map(parse_string, JsonValue::String),
        map(parse_number, JsonValue::Number),
        map(parse_bool, JsonValue::Bool),
        map(parse_null, |_| JsonValue::Null),
    ))
    .parse(i)
}

Atome d’un S-expression

fn expr_atom(i: &str) -> nom::IResult<&str, Expr> {
    alt((
        map(parse_symbol, Expr::Symbol),
        map(parse_string, Expr::String),
        map(parse_number, Expr::Number),
    ))
    .parse(i)
}

Options de configuration sans ordre fixe

use nom::branch::permutation;
use nom::bytes::complete::{tag, take_until};
use nom::character::complete::digit1;
use nom::combinator::map;
use nom::sequence::preceded;

fn parse_bind(i: &str) -> nom::IResult<&str, &str> {
    preceded(tag("bind="), take_until(";")).parse(i)
}

fn parse_timeout(i: &str) -> nom::IResult<&str, u64> {
    map(
        preceded(tag("timeout="), digit1),
        |n: &str| n.parse().unwrap(),
    )
    .parse(i)
}

fn parse_settings(i: &str) -> nom::IResult<&str, (&str, u64)> {
    permutation((parse_bind, parse_timeout)).parse(i)
}

assert_eq!(
    parse_settings("bind=127.0.0.1;timeout=30;"),
    Ok((";", ("127.0.0.1", 30)))
);
assert_eq!(
    parse_settings("timeout=30;bind=127.0.0.1;"),
    Ok((";", ("127.0.0.1", 30)))
);

Stratégies de test

Il est utile de vérifier chaque branche isolément, mais aussi les interactions :

  • tester le succès de chaque branche de alt ;
  • vérifier que permutation accepte tous les ordres possibles ;
  • contrôler le comportement face à une entrée partielle ;
  • s’assurer que l’erreur finale contient l’information attendue.
#[test]
fn alt_returns_first_match() {
    assert_eq!(parse_keyword("fn main()"), Ok((" main()", "fn")));
    assert_eq!(parse_keyword("use std"), Ok((" std", "use")));
}

Bonnes pratiques

  • Ordre des branches : placez en premier les cas les plus fréquents ou ceux qui échouent le plus vite.
  • Regroupement : quand alt contient trop de branches, décomposez-le en sous-combinateurs.
  • Erreurs : utilisez un type d’erreur personnalisé pour conserver le contexte de chaque échec.
  • Copies : évitez de cloner l’entrée dans vos parseurs personnalisés ; alt restitue l’entrée originale lorsqu’une branche échoue.
  • Grand nombre de branches : préférez un tableau à un très grand n-uplet.

Pièges courants

  • Une branche de alt consomme partiellement l’entrée puis échoue : l’erreur est renvoyée immédiatement. Utilisez peek ou cut selon que l’erreur est récupérable ou non.
  • permutation renvoie un tuple ordonné selon la déclaration, pas selon l’ordre d’apparition dans l’entrée.
  • Un parseur récursif dans alt sans borne peut provoquer un débordement de pile ; utilisez un combinateur de récursion ou un mode itératif.

Étiquettes: nom Rust parser-combinator alt permutation

Publié le 23 juin à 16h46